//! The **Details** output view displays each file as a row in a table.
//!
//! It’s used in the following situations:
//!
//! - Most commonly, when using the `--long` command-line argument to display the
//!   details of each file, which requires using a table view to hold all the data;
//! - When using the `--tree` argument, which uses the same table view to display
//!   each file on its own line, with the table providing the tree characters;
//! - When using both the `--long` and `--grid` arguments, which constructs a
//!   series of tables to fit all the data on the screen.
//!
//! You will probably recognise it from the `ls --long` command. It looks like
//! this:
//!
//! ```text
//!     .rw-r--r--  9.6k ben 29 Jun 16:16 Cargo.lock
//!     .rw-r--r--   547 ben 23 Jun 10:54 Cargo.toml
//!     .rw-r--r--  1.1k ben 23 Nov  2014 LICENCE
//!     .rw-r--r--  2.5k ben 21 May 14:38 README.md
//!     .rw-r--r--  382k ben  8 Jun 21:00 screenshot.png
//!     drwxr-xr-x     - ben 29 Jun 14:50 src
//!     drwxr-xr-x     - ben 28 Jun 19:53 target
//! ```
//!
//! The table is constructed by creating a `Table` value, which produces a `Row`
//! value for each file. These rows can contain a vector of `Cell`s, or they can
//! contain depth information for the tree view, or both. These are described
//! below.
//!
//!
//! ## Constructing Detail Views
//!
//! When using the `--long` command-line argument, the details of each file are
//! displayed next to its name.
//!
//! The table holds a vector of all the column types. For each file and column, a
//! `Cell` value containing the ANSI-coloured text and Unicode width of each cell
//! is generated, with the row and column determined by indexing into both arrays.
//!
//! The column types vector does not actually include the filename. This is
//! because the filename is always the rightmost field, and as such, it does not
//! need to have its width queried or be padded with spaces.
//!
//! To illustrate the above:
//!
//! ```text
//!     ┌─────────────────────────────────────────────────────────────────────────┐
//!     │ columns: [ Permissions,  Size,   User,  Date(Modified) ]                │
//!     ├─────────────────────────────────────────────────────────────────────────┤
//!     │   rows:  cells:                                            filename:    │
//!     │   row 1: [ ".rw-r--r--", "9.6k", "ben", "29 Jun 16:16" ]   Cargo.lock   │
//!     │   row 2: [ ".rw-r--r--",  "547", "ben", "23 Jun 10:54" ]   Cargo.toml   │
//!     │   row 3: [ "drwxr-xr-x",    "-", "ben", "29 Jun 14:50" ]   src          │
//!     │   row 4: [ "drwxr-xr-x",    "-", "ben", "28 Jun 19:53" ]   target       │
//!     └─────────────────────────────────────────────────────────────────────────┘
//! ```
//!
//! Each column in the table needs to be resized to fit its widest argument. This
//! means that we must wait until every row has been added to the table before it
//! can be displayed, in order to make sure that every column is wide enough.

use std::io::{self, Write};
use std::path::PathBuf;
use std::vec::IntoIter as VecIntoIter;

use log::error;
use nu_ansi_term::Style;

use crate::fs::dir_action::RecurseOptions;
use crate::fs::feature::VcsCache;
use crate::fs::feature::xattr::{Attribute, FileAttributes};
use crate::fs::filter::FileFilter;
use crate::fs::{Dir, File};
use crate::output::cell::TextCell;
use crate::output::file_name::Options as FileStyle;
use crate::output::table::{Options as TableOptions, Row as TableRow, Table};
use crate::output::tree::{TreeDepth, TreeParams, TreeTrunk};
use crate::theme::Theme;

/// With the **Details** view, the output gets formatted into columns, with
/// each `Column` object showing some piece of information about the file,
/// such as its size, or its permissions.
///
/// To do this, the results have to be written to a table, instead of
/// displaying each file immediately. Then, the width of each column can be
/// calculated based on the individual results, and the fields are padded
/// during output.
///
/// Almost all the heavy lifting is done in a Table object, which handles the
/// columns for each row.
#[derive(PartialEq, Eq, Debug)]
pub struct Options {
    /// Options specific to drawing a table.
    ///
    /// Directories themselves can pick which columns are *added* to this
    /// list, such as the Git column.
    pub table: Option<TableOptions>,

    /// Whether to show a header line or not.
    pub header: bool,

    /// Whether to show each file’s extended attributes.
    pub xattr: bool,

    /// Whether to probe for the `@` indicator in the permissions
    /// column.  Independent of `xattr`: an indicator probe is a
    /// single `listxattr` per file, whereas full enumeration is
    /// `listxattr` plus a `getxattr` per attribute.
    pub xattr_indicator: bool,
}

pub struct Render<'a> {
    pub dir: Option<&'a Dir>,
    pub files: Vec<File<'a>>,
    pub theme: &'a Theme,
    pub file_style: &'a FileStyle,
    pub opts: &'a Options,

    /// Whether to recurse through directories with a tree view, and if so,
    /// which options to use. This field is only relevant here if the `tree`
    /// field of the `RecurseOptions` is `true`.
    pub recurse: Option<RecurseOptions>,

    /// How to sort and filter the files after getting their details.
    pub filter: &'a FileFilter,

    /// Whether we are skipping VCS-ignored files.
    pub vcs_ignoring: bool,

    pub vcs: Option<&'a dyn VcsCache>,
}

struct Egg<'a> {
    table_row: Option<TableRow>,
    xattrs: Vec<Attribute>,
    errors: Vec<(io::Error, Option<PathBuf>)>,
    dir: Option<Dir>,
    file: &'a File<'a>,
}

impl<'a> AsRef<File<'a>> for Egg<'a> {
    fn as_ref(&self) -> &File<'a> {
        self.file
    }
}

impl<'a> Render<'a> {
    /// Render the file listing.  Returns `(item_count, size_total)`:
    /// item count excludes header/xattr rows; size total sums the
    /// displayed file sizes (for unexpanded directories, uses the
    /// total-size if active; expanded directories are skipped to
    /// avoid double-counting).
    pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<(usize, u64)> {
        let mut rows = Vec::new();

        if let Some(ref table) = self.opts.table {
            match (self.vcs, self.dir) {
                (Some(g), Some(d)) => {
                    if !g.has_anything_for(&d.path) {
                        self.vcs = None;
                    }
                }
                (Some(g), None) => {
                    if !self.files.iter().any(|f| g.has_anything_for(&f.path)) {
                        self.vcs = None;
                    }
                }
                (None, _) => { /* Keep Git how it is */ }
            }

            let mut table = Table::new(table, self.vcs, self.theme);

            if self.opts.header {
                let header = table.header_row();
                table.add_widths(&header);
                rows.push(self.render_header(header));
            }

            // This is weird, but I can’t find a way around it:
            // https://internals.rust-lang.org/t/should-option-mut-t-implement-copy/3715/6
            let mut table = Some(table);
            let rows_before = rows.len();
            let mut size_total = 0u64;
            self.add_files_to_table(
                &mut table,
                &mut rows,
                &self.files,
                TreeDepth::root(),
                None,
                &mut size_total,
            );
            let file_count = rows.len() - rows_before;

            for row in self.iterate_with_table(table.unwrap(), rows) {
                writeln!(w, "{}", row.strings())?;
            }
            Ok((file_count, size_total))
        } else {
            let mut size_total = 0u64;
            self.add_files_to_table(
                &mut None,
                &mut rows,
                &self.files,
                TreeDepth::root(),
                None,
                &mut size_total,
            );

            let file_count = rows.len();
            for row in self.iterate(rows) {
                writeln!(w, "{}", row.strings())?;
            }
            Ok((file_count, size_total))
        }
    }

    /// Adds files to the table, possibly recursively. This is easily
    /// parallelisable, and uses a pool of threads.
    fn add_files_to_table<'dir>(
        &self,
        table: &mut Option<Table<'a>>,
        rows: &mut Vec<Row>,
        src: &[File<'dir>],
        depth: TreeDepth,
        // The root dev for this subtree.  None at the top of each tree
        // (a per-file dev is captured per top-level entry); Some(dev)
        // for any deeper recursion.
        root_dev: Option<u64>,
        size_total: &mut u64,
    ) {
        use super::table::Column;
        use crate::fs::feature::xattr;

        // When --total-size is active in tree mode, eagerly pre-compute
        // directory sizes in parallel.  This populates each File's
        // OnceLock so that render_size() reads cached values.
        let total_size_active = table
            .as_ref()
            .is_some_and(super::table::Table::total_size_active);
        if total_size_active && self.recurse.is_some() {
            use rayon::prelude::*;
            src.par_iter().for_each(|f| {
                if f.is_directory() {
                    let _ = f.total_size();
                }
            });
        }

        // Decide how much xattr work is needed per file:
        //  - full:  --extended is active → enumerate all xattrs (listxattr + getxattr per attr)
        //  - probe: permissions column needs the `@` indicator → single listxattr probe
        //  - none:  no permissions column, no --extended → skip entirely
        let needs_xattr_full = self.opts.xattr;
        let needs_xattr_probe = !needs_xattr_full
            && self.opts.xattr_indicator
            && self
                .opts
                .table
                .as_ref()
                .is_some_and(|t| t.columns.contains(&Column::Permissions));

        let mut file_eggs = src
            .iter()
            .map(|file| {
                let mut errors = Vec::new();
                let mut xattrs = Vec::new();
                let mut has_xattrs = false;

                if xattr::ENABLED && needs_xattr_full {
                    match file.path.attributes() {
                        Ok(xs) => {
                            has_xattrs = !xs.is_empty();
                            xattrs = xs;
                        }
                        Err(e) => {
                            errors.push((e, None));
                        }
                    }
                } else if xattr::ENABLED && needs_xattr_probe {
                    match file.path.has_attributes() {
                        Ok(h) => {
                            has_xattrs = h;
                        }
                        Err(e) => {
                            error!("Error probing xattr for {}: {e}", file.path.display());
                        }
                    }
                }

                let table_row = table.as_ref().map(|t| t.row_for_file(file, has_xattrs));

                let mut dir = None;
                // At the top level, follow a symlinked directory passed
                // as a positional argument: it's an explicit user intent
                // to enter that directory.  Below the top level,
                // symlink-following is governed by --symlinks=follow,
                // which is applied earlier as a filter.
                let entry_is_dir =
                    file.is_directory() || (depth.0 == 0 && file.points_to_directory());
                if let Some(r) = self.recurse
                    && entry_is_dir
                    && r.tree
                    && !r.is_too_deep(depth.0)
                    && !self.filter.is_pruned(file)
                    && self.allow_descent(file, depth, root_dev)
                {
                    match file.to_dir() {
                        Ok(d) => {
                            dir = Some(d);
                        }
                        Err(e) => {
                            errors.push((e, None));
                        }
                    }
                }

                Egg {
                    table_row,
                    xattrs,
                    errors,
                    dir,
                    file,
                }
            })
            .collect::<Vec<_>>();

        self.filter.sort_files(&mut file_eggs, self.vcs);

        for (tree_params, egg) in depth.iterate_over(file_eggs.into_iter()) {
            let mut files = Vec::new();
            let mut errors = egg.errors;

            if let (Some(ref mut t), Some(row)) = (table.as_mut(), egg.table_row.as_ref()) {
                t.add_widths(row);
            }

            let file_name = self
                .file_style
                .for_file(egg.file, self.theme)
                .with_link_paths()
                .paint()
                .promote();

            let row = Row {
                tree: tree_params,
                cells: egg.table_row,
                name: file_name,
            };

            rows.push(row);

            // Size accounting for -CZ summary.
            if egg.file.is_directory() {
                if egg.dir.is_none() {
                    // Not expanded (pruned or depth-limited): use total
                    // size if available, otherwise skip.
                    let total_size_active = table
                        .as_ref()
                        .is_some_and(super::table::Table::total_size_active);
                    if total_size_active
                        && let crate::fs::fields::Size::Some(s) = egg.file.total_size()
                    {
                        *size_total += s;
                    }
                }
                // Expanded directories: children account for themselves.
            } else if egg.file.is_file() {
                *size_total += egg.file.metadata().len();
            }

            if let Some(ref dir) = egg.dir {
                for file_to_add in dir.files(self.filter.dot_filter, self.vcs, self.vcs_ignoring) {
                    match file_to_add {
                        Ok(f) => {
                            files.push(f);
                        }
                        Err((path, e)) => {
                            errors.push((e, Some(path)));
                        }
                    }
                }

                self.filter.filter_child_files(&mut files);

                if !files.is_empty() {
                    for xattr in egg.xattrs {
                        rows.push(
                            self.render_xattr(&xattr, TreeParams::new(depth.deeper(), false)),
                        );
                    }

                    for (error, path) in errors {
                        rows.push(self.render_error(
                            &error,
                            TreeParams::new(depth.deeper(), false),
                            path,
                        ));
                    }

                    // At the tree root, each top-level entry establishes
                    // its own filesystem-boundary anchor; deeper levels
                    // inherit the anchor captured here.
                    let subtree_root_dev = if depth.0 == 0 {
                        use std::os::unix::fs::MetadataExt;
                        Some(egg.file.metadata().dev())
                    } else {
                        root_dev
                    };
                    self.add_files_to_table(
                        table,
                        rows,
                        &files,
                        depth.deeper(),
                        subtree_root_dev,
                        size_total,
                    );
                    continue;
                }
            }

            let count = egg.xattrs.len();
            for (index, xattr) in egg.xattrs.into_iter().enumerate() {
                let params =
                    TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1);
                let r = self.render_xattr(&xattr, params);
                rows.push(r);
            }

            let count = errors.len();
            for (index, (error, path)) in errors.into_iter().enumerate() {
                let params = TreeParams::new(depth.deeper(), index == count - 1);
                let r = self.render_error(&error, params, path);
                rows.push(r);
            }
        }
    }

    pub fn render_header(&self, header: TableRow) -> Row {
        Row {
            tree: TreeParams::new(TreeDepth::root(), false),
            cells: Some(header),
            name: TextCell::paint_str(self.theme.ui.header, "Name"),
        }
    }

    /// Whether tree descent may enter `file`, given its tree depth and
    /// the root device captured at depth 0.  Enforces
    /// `--filesystem={same,local}` by comparing `file`'s `st_dev` to
    /// `root_dev` and (for `local`) consulting `statfs(2)` at
    /// boundary crossings.  Top-level entries (`depth.0 == 0`) are
    /// always allowed: they're the roots of their own subtrees and
    /// there's nothing to compare against yet.
    fn allow_descent(&self, file: &File<'_>, depth: TreeDepth, root_dev: Option<u64>) -> bool {
        use crate::fs::dir_action::Filesystem;
        use crate::fs::feature::filesystem::is_network_fs;
        use std::os::unix::fs::MetadataExt;
        let Some(r) = self.recurse else { return true };
        if r.filesystem == Filesystem::All {
            return true;
        }
        if depth.0 == 0 {
            return true;
        }
        let Some(root) = root_dev else {
            return true;
        };
        // Same device — always allow regardless of mode.
        if file.metadata().dev() == root {
            return true;
        }
        // Cross-device boundary.  `same` refuses; `local` consults
        // statfs and refuses only if the new fs is network-backed.
        match r.filesystem {
            Filesystem::Same => false,
            Filesystem::Local => !is_network_fs(&file.path),
            Filesystem::All => true, // unreachable due to early return
        }
    }

    fn render_error(&self, error: &io::Error, tree: TreeParams, path: Option<PathBuf>) -> Row {
        let error_message = if let Some(path) = path {
            format!("<{}: {}>", path.display(), error)
        } else {
            format!("<{error}>")
        };

        // TODO: `broken_symlink` doesn’t quite seem like the right name for
        // the style that’s being used here.  Maybe split it in two?
        let name = TextCell::paint(self.theme.ui.broken_symlink, error_message);
        Row {
            cells: None,
            name,
            tree,
        }
    }

    fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
        let name = TextCell::paint(
            self.theme.ui.perms.attribute,
            format!("{} (len {})", xattr.name, xattr.size),
        );
        Row {
            cells: None,
            name,
            tree,
        }
    }

    pub fn render_file(&self, cells: TableRow, name: TextCell, tree: TreeParams) -> Row {
        Row {
            cells: Some(cells),
            name,
            tree,
        }
    }

    pub fn iterate_with_table(&'a self, table: Table<'a>, rows: Vec<Row>) -> TableIter<'a> {
        TableIter {
            tree_trunk: TreeTrunk::default(),
            total_width: table.widths().total(),
            table,
            inner: rows.into_iter(),
            tree_style: self.theme.ui.punctuation,
        }
    }

    pub fn iterate(&'a self, rows: Vec<Row>) -> Iter {
        Iter {
            tree_trunk: TreeTrunk::default(),
            inner: rows.into_iter(),
            tree_style: self.theme.ui.punctuation,
        }
    }
}

pub struct Row {
    /// Vector of cells to display.
    ///
    /// Most of the rows will be used to display files’ metadata, so this will
    /// almost always be `Some`, containing a vector of cells. It will only be
    /// `None` for a row displaying an attribute or error, neither of which
    /// have cells.
    pub cells: Option<TableRow>,

    /// This file’s name, in coloured output. The name is treated separately
    /// from the other cells, as it never requires padding.
    pub name: TextCell,

    /// Information used to determine which symbols to display in a tree.
    pub tree: TreeParams,
}

pub struct TableIter<'a> {
    inner: VecIntoIter<Row>,
    table: Table<'a>,

    total_width: usize,
    tree_style: Style,
    tree_trunk: TreeTrunk,
}

impl Iterator for TableIter<'_> {
    type Item = TextCell;

    fn next(&mut self) -> Option<Self::Item> {
        self.inner.next().map(|row| {
            let mut cell = if let Some(cells) = row.cells {
                self.table.render(cells)
            } else {
                let mut cell = TextCell::default();
                cell.add_spaces(self.total_width);
                cell
            };

            for tree_part in self.tree_trunk.new_row(row.tree) {
                cell.push(self.tree_style.paint(tree_part.ascii_art()), 4);
            }

            // If any tree characters have been printed, then add an extra
            // space, which makes the output look much better.
            if !row.tree.is_at_root() {
                cell.add_spaces(1);
            }

            cell.append(row.name);
            cell
        })
    }
}

pub struct Iter {
    tree_trunk: TreeTrunk,
    tree_style: Style,
    inner: VecIntoIter<Row>,
}

impl Iterator for Iter {
    type Item = TextCell;

    fn next(&mut self) -> Option<Self::Item> {
        self.inner.next().map(|row| {
            let mut cell = TextCell::default();

            for tree_part in self.tree_trunk.new_row(row.tree) {
                cell.push(self.tree_style.paint(tree_part.ascii_art()), 4);
            }

            // If any tree characters have been printed, then add an extra
            // space, which makes the output look much better.
            if !row.tree.is_at_root() {
                cell.add_spaces(1);
            }

            cell.append(row.name);
            cell
        })
    }
}
