use core::fmt::Write;

use chrono::naive::NaiveDateTime;
use chrono::{Datelike, Timelike, Weekday};
use tinygfx::color::BlackWhite::{self, Black, White};
use tinygfx::image::{MonoBitmapImage, MonoImageData};
use tinygfx::{Clip, MonoImage, Rectangle, Renderer, Text, TextAlignment};

use super::super::assets::*;
use super::super::information::{Information, TemperatureHumidity};

pub struct TileView {
    info: Information,
}

impl TileView {
    pub fn new(info: Information) -> TileView {
        // TODO: Optimization, prepare more values here.
        TileView { info }
    }

    pub fn draw(&self, renderer: &mut Renderer<BlackWhite>) {
        // Format the time.
        let datetime = NaiveDateTime::from_timestamp(self.info.time as i64, 0);
        let date = datetime.date();
        let time = datetime.time();

        let mut date_buffer = WriteBuffer {
            buffer: [0u8; 32],
            length: 0,
        };
        write!(
            date_buffer,
            "{}, {:02}.{:02}.{:04}",
            match date.weekday() {
                Weekday::Mon => "Montag",
                Weekday::Tue => "Dienstag",
                Weekday::Wed => "Mittwoch",
                Weekday::Thu => "Donnerstag",
                Weekday::Fri => "Freitag",
                Weekday::Sat => "Samstag",
                Weekday::Sun => "Sonntag",
            },
            date.day(),
            date.month(),
            date.year()
        )
        .ok();

        let mut time_buffer = WriteBuffer {
            buffer: [0u8; 8],
            length: 0,
        };
        write!(time_buffer, "{:02}:{:02}", time.hour(), time.minute(),).ok();

        // Format everything else.
        let outside_strings = self.info.outside.map(|x| TempHumStrings::new(x));
        let inside_strings = self.info.inside.map(|x| TempHumStrings::new(x));
        let bathroom_strings = self.info.bathroom.map(|x| TempHumStrings::new(x));
        let pressure_string = self.info.pressure.map(|x| PressureString::new(x));

        // Create the UI.
        // TODO: Insert values.
        let date = Text::new(8, 8, date_buffer.to_str(), &ROBOTO_22, Black);
        let mut time = Text::new(392, 8, time_buffer.to_str(), &ROBOTO_22, Black);
        time.align(TextAlignment::Right);
        let top_bar = Rectangle::new(0, 34, 400, 1, Black);

        let tile_width = 400 / 2 - 1;
        let tile_height = (300 - 35) / 2 - 1;

        let inside_tile = TempHumTile::new(
            "Innen",
            inside_strings.as_ref(),
            0,
            35,
            tile_width,
            tile_height,
        );
        let outside_tile = TempHumTile::new(
            "Außen",
            outside_strings.as_ref(),
            200,
            35,
            tile_width,
            tile_height,
        );
        let bathroom_tile = TempHumTile::new(
            "Badezimmer",
            bathroom_strings.as_ref(),
            0,
            35 + tile_height + 1,
            tile_width,
            tile_height,
        );
        let pressure_tile = PressureTile::new(
            pressure_string.as_ref(),
            200,
            35 + tile_height + 1,
            tile_width,
            tile_height,
        );

        let vert_separator = Rectangle::new(8, 35 + tile_height, 392, 1, Black);
        let hor_separator = Rectangle::new(tile_width, 35 + 8, 1, 300 - 35 - 16, Black);

        // Draw everything.
        let clip = renderer.full_frame();
        renderer.clear(White);

        top_bar.draw(clip, renderer);
        date.draw(clip, renderer);
        time.draw(clip, renderer);

        vert_separator.draw(clip, renderer);
        hor_separator.draw(clip, renderer);

        inside_tile.draw(clip, renderer);
        outside_tile.draw(clip, renderer);
        bathroom_tile.draw(clip, renderer);
        pressure_tile.draw(clip, renderer);
    }
}

struct WriteBuffer<T> {
    buffer: T,
    length: usize,
}

impl<T> Write for WriteBuffer<T>
where
    T: AsMut<[u8]>,
{
    fn write_str(&mut self, s: &str) -> core::fmt::Result {
        let bytes = s.as_bytes();
        if bytes.len() <= self.buffer.as_mut().len() - self.length {
            self.buffer.as_mut()[self.length..bytes.len() + self.length].copy_from_slice(bytes);
            self.length += bytes.len();
            Ok(())
        } else {
            Err(core::fmt::Error)
        }
    }
}

impl<T> WriteBuffer<T>
where
    T: AsRef<[u8]>,
{
    fn to_str<'a>(&'a self) -> &'a str {
        unsafe {
            // Safe, we did not use any non-UTF characters above.
            core::str::from_utf8_unchecked(&self.buffer.as_ref()[0..self.length])
        }
    }
}

struct TempHumStrings {
    temp: WriteBuffer<[u8; 12]>,
    hum: WriteBuffer<[u8; 12]>,
}

impl TempHumStrings {
    fn new(data: TemperatureHumidity) -> TempHumStrings {
        let mut strings = TempHumStrings {
            temp: WriteBuffer {
                buffer: [0u8; 12],
                length: 0,
            },
            hum: WriteBuffer {
                buffer: [0u8; 12],
                length: 0,
            },
        };
        write!(
            strings.temp,
            "{},{}°C",
            data.temperature / 10,
            (data.temperature % 10).abs()
        )
        .ok();
        let humidity = (data.humidity + 5) / 10;
        write!(strings.hum, "{},{}%", humidity / 10, humidity % 10).ok();
        strings
    }

    fn temperature<'a>(&'a self) -> &'a str {
        self.temp.to_str()
    }

    fn humidity<'a>(&'a self) -> &'a str {
        self.hum.to_str()
    }
}

struct TempHumTile<'a> {
    title: Text<'a, BlackWhite>,
    data: Option<(
        MonoImage<'static, MonoBitmapImage, BlackWhite>,
        Text<'a, BlackWhite>,
        MonoImage<'static, MonoBitmapImage, BlackWhite>,
        Text<'a, BlackWhite>,
    )>,
    error: Option<MonoImage<'static, MonoBitmapImage, BlackWhite>>,
}

impl<'a> TempHumTile<'a> {
    fn new(
        title: &'a str,
        data: Option<&'a TempHumStrings>,
        left: i32,
        top: i32,
        width: i32,
        height: i32,
    ) -> Self {
        let center = left + width / 2;

        let mut title = Text::new(center, top + 10, title, &ROBOTO_30, Black);
        title.align(TextAlignment::Center);

        let mut tile = Self {
            title,
            data: None,
            error: None,
        };

        if let Some(data) = data {
            let temp_width =
                ROBOTO_30.get_text_size(data.temperature()).0 + 1 + THERMOMETER_36.width();
            let temp_icon = MonoImage::new(
                center - (temp_width / 2) as i32,
                top + 48,
                &THERMOMETER_36,
                Black,
            );
            let mut temperature = Text::new(
                center + (temp_width - temp_width / 2) as i32,
                top + 48,
                data.temperature(),
                &ROBOTO_30,
                Black,
            );
            temperature.align(TextAlignment::Right);
            let hum_width = ROBOTO_30.get_text_size(data.humidity()).0 + 1 + THERMOMETER_36.width();
            let hum_icon = MonoImage::new(
                center - (hum_width / 2) as i32,
                top + 82,
                &WATER_PERCENT_36,
                Black,
            );
            let mut humidity = Text::new(
                center + (hum_width - hum_width / 2) as i32,
                top + 82,
                data.humidity(),
                &ROBOTO_30,
                Black,
            );
            humidity.align(TextAlignment::Right);

            tile.data = Some((temp_icon, temperature, hum_icon, humidity));
        } else {
            tile.error = Some(MonoImage::new(
                center - (WIFI_STRENGTH_ALERT_36.width() / 2) as i32,
                top + height / 2 - (WIFI_STRENGTH_ALERT_36.height() / 2) as i32,
                &WIFI_STRENGTH_ALERT_36,
                Black,
            ));
        }

        tile
    }

    fn draw(&self, clip: Clip, renderer: &mut Renderer<BlackWhite>) {
        self.title.draw(clip, renderer);
        if let Some(data) = self.data.as_ref() {
            data.0.draw(clip, renderer);
            data.1.draw(clip, renderer);
            data.2.draw(clip, renderer);
            data.3.draw(clip, renderer);
        }
        if let Some(error) = self.error.as_ref() {
            error.draw(clip, renderer);
        }
    }
}

struct PressureString(WriteBuffer<[u8; 12]>);

impl PressureString {
    fn new(pressure: u32) -> PressureString {
        let pressure = (pressure + 5) / 10;
        let mut buffer = WriteBuffer {
            buffer: [0u8; 12],
            length: 0,
        };
        write!(buffer, "{},{}hPa", pressure / 10, pressure % 10).ok();
        PressureString(buffer)
    }

    fn to_str<'a>(&'a self) -> &'a str {
        self.0.to_str()
    }
}

struct PressureTile<'a> {
    title: Text<'a, BlackWhite>,
    data: Option<Text<'a, BlackWhite>>,
    error: Option<MonoImage<'static, MonoBitmapImage, BlackWhite>>,
}

impl<'a> PressureTile<'a> {
    fn new(data: Option<&'a PressureString>, left: i32, top: i32, width: i32, height: i32) -> Self {
        let center = left + width / 2;

        let mut title = Text::new(center, top + 10, "Luftdruck", &ROBOTO_30, Black);
        title.align(TextAlignment::Center);

        let mut tile = Self {
            title,
            data: None,
            error: None,
        };

        if let Some(data) = data {
            let mut pressure = Text::new(center, top + 48, data.to_str(), &ROBOTO_30, Black);
            pressure.align(TextAlignment::Center);

            tile.data = Some(pressure);
        } else {
            tile.error = Some(MonoImage::new(
                center - (WIFI_STRENGTH_ALERT_36.width() / 2) as i32,
                top + height / 2 - (WIFI_STRENGTH_ALERT_36.height() / 2) as i32,
                &WIFI_STRENGTH_ALERT_36,
                Black,
            ));
        }

        tile
    }

    fn draw(&self, clip: Clip, renderer: &mut Renderer<BlackWhite>) {
        self.title.draw(clip, renderer);
        if let Some(data) = self.data.as_ref() {
            data.draw(clip, renderer);
        }
        if let Some(error) = self.error.as_ref() {
            error.draw(clip, renderer);
        }
    }
}
