//! Driver for LiquidCrystal Displays plugs to the GPIO of the board.
//! The LCD need to be an Hitachi HD44780 chipset compatible.
//!
//! This driver inspired from the one from the arduino libriaries
//! (https://github.com/arduino-libraries/LiquidCrystal) and the
//! documentation from wikipedia (https://en.wikipedia.org/wiki/Hitachi_HD44780_LCD_controller)
//! and the actual doc (https://www.sparkfun.com/datasheets/LCD/HD44780.pdf)

use crate::bsp::aarch64::time::time_manager;
use crate::gpio;
use crate::traits::time::TimeManager;
use core::time::Duration;

/// The font size
pub enum FontSize {
    Font5x8Dots,
    Font5x10Dots,
}

impl FontSize {
    fn function_code(&self) -> u8 {
        match self {
            Self::Font5x8Dots => Lcd::FUNCTION_SET_5X8_DOTS,
            Self::Font5x10Dots => Lcd::FUNCTION_SET_5X10_DOTS,
        }
    }
}

/// The structure handling the LCD periferic
pub struct Lcd {
    /// The Register Select pinout.
    rs_pin: usize,
    /// The Read/Write pinout.
    ///
    /// If set to None, assumes the rw pin of the lcd is connected to the ground (write state)
    rw_pin: Option<usize>,
    /// The Enable pinout.
    ///
    /// TODO: wiki says falling-edge triggered, arduino comment says activated by a HIGH pulse.
    e_pin: usize,
    /// First 4 data bits pins.
    ///
    /// Set to None if the lcd is in 4-bit mode.
    first_data_bits_pins: Option<[usize; 4]>,
    /// Last 4 data bits pins.
    last_data_bits_pins: [usize; 4],
    /// The number of line of the LCD.
    nb_lines: usize,
    /// The number of row of the LCD.
    nb_rows: usize,
    /// Weither or not the LCD has been initialized in the rigth state.
    initialized: bool,
}

impl Lcd {

    /// The instruction to turn off the display
    const INSTRUCTION_DISPLAY_OFF: u8 = 0b0000_1000;
    /// The instruction to clear the display
    const INSTRUCTION_DISPLAY_CLEAR: u8 = 0b0000_0001;

    /// The bit indicating a function set instruction
    const FUNCTION_SET_INDICATOR: u8 = 0b0010_0000;
    /// The bit indicating the use of only one line
    const FUNCTION_SET_ONE_LINE_INDICATOR: u8 = 0b0000_0000;
    /// The bit indicating the use of only two lines
    const FUNCTION_SET_TWO_LINES_INDICATOR: u8 = 0b0000_1000;
    /// The bit indicating the use of the 5x8 font
    const FUNCTION_SET_5X8_DOTS: u8 = 0b0000_0000;
    /// The bit indicating the use of the 5x10 font
    const FUNCTION_SET_5X10_DOTS: u8 = 0b0000_0100;

    /// Instanciate a [`Lcd`] object in 4 bits mode.
    pub const fn new_4bits_mode(
        rs_pin: usize,
        rw_pin: Option<usize>,
        e_pin: usize,
        db4: usize,
        db5: usize,
        db6: usize,
        db7: usize,
        nb_lines: usize,
        nb_rows: usize,
    ) -> Self {
        Self {
            rs_pin,
            rw_pin,
            e_pin,
            first_data_bits_pins: None,
            last_data_bits_pins: [db4, db5, db6, db7],
            nb_lines,
            nb_rows,
            initialized: false,
        }
    }

    /// Instanciate a [`Lcd`] object in 8 bits mode.
    pub const fn new_8bits_mode(
        rs_pin: usize,
        rw_pin: Option<usize>,
        e_pin: usize,
        db0: usize,
        db1: usize,
        db2: usize,
        db3: usize,
        db4: usize,
        db5: usize,
        db6: usize,
        db7: usize,
        nb_lines: usize,
        nb_rows: usize,
    ) -> Self {
        Self {
            rs_pin,
            rw_pin,
            e_pin,
            first_data_bits_pins: Some([db0, db1, db2, db3]),
            last_data_bits_pins: [db4, db5, db6, db7],
            nb_lines,
            nb_rows,
            initialized: false,
        }
    }

    /// Initialize the LCD.
    pub fn init(&mut self, font_size: FontSize) -> Result<(), &'static str> {
        gpio::set_pin_fonction(self.rs_pin, gpio::PinFunction::Output)?;
        gpio::set_pin_output_state(self.rs_pin, gpio::PinOutputState::Low)?;
        if let Some(rw_pin) = self.rw_pin {
            gpio::set_pin_fonction(rw_pin, gpio::PinFunction::Output)?;
            gpio::set_pin_output_state(rw_pin, gpio::PinOutputState::Low)?;
        }
        gpio::set_pin_fonction(self.e_pin, gpio::PinFunction::Output)?;
        gpio::set_pin_output_state(self.e_pin, gpio::PinOutputState::Low)?;
        if let Some(first_data_bits_pins) = self.first_data_bits_pins {
            for pin in first_data_bits_pins.into_iter() {
                gpio::set_pin_fonction(pin, gpio::PinFunction::Output)?;
                gpio::set_pin_output_state(pin, gpio::PinOutputState::Low)?;
            }
        }
        for pin in self.last_data_bits_pins.into_iter() {
            gpio::set_pin_fonction(pin, gpio::PinFunction::Output)?;
            gpio::set_pin_output_state(pin, gpio::PinOutputState::Low)?;
        }

        let db4 = self.last_data_bits_pins[0];
        let db5 = self.last_data_bits_pins[1];

        // The initialization process as described in figure 23/24 of the doc
        // First, wait 15 ms after poweron. To be sure, wait 30 ms
        time_manager().sleep(Duration::from_millis(30));

        // Currently the state of the lcd is undefined. We must make sur
        // to be in 8bits mode by sendy 0b0011_0000 3 times.
        gpio::set_pin_output_state(db4, gpio::PinOutputState::High)?;
        gpio::set_pin_output_state(db5, gpio::PinOutputState::High)?;
        self.pulse()?;
        time_manager().sleep(Duration::from_micros(4100));
        self.pulse()?;
        time_manager().sleep(Duration::from_micros(100));
        self.pulse()?;
        time_manager().sleep(Duration::from_micros(100));
        gpio::set_pin_output_state(db4, gpio::PinOutputState::Low)?;
        gpio::set_pin_output_state(db5, gpio::PinOutputState::Low)?;
        
        // Now we are in 8bits mode, if we only have 4 data pins, we
        // needs to set the mode to 4 bits.
        if self.first_data_bits_pins.is_none() {
            gpio::set_pin_output_state(db5, gpio::PinOutputState::High)?;
            self.pulse()?;
        }

        let mut function = Self::FUNCTION_SET_INDICATOR;
        if self.first_data_bits_pins.is_some() {
            function |= 0b0001_0000;
        }

        if self.nb_lines == 1 {
            function |= Self::FUNCTION_SET_ONE_LINE_INDICATOR;
        } else if self.nb_lines == 2 {
            function |= Self::FUNCTION_SET_TWO_LINES_INDICATOR;
        } else {
            return Err("Only 1 and 2 lines modes are supported");
        }

        function |= font_size.function_code();
        self.send_data(function)?;

        self.initialized = true;

        self.display_off()?;
        self.display_clear()?;
        // TODO: self.set_entry_mode(stuff)?;

        todo!();
    }

    /// Turn the display off.
    pub fn display_off(&self) -> Result<(), &'static str> {
        if self.initialized {
            self.send_data(Self::INSTRUCTION_DISPLAY_OFF)
        } else {
            Err("Cannot control the LCD without initializing it first")
        }
    }

    /// Clear the display
    pub fn display_clear(&self) -> Result<(), &'static str> {
        if self.initialized {
            self.send_data(Self::INSTRUCTION_DISPLAY_CLEAR)
        } else {
            Err("Cannot control the LCD without initializing it first")
        }
    }

    /// Tell the LCD that the data is ready to be read.
    fn pulse(&self) -> Result<(), &'static str> {
        gpio::set_pin_output_state(self.e_pin, gpio::PinOutputState::Low)?;
        time_manager().sleep(Duration::from_nanos(500));
        gpio::set_pin_output_state(self.e_pin, gpio::PinOutputState::High)?;
        time_manager().sleep(Duration::from_nanos(500));
        gpio::set_pin_output_state(self.e_pin, gpio::PinOutputState::Low)?;
        time_manager().sleep(Duration::from_micros(50));
        Ok(())
    }

    /// Send data to the lcd.
    fn send_data(&self, data: u8) -> Result<(), &'static str> {
        if let Some(first_data_bits_pins) = self.first_data_bits_pins {
            for (i, pin) in first_data_bits_pins.iter().enumerate() {
                let value = Self::get_bit_value(data, i);
                gpio::set_pin_output_state(*pin, value)?;
            }
            for (i, pin) in self.last_data_bits_pins.iter().enumerate() {
                let value = Self::get_bit_value(data, i+4);
                gpio::set_pin_output_state(*pin, value)?;
            }
            self.pulse()
        } else {
            for (i, pin) in self.last_data_bits_pins.iter().enumerate() {
                let value = Self::get_bit_value(data, i+4);
                gpio::set_pin_output_state(*pin, value)?;
            }
            self.pulse()?;
            for (i, pin) in self.last_data_bits_pins.iter().enumerate() {
                let value = Self::get_bit_value(data, i+4);
                gpio::set_pin_output_state(*pin, value)?;
            }
            self.pulse()
        }
    }

    /// Return the value of the pin correponding to the bit `i` of `data`.
    #[inline]
    fn get_bit_value(data: u8, i: usize) -> gpio::PinOutputState {
        if (data & (1 << i)) != 0 {
            gpio::PinOutputState::High
        } else {
            gpio::PinOutputState::Low
        }
    }
}
