use std::path::{Path, PathBuf};

use bstr::{BStr, ByteSlice};
use gix_glob::pattern::Case;
use gix_object::FindExt;

use crate::{
    PathIdMapping, Stack,
    stack::state::{AttributeMatchGroup, Attributes},
};

/// Various aggregate numbers related [`Attributes`].
#[derive(Default, Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Statistics {
    /// Amount of patterns buffers read from the index.
    pub patterns_buffers: usize,
    /// Amount of pattern files read from disk.
    pub pattern_files: usize,
    /// Amount of pattern files we tried to find on disk.
    pub tried_pattern_files: usize,
}

/// Decide where to read `.gitattributes` files from.
///
/// To Retrieve attribute files from id mappings, see
/// [State::id_mappings_from_index()][crate::stack::State::id_mappings_from_index()].
///
/// These mappings are typically produced from an index.
/// If a tree should be the source, build an attribute list from a tree instead, or convert a tree to an index.
///
#[derive(Default, Debug, Clone, Copy)]
pub enum Source {
    /// Use this when no worktree checkout is available, like in bare repositories, during clones, or when accessing blobs from
    /// other parts of the history which aren't checked out.
    #[default]
    IdMapping,
    /// Read from an id mappings and if not present, read from the worktree.
    ///
    /// This us typically used when *checking out* files.
    IdMappingThenWorktree,
    /// Read from the worktree and if not present, read them from the id mappings.
    ///
    /// This is typically used when *checking in* files, and it's possible for sparse worktrees not to have a `.gitattribute` file
    /// checked out even though it's available in the index.
    WorktreeThenIdMapping,
}

impl Source {
    /// Returns non-worktree variants of `self` if `is_bare` is true.
    pub fn adjust_for_bare(self, is_bare: bool) -> Self {
        if is_bare { Source::IdMapping } else { self }
    }
}

/// Initialization
impl Attributes {
    /// Create a new instance from an attribute match group that represents `globals`. It can more easily be created with
    /// [`AttributeMatchGroup::new_globals()`].
    ///
    /// * `globals` contribute first and consist of all globally available, static files.
    /// * `info_attributes` is a path that should refer to `.git/info/attributes`, and it's not an error if the file doesn't exist.
    /// * `case` is used to control case-sensitivity during matching.
    /// * `source` specifies from where the directory-based attribute files should be loaded from.
    pub fn new(
        globals: AttributeMatchGroup,
        info_attributes: Option<PathBuf>,
        source: Source,
        collection: gix_attributes::search::MetadataCollection,
    ) -> Self {
        Attributes {
            globals,
            stack: Default::default(),
            info_attributes,
            source,
            collection,
        }
    }
}

impl Attributes {
    pub(crate) fn pop_directory(&mut self) {
        self.stack.pop_pattern_list().expect("something to pop");
    }

    #[expect(clippy::too_many_arguments)]
    pub(crate) fn push_directory(
        &mut self,
        root: &Path,
        dir: &Path,
        rela_dir: &BStr,
        buf: &mut Vec<u8>,
        id_mappings: &[PathIdMapping],
        objects: &dyn gix_object::Find,
        stats: &mut Statistics,
    ) -> std::io::Result<()> {
        let attr_path_relative = gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes");
        let attr_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(attr_path_relative.as_ref()));
        // Git does not follow symbolic links as per documentation.
        let no_follow_symlinks = false;
        let read_macros_as_dir_is_root = root == dir;

        let mut added = false;
        match self.source {
            Source::IdMapping | Source::IdMappingThenWorktree => {
                if let Ok(idx) = attr_file_in_index {
                    let blob = objects
                        .find_blob(&id_mappings[idx].1, buf)
                        .map_err(std::io::Error::other)?;
                    let attr_path = gix_path::from_bstring(attr_path_relative.into_owned());
                    self.stack.add_patterns_buffer(
                        blob.data,
                        attr_path,
                        Some(Path::new("")),
                        &mut self.collection,
                        read_macros_as_dir_is_root,
                    );
                    added = true;
                    stats.patterns_buffers += 1;
                }
                if !added && matches!(self.source, Source::IdMappingThenWorktree) {
                    added = self.stack.add_patterns_file(
                        dir.join(".gitattributes"),
                        no_follow_symlinks,
                        Some(root),
                        buf,
                        &mut self.collection,
                        read_macros_as_dir_is_root,
                    )?;
                    stats.pattern_files += usize::from(added);
                    stats.tried_pattern_files += 1;
                }
            }
            Source::WorktreeThenIdMapping => {
                added = self.stack.add_patterns_file(
                    dir.join(".gitattributes"),
                    no_follow_symlinks,
                    Some(root),
                    buf,
                    &mut self.collection,
                    read_macros_as_dir_is_root,
                )?;
                stats.pattern_files += usize::from(added);
                stats.tried_pattern_files += 1;
                if let Some(idx) = attr_file_in_index.ok().filter(|_| !added) {
                    let blob = objects
                        .find_blob(&id_mappings[idx].1, buf)
                        .map_err(std::io::Error::other)?;
                    let attr_path = gix_path::from_bstring(attr_path_relative.into_owned());
                    self.stack.add_patterns_buffer(
                        blob.data,
                        attr_path,
                        Some(Path::new("")),
                        &mut self.collection,
                        read_macros_as_dir_is_root,
                    );
                    added = true;
                    stats.patterns_buffers += 1;
                }
            }
        }

        // Need one stack level per component so push and pop matches, but only if this isn't the root level which is never popped.
        if !added && self.info_attributes.is_none() {
            self.stack
                .add_patterns_buffer(&[], "<empty dummy>".into(), None, &mut self.collection, true);
        }

        // When reading the root, always the first call, we can try to also read the `.git/info/attributes` file which is
        // by nature never popped, and follows the root, as global.
        if let Some(info_attr) = self.info_attributes.take() {
            let added = self.stack.add_patterns_file(
                info_attr,
                true,
                None,
                buf,
                &mut self.collection,
                true, /* read macros */
            )?;
            stats.pattern_files += usize::from(added);
            stats.tried_pattern_files += 1;
        }

        Ok(())
    }

    pub(crate) fn matching_attributes(
        &self,
        relative_path: &BStr,
        case: Case,
        is_dir: Option<bool>,
        out: &mut gix_attributes::search::Outcome,
    ) -> bool {
        // assure `out` is ready to deal with possibly changed collections (append-only)
        out.initialize(&self.collection);

        let groups = [&self.globals, &self.stack];
        let mut has_match = false;
        groups.iter().rev().any(|group| {
            has_match |= group.pattern_matching_relative_path(relative_path, case, is_dir, out);
            out.is_done()
        });
        has_match
    }
}

/// Attribute matching specific methods
impl Stack {
    /// Creates a new container to store match outcomes for all attribute matches.
    ///
    /// ### Panics
    ///
    /// If attributes aren't configured.
    pub fn attribute_matches(&self) -> gix_attributes::search::Outcome {
        let mut out = gix_attributes::search::Outcome::default();
        out.initialize(&self.state.attributes_or_panic().collection);
        out
    }

    /// Creates a new container to store match outcomes for the given attributes.
    ///
    /// ### Panics
    ///
    /// If attributes aren't configured.
    pub fn selected_attribute_matches<'a>(
        &self,
        given: impl IntoIterator<Item = impl Into<&'a str>>,
    ) -> gix_attributes::search::Outcome {
        let mut out = gix_attributes::search::Outcome::default();
        out.initialize_with_selection(
            &self.state.attributes_or_panic().collection,
            given.into_iter().map(Into::into),
        );
        out
    }

    /// Return the metadata collection that enables initializing attribute match outcomes as done in
    /// [`attribute_matches()`][Stack::attribute_matches()] or [`selected_attribute_matches()`][Stack::selected_attribute_matches()]
    ///
    /// ### Panics
    ///
    /// If attributes aren't configured.
    pub fn attributes_collection(&self) -> &gix_attributes::search::MetadataCollection {
        &self.state.attributes_or_panic().collection
    }
}
