use core::{
    sync::atomic::{AtomicBool, Ordering},
    fmt,
};

use embedded_hal::{
    //digital::v1_compat::OldOutputPin,
    digital::v2::OutputPin,
    Qei,
    blocking::delay::{DelayUs, DelayMs},
};

use hd44780_driver::{
    HD44780,
    display_mode::DisplayMode,
    bus::DataBus,
};

use crate::{
    state::SystemState,
    config::{Menu, SystemConfig, FanOutput},
};

//--------------------------------------------------------------------------------------------------
/* GUI config */

pub const GUI_TICK_SEC: f32 = 0.2;
const GUI_ERROR_SEC: f32 = 0.8;

//---Temp Enum--------------------------------------------------------------------------------------
/// A simple enum to handle displaying temps on the GUI. Temperatures can be valid or not. An
/// invalid temperature is displayed as "inval°C" whereas a valid temperature is displayed as 
/// XX.XX°C
pub enum Temp {
    Valid (f32),
    Invalid,
}

impl fmt::Display for Temp {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Temp::Valid (deg) => formatter.write_fmt(format_args!("{:.1}", deg)),
            Temp::Invalid => formatter.write_str("inval"),
        }
    }
}

impl core::convert::From<Option<f32>> for Temp {
    fn from(val: Option<f32>) -> Self {
        match val {
            Some(deg) => Temp::Valid(deg),
            None => Temp::Invalid,
        }
    }
}

impl core::convert::From<f32> for Temp {
    fn from(val: f32) -> Self {
        Temp::Valid(val)
    }
}

impl core::cmp::PartialEq<Temp> for Temp {
    fn eq(&self, other: &Self) -> bool {
        match self {
            Self::Valid (val) => match other {
                Self::Invalid => false,
                Self::Valid (other_val) => val == other_val,
            },
            Self::Invalid => match other {
                Self::Invalid => true,
                _ => false,
            },
        }
    }
}

//---LCDGui Struct----------------------------------------------------------------------------------

pub enum GUIState {
    Off,
    Idle,
    Menu,
    Error (&'static str),
}

/// Manages the lcd screen and inputs (encoder + button) and display relevant information. Can also
/// be used to configure the system. The update() function should be called frequently to refresh
/// the GUI (every 0.2s is enough). 
pub struct LCDGui<L, Q, B> 
where
    L: LCDScreen,
    Q: Qei<Count = u16>,
    B: OutputPin,
{
    lcd: L,
    qei: Q,
    button: &'static AtomicBool,
    backlight: Option<B>, 
    count: i32,
    state: GUIState,
    menu: Menu,
    ext_deg: Temp,
    p1_deg: Temp,
    p2_deg: Temp,
    should_refresh: bool,
    blink_counter: i32,
}

impl<L, Q, B> LCDGui<L, Q, B>
where
    L: LCDScreen,
    Q: Qei<Count = u16>,
    B: OutputPin,
{

    /// Create and configure a new GUI. By default the screen will display the idle screen.
    pub fn new(lcd: L, qei: Q, button: &'static AtomicBool, backlight: Option<B>, 
               config: &SystemConfig) 
        -> LCDGui<L, Q, B> {

        use hd44780_driver::{Cursor, CursorBlink, Display};

        let count = qei.count() as i32;
        let mut gui = LCDGui {
            lcd, 
            qei, 
            button, 
            backlight, 
            count, 
            state: GUIState::Idle,
            menu: Menu::new(config),
            ext_deg: Temp::Invalid,
            p1_deg: Temp::Invalid,
            p2_deg: Temp::Invalid,
            should_refresh: true,
            blink_counter: 0,
        };

        if let Some(bl) = &mut gui.backlight { let _ = bl.set_high(); };

        gui.lcd.reset();
        gui.lcd.clear();
        gui.lcd.set_display_mode(
            DisplayMode {
                display: Display::On,
                cursor_visibility: Cursor::Invisible,
                cursor_blink: CursorBlink::Off,
        });

        gui
    }

    /// Perform the necessary operations after an action on the button or the encoder. This
    /// function should be called frequently (every 0.2s is enough) to keep track of the state of
    /// the encoder. For added reactivness, it can also be called on a button press. The function
    /// can modifify the system's config through the system state and will take care of updating
    /// the latter. Return true when something has been updated during the function call, usefull
    /// for implementing powersaving features.
    pub fn update<O: OutputPin>(&mut self, state: &mut SystemState<O>) -> bool {

        let mut input_update = false;

        // manage button press
        if self.button.swap(false, Ordering::AcqRel) {
            input_update = true;

            // update state machine
            match self.state {
                GUIState::Off => {
                    self.state = GUIState::Idle;
                    self.lcd.clear();
                    if let Some(bl) = &mut self.backlight { let _ = bl.set_high(); };
                    self.should_refresh = true;
                },
                GUIState::Idle => {
                    self.state = GUIState::Menu;
                    self.menu.display(&mut self.lcd);
                },
                GUIState::Menu => {

                    //TODO improve that
                    let mut config: SystemConfig = *state.config();
                    if self.menu.button_update(&mut config) {
                        self.state = GUIState::Idle;
                        self.update_temps(state);
                    } else {
                        state.update_config(config);
                        self.menu.display(&mut self.lcd);
                    }
                },
                GUIState::Error (_) => {
                    self.state = GUIState::Idle;
                    self.lcd.clear();
                    self.should_refresh = true;
        }}}

        // manage encoder movement
        let count = self.qei.count() as i32 / 2;
        let diff1 = count - self.count;
        let diff2 = 511 - i32::abs(diff1);
        let diff = if i32::abs(diff1) < diff2 { 
            diff1
        } else { 
            diff2 * -i32::signum(diff1) 
        };

        if diff != 0 {
            input_update = true;
            self.count = count;
        }

        // display relevant screen
        match self.state {
            GUIState::Off => {},
            GUIState::Idle => {
                if self.should_refresh {
                    display_idle_screen(&mut self.lcd, &self.ext_deg, &self.p1_deg, &self.p2_deg,
                                        &state.config().fan_output); 
                    self.should_refresh = false;
                }
            },
            GUIState::Menu => {
                if diff != 0 {
                    self.menu.movement_update(diff);
                    self.menu.display(&mut self.lcd);
                }
            },
            GUIState::Error (err)=> {
                self.blink_counter += 1;

                if self.blink_counter as f32 * GUI_TICK_SEC >= 2.0*GUI_ERROR_SEC {
                    let _ = write!(self.lcd, "Erreur: ");
                    self.lcd.set_cursor_pos(0x40);
                    let _ = write!(self.lcd, "{}", err);
                    self.blink_counter = 0;
                } else if self.blink_counter as f32 * GUI_TICK_SEC >= GUI_ERROR_SEC {
                    self.lcd.clear();
                }
        }}

        input_update
    }

    /// Update the temperatures to display on the idle screen. Should be called whenever the
    /// temperatures are modified.
    pub fn update_temps<O: OutputPin>(&mut self, state: &SystemState<O>) {

        self.ext_deg = state.ext_temp().into();
        self.p1_deg = state.p1_temp().into();
        self.p2_deg = state.p2_temp().into();

        self.should_refresh = true;
    }

    pub fn sleep(&mut self) {
        
        self.state = GUIState::Off;

        // reset menu in case the user was in-menu
        self.menu.reset();

        // manage lcd
        self.lcd.clear();
        //TODO set display off ?
        if let Some(bl) = &mut self.backlight { let _ = bl.set_low(); };
    }

    pub fn display_error(&mut self, error: &'static str) {
        self.state = GUIState::Error (error);
        self.blink_counter = 0;
    }
}

fn display_idle_screen<L: LCDScreen>(screen: &mut L, ext_deg: &Temp, p1_deg: &Temp, p2_deg: &Temp,
                                     fan_output: &FanOutput) {

    // display temperatures
    screen.clear();
    screen.set_cursor_pos(0);
    let _ = write!(screen, "Ext:{}", ext_deg);
    screen.set_cursor_pos(0x41);
    let _ = write!(screen, "1:{}", p1_deg);
    screen.set_cursor_pos(0x49);
    let _ = write!(screen, "2:{}", p2_deg);

    // display probe selection
    match fan_output {
        FanOutput::P1 => screen.set_cursor_pos(0x40),
        FanOutput::P2 => screen.set_cursor_pos(0x48),
    }
    let _ = write!(screen, "*");
}

//---LCDScreen trait--------------------------------------------------------------------------------
/// A wrapper trait to complement the write trait by adding the extra functions provided by a lcd
/// screen, such as cursor position.
pub trait LCDScreen: core::fmt::Write {

    fn reset(&mut self);
    fn clear(&mut self);
    fn set_display_mode(&mut self, mode: DisplayMode);
    fn set_cursor_pos(&mut self, positions: u8);
}

impl<D, DB> LCDScreen for HD44780<D, DB> 
where
    D: DelayUs<u16> + DelayMs<u8>,
    DB: DataBus,
{

    fn reset(&mut self) {
        self.reset()
    }

    fn clear(&mut self) {
        self.clear()
    }

    fn set_display_mode(&mut self, mode: DisplayMode) {
        self.set_display_mode(mode)
    }

    fn set_cursor_pos(&mut self, position: u8) {
        self.set_cursor_pos(position);
    }
}

