use std::collections::HashMap;
use std::default::Default;
use std::fmt;

use color_eyre::eyre::Result;
use crossterm::event::{KeyCode, KeyEvent};
use log::{debug, info, warn};
use procfs::process::all_processes;
use ratatui::layout::Constraint::{Fill, Length, Percentage};
use ratatui::widgets::block::{Position, Title};
use ratatui::widgets::TableState;
use ratatui::{prelude::*, widgets::*};
use tokio::sync::mpsc::UnboundedSender;
use tui_input::Input;

use super::{Component, Frame};
use crate::action::Action;
use crate::components::process::Order::{Command, Cpu, Name, NumberOfThreads, Pid};
use crate::model::{create_rows, to_brt_process, BrtProcess};

#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
pub enum Order {
    #[default]
    Pid,
    Name,
    Command,
    NumberOfThreads,
    Cpu,
}

impl Order {
    fn next(&self) -> Self {
        use Order::*;
        match *self {
            Pid => Name,
            Name => Command,
            Command => NumberOfThreads,
            NumberOfThreads => Cpu,
            Cpu => Pid,
        }
    }

    fn previous(&self) -> Self {
        use Order::*;
        match *self {
            Pid => Cpu,
            Cpu => NumberOfThreads,
            NumberOfThreads => Command,
            Command => Name,
            Name => Pid,
        }
    }
}

impl fmt::Display for Order {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Pid => write!(f, "pid"),
            Name => write!(f, "name"),
            Command => write!(f, "command"),
            NumberOfThreads => write!(f, "threads"),
            Cpu => write!(f, "cpu"),
        }
    }
}

#[derive(Default, Debug)]
pub struct Process {
    pub show_help: bool,
    pub app_ticker: usize,
    pub render_ticker: usize,
    pub input: Input,
    pub process_map: HashMap<i32, BrtProcess>,
    pub processes: Vec<BrtProcess>,
    pub order: Order,
    pub scrollbar_state: ScrollbarState,
    pub state: TableState,
    pub action_tx: Option<UnboundedSender<Action>>,
}

impl Process {
    pub fn new() -> Process {
        let mut process = Process::default();
        process.process_map = process.get_processes();
        process.processes = process.process_map.clone().into_values().collect();
        process.state = TableState::new().with_selected(Some(0));
        process
    }

    pub fn refresh(&mut self) {
        let length = self.process_map.len();
        let new_processes = self.get_processes();
        let mut updated_processes = HashMap::new();
        for (pid, process) in new_processes {
            let old_process_option = self.process_map.get(&pid);
            if old_process_option.is_some() {
                let mut old_process = old_process_option.unwrap().clone();
                old_process.cpus.push_back(process.cpu);
                old_process.cpus.pop_front();
                old_process.cpu_graph = crate::model::get_cpu_graph(&old_process.cpus);
                updated_processes.insert(pid, old_process);
            };
        }
        self.process_map = updated_processes;
        self.processes = self.process_map.clone().into_values().collect();
        self.scrollbar_state = self.scrollbar_state.content_length(length);
    }

    pub fn order_string(&mut self) -> String {
        format!("{} {} {}", "<".red(), self.order, ">".red())
    }

    pub fn tick(&mut self) {
        self.app_ticker = self.app_ticker.saturating_add(1);
        // if self.app_ticker % 5 == 0 {
        // self.processes = self.get_all_processes();
        self.refresh();

        self.order_by_enum();
        info!("Refreshed process list.");
        // }
    }

    fn get_processes(&mut self) -> HashMap<i32, BrtProcess> {
        let processes: HashMap<i32, BrtProcess> = all_processes()
            .expect("Can't read /proc")
            .filter_map(|p| match p {
                Ok(p) => {
                    let brt_process = to_brt_process(&p);
                    if brt_process.is_some() {
                        Some((p.pid, brt_process?))
                    } else {
                        None
                    }
                }
                Err(e) => match e {
                    procfs::ProcError::NotFound(_) => None,
                    procfs::ProcError::Io(_e, _path) => None,
                    x => {
                        warn!("Can't read process due to error {x:?}");
                        None
                    }
                },
            })
            .collect();
        processes
    }

    pub fn order_by_enum(&mut self) {
        let order = self.order;
        match order {
            Pid => self.order_by_pid(),
            Name => self.order_by_program(),
            Command => self.order_by_command(),
            NumberOfThreads => self.order_by_number_of_threads(),
            Cpu => self.order_by_cpu(),
        }
    }

    pub fn order_by_pid(&mut self) {
        self.processes.sort_by(|a, b| a.pid.cmp(&b.pid))
    }

    pub fn order_by_program(&mut self) {
        self.processes.sort_by(|a, b| a.program.cmp(&b.program))
    }

    pub fn order_by_command(&mut self) {
        self.processes.sort_by(|a, b| a.command.cmp(&b.command))
    }

    pub fn order_by_number_of_threads(&mut self) {
        self.processes.sort_by(|a, b| {
            a.number_of_threads
                .partial_cmp(&b.number_of_threads)
                .unwrap()
        })
    }

    pub fn order_by_cpu(&mut self) {
        self.processes
            .sort_by(|a, b| a.cpu.partial_cmp(&b.cpu).unwrap())
    }

    pub fn render_tick(&mut self) {
        info!("Render Tick");
        self.render_ticker = self.render_ticker.saturating_add(1);
    }

    pub fn jump(&mut self, steps: i64) {
        let location = self.state.selected().unwrap_or(0) as i64;
        let length = self.process_map.len() as i64;
        debug!(
            "Move {} steps in [{}..{}] when current location is {}.",
            steps, 0, length, location
        );
        let mut index = location + steps;
        while index < 0 {
            index += length;
        }
        let new_location = (index % length) as usize;
        debug!("New location is {}.", new_location);
        self.state.select(Some(new_location));
        self.scrollbar_state = self.scrollbar_state.position(new_location);
    }
}

impl Component for Process {
    fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
        self.action_tx = Some(tx);
        Ok(())
    }

    fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<Action>> {
        debug!("Handling {:?}.", key);
        let action = match key.code {
            KeyCode::Up => Action::Up,
            KeyCode::Down => Action::Down,
            KeyCode::PageUp => Action::PageUp,
            KeyCode::PageDown => Action::PageDown,
            KeyCode::Left => Action::Left,
            KeyCode::Right => Action::Right,
            KeyCode::Esc => Action::Quit,
            _ => Action::Update,
        };
        Ok(Some(action))
    }

    fn update(&mut self, action: Action) -> Result<Option<Action>> {
        match action {
            Action::Tick => self.tick(),
            Action::Render => self.render_tick(),
            Action::Up => self.jump(-1),
            Action::Down => self.jump(1),
            Action::PageUp => self.jump(-20),
            Action::PageDown => self.jump(20),
            Action::Left => {
                self.order = self.order.previous();
                self.order_by_enum();
            }
            Action::Right => {
                self.order = self.order.next();
                self.order_by_enum();
            }
            _ => (),
        }
        Ok(None)
    }

    fn draw(&mut self, f: &mut Frame<'_>, _rect: Rect) -> Result<()> {
        let layout = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Percentage(100)])
            .split(f.size());

        let rows = create_rows(&self.processes);

        let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
            .begin_symbol(Some("↑"))
            .end_symbol(Some("↓"))
            .track_symbol(Some(" "))
            .style(Color::White);

        let selected_style = Style::default()
            .bg(Color::Rgb(0xd4, 0x54, 0x54))
            .fg(Color::White)
            .add_modifier(Modifier::BOLD);

        let header = [
            Cell::new(Line::from("Pid:").alignment(Alignment::Right)),
            Cell::new("Program:"),
            Cell::new("Command:"),
            Cell::new(Line::from("Threads:").alignment(Alignment::Right)),
            Cell::new("User:"),
            Cell::new("MemB"),
            Cell::new(""),
            Cell::new("Cpu%"),
        ]
        .iter()
        .cloned()
        .map(Cell::from)
        .collect::<Row>()
        .height(1)
        .style(Style::default().bold());

        let processes = self.processes.len();
        let process = format!("{}/{}", self.state.selected().unwrap() + 1, processes);

        let block = Block::default()
            .title(Title::from("brt").alignment(Alignment::Center))
            .title(Title::from(self.order_string()).alignment(Alignment::Right))
            .title(
                Title::from(process)
                    .position(Position::Bottom)
                    .alignment(Alignment::Right),
            )
            .borders(Borders::ALL)
            .border_style(Style::default().fg(Color::White))
            .border_type(BorderType::Rounded);

        let widths = [
            Percentage(5),
            Percentage(15),
            Fill(1),
            Percentage(5),
            Percentage(5),
            Length(5),
            Length(5),
            Length(5),
        ];

        let table = Table::new(rows, widths)
            .block(block)
            .header(header)
            .highlight_style(selected_style);

        f.render_stateful_widget(table, layout[0], &mut self.state);
        f.render_stateful_widget(
            scrollbar,
            layout[0].inner(&Margin {
                vertical: 1,
                horizontal: 1,
            }),
            &mut self.scrollbar_state,
        );
        Ok(())
    }
}

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

    #[test]
    fn test_brt_process_new() {
        let process = BrtProcess::new();
        assert_eq!(process.pid, 0);
        assert_eq!(process.cpus, VecDeque::from(vec![0_f64; 10]));
    }

    #[test]
    fn test_process_jump() {
        let mut process = Process::new();
        process.process_map = process.get_processes();
        assert_eq!(process.state.selected(), Some(0));
        process.jump(5);
        assert_eq!(process.state.selected(), Some(5));
        process.jump(5);
        assert_eq!(process.state.selected(), Some(10));
        process.jump(-15);
        assert_eq!(
            process.state.selected(),
            Some(process.process_map.len() - 5)
        );
        process.jump(4);
        assert_eq!(
            process.state.selected(),
            Some(process.process_map.len() - 1)
        );
        process.jump(1);
        assert_eq!(process.state.selected(), Some(0));
        process.jump(1);
        assert_eq!(process.state.selected(), Some(1));
    }
}