use std::sync::{Arc, Condvar, Mutex};

#[derive(Debug)]
pub struct Display {
    inner: Arc<DisplayInner>,
}

#[derive(Debug, Default)]
struct DisplayInner {
    shared: Mutex<DisplayShared>,
    signal: Condvar,
}

#[derive(Debug, Default)]
struct DisplayShared {
    ready: bool,
    pending: bool,
    lines: [String; 4],
}

impl Display {
    pub fn new() -> Self {
        let inner: Arc<DisplayInner> = Default::default();
        std::thread::spawn({
            let inner = inner.clone();
            move || {
                display_thread(inner);
            }
        });
        Self { inner }
    }

    pub fn is_ready(&self) -> bool {
        self.inner.shared.lock().unwrap().ready
    }

    pub fn update_line(&mut self, line_index: u8, text: &str) {
        assert!(line_index < 4);
        let mut shared = self.inner.shared.lock().unwrap();
        let line = &mut shared.lines[usize::from(line_index)];
        if line != text {
            line.clear();
            line.push_str(text);
            shared.pending = true;
            self.inner.signal.notify_one();
        }
    }

    pub fn update_all(&mut self, texts: &[String; 4]) {
        let mut shared = self.inner.shared.lock().unwrap();
        for (i, text) in texts.iter().enumerate() {
            let line = &mut shared.lines[i];
            if line != text {
                line.clear();
                line.push_str(text);
                shared.pending = true;
            }
        }
        if shared.pending {
            self.inner.signal.notify_one();
        }
    }
}

fn display_thread(display_data: Arc<DisplayInner>) {
    let i2c = linux_embedded_hal::I2cdev::new("/dev/i2c-1").unwrap();

    let mut display_options =
        hd44780::setup::DisplayOptionsI2C::new(hd44780::memory_map::MemoryMap2004::new())
            .with_i2c_bus(i2c, 0x27);

    let mut display = loop {
        match hd44780::HD44780::new(display_options, &mut linux_embedded_hal::Delay) {
            Err((options_back, error)) => {
                log::warn!("Failed setting up display: {:?}, retrying...", error);
                std::thread::sleep(std::time::Duration::from_millis(500));
                display_options = options_back;
            }
            Ok(display) => break display,
        }
    };

    loop {
        let Err(e) = display.clear(&mut linux_embedded_hal::Delay) else {
            break;
        };
        log::warn!("Error clearing display: {:?}, retrying...", e);
        std::thread::sleep(std::time::Duration::from_millis(500));
    }

    loop {
        let Err(e) = display.reset(&mut linux_embedded_hal::Delay) else {
            break;
        };
        log::warn!("Error resetting display: {:?}, retrying...", e);
        std::thread::sleep(std::time::Duration::from_millis(500));
    }

    loop {
        let Err(e) = display.set_display_mode(
            hd44780::DisplayMode {
                cursor_visibility: hd44780::Cursor::Invisible,
                cursor_blink: hd44780::CursorBlink::Off,
                display: hd44780::Display::On,
            },
            &mut linux_embedded_hal::Delay,
        ) else {
            break;
        };
        log::warn!("Error configuring display: {:?}, retrying...", e);
        std::thread::sleep(std::time::Duration::from_millis(500));
    }

    display_data.shared.lock().unwrap().ready = true;

    let mut needs_update = [false; 4];
    let mut display_lines_old = [
        String::with_capacity(20),
        String::with_capacity(20),
        String::with_capacity(20),
        String::with_capacity(20),
    ];
    let mut display_lines = [
        String::with_capacity(20),
        String::with_capacity(20),
        String::with_capacity(20),
        String::with_capacity(20),
    ];
    loop {
        {
            let mut shared = display_data.shared.lock().unwrap();
            if !shared.pending {
                shared = display_data.signal.wait(shared).unwrap();
            }
            shared.pending = false;
            shared.lines.clone_into(&mut display_lines);
        };

        for ((old, new), needs_update) in display_lines_old
            .iter()
            .zip(&display_lines)
            .zip(needs_update.iter_mut())
        {
            *needs_update = old != new;
        }

        display_lines.clone_into(&mut display_lines_old);

        for ((i, line), needs_update) in display_lines.iter_mut().enumerate().zip(needs_update) {
            if !needs_update {
                continue;
            }

            // Retry display write three times
            for _ in 0..3 {
                let res = display.set_cursor_xy(
                    (0, u8::try_from(i).unwrap()),
                    &mut linux_embedded_hal::Delay,
                );
                match res {
                    Ok(_) => (),
                    Err(e) => {
                        log::warn!("Error moving display cursor: {:?}, retrying...", e);
                        continue;
                    }
                }

                line.push_str("                    ");
                line.truncate(20);

                let res = display.write_str(line, &mut linux_embedded_hal::Delay);
                match res {
                    Ok(_) => break,
                    Err(e) => {
                        log::warn!("Error writing data to display: {:?}, retrying...", e);
                        continue;
                    }
                }
            }
        }
    }
}
