use core::fmt;

use crate::lcd_gui::LCDScreen;

//---Config struct----------------------------------------------------------------------------------

#[derive(Copy, Clone)]
pub enum FanOutput {
    P1,
    P2,
}

impl FanOutput {

    pub fn from_bits(bits: u16) -> FanOutput {
        if bits == 0 { return Self::P1 }
        return Self::P2;
    }

    pub fn to_bits(&self) -> u16 {
        match self {
            Self::P1 => 0,
            Self::P2 => 1,
        }
    }
}

#[derive(Copy, Clone)]
pub struct CalData {
    pub offset: f32,
    pub raw_value: Option<f32>,
}

impl CalData {
    pub fn new(offset: f32) -> CalData {
        CalData {
            offset,
            raw_value: None,
        }
    }
}

#[derive(Copy, Clone)]
pub struct SystemConfig {
    pub max_temp_diff: f32,
    pub min_temp_diff: f32,
    pub ext_offset: CalData,
    pub p1_offset: CalData,
    pub p2_offset: CalData,
    pub fan_output: FanOutput,
}

impl SystemConfig {

    pub fn new() -> SystemConfig {
        SystemConfig {
            max_temp_diff: 8.0,
            min_temp_diff: 4.0,
            ext_offset: CalData::new(0.0),
            p1_offset: CalData::new(0.0), 
            p2_offset: CalData::new(0.0), 
            fan_output: FanOutput::P1,
        }
    }
}

//---Menu struct------------------------------------------------------------------------------------
const MENU_ENTRY_NB: usize = 6;

pub enum MenuState {
    Scroll,
    Entry,
}

pub struct Menu {
    entries: [Entry; MENU_ENTRY_NB],
    state: MenuState,
    should_refresh: bool,
    entry_index: usize,
}

impl Menu {

    pub fn new(config: &SystemConfig) -> Menu {
        
        Menu {
            entries: [
                Entry::new("fan out", EntryValue::FanOut (FanOutput::P1), config),
                Entry::new("max diff", EntryValue::MaxTempDiff (0.0), config),
                Entry::new("min diff", EntryValue::MinTempDiff (0.0), config),
                Entry::new("ex", EntryValue::ExtOffset (CalData::new(0.0)), config),
                Entry::new("s1", EntryValue::P1Offset (CalData::new(0.0)), config),
                Entry::new("s2", EntryValue::P2Offset (CalData::new(0.0)), config),
            ],
            state: MenuState::Scroll,
            should_refresh: true,
            entry_index: 0,
        }
    }

    pub fn button_update(&mut self, config: &mut SystemConfig) -> bool {

        self.should_refresh = true;

        // update state machine
        match self.state {
            MenuState::Scroll => {
                if self.entry_index == 0 { return true }
                // read desired value from config
                self.state = MenuState::Entry;
                self.entries[self.entry_index - 1].select(config);
            },
            MenuState::Entry => {
                // write modified value to config
                self.state = MenuState::Scroll;
                self.entries[self.entry_index - 1].deselect(config);
            }
        }
        
        false
    }

    pub fn movement_update(&mut self, movement: i32) {
        
        if movement != 0 {
            match self.state {
                MenuState::Scroll => {

                    // convert movement to entry index
                    let mut index = self.entry_index as i32 + movement;
                    if index > MENU_ENTRY_NB as i32 {
                        index = MENU_ENTRY_NB as i32;
                    } else if index < 0 {
                        index = 0;
                    }
                    self.entry_index = index as usize;
                },
                MenuState::Entry => {

                    // update entry
                    self.entries[self.entry_index - 1].update(-movement);
                    //note : "-" sign is just to revert encoder direction, feels better when using
                    //the interface
                },
            }
            
            self.should_refresh = true;
        }
    }

    pub fn reset(&mut self) {

        match self.state {
            MenuState::Entry => {
                // if an entry is selected, disguard all modifications
                self.entries[self.entry_index - 1].disgard();
                self.state = MenuState::Scroll;
            },
            _ => (),
        }

        self.entry_index = 0;
    }
    
    pub fn display<L: LCDScreen>(&mut self, screen: &mut L) {

        if self.should_refresh {
            
            // first line
            screen.clear();
            screen.set_cursor_pos(0);
            let _ =  write!(screen, "\u{007e}");
            if self.entry_index == 0 {
                let _ = write!(screen, "retour");
            } else {
                self.entries[self.entry_index - 1].display(screen);
            }

            // second line (if any)
            if self.entry_index < MENU_ENTRY_NB {
                screen.set_cursor_pos(0x40);
                let _ = write!(screen, " ");
                self.entries[self.entry_index].display(screen);
            }

            self.should_refresh = false;
        }
    }
}

pub enum EntryValue {
    MaxTempDiff (f32),
    MinTempDiff (f32),
    ExtOffset (CalData),
    P1Offset (CalData),
    P2Offset (CalData),
    FanOut (FanOutput),
}

impl EntryValue {

    fn modify(&mut self, n: i32) {
        use libm::{powf, copysignf, fabsf};

        use crate::utils::TEMP_RANGE;

        match self {
            Self::MaxTempDiff (val) | Self::MinTempDiff (val) => { 
                *val += copysignf(fabsf(n as f32) * 0.85 * powf(10.0, 0.15 * fabsf(n as f32)) 
                                  * 0.1, n as f32);
                // avoid display issues by limiting value range
                if *val > 99.9 { *val = 99.9; } 
                else if *val < -99.9 { *val = -99.9; } 
            },
            Self::ExtOffset (data) | Self::P1Offset (data) | Self::P2Offset (data) => {
                data.offset += copysignf(fabsf(n as f32) * 0.85 
                                          * powf(10.0, 0.15 * fabsf(n as f32)) * 0.1, n as f32);
                // avoid limiting issues like before, taking probe bounds into account
                if data.offset + TEMP_RANGE.end > 99.9 { 
                    data.offset = 99.9 - TEMP_RANGE.end;
                }
                else if data.offset + TEMP_RANGE.start < -99.9 {
                    data.offset = -99.9 - TEMP_RANGE.start;
                }
            },
            Self::FanOut (output) => {
                if n%2 != 0 {
                    match output {
                        FanOutput::P1 => *output = FanOutput::P2,
                        FanOutput::P2 => *output = FanOutput::P1,
        }}}}
    }

    fn load(&mut self, config: &SystemConfig) {
        match self {
            Self::MaxTempDiff (val) => *val = config.max_temp_diff,
            Self::MinTempDiff (val) => *val = config.min_temp_diff,
            Self::ExtOffset (data) => *data = config.ext_offset,
            Self::P1Offset (data) => *data = config.p1_offset,
            Self::P2Offset (data) => *data = config.p2_offset,
            Self::FanOut (val) => *val = config.fan_output,
        };
    }

    fn store(&mut self, config: &mut SystemConfig) {
        match self {
            Self::MaxTempDiff (val) => config.max_temp_diff = *val,
            Self::MinTempDiff (val) => config.min_temp_diff = *val,
            Self::ExtOffset (data) => config.ext_offset = *data,
            Self::P1Offset (data) => config.p1_offset = *data,
            Self::P2Offset (data) => config.p2_offset = *data,
            Self::FanOut (val) => config.fan_output = *val,
        };
    }
}

impl fmt::Display for EntryValue {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        use crate::lcd_gui::Temp;

        match self {
            Self::MaxTempDiff (val) | Self::MinTempDiff (val) => 
                formatter.write_fmt(format_args!("{:.1}", val)),
            Self::ExtOffset (data) | Self::P1Offset (data) | Self::P2Offset (data) => {
                let temp: Temp = data.raw_value.map(|val| val + data.offset).into();
                formatter.write_fmt(format_args!("{:.1}({:.1})", data.offset, temp))
            },
            Self::FanOut (output) => {
                match output {
                    FanOutput::P1 => formatter.write_fmt(format_args!("1")),
                    FanOutput::P2 => formatter.write_fmt(format_args!("2")),
        }}}
    }
}


pub struct Entry {
    text: &'static str,
    value: EntryValue,
    is_selected: bool,
}

impl Entry {

    pub fn new(text: &'static str, mut value: EntryValue, config: &SystemConfig) -> Entry {

        value.load(config);
        Entry {
            text,
            value,
            is_selected: false,
        }
    }

    pub fn display<B: core::fmt::Write>(&self, buffer: &mut B) {
        let _ = match self.is_selected {
            true => write!(buffer, "{}:{}", self.text, self.value),
            false => write!(buffer, "{} {}", self.text, self.value),
        };
    }

    pub fn select(&mut self, config: &SystemConfig) { 
        self.is_selected = true; 
        self.value.load(config);
    }
    pub fn deselect(&mut self, config: &mut SystemConfig) { 
        self.is_selected = false;
        self.value.store(config);
    }

    pub fn disgard(&mut self) {
        self.is_selected = false;
    }

    pub fn update(&mut self, n: i32) {
        self.value.modify(n);
    }
}
