use core::fmt::Write;

use embedded_hal::blocking::delay::DelayUs;
use embedded_hal::digital::v2::OutputPin;
use epd_waveshare::epd4in2::EPD4in2;
use epd_waveshare::prelude::{DisplayStream, WaveshareDisplay};
use mkl25z4_hal::time::CopyableMonoTimer;
use tinygfx::color::BlackWhite::{self, Black, White};
use tinygfx::image::{MonoBitmapImage, MonoImageData};
use tinygfx::{Clip, Frame, MonoImage, Rectangle, Renderer, Text, TextAlignment};

use super::assets::*;
use super::information::{Information, TemperatureHumidity};
use super::pins::{
    DisplayBmeSpi, DisplayBusy, DisplayCs, DisplayDc, DisplayPins, DisplayPwr, DisplayRst,
};

pub struct DisplayState {
    // TODO
}

pub struct Display {
    pwr: DisplayPwr,
    epd: EPD4in2<DisplayBmeSpi, DisplayCs, DisplayBusy, DisplayDc, DisplayRst>,
}

impl Display {
    pub fn init(
        mut pins: DisplayPins,
        spi: &mut DisplayBmeSpi,
        mut time: CopyableMonoTimer,
    ) -> Display {
        // Enable power.
        pins.pwr.set_low().ok();
        // Wait a bit for power to stabilize.
        time.delay_us(100u8);

        let epd = EPD4in2::new(spi, pins.cs, pins.busy, pins.dc, pins.rst, &mut time).unwrap(); // TODO: Error handling?

        // Disable power again.
        pins.pwr.set_high().ok();

        Display { pwr: pins.pwr, epd }
    }

    pub fn update(
        &mut self,
        _display_state: &DisplayState,
        info: &Information,
        spi: &mut DisplayBmeSpi,
        mut time: CopyableMonoTimer,
    ) {
        // Enable power.
        self.pwr.set_low().ok();
        // Wait a bit for power to stabilize.
        time.delay_us(100u8);

        // TODO: Error handling.
        self.epd.wake_up(spi, &mut time).ok();

        // TODO: Insert values.
        let date = Text::new(8, 8, "Samstag, 25.04.2020", &ROBOTO_22, Black);
        let mut time = Text::new(392, 8, "12:34", &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 outside_strings = info.outside.map(|x| TempHumStrings::new(x));
        let inside_strings = info.inside.map(|x| TempHumStrings::new(x));
        let bathroom_strings = info.bathroom.map(|x| TempHumStrings::new(x));
        let pressure_string = info.pressure.map(|x| PressureString::new(x));

        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);

        let mut frame = Frame::new(400, 300, |mut renderer| {
            let clip = renderer.full_frame();
            renderer.clear(White);

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

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

            inside_tile.draw(clip, &mut renderer);
            outside_tile.draw(clip, &mut renderer);
            bathroom_tile.draw(clip, &mut renderer);
            pressure_tile.draw(clip, &mut renderer);
        });
        frame.mirror_x(true);
        frame.mirror_y(true);
        //self.epd.set_background_color(Color::Black);
        let checkerboard = TinyGfxStream::new(frame);
        self.epd.update_frame_stream(spi, checkerboard).unwrap();
        self.epd.display_frame(spi).unwrap();

        // Disable power again.
        self.pwr.set_high().ok();
    }
}

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);
        }
    }
}

struct TinyGfxStream<Draw> {
    frame: Frame<Draw, BlackWhite>,
    buffer: [u8; 400 / 8],
    row: u32,
}

impl<Draw> TinyGfxStream<Draw>
where
    Draw: Fn(Renderer<BlackWhite>),
{
    fn new(frame: Frame<Draw, BlackWhite>) -> Self {
        Self {
            frame,
            buffer: [0u8; 400 / 8],
            row: 0,
        }
    }
}

impl<Draw> DisplayStream for TinyGfxStream<Draw>
where
    Draw: Fn(Renderer<BlackWhite>),
{
    fn next<'a>(&'a mut self) -> Option<&'a [u8]> {
        if self.row == self.frame.height() {
            None
        } else {
            self.frame.draw_row(self.row, &mut self.buffer);
            self.row += 1;
            Some(&self.buffer)
        }
    }
}
