use crate::color::{Colors, Elem};
use crate::flags::blocks::Block;
use crate::flags::{Display, Flags, HyperlinkOption, Layout};
use crate::git_theme::GitTheme;
use crate::icon::Icons;
use crate::meta::name::DisplayOption;
use crate::meta::{FileType, Meta, Name, OwnerCache};
use std::collections::HashMap;
use term_grid::{Alignment, Cell, Direction, Filling, Grid, GridOptions};
use terminal_size::terminal_size;
use unicode_width::UnicodeWidthStr;

const EDGE: &str = "\u{251c}\u{2500}\u{2500}"; // "├──"
const LINE: &str = "\u{2502}  "; // "│  "
const CORNER: &str = "\u{2514}\u{2500}\u{2500}"; // "└──"
const BLANK: &str = "   ";

pub fn grid(
    metas: &[Meta],
    flags: &Flags,
    colors: &Colors,
    icons: &Icons,
    git_theme: &GitTheme,
) -> String {
    let term_width = terminal_size().map(|(w, _)| w.0 as usize);
    let owner_cache = OwnerCache::default();

    inner_display_grid(
        &DisplayOption::FileName,
        metas,
        &owner_cache,
        flags,
        colors,
        icons,
        git_theme,
        1,
        term_width,
    )
}

pub fn tree(
    metas: &[Meta],
    flags: &Flags,
    colors: &Colors,
    icons: &Icons,
    git_theme: &GitTheme,
) -> String {
    let mut grid = Grid::new(GridOptions {
        filling: Filling::Spaces(2),
        direction: Direction::LeftToRight,
    });

    let padding_rules = get_padding_rules(metas, flags);
    let mut index = 0;
    for (i, block) in flags.blocks.0.iter().enumerate() {
        if block == &Block::Name {
            index = i;
            break;
        }
    }

    let owner_cache = OwnerCache::default();

    for cell in inner_display_tree(
        metas,
        &owner_cache,
        flags,
        colors,
        icons,
        git_theme,
        (0, ""),
        &padding_rules,
        index,
    ) {
        grid.add(cell);
    }

    grid.fit_into_columns(1).to_string()
}

#[allow(clippy::too_many_arguments)] // should wrap flags, colors, icons, git_theme into one struct
fn inner_display_grid(
    display_option: &DisplayOption,
    metas: &[Meta],
    owner_cache: &OwnerCache,
    flags: &Flags,
    colors: &Colors,
    icons: &Icons,
    git_theme: &GitTheme,
    depth: usize,
    term_width: Option<usize>,
) -> String {
    let mut output = String::new();
    let mut cells = Vec::new();

    let padding_rules = get_padding_rules(metas, flags);
    let mut grid = match flags.layout {
        Layout::OneLine => Grid::new(GridOptions {
            filling: Filling::Spaces(2),
            direction: Direction::LeftToRight,
        }),
        _ => Grid::new(GridOptions {
            filling: Filling::Spaces(2),
            direction: Direction::TopToBottom,
        }),
    };

    // The first iteration (depth == 0) corresponds to the inputs given by the
    // user. We defer displaying directories given by the user unless we've been
    // asked to display the directory itself (rather than its contents).
    let skip_dirs = (depth == 0) && (flags.display != Display::DirectoryOnly);

    // print the files first.
    for meta in metas {
        // Maybe skip showing the directory meta now; show its contents later.
        if skip_dirs
            && (matches!(meta.file_type, FileType::Directory { .. })
                || (matches!(meta.file_type, FileType::SymLink { is_dir: true }))
                    && flags.blocks.0.len() == 1)
        {
            continue;
        }

        let blocks = get_output(
            meta,
            owner_cache,
            colors,
            icons,
            git_theme,
            flags,
            display_option,
            &padding_rules,
            (0, ""),
        );

        for block in blocks {
            cells.push(Cell {
                width: get_visible_width(&block, flags.hyperlink == HyperlinkOption::Always),
                contents: block,
                alignment: Alignment::Left,
            });
        }
    }

    // Print block headers
    if flags.header.0 && flags.layout == Layout::OneLine && !cells.is_empty() {
        add_header(flags, &cells, &mut grid);
    }

    for cell in cells {
        grid.add(cell);
    }

    if flags.layout == Layout::Grid {
        if let Some(tw) = term_width {
            if let Some(gridded_output) = grid.fit_into_width(tw) {
                output += &gridded_output.to_string();
            } else {
                //does not fit into grid, usually because (some) filename(s)
                //are longer or almost as long as term_width
                //print line by line instead!
                output += &grid.fit_into_columns(1).to_string();
            }
        } else {
            output += &grid.fit_into_columns(1).to_string();
        }
    } else {
        output += &grid.fit_into_columns(flags.blocks.0.len()).to_string();
    }

    let should_display_folder_path = should_display_folder_path(depth, metas);

    // print the folder content
    for meta in metas {
        if let Some(content) = &meta.content {
            if should_display_folder_path {
                output += &display_folder_path(meta);
            }

            let display_option = DisplayOption::Relative {
                base_path: &meta.path,
            };

            output += &inner_display_grid(
                &display_option,
                content,
                owner_cache,
                flags,
                colors,
                icons,
                git_theme,
                depth + 1,
                term_width,
            );
        }
    }

    output
}

fn add_header(flags: &Flags, cells: &[Cell], grid: &mut Grid) {
    let num_columns: usize = flags.blocks.0.len();

    let mut widths = flags
        .blocks
        .0
        .iter()
        .map(|b| get_visible_width(b.get_header(), flags.hyperlink == HyperlinkOption::Always))
        .collect::<Vec<usize>>();

    // find max widths of each column
    for (index, cell) in cells.iter().enumerate() {
        let index = index % num_columns;
        widths[index] = std::cmp::max(widths[index], cell.width);
    }

    for (idx, block) in flags.blocks.0.iter().enumerate() {
        // center and underline header
        let underlined_header = crossterm::style::Stylize::attribute(
            format!("{: ^1$}", block.get_header(), widths[idx]),
            crossterm::style::Attribute::Underlined,
        )
        .to_string();

        grid.add(Cell {
            width: widths[idx],
            contents: underlined_header,
            alignment: Alignment::Left,
        });
    }
}

#[allow(clippy::too_many_arguments)]
fn inner_display_tree(
    metas: &[Meta],
    owner_cache: &OwnerCache,
    flags: &Flags,
    colors: &Colors,
    icons: &Icons,
    git_theme: &GitTheme,
    tree_depth_prefix: (usize, &str),
    padding_rules: &HashMap<Block, usize>,
    tree_index: usize,
) -> Vec<Cell> {
    let mut cells = Vec::new();
    let last_idx = metas.len();

    for (idx, meta) in metas.iter().enumerate() {
        let current_prefix = if tree_depth_prefix.0 > 0 {
            if idx + 1 != last_idx {
                // is last folder elem
                format!("{}{} ", tree_depth_prefix.1, EDGE)
            } else {
                format!("{}{} ", tree_depth_prefix.1, CORNER)
            }
        } else {
            tree_depth_prefix.1.to_string()
        };

        for block in get_output(
            meta,
            owner_cache,
            colors,
            icons,
            git_theme,
            flags,
            &DisplayOption::FileName,
            padding_rules,
            (tree_index, &current_prefix),
        ) {
            cells.push(Cell {
                width: get_visible_width(&block, flags.hyperlink == HyperlinkOption::Always),
                contents: block,
                alignment: Alignment::Left,
            });
        }

        if let Some(content) = &meta.content {
            let new_prefix = if tree_depth_prefix.0 > 0 {
                if idx + 1 != last_idx {
                    // is last folder elem
                    format!("{}{} ", tree_depth_prefix.1, LINE)
                } else {
                    format!("{}{} ", tree_depth_prefix.1, BLANK)
                }
            } else {
                tree_depth_prefix.1.to_string()
            };

            cells.extend(inner_display_tree(
                content,
                owner_cache,
                flags,
                colors,
                icons,
                git_theme,
                (tree_depth_prefix.0 + 1, &new_prefix),
                padding_rules,
                tree_index,
            ));
        }
    }

    cells
}

fn should_display_folder_path(depth: usize, metas: &[Meta]) -> bool {
    if depth > 0 {
        true
    } else {
        let folder_number = metas
            .iter()
            .filter(|x| {
                matches!(x.file_type, FileType::Directory { .. })
                    || (matches!(x.file_type, FileType::SymLink { is_dir: true }))
            })
            .count();

        folder_number > 1 || folder_number < metas.len()
    }
}

fn display_folder_path(meta: &Meta) -> String {
    format!("\n{}:\n", meta.path.to_string_lossy())
}

#[allow(clippy::too_many_arguments)]
fn get_output(
    meta: &Meta,
    owner_cache: &OwnerCache,
    colors: &Colors,
    icons: &Icons,
    _git_theme: &GitTheme,
    flags: &Flags,
    display_option: &DisplayOption,
    padding_rules: &HashMap<Block, usize>,
    tree: (usize, &str),
) -> Vec<String> {
    let mut strings: Vec<String> = Vec::new();
    let colorize_missing = |string: &str| colors.colorize(string, &Elem::NoAccess);

    for (i, block) in flags.blocks.0.iter().enumerate() {
        let mut block_vec = if Layout::Tree == flags.layout && tree.0 == i {
            vec![colors.colorize(tree.1, &Elem::TreeEdge)]
        } else {
            Vec::new()
        };

        match block {
            Block::INode => block_vec.push(match &meta.inode {
                Some(inode) => inode.render(colors),
                None => colorize_missing("?"),
            }),
            Block::Links => block_vec.push(match &meta.links {
                Some(links) => links.render(colors),
                None => colorize_missing("?"),
            }),
            Block::Permission => {
                block_vec.extend([
                    meta.file_type.render(colors),
                    match &meta.permissions_or_attributes {
                        Some(permissions_or_attributes) => {
                            permissions_or_attributes.render(colors, flags)
                        }
                        None => colorize_missing("?????????"),
                    },
                    match &meta.access_control {
                        Some(access_control) => access_control.render_method(colors),
                        None => colorize_missing(""),
                    },
                ]);
            }
            Block::User => block_vec.push(match &meta.owner {
                Some(owner) => owner.render_user(colors, owner_cache, flags),
                None => colorize_missing("?"),
            }),
            Block::Group => block_vec.push(match &meta.owner {
                Some(owner) => owner.render_group(colors, owner_cache, flags),
                None => colorize_missing("?"),
            }),
            Block::Context => block_vec.push(match &meta.access_control {
                Some(access_control) => access_control.render_context(colors),
                None => colorize_missing("?"),
            }),
            Block::Size => {
                let pad = if Layout::Tree == flags.layout && 0 == tree.0 && 0 == i {
                    None
                } else {
                    Some(padding_rules[&Block::SizeValue])
                };
                block_vec.push(match &meta.size {
                    Some(size) => size.render(colors, flags, pad),
                    None => colorize_missing("?"),
                })
            }
            Block::SizeValue => block_vec.push(match &meta.size {
                Some(size) => size.render_value(colors, flags),
                None => colorize_missing("?"),
            }),
            Block::Date => block_vec.push(match &meta.date {
                Some(date) => date.render(colors, flags),
                None => colorize_missing("?"),
            }),
            Block::Name => {
                block_vec.extend([
                    meta.name.render(
                        Name::builder(colors, icons, display_option)
                            .hyperlink(flags.hyperlink)
                            .literal(flags.literal.0)
                            .git_status(meta.git_status.as_ref())
                            .cached_canonical(meta.canonical_path.as_ref())
                    ),
                    meta.indicator.render(flags),
                ]);
                if !(flags.no_symlink.0 || flags.dereference.0 || flags.layout == Layout::Grid) {
                    block_vec.push(meta.symlink.render(colors, flags))
                }
            }
            Block::GitStatus => {
                // Git status is now shown through file/icon colors, not as a separate block
                // if let Some(_s) = &meta.git_status {
                //     block_vec.push(_s.render(colors, git_theme));
                // }
            }
        }
        strings.push(
            block_vec
                .into_iter()
                .map(|s| s.to_string())
                .collect::<Vec<String>>()
                .join(""),
        );
    }
    strings
}

fn get_visible_width(input: &str, hyperlink: bool) -> usize {
    let mut nb_invisible_char = 0;

    // If the input has color, do not compute the length contributed by the color to the actual length
    for (idx, _) in input.match_indices("\u{1b}[") {
        let (_, s) = input.split_at(idx);

        let m_pos = s.find('m');
        if let Some(len) = m_pos {
            // len points to the 'm' character, we must include 'm' to invisible characters
            nb_invisible_char += len + 1;
        }
    }

    if hyperlink {
        for (idx, _) in input.match_indices("\x1B]8;;") {
            let (_, s) = input.split_at(idx);

            let m_pos = s.find("\x1B\x5C");
            if let Some(len) = m_pos {
                // len points to the '\x1B' character, we must include both '\x1B' and '\x5C' to invisible characters
                nb_invisible_char += len + 2
            }
        }
    }

    // `UnicodeWidthStr::width` counts all unicode characters including escape '\u{1b}' and hyperlink '\x1B'
    UnicodeWidthStr::width(input) - nb_invisible_char
}

fn detect_size_lengths(metas: &[Meta], flags: &Flags) -> usize {
    let mut max_value_length: usize = 0;

    for meta in metas {
        let value_len = match &meta.size {
            Some(size) => size.value_string(flags).len(),
            None => 0,
        };

        if value_len > max_value_length {
            max_value_length = value_len;
        }

        if Layout::Tree == flags.layout
            && let Some(subs) = &meta.content {
                let sub_length = detect_size_lengths(subs, flags);
                if sub_length > max_value_length {
                    max_value_length = sub_length;
                }
            }
    }

    max_value_length
}

fn get_padding_rules(metas: &[Meta], flags: &Flags) -> HashMap<Block, usize> {
    let mut padding_rules: HashMap<Block, usize> = HashMap::new();

    if flags.blocks.0.contains(&Block::Size) {
        let size_val = detect_size_lengths(metas, flags);

        padding_rules.insert(Block::SizeValue, size_val);
    }

    padding_rules
}
