// SPDX-FileCopyrightText: 2023 CELESTIFYX Team
// SPDX-License-Identifier: GPL-3.0-or-later

use std::collections::{
    HashMap,
    HashSet
};

use super::{
    ports::{
        Port,
        Status
    },

    pkg::Action
};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(super) enum Mode {
    #[default]
    Browse,
    ActionMenu,
    Running,
    Toast,
    Quit
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Toast {
    pub(super) kind: ToastKind,
    pub(super) text: String
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ToastKind {
    Success,
    Error
}

#[derive(Debug, Clone, Default)]
pub(super) struct Selection {
    name: Option<String>
}

impl Selection {
    pub(super) fn index_in(&self, view: &[Port]) -> usize {
        self.name.as_ref().and_then(|n: &String| view.iter().position(|p: &Port| &p.name == n)).unwrap_or(0)
    }

    pub(super) fn sync_to_index(&mut self, view: &[Port], idx: usize) -> () {
        let idx: usize = idx.min(view.len().saturating_sub(1));
        self.name = view.get(idx).map(|p: &Port| p.name.clone());
    }
}

#[derive(Debug, Clone)]
pub(super) struct ActionMenu {
    pub(super) ports: Vec<Port>,
    pub(super) actions: Vec<Action>,
    pub(super) selected: usize
}

impl ActionMenu {
    pub(super) fn for_ports(ports: &[Port]) -> Self {
        let actions: Vec<Action> = if ports.len() == 1 {
            let port: &Port = &ports[0];

            match port.status {
                Status::Installed if port.needs_update() => vec![
                    Action::Remove,
                    Action::Update
                ],

                Status::Installed => vec![
                    Action::Remove,
                    Action::Rebuild
                ],

                Status::NotInstalled => vec![Action::Install]
            }
        } else {
            let all_installed: bool = ports.iter().all(|p: &Port| p.status == Status::Installed);
            let all_not_installed: bool = ports.iter().all(|p: &Port| p.status == Status::NotInstalled);
            let all_need_update: bool = ports.iter().all(|p: &Port| (p.status == Status::Installed) && p.needs_update());

            if all_installed && all_need_update {
                vec![
                    Action::Remove,
                    Action::Update
                ]
            } else if all_installed {
                vec![
                    Action::Remove,
                    Action::Rebuild
                ]
            } else if all_not_installed {
                vec![Action::Install]
            } else {
                vec![]
            }
        };

        Self {
            ports: ports.to_vec(),
            actions,
            selected: 0
        }
    }

    pub(super) fn selected_action(&self) -> Option<Action> {
        self.actions.get(self.selected).copied()
    }

    pub(super) fn move_selection(&mut self, delta: i32) -> () {
        if self.actions.is_empty() {
            return;
        }

        let len:     i32 = self.actions.len() as i32;
        let new_idx: i32 = (self.selected as i32 + delta).rem_euclid(len);

        self.selected = new_idx as usize;
    }
}

pub(super) struct App {
    pub(super) ports: Vec<Port>,
    pub(super) filter: String,
    pub(super) status_filter: StatusFilter,
    pub(super) selection: Selection,
    pub(super) multi_selection: HashSet<String>,
    pub(super) scroll: usize,
    pub(super) previews: HashMap<String, String>,
    pub(super) preview_target: Option<String>,
    pub(super) mode: Mode,
    pub(super) toast: Option<Toast>,
    pub(super) action_menu: Option<ActionMenu>
}

impl App {
    pub(super) fn new(ports: Vec<Port>) -> Self {
        let selection = Selection {
            name: ports.first().map(|p: &Port| p.name.clone())
        };

        Self {
            ports,
            filter: String::new(),
            status_filter: StatusFilter::All,
            selection,
            multi_selection: HashSet::new(),
            scroll: 0,
            previews: HashMap::new(),
            preview_target: None,
            mode: Mode::Browse,
            toast: None,
            action_menu: None
        }
    }

    pub(super) fn filtered_view(&self) -> Vec<Port> {
        let query: String = self.filter.trim().to_lowercase();

        self.ports.iter().filter(|p: &&Port| {
            let text_match: bool = query.is_empty() || p.name.to_lowercase().contains(&query) || p.rel_path.to_lowercase().contains(&query);

            let status_match: bool = match self.status_filter {
                StatusFilter::All          => true,
                StatusFilter::Installed    => p.status == Status::Installed,
                StatusFilter::NotInstalled => p.status == Status::NotInstalled,
                StatusFilter::NeedsUpdate  => p.needs_update()
            };

            text_match && status_match
        }).cloned().collect()
    }

    pub(super) fn cycle_status_filter(&mut self) -> () {
        self.status_filter = self.status_filter.cycle();
    }

    pub(super) fn selected_index(&self) -> usize {
        self.selection.index_in(&self.filtered_view())
    }

    pub(super) fn selected_port(&self) -> Option<Port> {
        let view: Vec<Port> = self.filtered_view();
        view.get(self.selection.index_in(&view)).cloned()
    }

    pub(super) fn move_cursor(&mut self, delta: i32) -> () {
        let view: Vec<Port> = self.filtered_view();

        if view.is_empty() {
            return;
        }

        let len:     i32 = view.len() as i32;
        let idx:     i32 = self.selection.index_in(&view) as i32;
        let new_idx: i32 = (idx + delta).rem_euclid(len);

        (&mut self.selection).sync_to_index(&view, new_idx as usize);
        self.preview_target = self.selected_port().map(|p: Port| p.name);
    }

    pub(super) fn jump_to(&mut self, end: ListEnd) -> () {
        let view: Vec<Port> = self.filtered_view();

        if view.is_empty() {
            return;
        }

        let idx: usize = match end {
            ListEnd::Top    => 0,
            ListEnd::Bottom => view.len() - 1
        };

        (&mut self.selection).sync_to_index(&view, idx);
        self.preview_target = self.selected_port().map(|p: Port| p.name);
    }

    pub(super) fn mark_toggled(&mut self, name: &str, new_status: Status) -> () {
        if let Some(p) = (&mut *self.ports).iter_mut().find(|p: &&mut Port| p.name == name) {
            p.status = new_status;

            if new_status == Status::Installed {
                p.installed_version = p.port_version.clone();
                p.installed_release = p.port_release.clone();
            } else {
                p.installed_version = String::new();
                p.installed_release = String::new();
            }
        }

        (&mut self.previews).remove(name);
    }

    pub(super) fn set_toast(&mut self, kind: ToastKind, text: impl Into<String>) -> () {
        self.toast = Some(Toast {
            kind,
            text: text.into()
        });

        self.mode = Mode::Toast;
    }

    pub(super) fn clear_toast(&mut self) -> () {
        if self.mode == Mode::Toast {
            self.toast = None;
            self.mode = Mode::Browse;
        }
    }

    pub(super) fn reload_ports(&mut self, new_ports: Vec<Port>) -> () {
        self.ports = new_ports;
        (&mut self.previews).clear();
        self.preview_target = None;

        let view: Vec<Port> = self.filtered_view();

        if !view.is_empty() {
            let idx: usize = self.selection.index_in(&view).min(view.len() - 1);
            (&mut self.selection).sync_to_index(&view, idx);
        }
    }

    pub(super) fn toggle_multi_select(&mut self) -> () {
        if let Some(port) = self.selected_port() {
            let name: String = port.name.clone();

            if (&mut self.multi_selection).contains(&name) {
                (&mut self.multi_selection).remove(&name);
                return;
            }

            (&mut self.multi_selection).insert(name);
        }
    }

    pub(super) fn get_selected_ports(&self) -> Vec<Port> {
        if self.multi_selection.is_empty() {
            return self.selected_port().map(|p: Port| vec![p]).unwrap_or_default();
        }

        self.ports.iter().filter(|p: &&Port| self.multi_selection.contains(&p.name)).cloned().collect()
    }

    pub(super) fn open_action_menu(&mut self) -> () {
        let selected: Vec<Port> = self.get_selected_ports();

        if selected.is_empty() {
            return;
        }

        let menu: ActionMenu = ActionMenu::for_ports(&selected);

        if menu.actions.is_empty() {
            (&mut *self).set_toast(ToastKind::Error, "Mixed selection: all packages must have the same status");
            return;
        }

        self.action_menu = Some(menu);
        self.mode = Mode::ActionMenu;
    }

    pub(super) fn close_action_menu(&mut self) -> () {
        self.action_menu = None;
        self.mode = Mode::Browse;
    }

    pub(super) fn confirm_action(&mut self) -> Option<(Vec<Port>, Action)> {
        let menu: &ActionMenu = self.action_menu.as_ref()?;
        let action: Action = menu.selected_action()?;
        let ports: Vec<Port> = menu.ports.clone();

        Some((ports, action))
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ListEnd {
    Top,
    Bottom
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(super) enum StatusFilter {
    #[default]
    All,
    Installed,
    NotInstalled,
    NeedsUpdate
}

impl StatusFilter {
    pub(super) fn label(self) -> &'static str {
        match self {
            StatusFilter::All          => "All",
            StatusFilter::Installed    => "Installed",
            StatusFilter::NotInstalled => "Not installed",
            StatusFilter::NeedsUpdate  => "Outdated"
        }
    }

    pub(super) fn cycle(self) -> Self {
        match self {
            StatusFilter::All          => StatusFilter::Installed,
            StatusFilter::Installed    => StatusFilter::NotInstalled,
            StatusFilter::NotInstalled => StatusFilter::NeedsUpdate,
            StatusFilter::NeedsUpdate  => StatusFilter::All
        }
    }
}
