use parking_lot::Mutex;
use ratatui::layout::{Constraint, Rect};
use std::{
    collections::{HashMap, HashSet},
    sync::Arc,
    time::Instant,
};
use tokio_util::sync::CancellationToken;
use uuid::Uuid;

use crate::{
    app_data::{AppData, ContainerId, Header, ScrollDirection},
    exec::ExecMode,
};

use super::Rerender;

#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)]
pub enum SelectablePanel {
    #[default]
    Containers,
    Commands,
    Logs,
}

impl SelectablePanel {
    pub const fn title(self) -> &'static str {
        match self {
            Self::Containers => "Containers",
            Self::Logs => "Logs",
            Self::Commands => "",
        }
    }
    pub const fn next(self) -> Self {
        match self {
            Self::Containers => Self::Commands,
            Self::Commands => Self::Logs,
            Self::Logs => Self::Containers,
        }
    }
    pub const fn prev(self) -> Self {
        match self {
            Self::Containers => Self::Logs,
            Self::Commands => Self::Containers,
            Self::Logs => Self::Commands,
        }
    }
}

#[derive(Debug, Copy, Clone)]
pub enum Region {
    Panel(SelectablePanel),
    Header(Header),
    HelpPanel,
    Delete(DeleteButton),
}

#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum DeleteButton {
    Confirm,
    Cancel,
}

#[allow(unused)]
#[derive(Debug, Clone, Copy)]
pub enum BoxLocation {
    TopLeft,
    TopCentre,
    TopRight,
    MiddleLeft,
    MiddleCentre,
    MiddleRight,
    BottomLeft,
    BottomCentre,
    BottomRight,
}

impl BoxLocation {
    /// Screen is divided into 3x3 sections
    pub const fn get_indexes(self) -> (usize, usize) {
        match self {
            Self::TopLeft => (0, 0),
            Self::TopCentre => (0, 1),
            Self::TopRight => (0, 2),
            Self::MiddleLeft => (1, 0),
            Self::MiddleCentre => (1, 1),
            Self::MiddleRight => (1, 2),
            Self::BottomLeft => (2, 0),
            Self::BottomCentre => (2, 1),
            Self::BottomRight => (2, 2),
        }
    }

    /// Get both the vertical and hoziztonal constrains
    pub const fn get_constraints(
        self,
        blank_horizontal: u16,
        blank_vertical: u16,
        text_lines: u16,
        text_width: u16,
    ) -> ([Constraint; 3], [Constraint; 3]) {
        (
            Self::get_horizontal_constraints(self, blank_horizontal, text_width),
            Self::get_vertical_constraints(self, blank_vertical, text_lines),
        )
    }

    const fn get_horizontal_constraints(
        self,
        blank_horizontal: u16,
        text_width: u16,
    ) -> [Constraint; 3] {
        match self {
            Self::TopLeft | Self::MiddleLeft | Self::BottomLeft => [
                Constraint::Min(text_width),
                Constraint::Max(blank_horizontal),
                Constraint::Max(blank_horizontal),
            ],
            Self::TopCentre | Self::MiddleCentre | Self::BottomCentre => [
                Constraint::Max(blank_horizontal),
                Constraint::Min(text_width),
                Constraint::Max(blank_horizontal),
            ],
            Self::TopRight | Self::MiddleRight | Self::BottomRight => [
                Constraint::Max(blank_horizontal),
                Constraint::Max(blank_horizontal),
                Constraint::Min(text_width),
            ],
        }
    }

    const fn get_vertical_constraints(
        self,
        blank_vertical: u16,
        number_lines: u16,
    ) -> [Constraint; 3] {
        match self {
            Self::TopLeft | Self::TopCentre | Self::TopRight => [
                Constraint::Min(number_lines),
                Constraint::Max(blank_vertical),
                Constraint::Max(blank_vertical),
            ],
            Self::MiddleLeft | Self::MiddleCentre | Self::MiddleRight => [
                Constraint::Max(blank_vertical),
                Constraint::Min(number_lines),
                Constraint::Max(blank_vertical),
            ],
            Self::BottomLeft | Self::BottomCentre | Self::BottomRight => [
                Constraint::Max(blank_vertical),
                Constraint::Max(blank_vertical),
                Constraint::Min(number_lines),
            ],
        }
    }
}

// loading animation frames
const FRAMES: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
const FRAMES_LEN: u8 = 9;

/// The application gui state can be in multiple of these four states at the same time
/// Various functions (e.g input handler), operate differently depending upon current Status
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Status {
    DeleteConfirm,
    DockerConnect(Option<String>),
    Error,
    Exec,
    Filter,
    Help,
    Init,
    Inspect,
    Logs,
    SearchLogs,
}

#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq)]
pub struct ScrollOffset {
    pub x: usize,
    pub y: usize,
}

/// Global gui_state, stored in an Arc<Mutex>
#[derive(Debug)]
pub struct GuiState {
    delete_container_id: Option<ContainerId>,
    exec_mode: Option<ExecMode>,
    intersect_delete: HashMap<DeleteButton, Rect>,
    intersect_heading: HashMap<Header, Rect>,
    intersect_help: Option<Rect>,
    intersect_panel: HashMap<SelectablePanel, Rect>,
    loading_handle: Option<CancellationToken>,
    loading_index: u8,
    loading_set: HashSet<Uuid>,
    log_height: u16,
    rerender: Arc<Rerender>,
    selected_panel: SelectablePanel,
    screen_width: u16,
    show_logs: bool,
    inspect_offset: ScrollOffset,
    inspect_offset_max: ScrollOffset,
    status: HashSet<Status>,
    pub info_box_text: Option<(String, Instant)>,
}
impl GuiState {
    pub fn new(redraw: &Arc<Rerender>, show_logs: bool) -> Self {
        Self {
            delete_container_id: None,
            exec_mode: None,
            info_box_text: None,
            intersect_delete: HashMap::new(),
            intersect_heading: HashMap::new(),
            intersect_help: None,
            intersect_panel: HashMap::new(),
            inspect_offset: ScrollOffset::default(),
            inspect_offset_max: ScrollOffset::default(),
            loading_handle: None,
            loading_index: 0,
            loading_set: HashSet::new(),
            log_height: 75,
            screen_width: 0,
            rerender: Arc::clone(redraw),
            selected_panel: SelectablePanel::default(),
            show_logs,
            status: HashSet::new(),
        }
    }
    /// Increase the height of the log panel, then rerender
    pub fn log_height_increase(&mut self) {
        if self.show_logs && self.log_height <= 75 {
            self.log_height = self.log_height.saturating_add(5);
            self.rerender.update_draw();
        }
    }

    /// Reduce the height of the logs panel, then rerender
    /// Unselect logs panel if currently selected
    pub fn log_height_decrease(&mut self) {
        if self.show_logs {
            self.log_height = self.log_height.saturating_sub(5);
            if self.log_height == 0 && self.selected_panel == SelectablePanel::Logs {
                self.show_logs = false;
                self.selected_panel = SelectablePanel::Containers;
            }
            self.rerender.update_draw();
        }
    }

    pub fn set_inspect_offset(&mut self, sd: &ScrollDirection) {
        match sd {
            ScrollDirection::Up => self.inspect_offset.y = self.inspect_offset.y.saturating_sub(1),
            ScrollDirection::Down => {
                self.inspect_offset.y = self
                    .inspect_offset
                    .y
                    .saturating_add(1)
                    .min(self.inspect_offset_max.y)
            }
            ScrollDirection::Left => {
                self.inspect_offset.x = self.inspect_offset.x.saturating_sub(1)
            }
            ScrollDirection::Right => {
                self.inspect_offset.x = self
                    .inspect_offset
                    .x
                    .saturating_add(1)
                    .min(self.inspect_offset_max.x)
            }
        }
        self.rerender.update_draw();
    }

    pub fn get_inspect_offset(&self) -> ScrollOffset {
        self.inspect_offset
    }

    pub fn set_inspect_offset_max(&mut self, offset: ScrollOffset) {
        self.inspect_offset_max = offset
    }

    pub fn set_inspect_offset_y_to_max(&mut self) {
        self.inspect_offset.y = self.inspect_offset_max.y;
        self.rerender.update_draw();
    }

    pub fn clear_inspect_offset(&mut self) {
        self.inspect_offset.x = 0;
        self.inspect_offset.y = 0;
        self.inspect_offset_max = ScrollOffset::default();
        self.rerender.update_draw();
    }

    /// Set the screen width, used for offset char calculations
    pub const fn set_screen_width(&mut self, width: u16) {
        self.screen_width = width;
    }

    /// Get the screen width, used for offset char calculations
    pub const fn get_screen_width(&self) -> u16 {
        self.screen_width
    }

    pub const fn get_show_logs(&self) -> bool {
        self.show_logs
    }

    pub fn toggle_show_logs(&mut self) {
        self.show_logs = !self.show_logs;
        if !self.show_logs && self.selected_panel == SelectablePanel::Logs {
            self.selected_panel = SelectablePanel::Containers;
        }
        self.rerender.update_draw();
    }

    /// Set the log_height to zero, for now only used by tests
    #[cfg(test)]
    pub const fn log_height_zero(&mut self) {
        self.log_height = 0;
    }

    /// Get the log height, *should* be a u8 between 0 and 80, essentially a percentage
    pub const fn get_log_height(&self) -> u16 {
        self.log_height
    }

    /// Clear panels hash map, so on resize can fix the sizes for mouse clicks
    pub fn clear_area_map(&mut self) {
        self.intersect_panel.clear();
    }

    /// Set the rerender clear to true, to flush the screen and redraw
    pub fn set_clear(&self) {
        self.rerender.set_clear();
    }

    /// Get the currently selected panel
    pub const fn get_selected_panel(&self) -> SelectablePanel {
        self.selected_panel
    }

    /// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
    pub fn check_panel_intersect(&mut self, rect: Rect) {
        if let Some(data) = self
            .intersect_panel
            .iter()
            .filter(|i| i.1.intersects(rect))
            .collect::<Vec<_>>()
            .first()
        {
            self.selected_panel = *data.0;
            self.rerender.update_draw();
        }
    }

    /// Check if a given Rect (a clicked area of 1x1), interacts with any known delete button
    pub fn get_intersect_button(&self, rect: Rect) -> Option<DeleteButton> {
        self.intersect_delete
            .iter()
            .filter(|i| i.1.intersects(rect))
            .collect::<Vec<_>>()
            .first()
            .map(|data| *data.0)
    }

    /// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
    pub fn get_intersect_header(&self, rect: Rect) -> Option<Header> {
        self.intersect_heading
            .iter()
            .filter(|i| i.1.intersects(rect))
            .collect::<Vec<_>>()
            .first()
            .map(|data| *data.0)
    }

    /// Check if a the "show/hide help" section has been clicked
    pub fn get_intersect_help(&self, rect: Rect) -> bool {
        self.intersect_help
            .as_ref()
            .is_some_and(|i| i.intersects(rect))
    }

    /// Insert, or updates header area panel into heading_map
    pub fn update_region_map(&mut self, region: Region, area: Rect) {
        match region {
            Region::Header(header) => {
                self.intersect_heading
                    .entry(header)
                    .and_modify(|w| *w = area)
                    .or_insert(area);
            }
            Region::Panel(panel) => {
                self.intersect_panel
                    .entry(panel)
                    .and_modify(|w| *w = area)
                    .or_insert(area);
            }
            Region::Delete(button) => {
                self.intersect_delete
                    .entry(button)
                    .and_modify(|w| *w = area)
                    .or_insert(area);
            }
            Region::HelpPanel => {
                self.intersect_help = Some(area);
            }
        }
    }

    /// Check if an ContainerId is set in the delete_container field
    pub fn get_delete_container(&self) -> Option<ContainerId> {
        self.delete_container_id.clone()
    }

    /// Set either a ContainerId, or None, to the delete_container field
    /// If Some, will also insert the DeleteConfirm status into self.status
    pub fn set_delete_container(&mut self, id: Option<ContainerId>) {
        if id.is_some() {
            self.status.insert(Status::DeleteConfirm);
        } else {
            self.intersect_delete.clear();
            self.status_del(Status::DeleteConfirm);
        }
        self.delete_container_id = id;
        self.rerender.update_draw();
    }

    /// Return a copy of the Status HashSet
    pub fn get_status(&self) -> HashSet<Status> {
        self.status.clone()
    }

    /// Remove a gui_status into the current gui_status HashSet
    /// Remove exec mode & deleteConfirm is required
    pub fn status_del(&mut self, status: Status) {
        self.status.remove(&status);
        match status {
            Status::DeleteConfirm => {
                self.status.remove(&Status::DeleteConfirm);
            }
            Status::Exec => {
                self.exec_mode = None;
            }
            _ => (),
        }
        self.rerender.update_draw();
    }

    /// Inset the ExecMode into self, and set the Status as exec
    /// Using StatusPush with Status::Exec won't insert into the hash map
    /// To force self.exec_mode to be set
    pub fn set_exec_mode(&mut self, mode: ExecMode) {
        self.exec_mode = Some(mode);
        self.status.insert(Status::Exec);
        self.rerender.update_draw();
    }

    pub fn get_exec_mode(&self) -> Option<ExecMode> {
        self.exec_mode.clone()
    }

    /// Insert a gui_status into the current gui_status HashSet
    /// If the status is Exec, it won't get inserted, set_exec_mode() should be used instead
    pub fn status_push(&mut self, status: Status) {
        if status != Status::Exec {
            self.status.insert(status);
            self.rerender.update_draw();
        }
    }

    pub fn set_logs_panel_selected(&mut self, app_data: &Arc<Mutex<AppData>>) {
        self.selected_panel = SelectablePanel::Logs;
        if (app_data.lock().get_container_len() == 0
            && self.get_selected_panel() == SelectablePanel::Commands)
            || (self.log_height == 0 && self.get_selected_panel() == SelectablePanel::Logs)
        {
            self.selected_panel = self.selected_panel.next();
        }
        self.rerender.update_draw();
    }
    /// Change to next selectable panel
    pub fn selectable_panel_next(&mut self, app_data: &Arc<Mutex<AppData>>) {
        self.selected_panel = self.selected_panel.next();
        if (app_data.lock().get_container_len() == 0
            && self.get_selected_panel() == SelectablePanel::Commands)
            || (self.log_height == 0 && self.get_selected_panel() == SelectablePanel::Logs)
        {
            self.selected_panel = self.selected_panel.next();
        }
        self.rerender.update_draw();
    }

    /// Change to previous selectable panel
    pub fn selectable_panel_previous(&mut self, app_data: &Arc<Mutex<AppData>>) {
        self.selected_panel = self.selected_panel.prev();
        if (app_data.lock().get_container_len() == 0
            && self.get_selected_panel() == SelectablePanel::Commands)
            || (self.log_height == 0 && self.get_selected_panel() == SelectablePanel::Logs)
        {
            self.selected_panel = self.selected_panel.prev();
        }
        self.rerender.update_draw();
    }

    /// Insert a new loading_uuid into HashSet, and advance the loading_index by one frame, or reset to 0 if at end of array
    pub fn next_loading(&mut self, uuid: Uuid) {
        if self.loading_index == FRAMES_LEN {
            self.loading_index = 0;
        } else {
            self.loading_index += 1;
        }
        self.loading_set.insert(uuid);
        self.rerender.update_draw();
    }

    pub fn is_loading(&self) -> bool {
        !self.loading_set.is_empty()
    }
    /// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' '
    pub fn get_loading(&self) -> char {
        if self.is_loading() {
            FRAMES[usize::from(self.loading_index)]
        } else {
            ' '
        }
    }

    /// Animate the loading icon in its own Tokio thread
    /// This should only be able to executed once, rather than multiple spawns
    pub fn start_loading_animation(gui_state: &Arc<Mutex<Self>>, loading_uuid: Uuid) {
        if !gui_state.lock().is_loading() {
            let inner_state = Arc::clone(gui_state);
            let token = CancellationToken::new();

            gui_state.lock().loading_handle = Some(token.clone());
            tokio::spawn(async move {
                tokio::select! {
                    _ = token.cancelled() => (),
                    _ = async {
                        loop {
                            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
                            inner_state.lock().next_loading(loading_uuid);
                        }
                    } => (),
                }
            });
        }
        gui_state.lock().next_loading(loading_uuid);
    }

    /// Stop the loading_spin function, and reset gui loading status
    pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) {
        self.loading_set.remove(&loading_uuid);
        self.rerender.update_draw();
        if self.loading_set.is_empty() {
            self.loading_index = 0;
            if let Some(h) = &self.loading_handle {
                h.cancel();
            }
            self.loading_handle = None;
        }
    }

    /// Set info box content
    pub fn set_info_box(&mut self, text: &str) {
        self.info_box_text = Some((text.to_owned(), std::time::Instant::now()));
        self.rerender.update_draw();
    }

    /// Remove info box content
    pub fn reset_info_box(&mut self) {
        self.info_box_text = None;
        self.rerender.update_draw();
    }
}
