use crate::parts::mos6522_peripheral::Mos6522Peripheral;
use std::time::{Duration, Instant};

#[derive(Debug)]
pub struct HD44780 {
    // Bus inputs
    data_bus: u8,
    rs: bool,
    rw: bool,
    enable: bool,
    prev_enable: bool,

    // Internal memory
    ddram: [u8; 80],
    cgram: [u8; 64],

    // Cursor & display state
    cursor_position: u8,
    display_on: bool,
    cursor_on: bool,
    blink_on: bool,
    entry_increment: bool,
    entry_shift: bool,

    // Function set flags
    data_length_8bit: bool,
    two_line_mode: bool,
    font_5x10: bool,

    // Busy flag
    busy: bool,
    last_command_time: Instant,
}

impl Mos6522Peripheral for HD44780 {
    fn write(&mut self, port: u8) {
        println!("Writing {port:08b}/{port:02x} to HD44780");
        self.set_data_bus(port);
    }

    /// Control
    ///
    /// Takes a u8 with the bottom 3 bits representing
    /// X X X X   X rs rw en
    fn control(&mut self, control: u8) {
        self.write_control_lines(
            control & 1 << 2 == 0,
            control & 1 << 1 == 0,
            control & 1 == 0,
        );
    }

    fn read(&mut self) -> u8 {
        self.data_bus
    }

    fn tick(&mut self) {
        println!("TICK OF HD44780");
        if self.busy && self.last_command_time.elapsed() > Duration::from_micros(50) {
            self.busy = false;
        }
    }
}

impl HD44780 {
    pub fn new() -> Self {
        Self {
            data_bus: 0,
            rs: false,
            rw: false,
            enable: false,
            prev_enable: false,

            ddram: [b' '; 80],
            cgram: [0; 64],

            cursor_position: 0,
            display_on: true,
            cursor_on: false,
            blink_on: false,
            entry_increment: true,
            entry_shift: false,

            data_length_8bit: true,
            two_line_mode: true,
            font_5x10: false,

            busy: false,
            last_command_time: Instant::now(),
        }
    }

    pub fn write_control_lines(&mut self, rs: bool, rw: bool, enable: bool) {
        self.rs = rs;
        self.rw = rw;

        // On rising edge of Enable
        if enable && !self.prev_enable {
            self.evaluate();
        }

        self.prev_enable = enable;
    }

    pub fn set_data_bus(&mut self, value: u8) {
        self.data_bus = value;
    }

    pub fn read_data_bus(&self) -> u8 {
        if !self.rs && self.rw {
            // Return busy flag + current address
            let busy_flag = if self.busy { 0x80 } else { 0x00 };
            busy_flag | (self.cursor_position & 0x7F)
        } else {
            // Not implemented: read from DDRAM/CGRAM
            0
        }
    }

    fn evaluate(&mut self) {
        if self.rw {
            return;
        }

        if self.rs {
            self.write_data(self.data_bus);
        } else {
            self.execute_command(self.data_bus);
        }

        self.busy = true;
        self.last_command_time = Instant::now();
    }

    fn write_data(&mut self, data: u8) {
        if self.cursor_position < 0x50 {
            self.ddram[self.cursor_position as usize] = data;
            self.cursor_position = self.cursor_position.wrapping_add(1);
        }
    }

    fn execute_command(&mut self, cmd: u8) {
        match cmd {
            0x01 => {
                self.ddram.fill(b' ');
                self.cursor_position = 0;
            }
            0x02 => {
                self.cursor_position = 0;
            }
            0x04..=0x07 => {
                self.entry_increment = (cmd & 0b10) != 0;
                self.entry_shift = (cmd & 0b01) != 0;
            }
            0x08..=0x0F => {
                self.display_on = (cmd & 0b100) != 0;
                self.cursor_on = (cmd & 0b010) != 0;
                self.blink_on = (cmd & 0b001) != 0;
            }
            0x10..=0x1F => {
                // Cursor/display shift — not yet implemented
            }
            0x20..=0x3F => {
                self.data_length_8bit = (cmd & 0b10000) != 0;
                self.two_line_mode = (cmd & 0b1000) != 0;
                self.font_5x10 = (cmd & 0b100) != 0;
            }
            0x40..=0x7F => {
                // Set CGRAM address — stub
            }
            0x80..=0xFF => {
                self.cursor_position = cmd & 0x7F;
            }
            _ => {}
        }
    }

    pub fn get_display_lines(&self) -> (String, String) {
        let row1: String = self.ddram[0x00..0x10].iter().map(|&b| b as char).collect();
        let row2: String = self.ddram[0x40..0x50].iter().map(|&b| b as char).collect();
        (row1, row2)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn pulse_enable(lcd: &mut HD44780) {
        lcd.write_control_lines(lcd.rs, lcd.rw, true);
        lcd.write_control_lines(lcd.rs, lcd.rw, false);
    }

    #[test]
    fn test_clear_display() {
        let mut lcd = HD44780::new();
        lcd.set_data_bus(0x01);
        pulse_enable(&mut lcd);
        lcd.tick();
        assert!(lcd.ddram.iter().all(|&b| b == b' '));
    }

    #[test]
    #[ignore]
    fn test_write_data() {
        let mut lcd = HD44780::new();
        lcd.set_data_bus(0x41); // 'A'
        pulse_enable(&mut lcd);
        lcd.tick();
        assert_eq!(lcd.ddram[0], b'A');
    }

    #[test]
    fn test_set_cursor() {
        let mut lcd = HD44780::new();
        lcd.set_data_bus(0x80 | 0x40);
        pulse_enable(&mut lcd);
        lcd.tick();
        assert_eq!(lcd.cursor_position, 0x40);
    }

    #[test]
    fn test_read_busy_flag_and_address() {
        let mut lcd = HD44780::new();
        lcd.busy = true;
        lcd.cursor_position = 0x15;
        lcd.write_control_lines(false, true, true);
        let data = lcd.read_data_bus();
        assert_eq!(data, 0x80 | 0x15);
    }

    #[test]
    fn hello_world_display() {
        let mut lcd = HD44780::new();

        let setup_instructions = [
            0b0000_0000, // cls
            0b0011_1000, // display mode
            0b0000_1110, // display on
            0b0000_0110, // entry mode
        ];

        for instruction in setup_instructions {
            lcd.write_control_lines(false, false, true);
            lcd.set_data_bus(instruction);
            pulse_enable(&mut lcd);
            lcd.tick();
        }

        let letter_instructions = [
            0b0100_1000, // H
            0b0100_0101, // E
            0b0100_1100, // L
            0b0100_1100, // L
            0b0100_1111, // O
            0b0100_0000, // _
            0b0101_0111, // W
            0b0100_1111, // O
            0b0101_0010, // R
            0b0100_1100, // L
            0b0100_0100, // D
        ];

        // move cursor to start of next line.
        lcd.write_control_lines(true, false, true);
        lcd.set_data_bus(0b1100_0000);
        pulse_enable(&mut lcd);
        lcd.tick();

        let (line1, line2) = lcd.get_display_lines();
        println!("LINE1 -> [{}]", line1);
        println!("LINE2 -> [{}]", line2);
    }
}
