mod readmap_helper;
use crossterm::event::{KeyCode, KeyEvent};
use readmap::types::{MemoryMap, NamedMemoryArea, Symbol};
use std::{convert::From, path::PathBuf};
use tool_helper::Error;
use ratatui::{
    layout::{Alignment, Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, BorderType, Gauge, List, ListItem, ListState, Paragraph, Tabs, block::Title}
};

pub use readmap_helper::get_tool_output;

pub type EventReceiver = std::sync::mpsc::Receiver<Event<crossterm::event::KeyEvent>>;
pub type Terminal      = ratatui::Terminal<ratatui::backend::CrosstermBackend<std::io::Stdout>>;
type ConsoleFrame<'a>  = ratatui::Frame<'a>;

pub enum Event<I> {
    Input(I),
    Tick,
    ForceRender,
}

pub enum UIState {
    Alive,
    Render,
    Terminated
}

#[derive(Copy, Clone)]
enum MenuSelection {
    Stats,
    List,
    Quit,
}

impl MenuSelection {
    const ENUM_LENGTH:i32 = 3;

    fn next(&self) -> MenuSelection {
        Self::from(*self as i32 + 1)
    }

    fn prev(&self) -> MenuSelection {
        Self::from(*self as i32 - 1)        
    }

    fn get_id(&self) -> usize {
        *self as usize
    }
}

impl From<i32> for MenuSelection {
    fn from(value: i32) -> Self {
        if value < 0 {
            return Self::from(value + Self::ENUM_LENGTH);
        }
        
        match value%Self::ENUM_LENGTH {
            x if x == MenuSelection::Stats as i32 => MenuSelection::Stats,
            x if x == MenuSelection::List  as i32 => MenuSelection::List,
            x if x == MenuSelection::Quit  as i32 => MenuSelection::Quit,
            _                                     => MenuSelection::Quit
        }
    }
}

#[derive(Copy, Clone)]
enum SectionMode {
    UpperSection,
    LowerSection,
}

impl ID for SectionMode {
    fn get_id(&self) -> usize {
        *self as usize
    }
}

impl Default for SectionMode {
    fn default() -> Self {
        SectionMode::UpperSection
    }
}

#[derive(Copy, Clone)]
enum ListSectionMode {
    TopSection,
    Section,
}

impl ID for ListSectionMode {
    fn get_id(&self) -> usize {
        *self as usize
    }
}

impl Default for ListSectionMode {
    fn default() -> Self {
        ListSectionMode::TopSection
    }
}

struct DualSelection<T:ID> {
    mode:     T,
    idx:      [i64; 2]
}

impl<T:ID + Copy> DualSelection<T> {
    fn increment(&mut self) {
        self.idx[self.mode.get_id()] += 1;
    }

    fn decrement(&mut self) {
        self.idx[self.mode.get_id()] -= 1;
    }

    fn normalize_with(&mut self, max: i64) {
        if max == 0 {
            self.idx[self.mode.get_id()] = 0;
        }

        else {
            let mut cur_idx = self.idx[self.mode.get_id()];

            while cur_idx < 0 {
                cur_idx += max;
            }
            
            if cur_idx >= max {
                cur_idx %= max;
            }

            self.idx[self.mode.get_id()] = cur_idx;
        }
    }

    fn switch_mode_to(&mut self, section_mode: T) {
        self.mode = section_mode;
    }

    fn reset_mode_to(&mut self, section_mode: T) {
        let cur_id = &mut self.idx[self.mode.get_id()%2];

        *cur_id = 0;
        self.switch_mode_to(section_mode)
    }

    fn get_section_mode_id(&self) -> usize {
        self.mode.get_id()
    }

    fn get_selection_for(&self, section_mode: T) -> usize {
        self.idx[section_mode.get_id()] as usize
    }

    fn get_selection_state_for(&self, section_mode: T) -> ListState {
        let mut state = ListState::default();

        state.select(Some(self.get_selection_for(section_mode)));
        state
    }

    fn get_current_selection_state(&self) -> ListState {
        self.get_selection_state_for(self.mode)
    }
}

impl<T:ID + Default> Default for DualSelection<T> {
    fn default() -> Self {
        Self{mode: T::default(), idx: [0, 0]}
    }
}

trait ID {
    fn get_id(&self) -> usize;
}

pub struct ConsoleUI {
    data:                   ConsoleUIData,
    rx:                     EventReceiver,
    terminal:               Terminal,
    console_size:           Rect,
    menu_selection:         MenuSelection,
    section_selection:      DualSelection<SectionMode>,
    list_section_selection: DualSelection<ListSectionMode>,
}

impl ConsoleUI {
    pub fn new(rx: EventReceiver, terminal: Terminal) -> ConsoleUI {
        ConsoleUI{data: ConsoleUIData::default(), rx, terminal, console_size: Rect::default(), menu_selection: MenuSelection::Stats, section_selection: DualSelection::default(), list_section_selection: DualSelection::default()}
    }

    pub fn update_data(&mut self, memory_map: MemoryMap) -> Result<(), Error> {
        self.data                   = ConsoleUIData::from(memory_map)?;
        self.section_selection      = DualSelection::default();
        self.list_section_selection = DualSelection::default();

        Ok(())
    }

    pub fn update(&mut self) -> Result<UIState, Error> {
        let input_result = {
            match self.rx.recv()? {
                Event::Input(event) => {
                    let state_update = {
                        match self.menu_selection {
                            MenuSelection::Stats => self.update_stats(event),
                            MenuSelection::List  => self.update_list(event),
                            MenuSelection::Quit  => self.update_quit(event),
                        }
                    };

                    if let Some(result) = state_update {
                        Ok(result)
                    }

                    else {
                        match event.code {
                            KeyCode::Char('l') => {
                                self.menu_selection = MenuSelection::List;
                                Ok(UIState::Render)
                            },
                            KeyCode::Char('s') => {
                                self.menu_selection = MenuSelection::Stats;
                                Ok(UIState::Render)
                            },
                            KeyCode::Char('q') => Ok(UIState::Terminated),
                            KeyCode::Left => {
                                self.menu_selection = self.menu_selection.prev();
                                Ok(UIState::Render)
                            },
                            KeyCode::Right => {
                                self.menu_selection = self.menu_selection.next();
                                Ok(UIState::Render)
                            },
                            _ => Ok(UIState::Alive)
                        }
                    }
                },
                Event::Tick => {
                    let terminal_size = self.terminal.size().expect("Getting size of terminal");
                    if terminal_size != self.console_size {
                        self.console_size = terminal_size;
                        Ok(UIState::Render)
                    } 

                    else {
                        Ok(UIState::Alive)
                    }
                },
                Event::ForceRender => Ok(UIState::Render)
            }
        };

        input_result
    }

    pub fn render(&mut self) -> Result<(), Error> {
        self.terminal.draw(|frame| {
            // Create layout elements
            let main_layout = Layout::default().direction(Direction::Vertical).constraints([
                Constraint::Length(3),
                Constraint::Min(3)
            ]).split(frame.size()).to_vec();

            // Elements
            let menu_bar = {
                const MENU_TITLES: &'static [&'static str] = &["Stats", "List", "Quit"];

                let menu = MENU_TITLES.iter().map(|t| {
                    let (first, rest) = t.split_at(1);
                    Line::from(vec![
                        Span::styled(first, Style::default().fg(Color::Yellow).add_modifier(Modifier::UNDERLINED),),
                        Span::styled(rest,  Style::default().fg(Color::White)),
                    ])
                }).collect();
                
                Tabs::new(menu).select(self.menu_selection.get_id()).block(
                    Block::default()
                    .title("psxreadmap - Menu").borders(Borders::ALL))
                    .style(Style::default().fg(Color::White))
                    .highlight_style(Style::default().bg(Color::LightYellow))
                    .divider(Span::raw("|")
                )
            };

            // Render screen
            frame.render_widget(menu_bar, main_layout[0]);
            match self.menu_selection {
                MenuSelection::Stats => {
                    Self::render_stats(frame, main_layout[1], &self.section_selection, &self.data);
                },
                MenuSelection::List => {
                    Self::render_list(frame, main_layout[1], &self.list_section_selection, &self.data);
                },
                MenuSelection::Quit  => {
                    Self::render_quit(frame, main_layout[1]);
                }
            }
        })?;
        Ok(())
    }

    pub fn close(mut self) -> Result<(), Error> {
        crossterm::terminal::disable_raw_mode()?;
        self.terminal.show_cursor()?;
        self.terminal.clear()?;

        Ok(())
    }

    fn update_stats(&mut self, key: KeyEvent) -> Option<UIState> {
        let result = {
            match key.code {
                KeyCode::Down => {
                    self.section_selection.increment();
                    Some(UIState::Render)
                },
                KeyCode::Up => {
                    self.section_selection.decrement();
                    Some(UIState::Render)
                },
                KeyCode::Char('a') => {
                    self.section_selection.switch_mode_to(SectionMode::UpperSection);
                    Some(UIState::Render)
                },
                KeyCode::Char('s') => {
                    self.section_selection.switch_mode_to(SectionMode::LowerSection);
                    Some(UIState::Render)
                },
                _ => None
            }
        };

        self.section_selection.normalize_with(self.data.section_info.len() as i64);
        result
    }

    fn update_list(&mut self, key: KeyEvent) -> Option<UIState> {
        let result = {
            match key.code {
                KeyCode::Down => {
                    self.list_section_selection.increment();
                    Some(UIState::Render)
                },
                KeyCode::Up => {
                    self.list_section_selection.decrement();
                    Some(UIState::Render)
                },
                KeyCode::Enter => {
                    self.list_section_selection.switch_mode_to(ListSectionMode::Section);
                    Some(UIState::Render)
                }
                KeyCode::Esc => {
                    self.list_section_selection.reset_mode_to(ListSectionMode::TopSection);
                    Some(UIState::Render)
                }
                _ => None            
            }
        };

        let length = {
            match self.list_section_selection.mode {
                ListSectionMode::TopSection => self.data.section_info.len() as i64,
                ListSectionMode::Section    => Self::get_selected_top_section(&self.list_section_selection, &self.data).symbols.len() as i64
            }
        };

        self.list_section_selection.normalize_with(length);
        result
    }
    
    fn update_quit(&mut self, key: KeyEvent) -> Option<UIState> {
        match key.code {
            KeyCode::Enter => Some(UIState::Terminated),
            _              => None,
        }
    }

    fn render_stats(frame: &mut ConsoleFrame, layout: Rect, section_selection: &DualSelection<SectionMode>, data: &ConsoleUIData) {
        const HIGHLIGHT_COLOR:[Color;2] = [Color::Cyan, Color::Red];
        let layout = Layout::default().direction(Direction::Vertical).constraints([
            Constraint::Length(3),
            Constraint::Max(3),
            Constraint::Max(3),
        ]).split(layout).to_vec();
        let content_list = List::new(data.section_info.iter().map(|section| {
                ListItem::new(section.name.clone())
            }).collect::<Vec<_>>()).style(Style::default().fg(Color::White))
        .block(Self::create_default_border(""))
        .highlight_style(Style::default().bg(HIGHLIGHT_COLOR[section_selection.get_section_mode_id()]));

        let first_mem_gauge  = Self::create_mem_gauge_from(&data.section_info[section_selection.get_selection_for(SectionMode::UpperSection)], "a", HIGHLIGHT_COLOR[0]);
        let second_mem_gauge = Self::create_mem_gauge_from(&data.section_info[section_selection.get_selection_for(SectionMode::LowerSection)], "s", HIGHLIGHT_COLOR[1]);
                
        frame.render_stateful_widget(content_list, layout[0], &mut section_selection.get_current_selection_state());
        frame.render_widget(first_mem_gauge, layout[1]);
        frame.render_widget(second_mem_gauge, layout[2]);
    }

    fn render_list(frame: &mut ConsoleFrame, layout: Rect, section_selection: &DualSelection<ListSectionMode>, data: &ConsoleUIData) {
        let (info_layout, list_layout) = {
            let layout = Layout::default().direction(Direction::Vertical).constraints([
                Constraint::Min(5),
                Constraint::Max(5),
            ]).split(layout);

            (layout[1], Layout::default().direction(Direction::Horizontal).constraints([
                Constraint::Percentage(50),
                Constraint::Percentage(50)
            ]).split(layout[0]))
        };

        let section_list = List::new(data.section_info.iter().map(
            |section| {
                ListItem::new(Line::from(vec![{
                        if let Some(color) = section.color_share {
                            Span::styled("*", Style::default().fg(color))
                        }
                        else {
                            Span::raw("")
                        }
                    },
                    Span::raw(
                        if section.is_global {
                            String::from("<GLOBAL>")
                        }

                        else {
                            section.name.clone()
                        }
                    )
                ]))
            }).collect::<Vec<_>>()).style(Style::default().fg(Color::White))
        .block(Self::create_default_border("Section"))
        .highlight_style(Style::default().add_modifier(Modifier::REVERSED|Modifier::BOLD));

        let     top_section                     = Self::get_selected_top_section(section_selection, data);
        let mut info_data: &dyn NamedMemoryArea = top_section;

        frame.render_stateful_widget(section_list, list_layout[0], &mut section_selection.get_selection_state_for(ListSectionMode::TopSection));
        
        if matches!(section_selection.mode, ListSectionMode::TopSection) {
            let text = Paragraph::new("Press \"ENTER\" to display")
                .style(Style::default().fg(Color::White))
                .alignment(Alignment::Center)
                .block(Self::create_default_border("Symbols"));

            frame.render_widget(text, list_layout[1]);
        }

        else {
            let symbol_list = List::new(top_section.symbols.iter().map(
                |symbol| {
                    ListItem::new(symbol.name.clone())
                }).collect::<Vec<_>>()).style(Style::default().fg(Color::White))
            .block(Self::create_default_border(format!("Symbols in {}", top_section.name)))
            .highlight_style(Style::default().bg(Color::White));

            let current_selection = section_selection.get_selection_for(ListSectionMode::Section);
            if top_section.symbols.len() > 0 {
                info_data = &top_section.symbols[current_selection];
            }

            frame.render_stateful_widget(symbol_list, list_layout[1], &mut section_selection.get_selection_state_for(ListSectionMode::Section));
        }

        let info_text = Paragraph::new(format!("Name: {}\nAdr:  0x{:X} - 0x{:X}\nSize: {} Bytes", info_data.get_name(), info_data.get_adr(), info_data.get_end_adr(), info_data.get_size()))
        .style(Style::default().fg(Color::White))
        .alignment(Alignment::Left)
        .block(Self::create_default_border("Information"));
        frame.render_widget(info_text, info_layout);
    }

    fn render_quit(frame: &mut ConsoleFrame, layout: Rect) {
        let text = Paragraph::new("Press \"ENTER\" to exit")
            .style(Style::default().fg(Color::White))
            .alignment(Alignment::Center)
            .block(Self::create_default_border("QUIT"));

        frame.render_widget(text, layout);
    }

    fn create_default_border<'a, T: Into<Title<'a>>>(title: T) -> Block<'a> {
        Block::default()
            .borders(Borders::ALL)
            .style(Style::default().fg(Color::White))
            .border_type(BorderType::Plain)
            .title(title)
    }

    fn create_mem_gauge_from<'a>(section_info: &SectionInfo, string: &'a str, color: Color) -> Gauge<'a> {
        const PERCISION:usize         = 5;
        const FLOAT_DISPLAY_LIMIT:f64 = {
            let mut base = 1.0f64;
            let mut i    = 0;

            while i < PERCISION {
                base = base/10.0f64;
                i += 1;

            }
            base
        };

        let adr_ratio = section_info.get_size_ratio();
        Gauge::default().block(Block::default().borders(Borders::ALL)
            .title(Line::from(vec![
                Span::styled(string, Style::default().fg(color)),
                Span::raw(format!(": {} @0x{:x} - 0x{:x} [{}b/{}b]", 
                section_info.name, section_info.start_adr, section_info.get_end_adr(), section_info.size, section_info.max_size)),
            ])))
            .gauge_style(Style::default().fg(color).bg(Color::Black).add_modifier(Modifier::BOLD))
            .ratio(adr_ratio).label({
                let adr_ratio = adr_ratio*100.0f64;
                if adr_ratio > FLOAT_DISPLAY_LIMIT {
                    format!("{:.width$}%", adr_ratio, width=PERCISION)
                }

                else {
                    format!("< {:.width$}%", FLOAT_DISPLAY_LIMIT, width=PERCISION)
                }
            })
    }

    fn get_selected_top_section<'a>(section_selection: &DualSelection<ListSectionMode>, data: &'a ConsoleUIData) -> &'a SectionInfo {
        let current_selection = section_selection.get_selection_for(ListSectionMode::TopSection);
        &data.section_info[current_selection]
    }
}

#[derive(Default)]
struct ConsoleUIData {
    section_info: Vec<SectionInfo>
}

impl ConsoleUIData {
    pub const RAM_BASE_ADDRESS:u64    = 0x80000000;
    pub const RAM_SIZE:usize          = 2*1024*1024;

    pub fn from(memory_map: MemoryMap) -> Result<Self, Error> {
        const COLORS:[Color;4] = [Color::Red, Color::Green, Color::Blue, Color::Yellow];
        fn get_last_section_end_adr(memory_map: &MemoryMap) -> u64 {
            if let Some(section) = memory_map.sections.last() {
                return section.adr + section.size as u64;
            }
            0u64
        }

        let mut new_self     = ConsoleUIData::default();
        let mut color_idx    = 0;
        let     highest_adr  = get_last_section_end_adr(&memory_map);
        let     program_size = (highest_adr - Self::RAM_BASE_ADDRESS) as usize;

        new_self.section_info.push(SectionInfo{
            name:        String::from("<overall usage>"),
            symbols:     memory_map.global,
            start_adr:   Self::RAM_BASE_ADDRESS,
            size:        program_size,
            max_size:    Self::RAM_SIZE,
            color_share: None,
            is_global:   true
        });

        for section in memory_map.sections {
            new_self.section_info.push(SectionInfo{
                name:        section.name,
                symbols:     section.symbols,
                start_adr:   section.adr,
                size:        section.size,
                max_size:    program_size,
                color_share: None,
                is_global:   false
            });
        }

        let mut iter = new_self.section_info.iter_mut().peekable();
        while let Some(section) = iter.next() {
            if let Some(next_section) = iter.peek_mut() {
                if section.start_adr == next_section.start_adr {
                    let color = {
                        if let Some(color) = section.color_share {
                            color
                        }

                        else {
                            // next color
                            let color = COLORS[color_idx];

                            color_idx = (color_idx + 1)%COLORS.len();
                            color
                        }
                    };

                    section.color_share      = Some(color);
                    next_section.color_share = Some(color);
                }
            }
        }

        Ok(new_self)
    }
}

struct SectionInfo {
    pub name:        String,
    pub symbols:     Vec<Symbol>,
    pub start_adr:   u64,
    pub size:        usize,
    pub max_size:    usize,
    pub color_share: Option<Color>,
    pub is_global:   bool,
}

impl SectionInfo {
    fn get_size_ratio(&self) -> f64 {
        self.size as f64 / self.max_size as f64
    }
}

impl NamedMemoryArea for SectionInfo {
    fn get_name(&self) -> &str {
        self.name.as_str()
    }

    fn get_adr(&self) -> u64 {
        self.start_adr
    }

    fn get_size(&self) -> usize {
        self.size
    }
}

pub fn load_memory_map(use_wsl: bool, input: PathBuf) -> Result<MemoryMap, Error> {
    let memory_map = readmap_helper::generate_memory_map(use_wsl, input)?;

    if memory_map.is_empty() {
        Err(Error::from_str("Failed reading memory map. Is the path correct?"))
    }

    else {
        Ok(memory_map)
    }
}