// bibiman - a TUI for managing BibLaTeX databases
// Copyright (C) 2024  lukeflo
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
/////

use crate::bibiman::CurrentArea;
use crate::cliargs::CLIArgs;
use crate::config::BibiConfig;
use crate::tui::commands::InputCmdAction;
use crate::tui::popup::{PopupItem, PopupKind};
use crate::tui::{self, Tui};
use crate::{bibiman::Bibiman, tui::commands::CmdAction};
use color_eyre::eyre::Result;
use crossterm::event::KeyCode;
use log::warn;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tui::Event;
use tui_input::Input;
use tui_input::backend::crossterm::EventHandler;

// Application.
#[derive(Debug)]
pub struct App {
    // Is the application running?
    pub running: bool,
    // bibimain
    pub bibiman: Bibiman,
    // Input mode
    pub input: Input,
    // Input mode bool
    pub input_mode: bool,
}

impl App {
    // Constructs a new instance of [`App`].
    pub fn new(args: &mut CLIArgs, cfg: &mut BibiConfig) -> Result<Self> {
        // Self::default()
        let running = true;
        let input = Input::default();
        let bibiman = Bibiman::new(args, cfg)?;
        Ok(Self {
            running,
            bibiman,
            input,
            input_mode: false,
        })
    }

    pub async fn run(&mut self, cfg: &BibiConfig) -> Result<()> {
        let mut tui = tui::Tui::new()?;
        tui.enter()?;

        // Start the main loop.
        while self.running {
            // Render the user interface.
            tui.draw(self, cfg)?;
            // Handle events.
            match tui.next().await? {
                Event::Tick => self.tick(),
                // Event::Key(key_event) => handle_key_events(key_event, self, &mut tui)?,
                // Event::Mouse(_) => {}
                Event::Key(key_event) => {
                    // Automatically close message popups on next keypress
                    if let Some(PopupKind::MessageConfirm) = self.bibiman.popup_area.popup_kind {
                        self.bibiman.close_popup()
                    } else if let Some(PopupKind::MessageError) = self.bibiman.popup_area.popup_kind
                    {
                        self.bibiman.close_popup()
                    } else if let Some(PopupKind::YankItem) | Some(PopupKind::OpenRes) =
                        self.bibiman.popup_area.popup_kind
                    {
                        self.bibiman.fast_selection(cfg, &mut tui, key_event.code)?;
                        // if a fast match char was used, restart event-loop.
                        // otherwise, the fast match char will be executed as command
                        match key_event.code {
                            KeyCode::Char('o' | 'l' | 'n' | 'y') => continue,
                            _ => {}
                        }
                    }
                    let command = if self.input_mode {
                        CmdAction::Input(InputCmdAction::parse(key_event, &self.input))
                    } else {
                        CmdAction::from(key_event)
                    };
                    self.run_command(command, cfg, &mut tui)?
                }
                Event::Mouse(mouse_event) => {
                    self.run_command(CmdAction::from(mouse_event), cfg, &mut tui)?
                }

                Event::Resize(_, _) => {}
            }
        }

        // Exit the user interface.
        tui.exit()?;
        Ok(())
    }

    // Handles the tick event of the terminal.
    pub fn tick(&self) {}

    // General commands

    // Set running to false to quit the application.
    pub fn quit(&mut self) {
        self.running = false;
    }

    pub fn run_command(&mut self, cmd: CmdAction, cfg: &BibiConfig, tui: &mut Tui) -> Result<()> {
        match cmd {
            CmdAction::Input(cmd) => match cmd {
                InputCmdAction::Nothing => {}
                InputCmdAction::Handle(event) => {
                    self.input.handle_event(&event);
                    if let CurrentArea::SearchArea = self.bibiman.current_area {
                        self.bibiman.search_list_by_pattern(&self.input);
                    }
                }
                InputCmdAction::Enter => {
                    self.input_mode = true;
                    // Logic for TABS to be added
                    // self.bibiman.enter_search_area();
                }
                InputCmdAction::Confirm => {
                    // Logic for TABS to be added
                    if let CurrentArea::SearchArea = self.bibiman.current_area {
                        self.bibiman.confirm_search();
                    } else if let CurrentArea::PopupArea = self.bibiman.current_area {
                        match self.bibiman.popup_area.popup_kind {
                            Some(PopupKind::AddEntry) => {
                                let doi = self.input.value();
                                self.bibiman.close_popup();
                                self.input_mode = false;
                                // Check if the DOI pattern is valid. If not, show warning and break
                                if doi.starts_with("10.")
                                    || doi.starts_with("https://doi.org")
                                    || doi.starts_with("https://dx.doi.org")
                                    || doi.starts_with("http://doi.org")
                                    || doi.starts_with("http://dx.doi.org")
                                {
                                    self.bibiman.handle_new_entry_submission(doi)?;
                                } else {
                                    self.bibiman.open_popup(
                                        PopupKind::MessageError,
                                        Some("No valid DOI pattern: "),
                                        Some(doi),
                                        None,
                                    );
                                }
                            }
                            _ => {}
                        }
                    }
                    self.input = Input::default();
                    self.input_mode = false;
                }
                InputCmdAction::Exit => {
                    self.input = Input::default();
                    self.input_mode = false;
                    if let CurrentArea::SearchArea = self.bibiman.current_area {
                        self.bibiman.break_search();
                    } else if let CurrentArea::PopupArea = self.bibiman.current_area {
                        self.bibiman.close_popup();
                    }
                }
            },
            CmdAction::SelectNextRow(amount) => match self.bibiman.current_area {
                // Here add logic to select TAB
                CurrentArea::EntryArea => {
                    self.bibiman.select_next_entry(amount);
                }
                CurrentArea::TagArea => {
                    self.bibiman.select_next_tag(amount);
                }
                CurrentArea::PopupArea => match self.bibiman.popup_area.popup_kind {
                    Some(PopupKind::Help) => {
                        self.bibiman.popup_area.popup_scroll_down();
                    }
                    Some(PopupKind::OpenRes)
                    | Some(PopupKind::AppendToFile)
                    | Some(PopupKind::YankItem)
                    | Some(PopupKind::CreateNote)
                    | Some(PopupKind::SearchField) => {
                        self.bibiman.popup_area.popup_state.scroll_down_by(1)
                    }
                    _ => {}
                },
                _ => {}
            },
            CmdAction::SelectPrevRow(amount) => match self.bibiman.current_area {
                // Here add logic to select TAB
                CurrentArea::EntryArea => {
                    self.bibiman.select_previous_entry(amount);
                }
                CurrentArea::TagArea => {
                    self.bibiman.select_previous_tag(amount);
                }
                CurrentArea::PopupArea => match self.bibiman.popup_area.popup_kind {
                    Some(PopupKind::Help) => {
                        self.bibiman.popup_area.popup_scroll_up();
                    }
                    Some(PopupKind::OpenRes)
                    | Some(PopupKind::AppendToFile)
                    | Some(PopupKind::YankItem)
                    | Some(PopupKind::CreateNote)
                    | Some(PopupKind::SearchField) => {
                        self.bibiman.popup_area.popup_state.scroll_up_by(1)
                    }
                    _ => {}
                },
                _ => {}
            },
            CmdAction::SelectNextCol => {
                if let CurrentArea::EntryArea = self.bibiman.current_area {
                    self.bibiman.select_next_column();
                }
            }
            CmdAction::SelectPrevCol => {
                if let CurrentArea::EntryArea = self.bibiman.current_area {
                    self.bibiman.select_prev_column();
                }
            }
            CmdAction::ScrollInfoDown => {
                self.bibiman.scroll_info_down();
            }
            CmdAction::ScrollInfoUp => {
                self.bibiman.scroll_info_up();
            }
            CmdAction::Bottom => match self.bibiman.current_area {
                CurrentArea::EntryArea => {
                    self.bibiman.select_last_entry();
                }
                CurrentArea::TagArea => {
                    self.bibiman.select_last_tag();
                }
                _ => {}
            },
            CmdAction::Top => match self.bibiman.current_area {
                CurrentArea::EntryArea => {
                    self.bibiman.select_first_entry();
                }
                CurrentArea::TagArea => {
                    self.bibiman.select_first_tag();
                }
                _ => {}
            },
            CmdAction::ToggleArea => {
                self.bibiman.toggle_area();
            }
            CmdAction::SearchList => {
                self.input_mode = true;
                self.bibiman
                    .search_struct
                    .set_search_field(crate::bibiman::search::SearchField::All);
                self.bibiman.enter_search_area();
            }
            CmdAction::SearchField => {
                if let CurrentArea::EntryArea = self.bibiman.current_area {
                    self.bibiman.open_popup(
                        PopupKind::SearchField,
                        None,
                        None,
                        Some(vec![
                            (
                                "Author".into(),
                                "".into(),
                                PopupItem::SearchField(crate::bibiman::search::SearchField::Author),
                            ),
                            (
                                "Title".into(),
                                "".into(),
                                PopupItem::SearchField(crate::bibiman::search::SearchField::Title),
                            ),
                            (
                                "Abstract".into(),
                                "".into(),
                                PopupItem::SearchField(
                                    crate::bibiman::search::SearchField::Abstract,
                                ),
                            ),
                            (
                                "Custom Column".into(),
                                "".into(),
                                PopupItem::SearchField(
                                    crate::bibiman::search::SearchField::CustomColumn,
                                ),
                            ),
                        ]),
                    );
                }
            }
            CmdAction::Reset => {
                if let CurrentArea::PopupArea = self.bibiman.current_area {
                    if let Some(PopupKind::Help) = self.bibiman.popup_area.popup_kind {
                        self.bibiman.popup_area.popup_scroll_pos = 0;
                    }
                    self.bibiman.close_popup();
                } else {
                    self.bibiman.reset_current_list();
                }
            }
            CmdAction::Confirm => {
                if let CurrentArea::TagArea = self.bibiman.current_area {
                    self.bibiman.filter_for_tags();
                } else if let CurrentArea::PopupArea = self.bibiman.current_area {
                    if let Some(PopupKind::Help) = self.bibiman.popup_area.popup_kind {
                        self.bibiman.close_popup();
                    } else if let Some(PopupKind::OpenRes) = self.bibiman.popup_area.popup_kind {
                        self.bibiman.open_connected_res(cfg, tui)?;
                    } else if let Some(PopupKind::AppendToFile) = self.bibiman.popup_area.popup_kind
                    {
                        match self.bibiman.append_entry_to_file(cfg) {
                            std::result::Result::Ok(_) => {}
                            Err(e) => self.bibiman.open_popup(
                                PopupKind::MessageError,
                                Some(&e.to_string()),
                                None,
                                None,
                            ),
                        }
                    } else if let Some(PopupKind::YankItem) = self.bibiman.popup_area.popup_kind {
                        self.bibiman.yank_entry_field(cfg)?
                    } else if let Some(PopupKind::CreateNote) = self.bibiman.popup_area.popup_kind {
                        self.bibiman.create_note(cfg)?
                    } else if let Some(PopupKind::SearchField) = self.bibiman.popup_area.popup_kind
                    {
                        self.bibiman.search_struct.set_search_field(
                            if let PopupItem::SearchField(field) =
                                self.bibiman.popup_area.get_sel_popup_item()
                            {
                                field.clone()
                            } else {
                                crate::bibiman::search::SearchField::All
                            },
                        );
                        self.bibiman.close_popup();
                        self.input_mode = true;
                        self.bibiman.enter_search_area();
                    }
                }
            }
            CmdAction::SortList => {
                if let CurrentArea::EntryArea = self.bibiman.current_area {
                    self.bibiman.entry_table.sort_entry_table(true);
                }
            }
            CmdAction::SortById => {
                self.bibiman.entry_table.sort_by_id();
            }
            CmdAction::YankItem => {
                if let CurrentArea::EntryArea = self.bibiman.current_area {
                    match self.bibiman.entry_table.get_selected_entry() {
                        Ok(entry) => {
                            let mut items = vec![(
                                "Citekey: ".to_string(),
                                entry.citekey.clone(),
                                PopupItem::Citekey,
                            )];
                            if let Some(doi) = &entry.doi_url {
                                items.push(("Weblink: ".into(), doi.clone(), PopupItem::Link))
                            }
                            if let Some(path) = &entry.filepath {
                                path.iter().for_each(|p| {
                                    items.push((
                                        "Filepath: ".into(),
                                        p.clone().into_string().unwrap(),
                                        PopupItem::Entryfile,
                                    ))
                                });
                            }
                            self.bibiman
                                .open_popup(PopupKind::YankItem, None, None, Some(items));
                        }
                        Err(msg) => {
                            warn!("{}", &msg);
                            self.bibiman.open_popup(
                                PopupKind::MessageError,
                                Some(&msg),
                                None,
                                None,
                            );
                        }
                    }
                }
            }
            CmdAction::EditFile => {
                if let CurrentArea::EntryArea = self.bibiman.current_area {
                    self.bibiman.run_editor(cfg, tui)?;
                }
            }
            CmdAction::Open => {
                if let CurrentArea::EntryArea = self.bibiman.current_area {
                    match self
                        .bibiman
                        .entry_table
                        .get_selected_entry()
                        .map(|e| e.to_owned())
                    {
                        Ok(entry) => {
                            let mut items: Vec<(String, String, PopupItem)> = vec![];
                            if entry.filepath.is_some()
                                || entry.doi_url.is_some()
                                || entry.notes.is_some()
                            {
                                if let Some(doi) = &entry.doi_url {
                                    items.push(("Link: ".into(), doi.clone(), PopupItem::Link))
                                }
                                if let Some(files) = &entry.filepath {
                                    files.iter().for_each(|p| {
                                        items.push((
                                            "File: ".into(),
                                            // p.clone().into_string().unwrap(),
                                            if entry.file_field && cfg.general.file_prefix.is_some()
                                            {
                                                cfg.general
                                                    .file_prefix
                                                    .clone()
                                                    .unwrap()
                                                    .join(p)
                                                    .into_os_string()
                                                    .into_string()
                                                    .unwrap_or_else(|os| {
                                                        os.to_string_lossy().to_string()
                                                    })
                                            } else {
                                                p.clone().into_string().unwrap_or_else(|os| {
                                                    os.to_string_lossy().to_string()
                                                })
                                            },
                                            PopupItem::Entryfile,
                                        ))
                                    });
                                }
                                if let Some(notes) = &entry.notes {
                                    notes.iter().for_each(|n| {
                                        items.push((
                                            "Note: ".into(),
                                            n.clone().into_string().unwrap_or_else(|os| {
                                                os.to_string_lossy().to_string()
                                            }),
                                            PopupItem::Notefile,
                                        ));
                                    });
                                }

                                self.bibiman.open_popup(
                                    PopupKind::OpenRes,
                                    None,
                                    None,
                                    Some(items),
                                );
                            } else {
                                self.bibiman.open_popup(
                                    PopupKind::MessageError,
                                    Some("Selected entry has no connected resources: "),
                                    Some(&entry.citekey),
                                    None,
                                );
                            }
                        }
                        Err(msg) => {
                            warn!("{}", &msg);
                            self.bibiman.open_popup(
                                PopupKind::MessageError,
                                Some(&msg),
                                None,
                                None,
                            );
                        }
                    }
                }
            }
            CmdAction::AddEntry => {
                if let CurrentArea::EntryArea = self.bibiman.current_area {
                    self.input_mode = true;
                    self.bibiman.add_entry();
                }
            }
            CmdAction::CreateNote => {
                if let CurrentArea::EntryArea = self.bibiman.current_area {
                    match self.bibiman.entry_table.get_selected_entry() {
                        Ok(entry) => {
                            let citekey = entry.citekey.clone();
                            // disallow chars which can cause other shell executions
                            if citekey.contains("/")
                                | citekey.contains("|")
                                | citekey.contains("#")
                                | citekey.contains("\\")
                                | citekey.contains("*")
                                | citekey.contains("\"")
                                | citekey.contains(";")
                                | citekey.contains("!")
                                | citekey.contains("\'")
                            {
                                self.bibiman.open_popup(
                                    PopupKind::MessageError,
                                    Some("Selected entrys citekey contains special char: "),
                                    Some(&citekey),
                                    None,
                                );
                            } else if let Some(ext) = &cfg.general.note_extensions
                                && cfg.general.note_path.is_some()
                                && entry.notes.is_none()
                            {
                                let mut items = vec![];
                                for ex in ext {
                                    items.push((
                                        entry.citekey().to_string(),
                                        ex.clone(),
                                        PopupItem::Notefile,
                                    ));
                                }
                                self.bibiman.open_popup(
                                    PopupKind::CreateNote,
                                    None,
                                    None,
                                    Some(items),
                                );
                            } else if cfg.general.note_path.is_some() && entry.notes.is_some() {
                                self.bibiman.open_popup(
                                    PopupKind::MessageError,
                                    Some("Selected entry already has a connected note"),
                                    None,
                                    None,
                                );
                            } else {
                                self.bibiman.open_popup(
                                    PopupKind::MessageError,
                                    Some("No note path found. Set it in config file."),
                                    None,
                                    None,
                                );
                            }
                        }
                        Err(msg) => {
                            warn!("Create note: {}", &msg);
                            self.bibiman.open_popup(
                                PopupKind::MessageError,
                                Some(&msg),
                                None,
                                None,
                            );
                        }
                    }
                }
            }
            CmdAction::ShowHelp => {
                self.bibiman.open_popup(PopupKind::Help, None, None, None);
            }
            CmdAction::Exit => {
                self.quit();
            }
            CmdAction::Nothing => {}
        }
        Ok(())
    }
}

pub fn open_connected_file(cfg: &BibiConfig, file: &OsStr) -> Result<()> {
    // Build command to execute pdf-reader. 'xdg-open' is Linux standard
    let cmd = &cfg.general.pdf_opener;

    // Pass filepath as argument, pipe stdout and stderr to /dev/null
    // to keep the TUI clean (where is it piped on Windows???)
    match Command::new(cmd)
        .arg(file)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
    {
        Ok(_) => Ok(()),
        Err(e) => Err(e.into()),
    }
}

pub fn open_connected_link(cfg: &BibiConfig, link: &str) -> Result<()> {
    // Build command to execute pdf-reader. 'xdg-open' is Linux standard
    let cmd = &cfg.general.url_opener;
    // Pass filepath as argument, pipe stdout and stderr to /dev/null
    // to keep the TUI clean (where is it piped on Windows???)
    match Command::new(cmd)
        .arg(link)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
    {
        Ok(_) => Ok(()),
        Err(e) => Err(e.into()),
    }
}

pub fn prepare_weblink(url: &str) -> String {
    let url = if url.starts_with("10.") {
        "https://doi.org/".to_string() + url
    } else if url.starts_with("www.") {
        "https://".to_string() + url
    } else {
        url.to_string()
    };
    url
}

/// Expand leading tilde (`~`) to `/home/user`
pub fn expand_home<P: AsRef<Path>>(path: P) -> PathBuf {
    if path.as_ref().starts_with("~") {
        let home: PathBuf = if let Some(home) = dirs::home_dir() {
            home
        } else if let Ok(home) = std::env::var("HOME") {
            PathBuf::from(home)
        } else {
            PathBuf::new()
        };
        let path = path.as_ref().strip_prefix("~").unwrap_or(Path::new("./"));
        home.join(path)
    } else {
        path.as_ref().into()
    }
}

/// Convert `Vec<(&str, &str)` to `Vec<(String, String)`
pub fn convert_to_owned_vec(mut items: Vec<(&str, &str)>) -> Vec<(String, String)> {
    items
        .iter_mut()
        .map(|(msg, obj)| (msg.to_string(), obj.to_string()))
        .collect()
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_home_expansion() {
        let path: PathBuf = "~/path/to/file.txt".into();

        let path = expand_home(&path);

        let home: String = dirs::home_dir().unwrap().to_str().unwrap().to_string();

        let full_path = home + "/path/to/file.txt";

        assert_eq!(path, PathBuf::from(full_path))
    }
}
