//! Pathspec plumbing and abstractions
use gix_macros::momo;
pub use gix_pathspec::*;

use crate::{bstr::BStr, AttributeStack, Pathspec, PathspecDetached, Repository};

///
#[allow(clippy::empty_docs)]
pub mod init {
    /// The error returned by [`Pathspec::new()`](super::Pathspec::new()).
    #[derive(Debug, thiserror::Error)]
    #[allow(missing_docs)]
    pub enum Error {
        #[error(transparent)]
        MakeAttributes(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
        #[error(transparent)]
        Defaults(#[from] crate::repository::pathspec_defaults_ignore_case::Error),
        #[error(transparent)]
        ParseSpec(#[from] gix_pathspec::parse::Error),
        #[error(
            "Could not obtain the repository prefix as the relative path of the CWD as seen from the working tree"
        )]
        NormalizeSpec(#[from] gix_pathspec::normalize::Error),
        #[error(transparent)]
        RepoPrefix(#[from] gix_path::realpath::Error),
    }
}

/// Lifecycle
impl<'repo> Pathspec<'repo> {
    /// Create a new instance by parsing `patterns` into [`Pathspecs`](Pattern) to make them usable for searches.
    /// `make_attribute` may be called if one of the patterns has a `(attr:a)` element which requires attribute matching. It should
    /// be used to control where attributes are coming from.
    /// If `inherit_ignore_case` is `true`, the pathspecs may have their ignore-case default overridden to be case-insensitive by default.
    /// This only works towards turning ignore-case for pathspecs on, but won't ever turn that setting off if.
    /// If `empty_patterns_match_prefix` is `true`, then even empty patterns will match only what's inside of the prefix. Otherwise
    /// they will match everything.
    ///
    /// ### Deviation
    ///
    /// Pathspecs can declare to be case-insensitive as part of their elements, which is a setting that is now respected for attribute
    /// queries as well.
    pub fn new(
        repo: &'repo Repository,
        empty_patterns_match_prefix: bool,
        patterns: impl IntoIterator<Item = impl AsRef<BStr>>,
        inherit_ignore_case: bool,
        make_attributes: impl FnOnce() -> Result<gix_worktree::Stack, Box<dyn std::error::Error + Send + Sync + 'static>>,
    ) -> Result<Self, init::Error> {
        let defaults = repo.pathspec_defaults_inherit_ignore_case(inherit_ignore_case)?;
        let patterns = patterns
            .into_iter()
            .map(move |p| parse(p.as_ref(), defaults))
            .collect::<Result<Vec<_>, _>>()?;
        let needs_cache = patterns.iter().any(|p| !p.attributes.is_empty());
        let prefix = if patterns.is_empty() && !empty_patterns_match_prefix {
            None
        } else {
            repo.prefix()?
        };
        let search = Search::from_specs(
            patterns,
            prefix,
            &gix_path::realpath_opts(
                repo.work_dir().unwrap_or_else(|| repo.git_dir()),
                repo.options.current_dir_or_empty(),
                gix_path::realpath::MAX_SYMLINKS,
            )?,
        )?;
        let cache = needs_cache.then(make_attributes).transpose()?;

        gix_trace::debug!(
            longest_prefix = ?search.longest_common_directory(),
            prefix_dir = ?search.prefix_directory(),
            patterns = ?search.patterns().map(gix_pathspec::Pattern::path).collect::<Vec<_>>()
        );

        Ok(Self {
            repo,
            search,
            stack: cache,
        })
    }
    /// Turn ourselves into the functional parts for direct usage.
    /// Note that the [`cache`](AttributeStack) is only set if one of the [`search` patterns](Search)
    /// is specifying attributes to match for.
    pub fn into_parts(self) -> (Search, Option<AttributeStack<'repo>>) {
        (
            self.search,
            self.stack.map(|stack| AttributeStack::new(stack, self.repo)),
        )
    }

    /// Turn ourselves into an implementation that works without a repository instance and that is rather minimal.
    pub fn detach(self) -> std::io::Result<PathspecDetached> {
        Ok(PathspecDetached {
            search: self.search,
            stack: self.stack,
            odb: self.repo.objects.clone().into_arc()?,
        })
    }
}

/// Access
impl<'repo> Pathspec<'repo> {
    /// Return the attributes cache which is used when matching attributes in pathspecs, or `None` if none of the pathspecs require that.
    pub fn attributes(&self) -> Option<&gix_worktree::Stack> {
        self.stack.as_ref()
    }

    /// Return the search itself which can be used for matching paths or accessing the actual patterns that will be used.
    pub fn search(&self) -> &gix_pathspec::Search {
        &self.search
    }

    /// Return the first [`Match`](search::Match) of `relative_path`, or `None`.
    /// Note that the match might [be excluded](search::Match::is_excluded()).
    /// `is_dir` is true if `relative_path` is a directory.
    #[doc(
        alias = "match_diff",
        alias = "match_tree",
        alias = "match_index",
        alias = "match_workdir",
        alias = "matches_path",
        alias = "git2"
    )]
    #[momo]
    pub fn pattern_matching_relative_path<'a>(
        &mut self,
        relative_path: impl Into<&'a BStr>,
        is_dir: Option<bool>,
    ) -> Option<gix_pathspec::search::Match<'_>> {
        self.search.pattern_matching_relative_path(
            relative_path.into(),
            is_dir,
            &mut |relative_path, case, is_dir, out| {
                let stack = self.stack.as_mut().expect("initialized in advance");
                stack
                    .set_case(case)
                    .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.repo.objects)
                    .map_or(false, |platform| platform.matching_attributes(out))
            },
        )
    }

    /// The simplified version of [`pattern_matching_relative_path()`](Self::pattern_matching_relative_path()) which returns
    /// `true` if `relative_path` is included in the set of positive pathspecs, while not being excluded.
    #[momo]
    pub fn is_included<'a>(&mut self, relative_path: impl Into<&'a BStr>, is_dir: Option<bool>) -> bool {
        self.pattern_matching_relative_path(relative_path, is_dir)
            .map_or(false, |m| !m.is_excluded())
    }

    /// Return an iterator over all entries along with their path if the path matches the pathspec, or `None` if the pathspec is
    /// known to match no entry.
    // TODO: tests
    pub fn index_entries_with_paths<'s: 'repo, 'a: 'repo>(
        &'s mut self,
        index: &'a gix_index::State,
    ) -> Option<impl Iterator<Item = (&'a BStr, &'a gix_index::Entry)> + 'repo + 's> {
        index.prefixed_entries(self.search.common_prefix()).map(|entries| {
            entries.iter().filter_map(move |entry| {
                let path = entry.path(index);
                self.is_included(path, Some(false)).then_some((path, entry))
            })
        })
    }
}

/// Access
impl PathspecDetached {
    /// Return the first [`Match`](search::Match) of `relative_path`, or `None`.
    /// Note that the match might [be excluded](search::Match::is_excluded()).
    /// `is_dir` is true if `relative_path` is a directory.
    #[doc(
        alias = "match_diff",
        alias = "match_tree",
        alias = "match_index",
        alias = "match_workdir",
        alias = "matches_path",
        alias = "git2"
    )]
    #[momo]
    pub fn pattern_matching_relative_path<'a>(
        &mut self,
        relative_path: impl Into<&'a BStr>,
        is_dir: Option<bool>,
    ) -> Option<gix_pathspec::search::Match<'_>> {
        self.search.pattern_matching_relative_path(
            relative_path.into(),
            is_dir,
            &mut |relative_path, case, is_dir, out| {
                let stack = self.stack.as_mut().expect("initialized in advance");
                stack
                    .set_case(case)
                    .at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &self.odb)
                    .map_or(false, |platform| platform.matching_attributes(out))
            },
        )
    }

    /// The simplified version of [`pattern_matching_relative_path()`](Self::pattern_matching_relative_path()) which returns
    /// `true` if `relative_path` is included in the set of positive pathspecs, while not being excluded.
    #[momo]
    pub fn is_included<'a>(&mut self, relative_path: impl Into<&'a BStr>, is_dir: Option<bool>) -> bool {
        self.pattern_matching_relative_path(relative_path, is_dir)
            .map_or(false, |m| !m.is_excluded())
    }
}

fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode {
    if is_dir {
        gix_index::entry::Mode::DIR
    } else {
        gix_index::entry::Mode::FILE
    }
}
