// 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 std::path::PathBuf;

use super::popup::{PopupArea, PopupItem};
use crate::App;
use crate::bibiman::bibisetup::CustomField;
use crate::bibiman::entries::EntryTableColumn;
use crate::bibiman::search::SearchField;
use crate::bibiman::{CurrentArea, FormerArea};
use crate::cliargs::CLIArgs;
use crate::config::BibiConfig;
use crate::tui::popup::PopupKind;
use itertools::Itertools;
use ratatui::Frame;
use ratatui::layout::{Direction, Position};
use ratatui::widgets::Clear;
use ratatui::{
    layout::{Alignment, Constraint, Flex, Layout, Rect},
    style::{Color, Modifier, Style, Stylize},
    symbols,
    text::{Line, Span, Text},
    widgets::{
        Block, Borders, Cell, HighlightSpacing, List, ListItem, Padding, Paragraph, Row, Scrollbar,
        ScrollbarOrientation, Table, Wrap,
    },
};
use walkdir::WalkDir;

// Symbols
static SORTED_ENTRIES: &str = "▼";
static SORTED_ENTRIES_REVERSED: &str = "▲";
static SCROLLBAR_UPPER_CORNER: Option<&str> = Some("┓");
static SCROLLBAR_LOWER_CORNER: Option<&str> = Some("┛");

pub fn color_list(
    args: &CLIArgs,
    list_item: i32,
    sel_item: i32,
    highlight: u8,
    max_diff: i32,
) -> Color {
    match args.light_theme {
        false => {
            if list_item == sel_item {
                Color::Indexed(highlight)
            } else if (list_item - sel_item) > max_diff
                || (sel_item - list_item) > max_diff
                || -(list_item - sel_item) > max_diff
                || -(sel_item - list_item) > max_diff
            {
                Color::Indexed(highlight - max_diff as u8)
            } else if list_item < sel_item {
                Color::Indexed(highlight - (sel_item - list_item) as u8)
            } else {
                Color::Indexed(highlight - (list_item - sel_item) as u8)
            }
        }
        true => {
            if list_item == sel_item {
                Color::Indexed(highlight)
            } else if (list_item - sel_item) > max_diff
                || (sel_item - list_item) > max_diff
                || -(list_item - sel_item) > max_diff
                || -(sel_item - list_item) > max_diff
            {
                Color::Indexed(highlight + max_diff as u8)
            } else if list_item < sel_item {
                Color::Indexed(highlight + (sel_item - list_item) as u8)
            } else {
                Color::Indexed(highlight + (list_item - sel_item) as u8)
            }
        }
    }
}

fn count_files(files: &[PathBuf]) -> u16 {
    let mut count = 0;
    for f in files {
        if f.is_file() {
            count += 1
        } else if f.is_dir() {
            for e in WalkDir::new(f) {
                let f = e.unwrap().into_path();
                if f.is_file() && f.extension().unwrap() == "bib" {
                    count += 1
                }
            }
        }
    }
    count
}

pub fn render_ui(app: &mut App, cfg: &BibiConfig, frame: &mut Frame) {
    let [header_area, main_area, footer_area] = Layout::new(
        Direction::Vertical,
        [
            Constraint::Length(1),
            Constraint::Fill(1),
            Constraint::Length(if let CurrentArea::SearchArea = app.bibiman.current_area {
                1
            } else {
                0
            }),
        ],
    )
    .direction(Direction::Vertical)
    .areas(frame.area());

    let [list_area, item_area] =
        Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(main_area);

    let [entry_area, entry_info_area] =
        Layout::vertical([Constraint::Fill(1), Constraint::Length(2)]).areas(list_area);

    let [tag_area, info_area] =
        Layout::horizontal([Constraint::Max(25), Constraint::Min(35)]).areas(item_area);

    render_header(cfg, frame, header_area);
    if let CurrentArea::SearchArea = app.bibiman.current_area {
        render_footer(app, cfg, frame, footer_area);
    }
    render_entrytable(app, cfg, frame, entry_area);
    render_selected_item(app, cfg, frame, info_area);
    render_taglist(app, cfg, frame, tag_area);
    render_file_info(app, cfg, frame, entry_info_area);
    if app.bibiman.popup_area.is_popup {
        render_popup(app, cfg, frame);
    }
}

pub fn render_popup(app: &mut App, cfg: &BibiConfig, frame: &mut Frame) {
    match app.bibiman.popup_area.popup_kind {
        Some(PopupKind::Help) => {
            let block = Block::bordered()
                .title_top(" Keybindings ".bold())
                .title_bottom(" (j,k|↓,↑) ━ (ESC|ENTER) ".bold())
                .title_alignment(Alignment::Center)
                .style(
                    Style::new()
                        .fg(cfg.colors.main_text_color)
                        .bg(cfg.colors.popup_bg_color),
                )
                .border_set(symbols::border::THICK)
                .border_style(Style::new().fg(cfg.colors.entry_color));

            let text: Text = PopupArea::popup_help(cfg);

            // Calculate max scroll position depending on hight of terminal window
            // Needed length is number of text lines plus two for borders at bottom and top
            // minus half the height of the frame (or the height set for the popup)
            let popup_height: u16 = frame.area().height / 2;

            let scroll_pos = if app.bibiman.popup_area.popup_scroll_pos
                > text.lines.len() as u16 + 2 - popup_height
            {
                app.bibiman.popup_area.popup_scroll_pos =
                    text.lines.len() as u16 + 2 - popup_height;
                app.bibiman.popup_area.popup_scroll_pos
            } else {
                app.bibiman.popup_area.popup_scroll_pos
            };

            let par = Paragraph::new(text).scroll((scroll_pos, 0)).block(block);
            let par_width = par.line_width();

            let popup_area = popup_area(frame.area(), par_width as u16, popup_height);

            frame.render_widget(Clear, popup_area);
            frame.render_widget(par, popup_area)
        }

        Some(PopupKind::AddEntry) => {
            let area = frame.area();

            let block = Block::bordered()
                .title_top(" Add Entry ".bold())
                .title_bottom(" (ESC) ━ (ENTER) ".bold())
                .title_alignment(Alignment::Center)
                .style(
                    Style::new()
                        .fg(cfg.colors.main_text_color)
                        .bg(cfg.colors.popup_bg_color),
                )
                .border_set(symbols::border::THICK)
                .border_style(Style::new().fg(cfg.colors.entry_color));

            // Prepare the input fields
            let content = vec![Line::from(vec![
                Span::styled("DOI: ", Style::new().fg(cfg.colors.entry_color)),
                Span::raw(app.input.value().to_string().clone()),
            ])];
            let paragraph = Paragraph::new(content)
                .block(block.clone())
                .style(Style::new().fg(cfg.colors.main_text_color))
                .wrap(Wrap { trim: false });

            let doi_lines = paragraph.line_count(area.width / 2);
            // Calculate popup size
            let popup_width = area.width / 4 * 3;
            let popup_height = doi_lines as u16; // Adjust as needed
            let popup_area = popup_area(area, popup_width, popup_height);

            // Render the popup
            frame.render_widget(Clear, popup_area);
            render_cursor(app, frame, popup_area, 6, doi_lines as u16 - 1);
            frame.render_widget(paragraph, popup_area);
        }
        Some(PopupKind::MessageConfirm) => {
            let area = frame.area();

            let block = Block::bordered()
                .title_top(" Message ".bold().fg(cfg.colors.confirm_color))
                .border_style(Style::new().fg(cfg.colors.confirm_color))
                .style(
                    Style::new()
                        .fg(cfg.colors.main_text_color)
                        .bg(cfg.colors.popup_bg_color),
                );

            let content = Paragraph::new(app.bibiman.popup_area.popup_message.clone())
                .block(block)
                .style(Style::new().fg(cfg.colors.confirm_color));

            // Calculate popup size. Width is number of string chars plus 2 for border
            let popup_area = popup_area(
                area,
                (app.bibiman.popup_area.popup_message.chars().count() + 2) as u16,
                3,
            );

            // Clear area and draw popup
            frame.render_widget(Clear, popup_area);
            frame.render_widget(&content, popup_area)
        }
        Some(PopupKind::MessageError) => {
            let area = frame.area();

            let block = Block::bordered()
                .title_top(" Warning ".bold().fg(cfg.colors.warn_color))
                .border_style(Style::new().fg(Color::Red))
                .style(
                    Style::new()
                        .fg(cfg.colors.main_text_color)
                        .bg(cfg.colors.popup_bg_color),
                );

            let content = Paragraph::new(app.bibiman.popup_area.popup_message.clone())
                .block(block)
                .style(Style::new().fg(cfg.colors.warn_color));

            // Calculate popup size. Width is number of string chars plus 2 for border
            let popup_area = popup_area(
                area,
                (app.bibiman.popup_area.popup_message.chars().count() + 2) as u16,
                3,
            );

            // Clear area and draw popup
            frame.render_widget(Clear, popup_area);
            frame.render_widget(&content, popup_area)
        }
        Some(PopupKind::OpenRes)
        | Some(PopupKind::AppendToFile)
        | Some(PopupKind::YankItem)
        | Some(PopupKind::CreateNote)
        | Some(PopupKind::SearchField) => {
            let list_items: Vec<ListItem> = if let Some(PopupKind::CreateNote) =
                app.bibiman.popup_area.popup_kind
            {
                app.bibiman
                    .popup_area
                    .popup_list
                    .iter()
                    .map(|(m, o, _i)| {
                        ListItem::from(Line::from(vec![Span::raw(m), Span::raw("."), Span::raw(o)]))
                            .fg(cfg.colors.note_color)
                    })
                    .collect()
            } else {
                app.bibiman
                    .popup_area
                    .popup_list
                    .iter()
                    .map(
                        |(mes, obj, i)| {
                            let style: Color = match i {
                                PopupItem::Bibfile => cfg.colors.entry_color,
                                PopupItem::Citekey => cfg.colors.entry_color,
                                PopupItem::SearchField(_) => cfg.colors.entry_color,
                                PopupItem::Entryfile => cfg.colors.file_color,
                                PopupItem::Notefile => cfg.colors.note_color,
                                PopupItem::Link => cfg.colors.link_color,
                                PopupItem::Default => cfg.colors.main_text_color,
                                PopupItem::None => cfg.colors.main_text_color,
                            };
                            ListItem::from(
                                Line::from(vec![
                                    Span::styled(mes, Style::new().bold()),
                                    Span::raw(obj),
                                ])
                                .fg(style),
                            )
                        }, // ListItem::from(mes.to_owned() + obj)
                    )
                    .collect()
            };

            let title = if let Some(kind) = &app.bibiman.popup_area.popup_kind {
                match kind {
                    PopupKind::OpenRes => " Open ",
                    PopupKind::AppendToFile => " Select file to append entry ",
                    PopupKind::YankItem => " Yank to clipboard ",
                    PopupKind::CreateNote => " Select note extension ",
                    PopupKind::SearchField => " Select field to search ",
                    _ => " Select ",
                }
            } else {
                " Select "
            };

            let bottom_info = if let Some(PopupKind::OpenRes) = app.bibiman.popup_area.popup_kind {
                " (j,k|↓,↑) ━ (o,l,n) ━ (ENTER) ━ (ESC) "
            } else if let Some(PopupKind::YankItem) = app.bibiman.popup_area.popup_kind {
                " (j,k|↓,↑) ━ (y) ━ (ENTER) ━ (ESC) "
            } else {
                " (j,k|↓,↑) ━ (ENTER) ━ (ESC) "
            };

            let block = Block::bordered()
                .title_top(title.bold())
                .title_bottom(bottom_info.bold())
                .title_alignment(Alignment::Center)
                .style(
                    Style::new()
                        .fg(cfg.colors.main_text_color)
                        .bg(cfg.colors.popup_bg_color),
                )
                .border_set(symbols::border::THICK)
                .border_style(Style::new().fg(cfg.colors.popup_fg_color));

            let list = List::new(list_items).block(block).highlight_style(
                Style::new()
                    // .fg(cfg.colors.entry_color)
                    .add_modifier(Modifier::BOLD)
                    .add_modifier(Modifier::REVERSED),
            );

            // To find the longest line, we need to collect the chars of every item
            // and add.
            let list_widths = app
                .bibiman
                .popup_area
                .popup_list
                .iter()
                .max_by(|(mes, obj, _ik), (m, o, _i)| {
                    let x = mes.chars().count() + obj.chars().count();
                    let y = m.chars().count() + o.chars().count();
                    x.cmp(&y)
                })
                .unwrap();
            // .map(|(m, o)| m.chars().count() as u16 + o.chars().count() as u16)
            // .collect();

            // Now take the max number for the width of the popup
            // let max_item = list_widths.iter().max().unwrap().to_owned();
            let max_item = list_widths.0.clone() + &list_widths.1;
            // list_widths.0.chars().count() as u16 + list_widths.1.chars().count() as u16;

            let fitting_width: u16 = {
                let lines = vec![title, bottom_info, &max_item];
                let lline = lines
                    .iter()
                    .max_by(|a, b| a.chars().count().cmp(&b.chars().count()))
                    .unwrap();
                // lines.first().unwrap().chars().count() as u16
                lline.chars().count() as u16
            };
            // Check if the popup would exceed the terminal frame width
            let popup_width = if fitting_width + 2 > frame.area().width - 2 {
                frame.area().width - 2
            } else {
                fitting_width + 2
            };
            // } else if title.chars().count() as u16 > max_item {
            //     (title.chars().count() + 2) as u16
            // } else {
            //     max_item + 2
            // };
            let popup_heigth = list.len() + 2;
            let popup_area = popup_area(frame.area(), popup_width, popup_heigth as u16);

            frame.render_widget(Clear, popup_area);
            frame.render_stateful_widget(list, popup_area, &mut app.bibiman.popup_area.popup_state)
        }
        None => {}
    }
}

pub fn render_header(cfg: &BibiConfig, frame: &mut Frame, rect: Rect) {
    let main_header = Paragraph::new("BIBIMAN – BibLaTeX manager TUI")
        .bold()
        .fg(cfg.colors.entry_color)
        .centered();
    frame.render_widget(main_header, rect)
}

pub fn render_footer(app: &mut App, cfg: &BibiConfig, frame: &mut Frame, rect: Rect) {
    let search_title = {
        match app.bibiman.former_area {
            Some(FormerArea::EntryArea) => {
                format!(
                    "Search Entries [{}]: ",
                    app.bibiman.search_struct.search_field().as_ref()
                )
            }
            Some(FormerArea::TagArea) => "Search Keywords: ".to_string(),
            _ => " Search ".to_string(),
        }
    };

    let title_lenght: u16 = search_title.chars().count() as u16;

    let block = Block::new()
        .padding(Padding::horizontal(1))
        .bg(cfg.colors.bar_bg_color);

    let search_string = Paragraph::new(Line::from(vec![
        Span::styled(
            search_title,
            if let Some(FormerArea::EntryArea) = app.bibiman.former_area {
                Style::new()
                    .fg(cfg.colors.entry_color)
                    .add_modifier(Modifier::BOLD)
            } else if let Some(FormerArea::TagArea) = app.bibiman.former_area {
                Style::new()
                    .fg(cfg.colors.keyword_color)
                    .add_modifier(Modifier::BOLD)
            } else {
                Style::new()
                    .fg(cfg.colors.highlight_text_color)
                    .add_modifier(Modifier::BOLD)
            },
        ),
        Span::raw(app.bibiman.search_struct.search_string.clone())
            .fg(cfg.colors.highlight_text_color),
    ]))
    .block(block);

    render_cursor(app, frame, rect, title_lenght + 1, 1);
    frame.render_widget(search_string, rect);
}

// Render info of the current file and process
// 1. Basename of the currently loaded file
// 2. Keyword by which the entries are filtered at the moment
// 3. Currently selected entry and total count of entries
pub fn render_file_info(app: &mut App, cfg: &BibiConfig, frame: &mut Frame, rect: Rect) {
    let block = Block::new() // can also be Block::new
        // Leave Top empty to simulate one large box with borders of entry list
        .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM)
        .border_set(if let CurrentArea::EntryArea = app.bibiman.current_area {
            symbols::border::THICK
        } else {
            symbols::border::PLAIN
        })
        .border_style(if let CurrentArea::EntryArea = app.bibiman.current_area {
            Style::new().fg(cfg.colors.highlight_text_color)
        } else {
            Style::new().fg(cfg.colors.main_text_color)
        });

    frame.render_widget(block, rect);

    let [file_area, keyword_area, count_area] = Layout::horizontal([
        Constraint::Fill(3),
        Constraint::Fill(4),
        Constraint::Fill(1),
    ])
    .horizontal_margin(1)
    .areas(rect);

    let file_info = if app.bibiman.main_bibfiles.len() == 1
        && app.bibiman.main_bibfiles.first().unwrap().is_file()
    {
        Line::from(vec![
            Span::raw("File: ").fg(cfg.colors.main_text_color).bold(),
            Span::raw(
                app.bibiman.main_bibfiles[0]
                    .file_name()
                    .unwrap()
                    .to_string_lossy(),
            )
            .fg(cfg.colors.main_text_color)
            .bold(),
        ])
        .bg(cfg.colors.bar_bg_color)
    } else {
        Line::from(vec![
            Span::raw("Multiple files (")
                .fg(cfg.colors.main_text_color)
                .bold(),
            Span::raw(count_files(&app.bibiman.main_bibfiles).to_string())
                .fg(cfg.colors.main_text_color)
                .bold(),
            Span::raw(")").fg(cfg.colors.main_text_color).bold(),
        ])
        .bg(cfg.colors.bar_bg_color)
    };

    let cur_keywords = Line::from(if !app.bibiman.tag_list.selected_keywords.is_empty() {
        vec![
            Span::raw("Selected keywords: ").fg(cfg.colors.main_text_color),
            // Show all keywords in correct order if list is filtered
            // successively by multiple keywords
            Span::raw(app.bibiman.tag_list.selected_keywords.join(" → "))
                .bold()
                .green(),
        ]
    } else {
        vec![Span::raw(" ")]
    })
    .bg(cfg.colors.bar_bg_color);
    // .render(keyword_area, buf);

    let item_count = Line::from(
        if app
            .bibiman
            .entry_table
            .entry_table_state
            .selected()
            .is_some()
        {
            vec![
                Span::raw(
                    // Because method scroll_down_by() of TableState lets numbers
                    // printed overflow for short moment, we have to check manually
                    // that we do not print a number higher than len() of table
                    if app
                        .bibiman
                        .entry_table
                        .entry_table_state
                        .selected()
                        .unwrap()
                        + 1
                        > app.bibiman.entry_table.entry_table_items.len()
                    {
                        app.bibiman.entry_table.entry_table_items.len().to_string()
                    } else {
                        (app.bibiman
                            .entry_table
                            .entry_table_state
                            .selected()
                            .unwrap()
                            + 1)
                        .to_string()
                    },
                )
                .fg(cfg.colors.main_text_color)
                .bold(),
                Span::raw("/").fg(cfg.colors.main_text_color),
                Span::raw(app.bibiman.entry_table.entry_table_items.len().to_string())
                    .fg(cfg.colors.main_text_color),
            ]
        } else {
            vec![Span::raw("No entries")]
        },
    )
    .right_aligned()
    .bg(cfg.colors.bar_bg_color);
    frame.render_widget(file_info, file_area);
    frame.render_widget(cur_keywords, keyword_area);
    frame.render_widget(item_count, count_area);
}

pub fn render_entrytable(app: &mut App, cfg: &BibiConfig, frame: &mut Frame, rect: Rect) {
    let entry_box_selected_border_style: Style = Style::new().fg(cfg.colors.highlight_text_color);
    let entry_box_selected_title_style: Style = Style::new()
        .fg(cfg.colors.entry_color)
        .add_modifier(Modifier::BOLD);
    let entry_box_unselected_border_style: Style = Style::new().fg(cfg.colors.main_text_color);
    let entry_box_unselected_title_style: Style = Style::new()
        .fg(cfg.colors.entry_color)
        .add_modifier(Modifier::BOLD);
    let selected_table_col_style: Style = Style::new().add_modifier(Modifier::BOLD);
    let selectec_table_cell_style: Style = Style::new().add_modifier(Modifier::REVERSED);
    let entry_selected_row_style: Style = Style::new()
        .fg(cfg.colors.entry_color)
        .add_modifier(Modifier::BOLD)
        .add_modifier(Modifier::REVERSED);

    let block = Block::new() // can also be Block::new
        .title(
            Line::styled(
                " Bibliographic Entries ",
                if let CurrentArea::EntryArea = app.bibiman.current_area {
                    entry_box_selected_title_style
                } else {
                    entry_box_unselected_title_style
                },
            )
            .centered(),
        )
        .borders(Borders::LEFT | Borders::RIGHT | Borders::TOP)
        .border_set(if let CurrentArea::EntryArea = app.bibiman.current_area {
            symbols::border::THICK
        } else {
            symbols::border::PLAIN
        })
        .border_style(if let CurrentArea::EntryArea = app.bibiman.current_area {
            entry_box_selected_border_style
        } else {
            entry_box_unselected_border_style
        });

    let header_style = Style::default()
        .bold()
        .fg(cfg.colors.main_text_color)
        .bg(cfg.colors.bar_bg_color);

    let custom_col_title: &str = match cfg.general.custom_column {
        CustomField::Journaltitle => "Journaltitle",
        CustomField::Organization => "Organization",
        CustomField::Institution => "Institution",
        CustomField::Series => "Series",
        CustomField::Publisher => "Publisher",
        CustomField::Pubtype => "Pubtype",
    };

    let header = Row::new(vec![
        Cell::from(Line::from("Res.")).bg(cfg.colors.bar_bg_color),
        Cell::from(
            Line::from(vec![{ Span::raw("Author") }, {
                if let Some(EntryTableColumn::Authors) =
                    app.bibiman.entry_table.entry_table_sorted_by_col
                {
                    Span::raw(format!(
                        " {}",
                        if app.bibiman.entry_table.entry_table_reversed_sort {
                            SORTED_ENTRIES_REVERSED
                        } else {
                            SORTED_ENTRIES
                        }
                    ))
                } else {
                    Span::raw("")
                }
            }])
            .bg(
                if let EntryTableColumn::Authors =
                    app.bibiman.entry_table.entry_table_selected_column
                {
                    cfg.colors.selected_row_bg_color
                } else {
                    cfg.colors.bar_bg_color
                },
            ),
        ),
        Cell::from(
            Line::from(vec![{ Span::raw("Title") }, {
                if let Some(EntryTableColumn::Title) =
                    app.bibiman.entry_table.entry_table_sorted_by_col
                {
                    Span::raw(format!(
                        " {}",
                        if app.bibiman.entry_table.entry_table_reversed_sort {
                            SORTED_ENTRIES_REVERSED
                        } else {
                            SORTED_ENTRIES
                        }
                    ))
                } else {
                    Span::raw("")
                }
            }])
            .bg(
                if let EntryTableColumn::Title = app.bibiman.entry_table.entry_table_selected_column
                {
                    cfg.colors.selected_row_bg_color
                } else {
                    cfg.colors.bar_bg_color
                },
            ),
        ),
        Cell::from(
            Line::from(vec![{ Span::raw("Year") }, {
                if let Some(EntryTableColumn::Year) =
                    app.bibiman.entry_table.entry_table_sorted_by_col
                {
                    Span::raw(format!(
                        " {}",
                        if app.bibiman.entry_table.entry_table_reversed_sort {
                            SORTED_ENTRIES_REVERSED
                        } else {
                            SORTED_ENTRIES
                        }
                    ))
                } else {
                    Span::raw("")
                }
            }])
            .bg(
                if let EntryTableColumn::Year = app.bibiman.entry_table.entry_table_selected_column
                {
                    cfg.colors.selected_row_bg_color
                } else {
                    cfg.colors.bar_bg_color
                },
            ),
        ),
        Cell::from(
            Line::from(vec![{ Span::raw(custom_col_title) }, {
                if let Some(EntryTableColumn::Pubtype) =
                    app.bibiman.entry_table.entry_table_sorted_by_col
                {
                    Span::raw(format!(
                        " {}",
                        if app.bibiman.entry_table.entry_table_reversed_sort {
                            SORTED_ENTRIES_REVERSED
                        } else {
                            SORTED_ENTRIES
                        }
                    ))
                } else {
                    Span::raw("")
                }
            }])
            .bg(
                if let EntryTableColumn::Pubtype =
                    app.bibiman.entry_table.entry_table_selected_column
                {
                    cfg.colors.selected_row_bg_color
                } else {
                    cfg.colors.bar_bg_color
                },
            ),
        ),
    ])
    .style(header_style)
    .height(1);

    // Iterate over vector storing each entries data fields
    let rows = app
        .bibiman
        .entry_table
        .entry_table_items
        .iter()
        .map(|data| {
            let item = data.ref_vec();

            let mut symbol_vec = vec![];

            // use default or custom symbols for resources
            // if an entry has no, replace it with the correct number
            // of whitespace to align the symbols correct
            if let Some(f) = &item.symbols[0] {
                symbol_vec.push(Span::styled(
                    f,
                    Style::new().fg(cfg.colors.file_color).bold(),
                ));
            } else {
                symbol_vec.push(Span::raw(
                    " ".repeat(cfg.general.file_symbol.chars().count()),
                ));
            }
            if let Some(l) = &item.symbols[1] {
                symbol_vec.push(Span::styled(
                    l,
                    Style::new().fg(cfg.colors.link_color).bold(),
                ));
            } else {
                symbol_vec.push(Span::raw(
                    " ".repeat(cfg.general.link_symbol.chars().count()),
                ));
            }
            if let Some(n) = &item.symbols[2] {
                symbol_vec.push(Span::styled(
                    n,
                    Style::new().fg(cfg.colors.note_color).bold(),
                ))
            } else {
                symbol_vec.push(Span::raw(
                    " ".repeat(cfg.general.note_symbol.chars().count()),
                ));
            }

            let row = Row::new(vec![
                Cell::from(Line::from(symbol_vec)),
                Cell::from(Line::from(build_spans(
                    if let SearchField::Author | SearchField::All =
                        app.bibiman.search_struct.search_field()
                        && !app.bibiman.entry_table.search_string.is_empty()
                    {
                        true
                    } else {
                        false
                    },
                    None,
                    item.authors,
                    &app.bibiman.entry_table.search_string,
                    Style::default(),
                ))),
                Cell::from(Line::from(build_spans(
                    if let SearchField::Title | SearchField::All =
                        app.bibiman.search_struct.search_field()
                        && !app.bibiman.entry_table.search_string.is_empty()
                    {
                        true
                    } else {
                        false
                    },
                    None,
                    item.title,
                    &app.bibiman.entry_table.search_string,
                    Style::default(),
                ))),
                Cell::from(Line::from(build_spans(
                    if let SearchField::All = app.bibiman.search_struct.search_field()
                        && !app.bibiman.entry_table.search_string.is_empty()
                    {
                        true
                    } else {
                        false
                    },
                    None,
                    item.year,
                    &app.bibiman.entry_table.search_string,
                    Style::default(),
                ))),
                Cell::from(Line::from(build_spans(
                    if let SearchField::CustomColumn | SearchField::All =
                        app.bibiman.search_struct.search_field()
                        && !app.bibiman.entry_table.search_string.is_empty()
                    {
                        true
                    } else {
                        false
                    },
                    None,
                    item.custom_field_value,
                    &app.bibiman.entry_table.search_string,
                    Style::default(),
                ))),
            ]);

            // let row = item
            //     .into_iter()
            //     .map(|content| Cell::from(Text::from(content.to_string())))
            //     .collect::<Row>();

            row.style(
                Style::new().fg(if let CurrentArea::EntryArea = app.bibiman.current_area {
                    cfg.colors.highlight_text_color
                } else {
                    cfg.colors.main_text_color
                }),
            )
            .height(1)
        });
    let entry_table = Table::new(
        rows,
        [
            Constraint::Length(
                (cfg.general.file_symbol.chars().count()
                    + cfg.general.link_symbol.chars().count()
                    + cfg.general.note_symbol.chars().count()) as u16,
            ),
            Constraint::Percentage(20),
            Constraint::Fill(1),
            Constraint::Length(
                if let Some(EntryTableColumn::Year) =
                    app.bibiman.entry_table.entry_table_sorted_by_col
                {
                    6
                } else {
                    4
                },
            ),
            Constraint::Percentage(10),
        ],
    )
    .block(block)
    .header(header)
    .column_spacing(2)
    .row_highlight_style(entry_selected_row_style)
    .column_highlight_style(selected_table_col_style)
    .cell_highlight_style(selectec_table_cell_style)
    .highlight_spacing(HighlightSpacing::Always);

    frame.render_stateful_widget(
        entry_table,
        rect,
        &mut app.bibiman.entry_table.entry_table_state,
    );

    // Scrollbar for entry table
    let scrollbar = Scrollbar::default()
        .orientation(ScrollbarOrientation::VerticalRight)
        .track_symbol(None)
        .begin_symbol(SCROLLBAR_UPPER_CORNER)
        .end_symbol(None)
        .thumb_style(Style::new().fg(Color::DarkGray));

    if let CurrentArea::EntryArea = app.bibiman.current_area {
        // render the scrollbar
        frame.render_stateful_widget(
            scrollbar,
            rect,
            &mut app.bibiman.entry_table.entry_scroll_state,
        );
    }
}

pub fn render_selected_item(app: &mut App, cfg: &BibiConfig, frame: &mut Frame, rect: Rect) {
    // We get the info depending on the item's state.
    let style_value = Style::new().bold().fg(cfg.colors.main_text_color);
    let lines = {
        if app
            .bibiman
            .entry_table
            .entry_table_state
            .selected()
            .is_some()
        {
            let idx = app
                .bibiman
                .entry_table
                .entry_table_state
                .selected()
                .unwrap();
            let cur_entry = &app.bibiman.entry_table.entry_table_items[idx];
            let mut lines = vec![];
            lines.push(Line::from(build_spans(
                if let SearchField::Author | SearchField::All =
                    app.bibiman.search_struct.search_field()
                    && !app.bibiman.entry_table.search_string.is_empty()
                {
                    true
                } else {
                    false
                },
                Some(Span::styled("Authors: ", style_value)),
                cur_entry.authors(),
                &app.bibiman.entry_table.search_string,
                Style::new().fg(cfg.colors.author_color),
            )));
            if let Some(subtitle) = cur_entry.subtitle() {
                let mut line = Line::from(build_spans(
                    if let SearchField::Title | SearchField::All =
                        app.bibiman.search_struct.search_field()
                        && !app.bibiman.entry_table.search_string.is_empty()
                    {
                        true
                    } else {
                        false
                    },
                    Some(Span::styled("Title: ", style_value)),
                    &cur_entry.title,
                    &app.bibiman.entry_table.search_string,
                    Style::new()
                        .fg(cfg.colors.title_color)
                        .add_modifier(Modifier::ITALIC),
                ));
                line.push_span(Span::styled(
                    ": ",
                    Style::new()
                        .fg(cfg.colors.title_color)
                        .add_modifier(Modifier::ITALIC),
                ));
                line.push_span(Span::styled(
                    subtitle,
                    Style::new()
                        .fg(cfg.colors.title_color)
                        .add_modifier(Modifier::ITALIC),
                ));
                lines.push(line);
            } else {
                lines.push(Line::from(build_spans(
                    if let SearchField::Title | SearchField::All =
                        app.bibiman.search_struct.search_field()
                        && !app.bibiman.entry_table.search_string.is_empty()
                    {
                        true
                    } else {
                        false
                    },
                    Some(Span::styled("Title: ", style_value)),
                    &cur_entry.title,
                    &app.bibiman.entry_table.search_string,
                    Style::new()
                        .fg(cfg.colors.title_color)
                        .add_modifier(Modifier::ITALIC),
                )));
            }
            lines.push(Line::from(vec![
                Span::styled("Year: ", style_value),
                Span::styled(cur_entry.year(), Style::new().fg(cfg.colors.year_color)),
            ]));
            // Render keywords in info box in Markdown code style
            if !cur_entry.keywords.is_empty() {
                let mut content = vec![Span::styled("Keywords: ", style_value)];
                for k in cur_entry.keywords.iter() {
                    // Add half block highlighted in bg color to enlarge block
                    content.push(Span::raw("▐").fg(cfg.colors.bar_bg_color));
                    for span in build_spans(
                        if let SearchField::All = app.bibiman.search_struct.search_field()
                            && !app.bibiman.entry_table.search_string.is_empty()
                        {
                            true
                        } else {
                            false
                        },
                        None,
                        k,
                        &app.bibiman.entry_table.search_string,
                        Style::default().bg(cfg.colors.bar_bg_color).fg(
                            // Highlight selected keyword green
                            if app
                                .bibiman
                                .tag_list
                                .selected_keywords
                                .iter()
                                .any(|e| e == k)
                            {
                                Color::Green
                            } else {
                                cfg.colors.main_text_color
                            },
                        ),
                    ) {
                        content.push(span);
                    }
                    // content.push(Span::styled(
                    //     k,
                    //     Style::default().bg(cfg.colors.bar_bg_color).fg(
                    //         // Highlight selected keyword green
                    //         if app
                    //             .bibiman
                    //             .tag_list
                    //             .selected_keywords
                    //             .iter()
                    //             .any(|e| e == k)
                    //         {
                    //             Color::Green
                    //         } else {
                    //             cfg.colors.main_text_color
                    //         },
                    //     ),
                    // ));
                    content.push(Span::raw("▌").fg(cfg.colors.bar_bg_color));
                }
                lines.push(Line::from(content))
            }
            if cur_entry.doi_url.is_some()
                || cur_entry.filepath.is_some()
                || cur_entry.notes.is_some()
            {
                lines.push(Line::raw(""));
            }
            if cur_entry.doi_url.is_some() {
                lines.push(Line::from(vec![
                    Span::styled("DOI/URL: ", style_value),
                    Span::styled(
                        cur_entry.doi_url(),
                        Style::new().fg(cfg.colors.link_color).underlined(),
                    ),
                ]));
            }
            if let Some(p) = &cur_entry.filepath {
                lines.push(Line::from(vec![
                    Span::styled("File: ", style_value),
                    Span::styled(
                        p.iter().map(|f| f.to_str().unwrap()).join("; "),
                        Style::new().fg(cfg.colors.file_color),
                    ),
                ]));
            }
            if let Some(n) = &cur_entry.notes {
                lines.push(Line::from(vec![
                    Span::styled("Note: ", style_value),
                    Span::styled(
                        n.iter().map(|n| n.to_str().unwrap()).join("; "),
                        Style::new().fg(cfg.colors.note_color),
                    ),
                ]));
            }
            lines.push(Line::from(""));
            lines.push(Line::from(build_spans(
                if let SearchField::Abstract = app.bibiman.search_struct.search_field()
                    && !app.bibiman.entry_table.search_string.is_empty()
                {
                    true
                } else {
                    false
                },
                None,
                &cur_entry.abstract_text,
                &app.bibiman.entry_table.search_string,
                Style::new().fg(cfg.colors.main_text_color),
            )));
            lines
        } else {
            let lines = vec![
                Line::from(" "),
                "No entry selected".bold().into_centered_line().red(),
            ];
            lines
        }
    };
    let info = Text::from(lines);

    // We show the list item's info under the list in this paragraph
    let block = Block::bordered()
        .title(
            Line::raw(" Entry Information ")
                .centered()
                .bold()
                .fg(cfg.colors.info_color),
        )
        .border_set(symbols::border::PLAIN)
        .border_style(Style::new().fg(cfg.colors.main_text_color))
        .padding(Padding::horizontal(1));

    // INFO: '.line_count' method only possible with unstable-rendered-line-info feature -> API might change: https://github.com/ratatui/ratatui/issues/293#ref-pullrequest-2027056434
    let box_height = Paragraph::new(info.clone())
        .block(block.clone())
        .wrap(Wrap { trim: false })
        .line_count(rect.width);
    // Make sure to allow scroll only if text is larger than the rendered area and stop scrolling when last line is reached
    let scroll_height = {
        if app.bibiman.entry_table.entry_info_scroll == 0 {
            app.bibiman.entry_table.entry_info_scroll
        } else if rect.height > box_height as u16 {
            app.bibiman.entry_table.entry_info_scroll = 0;
            app.bibiman.entry_table.entry_info_scroll
        } else if app.bibiman.entry_table.entry_info_scroll > (box_height as u16 + 2 - rect.height)
        {
            app.bibiman.entry_table.entry_info_scroll = box_height as u16 + 2 - rect.height;
            app.bibiman.entry_table.entry_info_scroll
        } else {
            app.bibiman.entry_table.entry_info_scroll
        }
    };

    // We can now render the item info
    let item_info = Paragraph::new(info)
        .block(
            block
                // Render arrows to show that info box has content outside the block
                .title_bottom(
                    Line::from(
                        if box_height > rect.height.into()
                            && app.bibiman.entry_table.entry_info_scroll
                                < box_height as u16 + 2 - rect.height
                        {
                            " ▼ "
                        } else {
                            ""
                        },
                    )
                    .alignment(Alignment::Right),
                )
                .title_top(
                    Line::from(if scroll_height > 0 { " ▲ " } else { "" })
                        .alignment(Alignment::Right),
                ),
        )
        .wrap(Wrap { trim: false })
        .scroll((scroll_height, 0));

    frame.render_widget(item_info, rect);
}

pub fn render_taglist(app: &mut App, cfg: &BibiConfig, frame: &mut Frame, rect: Rect) {
    let keyword_box_selected_border_style: Style = Style::new().fg(cfg.colors.highlight_text_color);
    let keyword_box_selected_title_style: Style = Style::new()
        .fg(cfg.colors.keyword_color)
        .add_modifier(Modifier::BOLD);
    let keyword_box_unselected_border_style: Style = Style::new().fg(cfg.colors.main_text_color);
    let keyword_box_unselected_title_style: Style = Style::new()
        .fg(cfg.colors.keyword_color)
        .add_modifier(Modifier::BOLD);
    let keyword_selected_row_style: Style = Style::new()
        .fg(cfg.colors.keyword_color)
        .add_modifier(Modifier::BOLD)
        .add_modifier(Modifier::REVERSED);

    let block = Block::bordered()
        .title(
            Line::styled(
                " Keywords ",
                if let CurrentArea::TagArea = app.bibiman.current_area {
                    keyword_box_selected_title_style
                } else {
                    keyword_box_unselected_title_style
                },
            )
            .centered(),
        )
        .border_set(if let CurrentArea::TagArea = app.bibiman.current_area {
            symbols::border::THICK
        } else {
            symbols::border::PLAIN
        })
        .border_style(if let CurrentArea::TagArea = app.bibiman.current_area {
            keyword_box_selected_border_style
        } else {
            keyword_box_unselected_border_style
        });

    // Iterate through all elements in the `items` and stylize them.
    let items: Vec<ListItem> = app
        .bibiman
        .tag_list
        .tag_list_items
        .iter()
        .map(|keyword| {
            ListItem::from(Line::from(build_spans(
                if !&app.bibiman.tag_list.search_string.is_empty() {
                    true
                } else {
                    false
                },
                None,
                &keyword,
                &app.bibiman.tag_list.search_string,
                Style::new().fg(cfg.colors.main_text_color),
            )))
        })
        .collect();

    // Create a List from all list items and highlight the currently selected one
    let list = List::new(items)
        .block(block)
        .highlight_style(keyword_selected_row_style);

    // Save list length for calculating scrollbar need
    // Add 2 to compmensate lines of the block border
    let list_length = list.len() + 2;

    frame.render_stateful_widget(list, rect, &mut app.bibiman.tag_list.tag_list_state);

    // Scrollbar for keyword list
    let scrollbar = Scrollbar::default()
        .orientation(ScrollbarOrientation::VerticalRight)
        .track_symbol(None)
        .begin_symbol(SCROLLBAR_UPPER_CORNER)
        .end_symbol(SCROLLBAR_LOWER_CORNER)
        .thumb_style(Style::new().fg(Color::DarkGray));

    if list_length > rect.height.into() {
        if let CurrentArea::TagArea = app.bibiman.current_area {
            // render the scrollbar
            frame.render_stateful_widget(
                scrollbar,
                rect,
                &mut app.bibiman.tag_list.tag_scroll_state,
            );
        }
    }
}

/// Render the cursor when in InputMode
fn render_cursor(app: &mut App, frame: &mut Frame, area: Rect, x_offset: u16, y_offset: u16) {
    let scroll = app.input.visual_scroll(area.width as usize);
    if app.input_mode {
        let (x, y) = (
            area.x + ((app.input.visual_cursor()).max(scroll) - scroll) as u16 + x_offset,
            // area.bottom().saturating_sub(1) + y_offset,
            area.bottom().saturating_sub(y_offset),
        );
        frame.render_widget(
            Clear,
            Rect {
                x,
                y,
                width: 1,
                height: 1,
            },
        );
        frame.set_cursor_position(Position::new(x, y));
    }
}

/// helper function to create a centered rect using up certain percentage of the available rect `r`
fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
    let vertical = Layout::vertical([Constraint::Length(percent_y)]).flex(Flex::Center);
    let horizontal = Layout::horizontal([Constraint::Length(percent_x)]).flex(Flex::Center);
    let [area] = vertical.areas(area);
    let [area] = horizontal.areas(area);
    area
}

const MATCH_STYLE: Style = Style::new().fg(Color::Black).bg(Color::Yellow);

/// Check if the passed string is matched by a search pattern if in a current
/// search situation
fn build_spans<'a>(
    search: bool,
    prefix: Option<Span<'a>>,
    input: &'a str,
    pattern: &'a str,
    style: Style,
) -> Vec<Span<'a>> {
    let mut spans = if let Some(pre) = prefix {
        vec![pre]
    } else {
        vec![]
    };
    if let Some(pat) = pattern.strip_prefix("'")
        && !pat.is_empty()
        && search
    {
        let mut slice = String::new();
        for char in input.chars() {
            slice.push(char);
            if slice.to_lowercase().ends_with(&pat.to_lowercase()) {
                let (subslice, pat_match) = slice
                    .clone()
                    .split_at_checked(slice.len() - pat.len())
                    .map(|(s, p)| (s.to_string(), p.to_string()))
                    .unwrap_or_default();
                spans.push(Span::styled(subslice, style));
                spans.push(Span::styled(pat_match, MATCH_STYLE));
                slice.clear();
                continue;
            }
        }
        spans.push(Span::styled(slice, style));
    } else {
        spans.push(Span::styled(input, style));
    }
    spans
}
