use ruff_db::system::SystemPath;

pub(crate) use exclude::{ExcludeFilter, ExcludeFilterBuilder};
pub(crate) use include::{IncludeFilter, IncludeFilterBuilder};
pub(crate) use portable::{
    AbsolutePortableGlobPattern, PortableGlobError, PortableGlobKind, PortableGlobPattern,
};

mod exclude;
mod include;
mod portable;

/// Path filtering based on an an exclude and include glob pattern set.
///
/// Exclude patterns take precedence over includes.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IncludeExcludeFilter {
    include: IncludeFilter,
    exclude: ExcludeFilter,
}

impl IncludeExcludeFilter {
    pub(crate) fn new(include: IncludeFilter, exclude: ExcludeFilter) -> Self {
        Self { include, exclude }
    }

    /// Returns whether this directory is included in this filter.
    ///
    /// Note, this function never returns [`IncludeResult::Included`] for a path that is not included or excluded.
    /// However, it may return [`IncludeResult::Included`] for directories that are not excluded, but where
    /// it requires traversal to decide if any of its subdirectories or files are included. This, for example,
    /// is the case when using wildcard include-patterns like `**/test`. Prefix wildcards require to traverse `src`
    /// because it can't be known ahead of time whether it contains a `test` directory or file.
    pub(crate) fn is_directory_maybe_included(
        &self,
        path: &SystemPath,
        mode: GlobFilterCheckMode,
    ) -> IncludeResult {
        if self.exclude.match_directory(path, mode) {
            IncludeResult::Excluded
        } else if self.include.match_directory(path) {
            IncludeResult::Included
        } else {
            IncludeResult::NotIncluded
        }
    }

    pub(crate) fn is_file_included(
        &self,
        path: &SystemPath,
        mode: GlobFilterCheckMode,
    ) -> IncludeResult {
        if self.exclude.match_file(path, mode) {
            IncludeResult::Excluded
        } else if self.include.match_file(path) {
            IncludeResult::Included
        } else {
            IncludeResult::NotIncluded
        }
    }
}

impl std::fmt::Display for IncludeExcludeFilter {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "include={}, exclude={}", &self.include, &self.exclude)
    }
}

#[derive(Copy, Clone, Eq, PartialEq)]
pub(crate) enum GlobFilterCheckMode {
    /// The paths are checked top-to-bottom and inclusion is determined
    /// for each path during the traversal.
    TopDown,

    /// An adhoc test if a single file or directory is included.
    ///
    /// This is more expensive than a [`Self::TopDown`] check
    /// because it may require testing every ancestor path in addition to the
    /// path itself to ensure no ancestor path matches an exclude rule.
    Adhoc,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum IncludeResult {
    /// The path matches or at least is a prefix of an include pattern.
    ///
    /// For directories: This isn't a guarantee that any file in this directory gets included
    /// but we need to traverse it to make this decision.
    Included,

    /// The path matches an exclude pattern.
    Excluded,

    /// The path matches neither an include nor an exclude pattern and, therefore,
    /// isn't included.
    NotIncluded,
}
