use crate::app::{HelpMenuLine, NodeFilterApplicable, NodeSorterApplicable};
use crate::app::{Node, ResolvedNode};
use crate::compat::{draw_custom_content, CustomContent};
use crate::config::PanelUiConfig;
use crate::lua;
use crate::permissions::Permissions;
use crate::{app, path};
use ansi_to_tui::IntoText;
use indexmap::IndexSet;
use lazy_static::lazy_static;
use lscolors::{Color as LsColorsColor, Style as LsColorsStyle};
use mlua::Lua;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::env;
use std::ops::BitXor;
use time::macros::format_description;
use tui::layout::Rect as TuiRect;
use tui::layout::{Constraint as TuiConstraint, Direction, Layout as TuiLayout};
use tui::style::{Color as TuiColor, Modifier as TuiModifier, Style as TuiStyle};
use tui::text::{Line, Span, Text};
use tui::widgets::{
    Block, BorderType as TuiBorderType, Borders as TuiBorders, Cell, List, ListItem,
    Paragraph, Row, Table,
};
use tui::Frame;

const DEFAULT_STYLE: TuiStyle = TuiStyle::new();

lazy_static! {
    pub static ref NO_COLOR: bool = env::var("NO_COLOR").is_ok();
}

fn read_only_indicator(app: &app::App) -> &str {
    if app.config.general.read_only {
        "(r)"
    } else {
        ""
    }
}

fn selection_indicator(app: &app::App) -> String {
    let count = app.selection.len();
    if count == 0 {
        String::new()
    } else {
        format!(" {{{count} sel}}")
    }
}

pub fn string_to_text<'a>(string: String) -> Text<'a> {
    if *NO_COLOR {
        Text::raw(string)
    } else {
        string
            .as_bytes()
            .into_text()
            .unwrap_or_else(|e| Text::raw(format!("{e:?}")))
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct LayoutOptions {
    #[serde(default)]
    pub margin: Option<u16>,

    #[serde(default)]
    pub horizontal_margin: Option<u16>,

    #[serde(default)]
    pub vertical_margin: Option<u16>,

    #[serde(default)]
    pub constraints: Option<Vec<Constraint>>,
}

impl LayoutOptions {
    pub fn extend(mut self, other: &Self) -> Self {
        self.margin = other.margin.or(self.margin);
        self.horizontal_margin = other.horizontal_margin.or(self.horizontal_margin);
        self.vertical_margin = other.vertical_margin.or(self.vertical_margin);
        self.constraints = other.constraints.to_owned().or(self.constraints);
        self
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub enum CustomPanel {
    CustomParagraph {
        #[serde(default)]
        ui: PanelUiConfig,
        body: String,
    },
    CustomList {
        #[serde(default)]
        ui: PanelUiConfig,
        body: Vec<String>,
    },
    CustomTable {
        #[serde(default)]
        ui: PanelUiConfig,
        widths: Vec<Constraint>,
        col_spacing: Option<u16>,
        body: Vec<Vec<String>>,
    },
    CustomLayout(Layout),
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub enum Layout {
    Nothing,
    Table,
    InputAndLogs,
    Selection,
    HelpMenu,
    SortAndFilter,
    Static(Box<CustomPanel>),
    Dynamic(String),
    Horizontal {
        config: LayoutOptions,
        splits: Vec<Layout>,
    },
    Vertical {
        config: LayoutOptions,
        splits: Vec<Layout>,
    },

    /// For compatibility only. A better choice is Static or Dymanic layout.
    CustomContent(Box<CustomContent>),
}

impl Default for Layout {
    fn default() -> Self {
        Self::Nothing
    }
}

impl Layout {
    pub fn extend(self, other: &Self) -> Self {
        match (self, other) {
            (s, Self::Nothing) => s,
            (
                Self::Horizontal {
                    config: sconfig,
                    splits: _,
                },
                Self::Horizontal {
                    config: oconfig,
                    splits: osplits,
                },
            ) => Self::Horizontal {
                config: sconfig.extend(oconfig),
                splits: osplits.to_owned(),
            },

            (
                Self::Vertical {
                    config: sconfig,
                    splits: _,
                },
                Self::Vertical {
                    config: oconfig,
                    splits: osplits,
                },
            ) => Self::Vertical {
                config: sconfig.extend(oconfig),
                splits: osplits.to_owned(),
            },
            (_, other) => other.to_owned(),
        }
    }

    pub fn replace(self, target: &Self, replacement: &Self) -> Self {
        match self {
            Self::Horizontal { splits, config } => Self::Horizontal {
                splits: splits
                    .into_iter()
                    .map(|s| s.replace(target, replacement))
                    .collect(),
                config,
            },
            Self::Vertical { splits, config } => Self::Vertical {
                splits: splits
                    .into_iter()
                    .map(|s| s.replace(target, replacement))
                    .collect(),
                config,
            },
            other => {
                if other == *target {
                    replacement.to_owned()
                } else {
                    other
                }
            }
        }
    }
}

#[derive(
    Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Serialize, Deserialize,
)]
#[serde(deny_unknown_fields)]
pub enum Border {
    Top,
    Right,
    Bottom,
    Left,
}

impl Border {
    pub fn bits(self) -> u8 {
        match self {
            Self::Top => TuiBorders::TOP.bits(),
            Self::Right => TuiBorders::RIGHT.bits(),
            Self::Bottom => TuiBorders::BOTTOM.bits(),
            Self::Left => TuiBorders::LEFT.bits(),
        }
    }
}

#[derive(
    Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Serialize, Deserialize,
)]
#[serde(deny_unknown_fields)]
pub enum BorderType {
    Plain,
    Rounded,
    Double,
    Thick,
}

impl Default for BorderType {
    fn default() -> Self {
        Self::Plain
    }
}

impl Into<TuiBorderType> for BorderType {
    fn into(self) -> TuiBorderType {
        match self {
            BorderType::Plain => TuiBorderType::Plain,
            BorderType::Rounded => TuiBorderType::Rounded,
            BorderType::Double => TuiBorderType::Double,
            BorderType::Thick => TuiBorderType::Thick,
        }
    }
}

#[derive(
    Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Serialize, Deserialize,
)]
#[serde(deny_unknown_fields)]
pub enum Modifier {
    Bold,
    Dim,
    Italic,
    Underlined,
    SlowBlink,
    RapidBlink,
    Reversed,
    Hidden,
    CrossedOut,
}

impl Modifier {
    pub fn bits(self) -> u16 {
        match self {
            Self::Bold => TuiModifier::BOLD.bits(),
            Self::Dim => TuiModifier::DIM.bits(),
            Self::Italic => TuiModifier::ITALIC.bits(),
            Self::Underlined => TuiModifier::UNDERLINED.bits(),
            Self::SlowBlink => TuiModifier::SLOW_BLINK.bits(),
            Self::RapidBlink => TuiModifier::RAPID_BLINK.bits(),
            Self::Reversed => TuiModifier::REVERSED.bits(),
            Self::Hidden => TuiModifier::HIDDEN.bits(),
            Self::CrossedOut => TuiModifier::CROSSED_OUT.bits(),
        }
    }
}

fn extend_optional_modifiers(
    a: Option<IndexSet<Modifier>>,
    b: Option<IndexSet<Modifier>>,
) -> Option<IndexSet<Modifier>> {
    match (a, b) {
        (Some(mut a), Some(b)) => {
            a.extend(b);
            Some(a)
        }
        (Some(a), None) => Some(a),
        (None, Some(b)) => Some(b),
        (None, None) => None,
    }
}

// raratui doesn't support directly serializing `Color::Rgb` and
// `Color::Indexed` anymore.
// See https://github.com/ratatui-org/ratatui/pull/934
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum Color {
    #[default]
    Reset,
    Black,
    Red,
    Green,
    Yellow,
    Blue,
    Magenta,
    Cyan,
    Gray,
    DarkGray,
    LightRed,
    LightGreen,
    LightYellow,
    LightBlue,
    LightMagenta,
    LightCyan,
    White,
    Rgb(u8, u8, u8),
    Indexed(u8),
}

impl From<Color> for TuiColor {
    fn from(value: Color) -> Self {
        match value {
            Color::Reset => TuiColor::Reset,
            Color::Black => TuiColor::Black,
            Color::Red => TuiColor::Red,
            Color::Green => TuiColor::Green,
            Color::Yellow => TuiColor::Yellow,
            Color::Blue => TuiColor::Blue,
            Color::Magenta => TuiColor::Magenta,
            Color::Cyan => TuiColor::Cyan,
            Color::Gray => TuiColor::Gray,
            Color::DarkGray => TuiColor::DarkGray,
            Color::LightRed => TuiColor::LightRed,
            Color::LightGreen => TuiColor::LightGreen,
            Color::LightYellow => TuiColor::LightYellow,
            Color::LightBlue => TuiColor::LightBlue,
            Color::LightMagenta => TuiColor::LightMagenta,
            Color::LightCyan => TuiColor::LightCyan,
            Color::White => TuiColor::White,
            Color::Rgb(r, g, b) => TuiColor::Rgb(r, g, b),
            Color::Indexed(index) => TuiColor::Indexed(index),
        }
    }
}

#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Style {
    pub fg: Option<Color>,
    pub bg: Option<Color>,
    pub add_modifiers: Option<IndexSet<Modifier>>,
    pub sub_modifiers: Option<IndexSet<Modifier>>,
}

impl Style {
    pub fn extend(mut self, other: &Self) -> Self {
        self.fg = other.fg.or(self.fg);
        self.bg = other.bg.or(self.bg);
        self.add_modifiers = extend_optional_modifiers(
            self.add_modifiers,
            other.add_modifiers.to_owned(),
        );
        self.sub_modifiers = extend_optional_modifiers(
            self.sub_modifiers,
            other.sub_modifiers.to_owned(),
        );
        self
    }
}

impl From<Style> for TuiStyle {
    fn from(val: Style) -> Self {
        fn xor(modifiers: Option<IndexSet<Modifier>>) -> u16 {
            modifiers
                .unwrap_or_default()
                .into_iter()
                .map(Modifier::bits)
                .fold(0, BitXor::bitxor)
        }
        if *NO_COLOR {
            DEFAULT_STYLE
        } else {
            TuiStyle {
                fg: val.fg.map(Into::into),
                bg: val.bg.map(Into::into),
                add_modifier: TuiModifier::from_bits_truncate(xor(val.add_modifiers)),
                sub_modifier: TuiModifier::from_bits_truncate(xor(val.sub_modifiers)),
            }
        }
    }
}

impl From<&LsColorsStyle> for Style {
    fn from(style: &LsColorsStyle) -> Self {
        fn convert_color(color: &LsColorsColor) -> Color {
            match color {
                LsColorsColor::Black => Color::Black,
                LsColorsColor::Red => Color::Red,
                LsColorsColor::Green => Color::Green,
                LsColorsColor::Yellow => Color::Yellow,
                LsColorsColor::Blue => Color::Blue,
                LsColorsColor::Magenta => Color::Magenta,
                LsColorsColor::Cyan => Color::Cyan,
                LsColorsColor::White => Color::Gray,
                LsColorsColor::BrightBlack => Color::DarkGray,
                LsColorsColor::BrightRed => Color::LightRed,
                LsColorsColor::BrightGreen => Color::LightGreen,
                LsColorsColor::BrightYellow => Color::LightYellow,
                LsColorsColor::BrightBlue => Color::LightBlue,
                LsColorsColor::BrightMagenta => Color::LightMagenta,
                LsColorsColor::BrightCyan => Color::LightCyan,
                LsColorsColor::BrightWhite => Color::White,
                LsColorsColor::Fixed(index) => Color::Indexed(*index),
                LsColorsColor::RGB(r, g, b) => Color::Rgb(*r, *g, *b),
            }
        }
        Self {
            fg: style.foreground.as_ref().map(convert_color),
            bg: style.background.as_ref().map(convert_color),
            add_modifiers: None,
            sub_modifiers: None,
        }
    }
}

impl From<Style> for nu_ansi_term::Style {
    fn from(val: Style) -> Self {
        fn convert_color(color: Color) -> Option<nu_ansi_term::Color> {
            match color {
                Color::Black => Some(nu_ansi_term::Color::Black),
                Color::Red => Some(nu_ansi_term::Color::Red),
                Color::Green => Some(nu_ansi_term::Color::Green),
                Color::Yellow => Some(nu_ansi_term::Color::Yellow),
                Color::Blue => Some(nu_ansi_term::Color::Blue),
                Color::Magenta => Some(nu_ansi_term::Color::Purple),
                Color::Cyan => Some(nu_ansi_term::Color::Cyan),
                Color::Gray => Some(nu_ansi_term::Color::LightGray),
                Color::DarkGray => Some(nu_ansi_term::Color::DarkGray),
                Color::LightRed => Some(nu_ansi_term::Color::LightRed),
                Color::LightGreen => Some(nu_ansi_term::Color::LightGreen),
                Color::LightYellow => Some(nu_ansi_term::Color::LightYellow),
                Color::LightBlue => Some(nu_ansi_term::Color::LightBlue),
                Color::LightMagenta => Some(nu_ansi_term::Color::LightMagenta),
                Color::LightCyan => Some(nu_ansi_term::Color::LightCyan),
                Color::White => Some(nu_ansi_term::Color::White),
                Color::Rgb(r, g, b) => Some(nu_ansi_term::Color::Rgb(r, g, b)),
                Color::Indexed(index) => Some(nu_ansi_term::Color::Fixed(index)),
                _ => None,
            }
        }
        fn match_modifiers<F>(style: &Style, f: F) -> bool
        where
            F: Fn(&IndexSet<Modifier>) -> bool,
        {
            style.add_modifiers.as_ref().map_or(false, f)
        }

        let mut style = nu_ansi_term::Style::new();
        style.foreground = val.fg.and_then(convert_color);
        style.background = val.bg.and_then(convert_color);
        style.is_bold = match_modifiers(&val, |m| m.contains(&Modifier::Bold));
        style.is_dimmed = match_modifiers(&val, |m| m.contains(&Modifier::Dim));
        style.is_italic = match_modifiers(&val, |m| m.contains(&Modifier::Italic));
        style.is_underline =
            match_modifiers(&val, |m| m.contains(&Modifier::Underlined));
        style.is_blink = match_modifiers(&val, |m| {
            m.contains(&Modifier::SlowBlink) || m.contains(&Modifier::RapidBlink)
        });
        style.is_reverse = match_modifiers(&val, |m| m.contains(&Modifier::Reversed));
        style.is_hidden = match_modifiers(&val, |m| m.contains(&Modifier::Hidden));
        style.is_strikethrough =
            match_modifiers(&val, |m| m.contains(&Modifier::CrossedOut));
        style
    }
}

#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WrapOptions {
    pub width: usize,
    pub initial_indent: Option<String>,
    pub subsequent_indent: Option<String>,
    pub break_words: Option<bool>,
}

impl WrapOptions {
    pub fn get_options(&self) -> textwrap::Options<'_> {
        let mut options = textwrap::Options::new(self.width);
        if let Some(initial_indent) = &self.initial_indent {
            options = options.initial_indent(initial_indent);
        }
        if let Some(subsequent_indent) = &self.subsequent_indent {
            options = options.subsequent_indent(subsequent_indent);
        }
        if let Some(break_words) = self.break_words {
            options = options.break_words(break_words);
        }
        options
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub enum Constraint {
    Percentage(u16),
    Ratio(u32, u32),
    Length(u16),
    LengthLessThanScreenHeight(u16),
    LengthLessThanScreenWidth(u16),
    LengthLessThanLayoutHeight(u16),
    LengthLessThanLayoutWidth(u16),
    Max(u16),
    MaxLessThanScreenHeight(u16),
    MaxLessThanScreenWidth(u16),
    MaxLessThanLayoutHeight(u16),
    MaxLessThanLayoutWidth(u16),
    Min(u16),
    MinLessThanScreenHeight(u16),
    MinLessThanScreenWidth(u16),
    MinLessThanLayoutHeight(u16),
    MinLessThanLayoutWidth(u16),
}

impl Constraint {
    pub fn to_tui(self, screen_size: TuiRect, layout_size: TuiRect) -> TuiConstraint {
        match self {
            Self::Percentage(n) => TuiConstraint::Percentage(n),
            Self::Ratio(x, y) => TuiConstraint::Ratio(x, y),
            Self::Length(n) => TuiConstraint::Length(n),
            Self::LengthLessThanScreenHeight(n) => {
                TuiConstraint::Length(screen_size.height.max(n) - n)
            }
            Self::LengthLessThanScreenWidth(n) => {
                TuiConstraint::Length(screen_size.width.max(n) - n)
            }
            Self::LengthLessThanLayoutHeight(n) => {
                TuiConstraint::Length(layout_size.height.max(n) - n)
            }
            Self::LengthLessThanLayoutWidth(n) => {
                TuiConstraint::Length(layout_size.width.max(n) - n)
            }
            Self::Max(n) => TuiConstraint::Max(n),
            Self::MaxLessThanScreenHeight(n) => {
                TuiConstraint::Max(screen_size.height.max(n) - n)
            }
            Self::MaxLessThanScreenWidth(n) => {
                TuiConstraint::Max(screen_size.width.max(n) - n)
            }
            Self::MaxLessThanLayoutHeight(n) => {
                TuiConstraint::Max(layout_size.height.max(n) - n)
            }
            Self::MaxLessThanLayoutWidth(n) => {
                TuiConstraint::Max(layout_size.width.max(n) - n)
            }
            Self::Min(n) => TuiConstraint::Min(n),
            Self::MinLessThanScreenHeight(n) => {
                TuiConstraint::Min(screen_size.height.max(n) - n)
            }
            Self::MinLessThanScreenWidth(n) => {
                TuiConstraint::Min(screen_size.width.max(n) - n)
            }
            Self::MinLessThanLayoutHeight(n) => {
                TuiConstraint::Min(layout_size.height.max(n) - n)
            }
            Self::MinLessThanLayoutWidth(n) => {
                TuiConstraint::Min(layout_size.width.max(n) - n)
            }
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedNodeUiMetadata {
    pub absolute_path: String,
    pub extension: String,
    pub is_dir: bool,
    pub is_file: bool,
    pub is_readonly: bool,
    pub mime_essence: String,
    pub size: u64,
    pub human_size: String,
    pub created: Option<u128>,
    pub last_modified: Option<u128>,
}

impl From<ResolvedNode> for ResolvedNodeUiMetadata {
    fn from(node: ResolvedNode) -> Self {
        Self {
            absolute_path: node.absolute_path.to_owned(),
            extension: node.extension.to_owned(),
            is_dir: node.is_dir,
            is_file: node.is_file,
            is_readonly: node.is_readonly,
            mime_essence: node.mime_essence.to_owned(),
            size: node.size,
            human_size: node.human_size,
            created: node.created,
            last_modified: node.last_modified,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeUiMetadata {
    // From Node
    pub parent: String,
    pub relative_path: String,
    pub absolute_path: String,
    pub extension: String,
    pub is_symlink: bool,
    pub is_broken: bool,
    pub is_dir: bool,
    pub is_file: bool,
    pub is_readonly: bool,
    pub mime_essence: String,
    pub size: u64,
    pub human_size: String,
    pub permissions: Permissions,
    pub canonical: Option<ResolvedNodeUiMetadata>,
    pub symlink: Option<ResolvedNodeUiMetadata>,
    pub created: Option<u128>,
    pub last_modified: Option<u128>,
    pub uid: u32,
    pub gid: u32,

    // Extra
    pub index: usize,
    pub relative_index: usize,
    pub is_before_focus: bool,
    pub is_after_focus: bool,
    pub tree: String,
    pub prefix: String,
    pub suffix: String,
    pub is_selected: bool,
    pub is_focused: bool,
    pub total: usize,
    pub meta: HashMap<String, String>,
    pub style: Style,
}

impl NodeUiMetadata {
    fn new(
        node: &Node,
        index: usize,
        relative_index: usize,
        is_before_focus: bool,
        is_after_focus: bool,
        tree: String,
        prefix: String,
        suffix: String,
        is_selected: bool,
        is_focused: bool,
        total: usize,
        meta: HashMap<String, String>,
        style: Style,
    ) -> Self {
        Self {
            parent: node.parent.to_owned(),
            relative_path: node.relative_path.to_owned(),
            absolute_path: node.absolute_path.to_owned(),
            extension: node.extension.to_owned(),
            is_symlink: node.is_symlink,
            is_broken: node.is_broken,
            is_dir: node.is_dir,
            is_file: node.is_file,
            is_readonly: node.is_readonly,
            mime_essence: node.mime_essence.to_owned(),
            size: node.size,
            human_size: node.human_size.to_owned(),
            permissions: node.permissions.to_owned(),
            canonical: node.canonical.to_owned().map(ResolvedNode::into),
            symlink: node.symlink.to_owned().map(ResolvedNode::into),
            created: node.created,
            last_modified: node.last_modified,
            uid: node.uid,
            gid: node.gid,
            index,
            relative_index,
            is_before_focus,
            is_after_focus,
            tree,
            prefix,
            suffix,
            is_selected,
            is_focused,
            total,
            meta,
            style,
        }
    }
}

pub fn block<'a>(config: PanelUiConfig, default_title: String) -> Block<'a> {
    Block::default()
        .borders(TuiBorders::from_bits_truncate(
            config
                .borders
                .to_owned()
                .unwrap_or_default()
                .iter()
                .map(|b| b.bits())
                .fold(0, |a, b| (a ^ b)),
        ))
        .title(Span::styled(
            config.title.format.unwrap_or(default_title),
            config.title.style,
        ))
        .style(config.style)
        .border_type(config.border_type.unwrap_or_default().into())
        .border_style(config.border_style)
}

fn draw_table(
    f: &mut Frame,
    screen_size: TuiRect,
    layout_size: TuiRect,
    app: &app::App,
    lua: &Lua,
) {
    let panel_config = &app.config.general.panel_ui;
    let config = panel_config.default.to_owned().extend(&panel_config.table);
    let app_config = app.config.to_owned();
    let header_height = app_config.general.table.header.height.unwrap_or(1);
    let height: usize =
        (layout_size.height.max(header_height + 2) - (header_height + 2)).into();
    let row_style = app_config.general.table.row.style.to_owned();

    let rows = app
        .directory_buffer
        .as_ref()
        .map(|dir| {
            dir.nodes
                .iter()
                .enumerate()
                .skip(height * (dir.focus / height.max(1)))
                .take(height)
                .map(|(index, node)| {
                    let is_focused = dir.focus == index;

                    let is_selected = app
                        .selection
                        .iter()
                        .any(|s| s.absolute_path == node.absolute_path);

                    let is_first = index == 0;
                    let is_last = index == dir.total.max(1) - 1;

                    let tree = app_config
                        .general
                        .table
                        .tree
                        .to_owned()
                        .map(|t| {
                            if is_last {
                                t.2.format
                            } else if is_first {
                                t.0.format
                            } else {
                                t.1.format
                            }
                        })
                        .unwrap_or_default();

                    let node_type = app_config.node_types.get(node);

                    let (relative_index, is_before_focus, is_after_focus) =
                        match dir.focus.cmp(&index) {
                            Ordering::Greater => (dir.focus - index, true, false),
                            Ordering::Less => (index - dir.focus, false, true),
                            Ordering::Equal => (0, false, false),
                        };

                    let (mut prefix, mut suffix, mut style) = {
                        let ui = app_config.general.default_ui.to_owned();
                        (ui.prefix, ui.suffix, ui.style.extend(&node_type.style))
                    };

                    if is_focused && is_selected {
                        let ui = app_config.general.focus_selection_ui.to_owned();
                        prefix = ui.prefix.to_owned().or(prefix);
                        suffix = ui.suffix.to_owned().or(suffix);
                        style = style.extend(&ui.style);
                    } else if is_selected {
                        let ui = app_config.general.selection_ui.to_owned();
                        prefix = ui.prefix.to_owned().or(prefix);
                        suffix = ui.suffix.to_owned().or(suffix);
                        style = style.extend(&ui.style);
                    } else if is_focused {
                        let ui = app_config.general.focus_ui.to_owned();
                        prefix = ui.prefix.to_owned().or(prefix);
                        suffix = ui.suffix.to_owned().or(suffix);
                        style = style.extend(&ui.style);
                    };

                    let meta = NodeUiMetadata::new(
                        node,
                        index,
                        relative_index,
                        is_before_focus,
                        is_after_focus,
                        tree.unwrap_or_default(),
                        prefix.unwrap_or_default(),
                        suffix.unwrap_or_default(),
                        is_selected,
                        is_focused,
                        dir.total,
                        node_type.meta,
                        style,
                    );

                    let cols = lua::serialize::<NodeUiMetadata>(lua, &meta)
                        .map(|v| {
                            app_config
                                .general
                                .table
                                .row
                                .cols
                                .to_owned()
                                .unwrap_or_default()
                                .iter()
                                .filter_map(|c| {
                                    c.format.as_ref().map(|f| {
                                        let out = lua::call(lua, f, v.clone())
                                            .unwrap_or_else(|e| format!("{e:?}"));
                                        (string_to_text(out), c.style.to_owned())
                                    })
                                })
                                .collect::<Vec<(Text, Style)>>()
                        })
                        .unwrap_or_default()
                        .into_iter()
                        .map(|(text, style)| Cell::from(text).style(style))
                        .collect::<Vec<Cell>>();

                    Row::new(cols).style(row_style.to_owned())
                })
                .collect::<Vec<Row>>()
        })
        .unwrap_or_default();

    let table_constraints: Vec<TuiConstraint> = app_config
        .general
        .table
        .col_widths
        .to_owned()
        .unwrap_or_default()
        .into_iter()
        .map(|c| c.to_tui(screen_size, layout_size))
        .collect();

    let pwd = if let Some(vroot) = app.vroot.as_ref() {
        app.pwd.strip_prefix(vroot).unwrap_or(&app.pwd)
    } else {
        &app.pwd
    }
    .trim_matches('/');

    let pwd = path::escape(pwd);

    let vroot_indicator = if app.vroot.is_some() { "vroot:" } else { "" };

    let node_count = app.directory_buffer.as_ref().map(|d| d.total).unwrap_or(0);
    let node_count = if node_count == 0 {
        String::new()
    } else {
        format!("({node_count}) ")
    };

    let table = Table::new(rows, table_constraints)
        .style(app_config.general.table.style.to_owned())
        .highlight_style(app_config.general.focus_ui.style.to_owned())
        .column_spacing(app_config.general.table.col_spacing.unwrap_or_default())
        .block(block(
            config,
            format!(" {vroot_indicator}/{pwd} {node_count}"),
        ));

    let table = table.to_owned().header(
        Row::new(
            app_config
                .general
                .table
                .header
                .cols
                .to_owned()
                .unwrap_or_default()
                .iter()
                .map(|c| {
                    Cell::from(c.format.to_owned().unwrap_or_default())
                        .style(c.style.to_owned())
                })
                .collect::<Vec<Cell>>(),
        )
        .height(header_height)
        .style(app_config.general.table.header.style.to_owned()),
    );

    f.render_widget(table, layout_size);
}

fn draw_selection(
    f: &mut Frame,
    _screen_size: TuiRect,
    layout_size: TuiRect,
    app: &app::App,
    lua: &Lua,
) {
    let panel_config = &app.config.general.panel_ui;
    let config = panel_config
        .default
        .to_owned()
        .extend(&panel_config.selection);

    let selection_count = app.selection.len();

    let selection: Vec<ListItem> = app
        .selection
        .iter()
        .rev()
        .take((layout_size.height.max(2) - 2).into())
        .rev()
        .map(|n| {
            let out = app
                .config
                .general
                .selection
                .item
                .format
                .as_ref()
                .map(|f| {
                    lua::serialize::<Node>(lua, n)
                        .and_then(|n| lua::call(lua, f, n))
                        .unwrap_or_else(|e| format!("{e:?}"))
                })
                .unwrap_or_else(|| n.absolute_path.clone());
            string_to_text(out)
        })
        .map(|i| {
            ListItem::new(i).style(app.config.general.selection.item.style.to_owned())
        })
        .collect();

    // Selected items
    let selection_count = if selection_count == 0 {
        String::new()
    } else {
        format!("({selection_count}) ")
    };

    let selection_list = List::new(selection)
        .block(block(config, format!(" Selection {selection_count}")));

    f.render_widget(selection_list, layout_size);
}

fn draw_help_menu(
    f: &mut Frame,
    _screen_size: TuiRect,
    layout_size: TuiRect,
    app: &app::App,
    _: &Lua,
) {
    let panel_config = &app.config.general.panel_ui;

    let config = panel_config
        .default
        .to_owned()
        .extend(&panel_config.help_menu);

    let help_menu_rows = app
        .mode
        .help_menu()
        .into_iter()
        .map(|l| match l {
            HelpMenuLine::Paragraph(p) => Row::new([Cell::from(p)].to_vec()),
            HelpMenuLine::KeyMap(k, remaps, h) => Row::new({
                if app.config.general.hide_remaps_in_help_menu {
                    [Cell::from(k), Cell::from(h)].to_vec()
                } else {
                    [Cell::from(k), Cell::from(remaps.join("|")), Cell::from(h)].to_vec()
                }
            }),
        })
        .collect::<Vec<Row>>();

    let widths = if app.config.general.hide_remaps_in_help_menu {
        vec![TuiConstraint::Percentage(20), TuiConstraint::Percentage(80)]
    } else {
        vec![
            TuiConstraint::Percentage(20),
            TuiConstraint::Percentage(20),
            TuiConstraint::Percentage(60),
        ]
    };
    let help_menu = Table::new(help_menu_rows, widths).block(block(
        config,
        format!(" Help [{}{}] ", &app.mode.name, read_only_indicator(app)),
    ));
    f.render_widget(help_menu, layout_size);
}

fn draw_input_buffer(
    f: &mut Frame,
    _screen_size: TuiRect,
    layout_size: TuiRect,
    app: &app::App,
    _: &Lua,
) {
    if let Some(input) = app.input.buffer.as_ref() {
        let panel_config = &app.config.general.panel_ui;
        let config = panel_config
            .default
            .to_owned()
            .extend(&panel_config.input_and_logs);

        let cursor_offset_left = config
            .borders
            .as_ref()
            .map(|b| b.contains(&Border::Left))
            .unwrap_or(false) as u16
            + app.input.prompt.chars().count() as u16;

        let cursor_offset_right = config
            .borders
            .as_ref()
            .map(|b| b.contains(&Border::Right))
            .unwrap_or(false) as u16
            + 1;

        let offset_width = cursor_offset_left + cursor_offset_right;
        let width = layout_size.width.max(offset_width) - offset_width;
        let scroll = input.visual_scroll(width.into()) as u16;

        let input_buf = Paragraph::new(Line::from(vec![
            Span::styled(
                app.input.prompt.to_owned(),
                app.config.general.prompt.style.to_owned(),
            ),
            Span::raw(input.value()),
        ]))
        .scroll((0, scroll))
        .block(block(
            config,
            format!(
                " Input [{}{}]{} ",
                app.mode.name,
                read_only_indicator(app),
                selection_indicator(app),
            ),
        ));

        f.render_widget(input_buf, layout_size);
        f.set_cursor(
            // Put cursor past the end of the input text
            layout_size.x
                + (input.visual_cursor() as u16).min(width)
                + cursor_offset_left,
            // Move one line down, from the border to the input line
            layout_size.y + 1,
        );
    };
}

fn draw_sort_n_filter(
    f: &mut Frame,
    _screen_size: TuiRect,
    layout_size: TuiRect,
    app: &app::App,
    _: &Lua,
) {
    let panel_config = &app.config.general.panel_ui;
    let config = panel_config
        .default
        .to_owned()
        .extend(&panel_config.sort_and_filter);
    let ui = app.config.general.sort_and_filter_ui.to_owned();
    let filter_by: &IndexSet<NodeFilterApplicable> = &app.explorer_config.filters;
    let sort_by: &IndexSet<NodeSorterApplicable> = &app.explorer_config.sorters;
    let search = app.explorer_config.searcher.as_ref();

    let defaultui = &ui.default_identifier;
    let forwardui = defaultui
        .to_owned()
        .extend(&ui.sort_direction_identifiers.forward);
    let reverseui = defaultui
        .to_owned()
        .extend(&ui.sort_direction_identifiers.reverse);

    let orderedui = defaultui
        .to_owned()
        .extend(&ui.search_direction_identifiers.ordered);
    let unorderedui = defaultui
        .to_owned()
        .extend(&ui.search_direction_identifiers.unordered);

    let is_ordered_search = search.as_ref().map(|s| !s.unordered).unwrap_or(false);

    let mut spans = filter_by
        .iter()
        .map(|f| {
            ui.filter_identifiers
                .get(&f.filter)
                .map(|u| {
                    let ui = defaultui.to_owned().extend(u);
                    (
                        Span::styled(
                            ui.format.to_owned().unwrap_or_default(),
                            ui.style.to_owned(),
                        ),
                        Span::styled(f.input.to_owned(), ui.style),
                    )
                })
                .unwrap_or((Span::raw("f"), Span::raw("")))
        })
        .chain(search.iter().map(|s| {
            ui.search_identifiers
                .get(&s.algorithm)
                .map(|u| {
                    let direction = if s.unordered {
                        &unorderedui
                    } else {
                        &orderedui
                    };
                    let ui = defaultui.to_owned().extend(u);
                    let f = ui
                        .format
                        .as_ref()
                        .map(|f| format!("{f}{p}", p = &s.pattern))
                        .unwrap_or_else(|| s.pattern.clone());
                    (
                        Span::styled(f, ui.style),
                        Span::styled(
                            direction.format.to_owned().unwrap_or_default(),
                            direction.style.to_owned(),
                        ),
                    )
                })
                .unwrap_or((Span::raw("/"), Span::raw(&s.pattern)))
        }))
        .chain(
            sort_by
                .iter()
                .map(|s| {
                    let direction = if s.reverse { &reverseui } else { &forwardui };
                    ui.sorter_identifiers
                        .get(&s.sorter)
                        .map(|u| {
                            let ui = defaultui.to_owned().extend(u);
                            (
                                Span::styled(
                                    ui.format.to_owned().unwrap_or_default(),
                                    ui.style,
                                ),
                                Span::styled(
                                    direction.format.to_owned().unwrap_or_default(),
                                    direction.style.to_owned(),
                                ),
                            )
                        })
                        .unwrap_or((Span::raw("s"), Span::raw("")))
                })
                .take(if !is_ordered_search { sort_by.len() } else { 0 }),
        )
        .zip(std::iter::repeat(Span::styled(
            ui.separator.format.to_owned().unwrap_or_default(),
            ui.separator.style.to_owned(),
        )))
        .flat_map(|((a, b), c)| vec![a, b, c])
        .collect::<Vec<Span>>();

    spans.pop();

    let item_count = filter_by.len() + sort_by.len();
    let item_count = if item_count == 0 {
        String::new()
    } else {
        format!("({item_count}) ")
    };

    let p = Paragraph::new(Line::from(spans))
        .block(block(config, format!(" Sort & filter {item_count}")));

    f.render_widget(p, layout_size);
}

fn draw_logs(
    f: &mut Frame,
    _screen_size: TuiRect,
    layout_size: TuiRect,
    app: &app::App,
    _: &Lua,
) {
    let panel_config = &app.config.general.panel_ui;
    let config = panel_config
        .default
        .to_owned()
        .extend(&panel_config.input_and_logs);
    let logs_config = app.config.general.logs.to_owned();
    let logs = if app.logs_hidden {
        vec![]
    } else {
        app.logs
            .iter()
            .rev()
            .take(layout_size.height as usize)
            .map(|log| {
                let fd = format_description!("[hour]:[minute]:[second]");
                let time = log.created_at.format(fd).unwrap_or_else(|_| "when?".into());
                let cfg = match log.level {
                    app::LogLevel::Info => &logs_config.info,
                    app::LogLevel::Warning => &logs_config.warning,
                    app::LogLevel::Success => &logs_config.success,
                    app::LogLevel::Error => &logs_config.error,
                };

                let prefix =
                    format!("{time}|{0}", cfg.format.to_owned().unwrap_or_default());

                let padding = " ".repeat(prefix.chars().count());

                let txt = log
                    .message
                    .lines()
                    .enumerate()
                    .map(|(i, line)| {
                        if i == 0 {
                            format!("{prefix}) {line}")
                        } else {
                            format!("{padding}  {line}")
                        }
                    })
                    .take(layout_size.height as usize)
                    .collect::<Vec<_>>()
                    .join("\n");

                ListItem::new(txt).style(cfg.style.to_owned())
            })
            .collect::<Vec<ListItem>>()
    };

    let logs_count = app.logs.len();
    let logs_count = if logs_count == 0 {
        String::new()
    } else {
        format!(" ({logs_count})")
    };

    let logs_list = List::new(logs).block(block(
        config,
        format!(
            " Logs{} [{}{}]{} ",
            logs_count,
            app.mode.name,
            read_only_indicator(app),
            selection_indicator(app)
        ),
    ));

    f.render_widget(logs_list, layout_size);
}

pub fn draw_nothing(
    f: &mut Frame,
    _screen_size: TuiRect,
    layout_size: TuiRect,
    app: &app::App,
    _lua: &Lua,
) {
    let panel_config = &app.config.general.panel_ui;
    let config = panel_config.default.to_owned();
    let nothing = Paragraph::new("").block(block(config, "".into()));
    f.render_widget(nothing, layout_size);
}

pub fn draw_dynamic(
    f: &mut Frame,
    screen_size: TuiRect,
    layout_size: TuiRect,
    app: &app::App,
    func: &str,
    lua: &Lua,
) {
    let ctx = ContentRendererArg {
        app: app.to_lua_ctx_light(),
        layout_size: layout_size.into(),
        screen_size: screen_size.into(),
    };

    let panel: CustomPanel = lua::serialize(lua, &ctx)
        .and_then(|arg| lua::call(lua, func, arg))
        .unwrap_or_else(|e| CustomPanel::CustomParagraph {
            ui: app.config.general.panel_ui.default.clone(),
            body: format!("{e:?}"),
        });

    draw_static(f, screen_size, layout_size, app, panel, lua);
}

pub fn draw_static(
    f: &mut Frame,
    screen_size: TuiRect,
    layout_size: TuiRect,
    app: &app::App,
    panel: CustomPanel,
    _lua: &Lua,
) {
    let defaultui = app.config.general.panel_ui.default.clone();
    match panel {
        CustomPanel::CustomParagraph { ui, body } => {
            let config = defaultui.extend(&ui);
            let body = string_to_text(body);
            let content = Paragraph::new(body).block(block(config, "".into()));
            f.render_widget(content, layout_size);
        }

        CustomPanel::CustomList { ui, body } => {
            let config = defaultui.extend(&ui);

            let items = body
                .into_iter()
                .map(string_to_text)
                .map(ListItem::new)
                .collect::<Vec<ListItem>>();

            let content = List::new(items).block(block(config, "".into()));
            f.render_widget(content, layout_size);
        }

        CustomPanel::CustomTable {
            ui,
            widths,
            col_spacing,
            body,
        } => {
            let config = defaultui.extend(&ui);
            let rows = body
                .into_iter()
                .map(|cols| {
                    Row::new(
                        cols.into_iter()
                            .map(string_to_text)
                            .map(Cell::from)
                            .collect::<Vec<Cell>>(),
                    )
                })
                .collect::<Vec<Row>>();

            let widths = widths
                .into_iter()
                .map(|w| w.to_tui(screen_size, layout_size))
                .collect::<Vec<TuiConstraint>>();

            let content = Table::new(rows, widths)
                .column_spacing(col_spacing.unwrap_or(1))
                .block(block(config, "".into()));

            f.render_widget(content, layout_size);
        }

        CustomPanel::CustomLayout(layout) => {
            draw_layout(layout, f, screen_size, layout_size, app, _lua);
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rect {
    x: u16,
    y: u16,
    height: u16,
    width: u16,
}

impl From<TuiRect> for Rect {
    fn from(tui: TuiRect) -> Self {
        Self {
            x: tui.x,
            y: tui.y,
            height: tui.height,
            width: tui.width,
        }
    }
}

#[derive(Debug, Clone, Serialize)]
pub struct ContentRendererArg {
    pub app: app::LuaContextLight,
    pub screen_size: Rect,
    pub layout_size: Rect,
}

pub fn draw_layout(
    layout: Layout,
    f: &mut Frame,
    screen_size: TuiRect,
    layout_size: TuiRect,
    app: &app::App,
    lua: &Lua,
) {
    match layout {
        Layout::Nothing => draw_nothing(f, screen_size, layout_size, app, lua),
        Layout::Table => draw_table(f, screen_size, layout_size, app, lua),
        Layout::SortAndFilter => {
            draw_sort_n_filter(f, screen_size, layout_size, app, lua)
        }
        Layout::HelpMenu => draw_help_menu(f, screen_size, layout_size, app, lua),
        Layout::Selection => draw_selection(f, screen_size, layout_size, app, lua),
        Layout::InputAndLogs => {
            if app.input.buffer.is_some() {
                draw_input_buffer(f, screen_size, layout_size, app, lua);
            } else {
                draw_logs(f, screen_size, layout_size, app, lua);
            };
        }
        Layout::Static(panel) => {
            draw_static(f, screen_size, layout_size, app, *panel, lua)
        }
        Layout::Dynamic(ref func) => {
            draw_dynamic(f, screen_size, layout_size, app, func, lua)
        }
        Layout::CustomContent(content) => {
            draw_custom_content(f, screen_size, layout_size, app, *content, lua)
        }
        Layout::Horizontal { config, splits } => {
            let chunks = TuiLayout::default()
                .direction(Direction::Horizontal)
                .constraints(
                    config
                        .constraints
                        .to_owned()
                        .unwrap_or_default()
                        .iter()
                        .map(|c| c.to_tui(screen_size, layout_size))
                        .collect::<Vec<TuiConstraint>>(),
                )
                .horizontal_margin(
                    config
                        .horizontal_margin
                        .or(config.margin)
                        .unwrap_or_default(),
                )
                .vertical_margin(
                    config.vertical_margin.or(config.margin).unwrap_or_default(),
                )
                .split(layout_size);

            splits
                .into_iter()
                .zip(chunks.iter())
                .for_each(|(split, chunk)| {
                    draw_layout(split, f, screen_size, *chunk, app, lua)
                });
        }

        Layout::Vertical { config, splits } => {
            let chunks = TuiLayout::default()
                .direction(Direction::Vertical)
                .constraints(
                    config
                        .constraints
                        .to_owned()
                        .unwrap_or_default()
                        .iter()
                        .map(|c| c.to_tui(screen_size, layout_size))
                        .collect::<Vec<TuiConstraint>>(),
                )
                .horizontal_margin(
                    config
                        .horizontal_margin
                        .or(config.margin)
                        .unwrap_or_default(),
                )
                .vertical_margin(
                    config.vertical_margin.or(config.margin).unwrap_or_default(),
                )
                .split(layout_size);

            splits
                .into_iter()
                .zip(chunks.iter())
                .for_each(|(split, chunk)| {
                    draw_layout(split, f, screen_size, *chunk, app, lua)
                });
        }
    }
}

pub fn draw(f: &mut Frame, app: &app::App, lua: &Lua) {
    let screen_size = f.size();
    let layout = app.mode.layout.as_ref().unwrap_or(&app.layout).to_owned();

    draw_layout(layout, f, screen_size, screen_size, app, lua);
}

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

    fn modifier(m: Modifier) -> Option<IndexSet<Modifier>> {
        let mut x = IndexSet::new();
        x.insert(m);
        Some(x)
    }

    #[test]
    fn test_extend_style() {
        let a = Style {
            fg: Some(Color::Red),
            bg: None,
            add_modifiers: modifier(Modifier::Bold),
            sub_modifiers: None,
        };

        let b = Style {
            fg: None,
            bg: Some(Color::Blue),
            add_modifiers: None,
            sub_modifiers: modifier(Modifier::Dim),
        };

        let c = Style {
            fg: Some(Color::Cyan),
            bg: Some(Color::Magenta),
            add_modifiers: modifier(Modifier::CrossedOut),
            sub_modifiers: modifier(Modifier::Italic),
        };

        assert_eq!(
            a.to_owned().extend(&b),
            Style {
                fg: Some(Color::Red),
                bg: Some(Color::Blue),
                add_modifiers: modifier(Modifier::Bold),
                sub_modifiers: modifier(Modifier::Dim),
            }
        );

        assert_eq!(
            b.extend(&a),
            Style {
                fg: Some(Color::Red),
                bg: Some(Color::Blue),
                add_modifiers: modifier(Modifier::Bold),
                sub_modifiers: modifier(Modifier::Dim),
            }
        );

        assert_eq!(
            a.to_owned().extend(&c),
            Style {
                fg: Some(Color::Cyan),
                bg: Some(Color::Magenta),
                add_modifiers: Some(
                    vec![Modifier::Bold, Modifier::CrossedOut]
                        .into_iter()
                        .collect()
                ),
                sub_modifiers: modifier(Modifier::Italic),
            }
        );

        assert_eq!(
            c.extend(&a),
            Style {
                fg: Some(Color::Red),
                bg: Some(Color::Magenta),
                add_modifiers: Some(
                    vec![Modifier::Bold, Modifier::CrossedOut]
                        .into_iter()
                        .collect()
                ),
                sub_modifiers: modifier(Modifier::Italic),
            }
        );
    }

    #[test]
    fn test_layout_replace() {
        let layout = Layout::Horizontal {
            config: LayoutOptions {
                margin: Some(2),
                horizontal_margin: Some(3),
                vertical_margin: Some(4),
                constraints: Some(vec![
                    Constraint::Percentage(80),
                    Constraint::Percentage(20),
                ]),
            },
            splits: vec![Layout::Table, Layout::HelpMenu],
        };

        let res = layout.clone().replace(&Layout::Table, &Layout::Selection);

        match (res, layout) {
            (
                Layout::Horizontal {
                    config: res_config,
                    splits: res_splits,
                },
                Layout::Horizontal {
                    config: layout_config,
                    splits: layout_splits,
                },
            ) => {
                assert_eq!(res_config, layout_config);
                assert_eq!(res_splits.len(), layout_splits.len());
                assert_eq!(res_splits[0], Layout::Selection);
                assert_ne!(res_splits[0], layout_splits[0]);
                assert_eq!(res_splits[1], layout_splits[1]);
            }
            _ => panic!("Unexpected layout"),
        }

        let res = Layout::Table.replace(&Layout::Table, &Layout::Selection);
        assert_eq!(res, Layout::Selection);

        let res = Layout::Table.replace(&Layout::Nothing, &Layout::Selection);
        assert_eq!(res, Layout::Table);
    }
}
