// 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::app::expand_home;
use crate::bibiman::citekeys::CitekeyFormatting;
use crate::bibiman::entries::EntryTableColumn;
use crate::bibiman::{bibisetup::*, search::BibiSearch};
use crate::cliargs::CLIArgs;
use crate::config::BibiConfig;
use crate::tui::Tui;
use crate::tui::popup::{PopupArea, PopupItem, PopupKind};
use crate::{app, cliargs};
use crate::{bibiman::entries::EntryTable, bibiman::keywords::TagList};
use biblatex::Bibliography;
use color_eyre::eyre::{Context, Result};
use crossterm::event::KeyCode;
use editor_command::EditorBuilder;
use log::{error, warn};
use ratatui::widgets::ScrollbarState;
use std::ffi::OsStr;
use std::fs::{self};
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::result::Result::Ok;
use std::time::{Duration, Instant};
use tui_input::Input;

pub mod bibisetup;
pub mod citekeys;
pub mod clipboard;
pub mod entries;
pub mod keywords;
pub mod search;

/// Module with function to sanitize text with LaTeX Macros into readable unicode text.
pub mod sanitize;

// Areas in which actions are possible
#[derive(Debug, Default, PartialEq, PartialOrd, Eq)]
pub enum CurrentArea {
    #[default]
    EntryArea,
    TagArea,
    SearchArea,
    PopupArea,
}

// Check which area was active when popup set active
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum FormerArea {
    EntryArea,
    TagArea,
    SearchArea,
}

// Application.
#[derive(Debug)]
pub struct Bibiman {
    // main bib file
    pub main_bibfiles: Vec<PathBuf>,
    // main bibliography
    pub main_biblio: BibiSetup,
    // search struct:
    pub search_struct: BibiSearch,
    // tag list
    pub tag_list: TagList,
    // table items
    pub entry_table: EntryTable,
    // scroll state info buffer
    pub scroll_info: u16,
    // area
    pub current_area: CurrentArea,
    // mode for popup window
    pub former_area: Option<FormerArea>,
    // active popup
    pub popup_area: PopupArea,
}

impl Bibiman {
    /// Constructs a new instance of [`Bibiman`].
    pub fn new(args: &mut CLIArgs, cfg: &mut BibiConfig) -> Result<Self> {
        let mut main_bibfiles: Vec<PathBuf> = args.pos_args.clone();
        if !args.cli_only() {
            if cfg.general.bibfiles.is_some() {
                main_bibfiles.append(cfg.general.bibfiles.as_mut().unwrap())
            };
        }
        let main_bibfiles = cliargs::parse_files(main_bibfiles);
        // TODO: insert workflow for formatting citekeys
        let main_biblio = BibiSetup::new(&main_bibfiles, cfg);
        let tag_list = TagList::new(main_biblio.keyword_list.clone());
        let search_struct = BibiSearch::default();
        let entry_table = EntryTable::new(main_biblio.entry_list.clone());
        let current_area = CurrentArea::EntryArea;
        Ok(Self {
            main_bibfiles,
            main_biblio,
            tag_list,
            search_struct,
            entry_table,
            scroll_info: 0,
            current_area,
            former_area: None,
            popup_area: PopupArea::default(),
        })
    }

    pub fn show_help(&mut self) {
        if let CurrentArea::EntryArea = self.current_area {
            self.former_area = Some(FormerArea::EntryArea);
        } else if let CurrentArea::TagArea = self.current_area {
            self.former_area = Some(FormerArea::TagArea);
        }
        self.popup_area.is_popup = true;
        self.current_area = CurrentArea::PopupArea;
        self.popup_area.popup_kind = Some(PopupKind::Help);
    }

    /// Close all current popups and select former tab of main app
    pub fn close_popup(&mut self) {
        // Reset all popup fields to default values
        self.popup_area = PopupArea::default();

        // Go back to previously selected area
        if let Some(FormerArea::EntryArea) = self.former_area {
            self.current_area = CurrentArea::EntryArea
        } else if let Some(FormerArea::TagArea) = self.former_area {
            self.current_area = CurrentArea::TagArea
        }

        // Clear former_area field
        self.former_area = None;
    }

    /// Open a popup
    ///
    /// Necessary arguments are:
    ///
    /// - `popup_kind`: a valid value of the `PopupKind` `enum`. This determines the
    /// further behaviour of the popup.
    /// - `message`: A message shown in the popup. This is needed for the `PopupKind`
    /// values `MessageConfirm` and `MessageError`. If not needed, set it to `None`.
    /// - `object`: An object passed as `&str` which might explain the current popup
    /// action. Its not needed, but very useful. Can be used with the `PopupKind`
    /// values `MessageConfirm`, `MessageError` and `YankItem`. If not needed, set it
    /// to `None`.
    /// - `items`: A vector of items which are needed if a selectable list is rendered.
    /// The vector consists of tuples including a pair of `String`. The second item of
    /// the tuple is considered kind of an object which can be used e.g. to open
    /// the given filepath etc. If not needed, set it to `None`.
    ///
    /// The function will panic if a needed argument for the particular `PopupKind`
    /// is missing
    pub fn open_popup(
        &mut self,
        popup_kind: PopupKind,
        message: Option<&str>,
        object: Option<&str>,
        items: Option<Vec<(String, String, PopupItem)>>,
    ) {
        if let CurrentArea::EntryArea = self.current_area {
            self.former_area = Some(FormerArea::EntryArea);
        } else if let CurrentArea::TagArea = self.current_area {
            self.former_area = Some(FormerArea::TagArea);
        }
        self.popup_area.is_popup = true;
        self.current_area = CurrentArea::PopupArea;

        match popup_kind {
            PopupKind::Help => {
                self.popup_area.popup_kind = Some(PopupKind::Help);
            }
            PopupKind::MessageConfirm => {
                self.popup_area.popup_kind = Some(PopupKind::MessageConfirm);
                if object.is_some() && message.is_some() {
                    self.popup_area.popup_message = message.unwrap().to_owned() + object.unwrap();
                } else if object.is_none() && message.is_some() {
                    self.popup_area.popup_message = message.unwrap().to_owned();
                } else {
                    panic!(
                        "You need to past at least a message via Some(&str) to create a message popup",
                    )
                }
            }
            PopupKind::MessageError => {
                self.popup_area.popup_kind = Some(PopupKind::MessageError);
                if object.is_some() && message.is_some() {
                    self.popup_area.popup_message = message.unwrap().to_owned() + object.unwrap();
                } else if object.is_none() && message.is_some() {
                    self.popup_area.popup_message = message.unwrap().to_owned();
                } else {
                    panic!(
                        "You need to past at least a message via Some(&str) to create a message popup",
                    )
                }
            }
            PopupKind::OpenRes => {
                if items.is_some() {
                    self.popup_area.popup_kind = Some(PopupKind::OpenRes);
                    self.popup_area.popup_selection(items.unwrap());
                    self.popup_area.popup_state.select(Some(0));
                } else {
                    panic!("No Vec<(String, String)> passed as argument to generate the items list",)
                }
            }
            PopupKind::AppendToFile => {
                if items.is_some() {
                    self.popup_area.popup_kind = Some(PopupKind::AppendToFile);
                } else {
                    panic!("No Vec<(String, String)> passed as argument to generate the items list",)
                }
            }
            PopupKind::AddEntry => {
                self.popup_area.popup_kind = Some(PopupKind::AddEntry);
            }
            PopupKind::YankItem => {
                if items.is_some() {
                    self.popup_area.popup_kind = Some(PopupKind::YankItem);
                    self.popup_area.popup_selection(items.unwrap());
                    self.popup_area.popup_state.select(Some(0));
                } else {
                    panic!("No Vec<(String, String)> passed as argument to generate the items list",)
                }
            }
            PopupKind::CreateNote => {
                if items.is_some() {
                    self.popup_area.popup_kind = Some(PopupKind::CreateNote);
                    self.popup_area.popup_selection(items.unwrap());
                    self.popup_area.popup_state.select(Some(0));
                } else {
                    panic!("No Vec<(String, String)> passed as argument to generate the items list",)
                }
            }
            PopupKind::SearchField => {
                if let Some(items) = items {
                    self.popup_area.popup_kind = Some(PopupKind::SearchField);
                    self.popup_area.popup_selection(items);
                    self.popup_area.popup_state.select(Some(0));
                } else {
                    panic!("No Vec<(String, String)> passed as argument to generate the items list",)
                }
            }
        }
    }

    pub fn update_lists(&mut self, cfg: &BibiConfig) {
        self.main_biblio = BibiSetup::new(&self.main_bibfiles, cfg);
        self.tag_list = TagList::new(self.main_biblio.keyword_list.clone());
        self.entry_table = EntryTable::new(self.main_biblio.entry_list.clone());
    }

    /// Toggle moveable list between entries and tags
    pub fn toggle_area(&mut self) {
        if let CurrentArea::EntryArea = self.current_area {
            self.entry_table.entry_scroll_state = self.entry_table.entry_scroll_state.position(0);
            self.current_area = CurrentArea::TagArea;
            self.tag_list.tag_list_state.select(Some(0));
            self.tag_list.tag_scroll_state = self
                .tag_list
                .tag_scroll_state
                .position(self.tag_list.tag_list_state.selected().unwrap());
        } else if let CurrentArea::TagArea = self.current_area {
            self.current_area = CurrentArea::EntryArea;
            self.tag_list.tag_list_state.select(None);
            self.entry_table.entry_scroll_state = self
                .entry_table
                .entry_scroll_state
                .position(self.entry_table.entry_table_state.selected().unwrap());
        }
    }

    pub fn reset_current_list(&mut self) {
        self.entry_table = EntryTable::new(self.main_biblio.entry_list.clone());
        self.tag_list = TagList::new(self.main_biblio.keyword_list.clone());
        if let CurrentArea::TagArea = self.current_area {
            self.tag_list.tag_list_state.select(Some(0))
        }
        self.entry_table.entry_table_at_search_start.clear();
        self.search_struct.filtered_tag_list.clear();
        self.search_struct.inner_search = false;
        self.search_struct.search_string.clear();
        self.former_area = None
    }

    /// Yank the passed string to system clipboard
    pub fn yank_text(cfg: &BibiConfig, selection: &str) -> Result<(), clipboard::ClipboardError> {
        cfg.get_clipboard().set_contents(selection)
    }

    pub fn scroll_info_down(&mut self) {
        self.entry_table.entry_info_scroll = self.entry_table.entry_info_scroll.saturating_add(1);
        self.entry_table.entry_info_scroll_state = self
            .entry_table
            .entry_info_scroll_state
            .position(self.entry_table.entry_info_scroll.into());
    }

    pub fn scroll_info_up(&mut self) {
        self.entry_table.entry_info_scroll = self.entry_table.entry_info_scroll.saturating_sub(1);
        self.entry_table.entry_info_scroll_state = self
            .entry_table
            .entry_info_scroll_state
            .position(self.entry_table.entry_info_scroll.into());
    }
}

impl Bibiman {
    // Entry Table commands

    /// Select next entry in Table holding the bibliographic entries.
    ///
    /// Takes u16 value as argument to specify number of entries which
    /// should be scrolled
    pub fn select_next_entry(&mut self, entries: u16) {
        self.entry_table.entry_info_scroll = 0;
        self.entry_table.entry_info_scroll_state =
            self.entry_table.entry_info_scroll_state.position(0);
        self.entry_table.entry_table_state.scroll_down_by(entries);
        self.entry_table.entry_scroll_state = self
            .entry_table
            .entry_scroll_state
            .position(self.entry_table.entry_table_state.selected().unwrap());
    }

    /// Select previous entry in Table holding the bib entries.
    ///
    /// Takes u16 value as argument to specify number of entries which
    /// should be scrolled
    pub fn select_previous_entry(&mut self, entries: u16) {
        self.entry_table.entry_info_scroll = 0;
        self.entry_table.entry_info_scroll_state =
            self.entry_table.entry_info_scroll_state.position(0);
        self.entry_table.entry_table_state.scroll_up_by(entries);
        self.entry_table.entry_scroll_state = self
            .entry_table
            .entry_scroll_state
            .position(self.entry_table.entry_table_state.selected().unwrap());
    }

    /// Select first entry in bib list
    pub fn select_first_entry(&mut self) {
        self.entry_table.entry_info_scroll = 0;
        self.entry_table.entry_info_scroll_state =
            self.entry_table.entry_info_scroll_state.position(0);
        self.entry_table.entry_table_state.select_first();
        self.entry_table.entry_scroll_state = self.entry_table.entry_scroll_state.position(0);
    }

    /// Select last entry in bib list
    pub fn select_last_entry(&mut self) {
        self.entry_table.entry_info_scroll = 0;
        self.entry_table.entry_info_scroll_state =
            self.entry_table.entry_info_scroll_state.position(0);
        // self.entry_table.entry_table_state.select_last(); // Does not work properly after upgrading to ratatui 0.29.0
        self.entry_table
            .entry_table_state
            .select(Some(self.entry_table.entry_table_items.len() - 1));
        self.entry_table.entry_scroll_state = self
            .entry_table
            .entry_scroll_state
            .position(self.entry_table.entry_table_items.len());
    }

    /// Select next (right) column of entry table
    pub fn select_next_column(&mut self) {
        if self
            .entry_table
            .entry_table_state
            .selected_column()
            .is_some_and(|col| col == 4)
        {
            self.entry_table.entry_table_state.select_column(Some(1));
        } else {
            self.entry_table.entry_table_state.select_next_column();
        }
        match self.entry_table.entry_table_selected_column {
            EntryTableColumn::Authors => {
                self.entry_table.entry_table_selected_column = EntryTableColumn::Title;
            }
            EntryTableColumn::Title => {
                self.entry_table.entry_table_selected_column = EntryTableColumn::Year;
            }
            EntryTableColumn::Year => {
                self.entry_table.entry_table_selected_column = EntryTableColumn::Pubtype;
            }
            EntryTableColumn::Pubtype => {
                self.entry_table.entry_table_selected_column = EntryTableColumn::Authors;
            }
        }
    }

    /// Select previous (left) column of entry table
    pub fn select_prev_column(&mut self) {
        if self
            .entry_table
            .entry_table_state
            .selected_column()
            .is_some_and(|col| col == 1)
        {
            self.entry_table.entry_table_state.select_last_column();
        } else {
            self.entry_table.entry_table_state.select_previous_column();
        }
        match self.entry_table.entry_table_selected_column {
            EntryTableColumn::Authors => {
                self.entry_table.entry_table_selected_column = EntryTableColumn::Pubtype;
            }
            EntryTableColumn::Title => {
                self.entry_table.entry_table_selected_column = EntryTableColumn::Authors;
            }
            EntryTableColumn::Year => {
                self.entry_table.entry_table_selected_column = EntryTableColumn::Title;
            }
            EntryTableColumn::Pubtype => {
                self.entry_table.entry_table_selected_column = EntryTableColumn::Year;
            }
        }
    }

    pub fn select_entry_by_citekey(&mut self, citekey: &str) {
        // Search for entry by matching citekeys
        let mut idx_count = 0;
        loop {
            if idx_count == self.entry_table.entry_table_items.len() {
                idx_count = 0;
                break;
            } else if self.entry_table.entry_table_items[idx_count]
                .citekey
                .contains(citekey)
            {
                break;
            }
            idx_count += 1
        }

        // Set selected entry to vec-index of match
        self.entry_table.entry_table_state.select(Some(idx_count));
    }

    pub fn run_editor(&mut self, cfg: &BibiConfig, tui: &mut Tui) -> Result<()> {
        // get filecontent and citekey for calculating line number
        let citekey: &str = if let Some(entry) = self.entry_table.entry_table_state.selected() {
            &self.entry_table.entry_table_items[entry].citekey.clone()
        } else {
            warn!("Can't get citekey for opening");
            return Err(color_eyre::Report::msg("Can't get selected entry"));
        };

        // Add comma as suffix that only
        // main citekeys are matched, not other fields like crossref
        let citekey_pattern: String = format!("{},", citekey);

        // Check if multiple files were passed to bibiman and
        // return the correct file path
        let filepath = if self.main_bibfiles.len() == 1 {
            self.main_bibfiles.first().unwrap().as_os_str()
        } else {
            let mut idx = 0;
            for f in &self.main_bibfiles {
                if search::search_pattern_in_file(&citekey_pattern, &f).is_some() {
                    break;
                }
                idx += 1;
            }
            self.main_bibfiles[idx].as_os_str()
        };

        let filecontent = match fs::read_to_string(&filepath) {
            Ok(str) => str,
            Err(e) => {
                error!(
                    "Can't read file {} to string for opening",
                    filepath.display()
                );
                return Err(e.into());
            }
        };

        // Search the line number to place the cursor at
        let mut line_count = 0;

        for line in filecontent.lines() {
            line_count += 1;
            // if reaching the citekey break the loop
            // if reaching end of lines without match, reset to 0
            if line.contains(&citekey_pattern) {
                break;
            } else if line_count == filecontent.len() {
                eprintln!(
                    "Citekey {} not found, opening file {} at line 1",
                    citekey,
                    filepath.to_string_lossy()
                );
                line_count = 0;
                break;
            }
        }

        // Exit TUI to enter editor
        tui.exit()?;
        // Use VISUAL or EDITOR. Set "vi" as last fallback
        let mut cmd: Command = EditorBuilder::new()
            .string(
                cfg.get_ext_editor()
                    .map(|(cmd, args)| cmd.to_owned() + " " + &args.unwrap_or_default().join(" ")),
            )
            .environment()
            .string(Some("vi"))
            .build()?
            .open_at(filepath, line_count as u32, 0);
        // Prepare arguments to open file at specific line
        let status = cmd.status()?;
        if !status.success() {
            eprintln!("Spawning editor failed with status {}", status);
        }

        // Enter TUI again
        tui.enter()?;
        tui.terminal.clear()?;

        // Update the database and the lists to show changes
        Self::update_lists(self, cfg);

        // Select entry which was selected before entering editor
        self.select_entry_by_citekey(citekey);

        Ok(())
    }

    pub fn open_connected_note(
        &mut self,
        cfg: &BibiConfig,
        tui: &mut Tui,
        file: &OsStr,
    ) -> Result<()> {
        // get filecontent and citekey for calculating line number

        match std::env::var("TERM") {
            Ok(sh) => {
                let editor = if let Some(e) = cfg.get_ext_editor() {
                    e.0.to_string()
                } else if let Ok(e) = std::env::var("VISUAL") {
                    e
                } else if let Ok(e) = std::env::var("EDITOR") {
                    e
                } else {
                    String::from("vi")
                };
                let _ = Command::new(sh)
                    .arg("-e")
                    .arg(editor)
                    .args(
                        if let Some(args) = cfg.get_ext_editor()
                            && let Some(a) = args.1
                        {
                            a.to_vec()
                        } else {
                            Vec::new()
                        },
                    )
                    .arg(file)
                    .stdout(Stdio::null())
                    .stderr(Stdio::null())
                    .spawn()
                    .wrap_err("Couldn't run editor");
            }
            Err(_e) => {
                let citekey: &str =
                    if let Some(entry) = self.entry_table.entry_table_state.selected() {
                        &self.entry_table.entry_table_items[entry].citekey.clone()
                    } else {
                        warn!("Can't get citekey for opening");
                        return Err(color_eyre::Report::msg("Can't get selected entry"));
                    };
                // Exit TUI to enter editor
                tui.exit()?;
                // Use VISUAL or EDITOR. Set "vi" as last fallback
                let mut note_cmd: Command = EditorBuilder::new()
                    .string(cfg.get_ext_editor().map(|(cmd, args)| {
                        cmd.to_owned() + " " + &args.unwrap_or_default().join(" ")
                    }))
                    .environment()
                    .string(Some("vi"))
                    .build()?
                    .open(file);
                // Prepare arguments to open file at specific line
                let status = note_cmd.status()?;
                if !status.success() {
                    eprintln!("Spawning editor failed with status {}", status);
                }

                // Enter TUI again
                tui.enter()?;
                tui.terminal.clear()?;

                // Update the database and the lists to show changes
                // Self::update_lists(self, cfg);

                // Select entry which was selected before entering editor
                self.select_entry_by_citekey(citekey);
            }
        }

        Ok(())
    }

    pub fn add_entry(&mut self) {
        if let CurrentArea::EntryArea = self.current_area {
            self.former_area = Some(FormerArea::EntryArea);
        }
        self.popup_area.is_popup = true;
        self.current_area = CurrentArea::PopupArea;
        self.popup_area.popup_kind = Some(PopupKind::AddEntry);
    }

    ///Try to resolve entered DOI. If successfull, choose file where to append
    ///the new entry via `append_to_file()` function. If not, show error popup
    ///
    ///The method needs two arguments: the CLIArgs struct and the `str` containing the DOI
    pub fn handle_new_entry_submission(&mut self, doi_string: &str) -> Result<()> {
        let doi_string = if doi_string.starts_with("10.") {
            "https://doi.org/".to_string() + doi_string
        } else {
            doi_string.to_owned()
        };

        // Send GET request to doi resolver
        let doi_entry = ureq::get(&doi_string)
            .header("Accept", "application/x-bibtex")
            .call();

        if let Ok(entry) = doi_entry {
            // Save generated bibtex entry in structs field
            let entry = entry
                .into_body()
                .read_to_string()
                .expect("Couldn't parse fetched entry into string");
            self.popup_area.popup_sel_item = entry;
            self.popup_area.popup_kind = Some(PopupKind::AppendToFile);
            self.append_to_file();
            self.former_area = Some(FormerArea::EntryArea);
            self.current_area = CurrentArea::PopupArea;
            self.popup_area.popup_state.select(Some(0))
        } else {
            self.open_popup(
                PopupKind::MessageError,
                Some("Can't find DOI: "),
                Some(&doi_string),
                None,
            );
            // self.popup_area
            //     .popup_message("Can't find DOI: ", &doi_string, false);
        }
        Ok(())
    }

    pub fn append_to_file(&mut self) {
        let mut items = vec![(
            "Create new file".to_owned(),
            "".to_string(),
            PopupItem::Default,
        )];
        if self.main_bibfiles.len() > 1 {
            for f in self.main_bibfiles.clone() {
                items.push((
                    "File: ".into(),
                    f.to_str().unwrap().to_owned(),
                    PopupItem::Bibfile,
                ));
            }
        } else {
            items.push((
                "File: ".into(),
                self.main_bibfiles
                    .first()
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .to_owned(),
                PopupItem::Bibfile,
            ));
        }
        self.popup_area.popup_selection(items);
    }

    pub fn append_entry_to_file(&mut self, cfg: &BibiConfig) -> Result<(), String> {
        // Index of selected popup field
        let popup_idx = self.popup_area.popup_state.selected().unwrap();
        let new_file = self.popup_area.popup_list[popup_idx]
            .0
            .contains("Create new file");

        let new_bib_entry = match Bibliography::parse(&self.popup_area.popup_sel_item) {
            Ok(bib) => bib,
            Err(e) => {
                error!("Couldn't parse downloaded bib entry: {}", e.to_string());
                return Err(format!(
                    "Couldn't parse downloaded bib entry: {}",
                    e.to_string()
                ));
            }
        };

        let formatted_struct =
            if let Some(formatter) = CitekeyFormatting::new(cfg, new_bib_entry.clone()) {
                Some(formatter.do_formatting())
            } else {
                None
            };

        let (new_citekey, mut entry_string) = if let Some(mut formatter) = formatted_struct {
            (
                formatter.get_citekey_pair(0).unwrap().1,
                formatter.print_updated_bib_as_string(),
            )
        } else {
            let keys = new_bib_entry.keys().collect::<Vec<&str>>();
            (keys[0].to_string(), new_bib_entry.to_biblatex_string())
        };
        self.close_popup();

        if let Some(formatted_str) = self.format_added_entry(cfg, entry_string.as_bytes()) {
            entry_string = formatted_str
        } else {
            self.open_popup(
                PopupKind::MessageError,
                Some("Formatting failed, will add new entry as is: "),
                Some(&new_citekey),
                None,
            );
        }

        // Check if new file or existing file was choosen
        let mut file = if new_file {
            let citekey = PathBuf::from(&new_citekey);
            // Get path of current files
            let path: PathBuf = if self.main_bibfiles[0].is_file() {
                self.main_bibfiles[0].parent().unwrap().to_owned()
            } else {
                dirs::home_dir().unwrap() // home dir as fallback
            };

            let citekey = citekey.with_extension("bib");

            let newfile = path.join(citekey);

            self.main_bibfiles.push(newfile.clone());

            match File::create_new(newfile) {
                Ok(file) => file,
                Err(e) => {
                    error!(
                        "Can't create file {} for new entry: {}.",
                        path.display(),
                        e.to_string()
                    );
                    return Err(format!(
                        "Can't create file {} for new entry.",
                        path.display()
                    ));
                }
            }
        } else {
            let file_path = &self.main_bibfiles[popup_idx - 1];

            match OpenOptions::new().append(true).open(file_path) {
                Ok(file) => file,
                Err(e) => {
                    error!(
                        "Can't open file {} for adding new entry: {}.",
                        file_path.display(),
                        e.to_string()
                    );
                    return Err(format!(
                        "Can't open file {} for adding new entry.",
                        file_path.display()
                    ));
                }
            }
        };
        // Optionally, add a newline before the content
        match file.write_all(b"\n") {
            Ok(_) => {}
            Err(e) => {
                error!("Couldn't write new entry to file: {}", e.to_string());
                return Err(format!("Couldn't write new entry to file"));
            }
        };
        // Write content to file
        file.write_all(entry_string.as_bytes()).unwrap_or_default();
        // Update the database and the lists to reflect the new content
        self.update_lists(cfg);

        // Select newly created entry
        self.select_entry_by_citekey(&new_citekey);

        Ok(())
    }

    fn format_added_entry(&mut self, cfg: &BibiConfig, entry_string: &[u8]) -> Option<String> {
        if let Some((cmd, args)) = cfg.get_ext_formatter() {
            let mut cmd = if let Ok(cmd) = Command::new(cmd)
                .args(if let Some(a) = args {
                    a.to_vec()
                } else {
                    vec![]
                })
                .stdin(Stdio::piped())
                .stdout(Stdio::piped())
                .stderr(Stdio::null())
                .spawn()
            {
                cmd
            } else {
                return None;
            };

            if let Some(ref mut stdin) = cmd.stdin {
                let max_time = Duration::new(3, 0);
                stdin
                    .write_all(entry_string)
                    .expect("Couldn't write to stdin");
                drop(cmd.stdin.take());
                let start = Instant::now();

                loop {
                    if let Ok(status) = cmd.try_wait() {
                        if status.is_some() {
                            break; // Process finished successfully
                        }
                    }

                    // Check if the timeout is reached
                    if start.elapsed() >= max_time {
                        let _ = cmd.kill(); // Aborts the process

                        return None;
                    }

                    // Sleep briefly before checking again
                    std::thread::sleep(Duration::from_millis(100));
                }
                let formatted_str = cmd
                    .wait_with_output()
                    .expect("No ouput for formatted string");
                let formatted_str = String::from_utf8(formatted_str.stdout)
                    .expect("Not able to read stdout into string");
                if !formatted_str.is_empty() {
                    return Some(formatted_str);
                } else {
                    return None;
                }
            } else {
                return None;
            }
        } else {
            None
        }
    }

    pub fn create_note(&mut self, cfg: &BibiConfig) -> Result<()> {
        // Index of selected entry
        let entry_idx = self.entry_table.entry_table_state.selected().unwrap();
        let citekey = self.entry_table.entry_table_items[entry_idx]
            .citekey
            .clone();

        // Index of selected popup field
        let popup_idx = self.popup_area.popup_state.selected().unwrap();
        let ext = self.popup_area.popup_list[popup_idx].1.clone();

        let basename = PathBuf::from(&citekey).with_extension(ext);
        let path = cfg.general.note_path.as_ref().unwrap();

        let new_file = path.join(basename);

        let new_file = if new_file.starts_with("~") {
            expand_home(&new_file)
        } else {
            new_file
        };

        File::create_new(new_file).unwrap();
        self.close_popup();
        self.update_lists(cfg);
        self.select_entry_by_citekey(&citekey);
        Ok(())
    }

    pub fn open_connected_res(&mut self, cfg: &BibiConfig, tui: &mut Tui) -> Result<()> {
        // Index of selected entry
        let entry_idx = self.entry_table.entry_table_state.selected().unwrap();

        // Index of selected popup field
        let popup_idx = self.popup_area.popup_state.selected().unwrap();
        let popup_entry = self.popup_area.popup_list[popup_idx].1.clone();

        // Choose ressource depending an selected popup field
        if let PopupItem::Link = self.popup_area.popup_list[popup_idx].2 {
            let object = self.entry_table.entry_table_items[entry_idx].doi_url();
            let url = app::prepare_weblink(object);
            self.close_popup();
            match app::open_connected_link(cfg, &url) {
                Ok(_) => {}
                Err(e) => {
                    warn!("Couldn't open link {}: {}", &url, e.to_string());
                    self.open_popup(
                        PopupKind::MessageError,
                        Some("Couldn't open link"),
                        None,
                        None,
                    );
                }
            }
        } else if let PopupItem::Entryfile = self.popup_area.popup_list[popup_idx].2 {
            // TODO: Selection for multiple files
            // let object = self.entry_table.entry_table_items[entry_idx].filepath()[0];
            let file = expand_home(&PathBuf::from(popup_entry.clone()));
            // let object: OsString = popup_entry.into();
            if file.is_file() {
                self.close_popup();
                match app::open_connected_file(cfg, &file.as_os_str()) {
                    Ok(_) => {}
                    Err(e) => {
                        warn!("Couldn't open file {}: {}", &file.display(), e.to_string());
                        self.open_popup(
                            PopupKind::MessageError,
                            Some("Couldn't open file"),
                            None,
                            None,
                        );
                    }
                }
            } else {
                self.open_popup(
                    PopupKind::MessageError,
                    Some("No valid file path: "),
                    Some(file.to_str().unwrap()),
                    None,
                );
            }
        } else if let PopupItem::Notefile = self.popup_area.popup_list[popup_idx].2 {
            let file = expand_home(&PathBuf::from(popup_entry.clone()));
            // let object: OsString = popup_entry.into();
            if file.is_file() {
                self.open_connected_note(cfg, tui, &file.into_os_string())?;
                self.close_popup();
            } else {
                self.open_popup(
                    PopupKind::MessageError,
                    Some("No valid file path: "),
                    Some(file.to_str().unwrap()),
                    None,
                );
            }
        } else {
            eprintln!("Unable to find ressource to open");
        };
        // run command to open file/Url

        Ok(())
    }

    pub fn yank_entry_field(&mut self, cfg: &BibiConfig) -> Result<()> {
        // Index of selected popup field
        let popup_idx = self.popup_area.popup_state.selected().unwrap();
        let popup_entry = self.popup_area.popup_list[popup_idx].1.clone();

        let kind = self.popup_area.popup_list[popup_idx]
            .0
            .to_lowercase()
            .split(":")
            .next()
            .unwrap()
            .to_owned();

        let msg = format!("Yanked {} to clipboard: ", &kind);

        match Bibiman::yank_text(cfg, &popup_entry) {
            Ok(_) => {
                self.open_popup(
                    PopupKind::MessageConfirm,
                    Some(&msg),
                    Some(&popup_entry),
                    None,
                );
            }
            Err(e) => {
                self.open_popup(
                    PopupKind::MessageError,
                    Some("Can't yank field. "),
                    Some(&e.to_string()),
                    None,
                );
            }
        }

        Ok(())
    }

    /// Fast opening/yanking of file/link or citekey through simple keypress in
    /// the particular popup mode:
    ///
    /// **Opening popup**
    ///
    /// `o` -> opens the first file of the `filepath` `Vec` for the current entry
    /// `l` -> opens the link of the current entry
    /// `n` -> opens the first note
    ///
    /// **Yanking popup**
    ///
    /// `y` -> yanks the citekey for the current entry
    pub fn fast_selection(
        &mut self,
        cfg: &BibiConfig,
        tui: &mut Tui,
        key_code: KeyCode,
    ) -> Result<()> {
        if let CurrentArea::PopupArea = self.current_area {
            match self.entry_table.get_selected_entry() {
                Ok(entry) => {
                    match self.popup_area.popup_kind {
                        Some(PopupKind::OpenRes) => match key_code {
                            KeyCode::Char('o') => {
                                // let file = entry.filepath.clone();
                                if let Some(files) = &entry.filepath {
                                    let file = if let Some(prefix) = &cfg.general.file_prefix
                                        && entry.file_field
                                    {
                                        prefix.join(&files[0]).into_os_string()
                                    } else {
                                        files[0].clone()
                                    };
                                    let file = expand_home(&PathBuf::from(file));
                                    // let object: OsString = popup_entry.into();
                                    if file.is_file() {
                                        self.close_popup();
                                        match app::open_connected_file(cfg, &file.as_os_str()) {
                                            Ok(_) => {}
                                            Err(e) => {
                                                warn!(
                                                    "Couldn't open file {}: {}",
                                                    &file.display(),
                                                    e.to_string()
                                                );
                                                self.open_popup(
                                                    PopupKind::MessageError,
                                                    Some("Couldn't open file"),
                                                    None,
                                                    None,
                                                );
                                            }
                                        }
                                    } else {
                                        self.open_popup(
                                            PopupKind::MessageError,
                                            Some("No valid file path: "),
                                            Some(file.to_str().unwrap()),
                                            None,
                                        );
                                    }
                                }
                            }
                            KeyCode::Char('n') => {
                                let file = entry.notes.clone();
                                if file.is_some() {
                                    let file =
                                        expand_home(&PathBuf::from(file.unwrap()[0].clone()));
                                    // let object: OsString = popup_entry.into();
                                    if file.is_file() {
                                        self.open_connected_note(cfg, tui, &file.into_os_string())?;
                                        self.close_popup();
                                    } else {
                                        self.open_popup(
                                            PopupKind::MessageError,
                                            Some("No valid file path: "),
                                            Some(file.to_str().unwrap()),
                                            None,
                                        );
                                    }
                                }
                            }
                            KeyCode::Char('l') => {
                                if entry.doi_url.is_some() {
                                    let object = entry.doi_url();
                                    let url = app::prepare_weblink(object);
                                    self.close_popup();
                                    match app::open_connected_link(cfg, &url) {
                                        Ok(_) => {}
                                        Err(e) => {
                                            warn!("Couldn't open link {}: {}", &url, e.to_string());
                                            self.open_popup(
                                                PopupKind::MessageError,
                                                Some("Couldn't open link"),
                                                None,
                                                None,
                                            );
                                        }
                                    }
                                }
                            }
                            _ => {}
                        },
                        Some(PopupKind::YankItem) => match key_code {
                            KeyCode::Char('y') => {
                                let key = entry.citekey.clone();
                                match Bibiman::yank_text(cfg, &key) {
                                    Ok(_) => {
                                        self.open_popup(
                                            PopupKind::MessageConfirm,
                                            Some("Yanked citekey to clipboard: "),
                                            Some(&key),
                                            None,
                                        );
                                    }
                                    Err(e) => {
                                        self.open_popup(
                                            PopupKind::MessageError,
                                            Some("Can't yank citekey. "),
                                            Some(&e.to_string()),
                                            None,
                                        );
                                    }
                                }
                            }
                            _ => {}
                        },
                        _ => {}
                    }
                }
                Err(msg) => {
                    warn!("Fast selection: {}", &msg);
                    self.open_popup(PopupKind::MessageError, Some(&msg), None, None);
                }
            }
        }
        Ok(())
    }

    // Search entry list
    pub fn search_entries(&mut self) {
        // Use snapshot of entry list saved when starting the search
        // so deleting a char, will show former entries too
        let orig_list = self.entry_table.entry_table_at_search_start.clone();
        let filtered_list = self
            .search_struct
            .search_entry_list(self.entry_table.search_string(), orig_list.clone());
        self.entry_table.entry_table_items = filtered_list;
        self.entry_table.sort_entry_table(false);
        self.entry_table.entry_scroll_state = ScrollbarState::content_length(
            self.entry_table.entry_scroll_state,
            self.entry_table.entry_table_items.len(),
        );
    }
}

impl Bibiman {
    // Tag List commands

    // Movement
    pub fn select_next_tag(&mut self, keywords: u16) {
        self.tag_list.tag_list_state.scroll_down_by(keywords);
        self.tag_list.tag_scroll_state = self
            .tag_list
            .tag_scroll_state
            .position(self.tag_list.tag_list_state.selected().unwrap_or_default());
    }

    pub fn select_previous_tag(&mut self, keywords: u16) {
        self.tag_list.tag_list_state.scroll_up_by(keywords);
        self.tag_list.tag_scroll_state = self
            .tag_list
            .tag_scroll_state
            .position(self.tag_list.tag_list_state.selected().unwrap_or_default());
    }

    pub fn select_first_tag(&mut self) {
        self.tag_list.tag_list_state.select_first();
        self.tag_list.tag_scroll_state = self.tag_list.tag_scroll_state.position(0);
    }

    pub fn select_last_tag(&mut self) {
        // self.tag_list.tag_list_state.select_last(); // Doesn't work properly after upgrade to ratatui v.0.29
        self.tag_list
            .tag_list_state
            .select(Some(self.tag_list.tag_list_items.len() - 1));
        self.tag_list.tag_scroll_state = self
            .tag_list
            .tag_scroll_state
            .position(self.tag_list.tag_list_items.len());
    }

    pub fn get_selected_tag(&self) -> Option<&str> {
        if let Some(idx) = self.tag_list.tag_list_state.selected() {
            Some(&self.tag_list.tag_list_items[idx])
        } else {
            None
        }
    }

    pub fn search_tags(&mut self) {
        let orig_list = &self.tag_list.tag_list_at_search_start;
        let filtered_list =
            BibiSearch::search_tag_list(self.tag_list.search_string(), orig_list.clone());
        self.tag_list.tag_list_items = filtered_list;
        // Update scrollbar length after filtering list
        self.tag_list.tag_scroll_state = ScrollbarState::content_length(
            self.tag_list.tag_scroll_state,
            self.tag_list.tag_list_items.len(),
        );
    }

    pub fn filter_tags_by_entries(&mut self) {
        let mut filtered_keywords: Vec<String> = Vec::new();

        let orig_list = &self.entry_table.entry_table_items;

        for e in orig_list {
            if !e.keywords.is_empty() {
                let mut key_vec: Vec<String> = e.keywords.clone();
                filtered_keywords.append(&mut key_vec);
                drop(key_vec);
            }
        }

        filtered_keywords.sort_by_key(|a| a.to_lowercase());
        filtered_keywords.dedup();

        self.search_struct.filtered_tag_list = filtered_keywords.clone();
        self.tag_list.tag_list_items = filtered_keywords;
        self.tag_list.tag_scroll_state = ScrollbarState::content_length(
            self.tag_list.tag_scroll_state,
            self.tag_list.tag_list_items.len(),
        );
    }

    // Filter the entry list by tags when hitting enter
    // If already inside a filtered tag or entry list, apply the filtering
    // to the already filtered list only
    pub fn filter_for_tags(&mut self) {
        if let Some(keyword) = self.get_selected_tag() {
            let orig_list = &self.entry_table.entry_table_items;
            let filtered_list = BibiSearch::filter_entries_by_tag(keyword, orig_list);
            // self.tag_list.selected_keyword = keyword.to_string();
            self.tag_list.selected_keywords.push(keyword.to_string());
            self.entry_table.entry_table_items = filtered_list;
            // Update scrollbar state with new lenght of itemlist
            self.entry_table.entry_scroll_state = ScrollbarState::content_length(
                self.entry_table.entry_scroll_state,
                self.entry_table.entry_table_items.len(),
            );
            self.tag_list.search_string.clear();
            self.filter_tags_by_entries();
            self.toggle_area();
            self.entry_table.entry_table_state.select(Some(0));
            self.former_area = Some(FormerArea::TagArea);
        } else {
            warn!("Can't get selected keyword by index");
        }
    }
}

impl Bibiman {
    // Search Area

    // Enter the search area
    pub fn enter_search_area(&mut self) {
        self.search_struct.search_string.clear();
        if let CurrentArea::EntryArea = self.current_area {
            if let Some(FormerArea::TagArea) = self.former_area {
                self.search_struct.inner_search = true
            }
            self.entry_table.entry_table_at_search_start =
                self.entry_table.entry_table_items.clone();
            self.former_area = Some(FormerArea::EntryArea);
            self.entry_table.entry_table_state.select(Some(0));
        } else if let CurrentArea::TagArea = self.current_area {
            self.tag_list.tag_list_at_search_start = self.tag_list.tag_list_items.clone();
            self.former_area = Some(FormerArea::TagArea);
            self.tag_list.tag_list_state.select(Some(0));
        }
        self.current_area = CurrentArea::SearchArea
    }

    // Confirm search: Search former list by pattern
    pub fn confirm_search(&mut self) {
        if let Some(FormerArea::EntryArea) = self.former_area {
            self.current_area = CurrentArea::EntryArea;
            self.entry_table.entry_table_state.select(Some(0));
            self.entry_table.entry_table_at_search_start.clear();
        } else if let Some(FormerArea::TagArea) = self.former_area {
            self.current_area = CurrentArea::TagArea;
            self.tag_list.tag_list_state.select(Some(0));
            self.tag_list.tag_list_at_search_start.clear();
        }
        self.former_area = Some(FormerArea::SearchArea);
        // self.search_struct.search_string.clear();
    }

    // Break search: leave search area without filtering list
    pub fn break_search(&mut self) {
        if let Some(FormerArea::EntryArea) = self.former_area {
            self.current_area = CurrentArea::EntryArea;
            self.entry_table.entry_table_state.select(Some(0));
            self.entry_table.entry_table_at_search_start.clear();
            self.entry_table.set_search_string(String::new());
        } else if let Some(FormerArea::TagArea) = self.former_area {
            self.current_area = CurrentArea::TagArea;
            self.tag_list.tag_list_state.select(Some(0));
            self.tag_list.tag_list_at_search_start.clear();
            self.tag_list.set_search_string(String::new());
        }
        // But keep filtering by tag if applied before entering search area
        if !self.search_struct.inner_search {
            self.reset_current_list();
        }
        self.former_area = None;
        // If search is canceled, reset default status of struct
        self.search_struct.search_string.clear();
    }

    pub fn search_list_by_pattern(&mut self, searchpattern: &Input) {
        self.search_struct.search_string = searchpattern.value().to_string();
        if let Some(FormerArea::EntryArea) = self.former_area {
            self.entry_table.set_search_string(searchpattern.value());
            self.search_entries();
            self.filter_tags_by_entries();
        } else if let Some(FormerArea::TagArea) = self.former_area {
            self.tag_list.set_search_string(searchpattern.value());
            self.search_tags();
        }
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn citekey_pattern() {
        let citekey = format!("{{{},", "a_key_2001");

        assert_eq!(citekey, "{a_key_2001,")
    }
}
