//! Filter which paths in a [`Distribution`](crate::Distribution) should be included.

use std::{fmt::Debug, ops::Not, path::Path};


pub use glob::{Pattern, PatternError};

/// Filter type to be passed into [`Distribution`](crate::Distribution),
/// to include or exclude files from the distribution.
#[derive(Clone, Default)]
pub enum DistributionFilter {
    /// Do not filter the distribution files, meaning all files will be included.
    #[default]
    Disabled,
    /// Filter the distribution files according to the provided patterns.
    /// Only files matched by one or more patterns will be included.
    Enabled(Vec<glob::Pattern>),
}

impl DistributionFilter {
    pub(crate) fn matches_file(&self, path: &Path) -> bool {
        match self {
            DistributionFilter::Disabled => true,
            DistributionFilter::Enabled(patterns) => {
                patterns.iter().any(|pattern|
                    pattern.matches_path(path)
                )
            }
        }
    }

    pub(crate) fn matches_dir(&self, path: &Path) -> bool {
        match self {
            DistributionFilter::Disabled => true,
            DistributionFilter::Enabled(patterns) => {
                patterns.iter().any(|pattern| {
                    Path::new(pattern.as_str())
                        .ancestors()
                        .any(|ancestor| {
                            let pattern_string = ancestor.to_string_lossy();

                            if pattern_string.is_empty().not() {
                                glob::Pattern::new(&pattern_string)
                                    .expect("Failed to parse '{pattern_string}' as glob pattern for matching on ancestor directory. This should never happen, since the full pattern had to be parsed before.")
                                    .matches_path(path)
                            } else {
                                false
                            }
                        })
                })
            }
        }
    }

    pub(crate) fn matches_path(&self, path: &Path) -> bool {
        if path.is_dir() {
            self.matches_dir(path)
        } else {
            self.matches_file(path)
        }
    }
}

impl Debug for DistributionFilter {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DistributionFilter::Disabled => write!(f, "Disabled()"),
            DistributionFilter::Enabled(filters) => {
                let string = join_patterns_to_string(filters);
                write!(f, "Enabled([{string}])")
            }
        }
    }
}
fn join_patterns_to_string(patterns: &[glob::Pattern]) -> String {
    match patterns {
        [] => String::new(),
        [head] => format!("\"{head}\""),
        [head, tail @ ..] => {
            let mut string = format!("\"{head}");
            for pattern in tail {
                string.push_str("\", \"");
                string.push_str(pattern.as_str());
            }
            string.push('"');
            string
        }
    }
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn should_match_all_paths_when_disabled() {
        let testee = DistributionFilter::Disabled;
        assert!(testee.matches_dir(Path::new("some_dir")));
        assert!(testee.matches_file(Path::new("some_file")));
    }

    #[test]
    fn should_match_files() -> cicero_core::Result<()> {
        let testee = DistributionFilter::Enabled(vec![
            glob::Pattern::new("a*")?,
            glob::Pattern::new("x")?,
        ]);
        assert!(testee.matches_file(Path::new("a")));
        assert!(testee.matches_file(Path::new("a/b")));
        assert!(testee.matches_file(Path::new("x")));

        assert!(!testee.matches_file(Path::new("other")));
        assert!(!testee.matches_file(Path::new("b")));

        Ok(())
    }


    /// We need to match parent directories, otherwise the distribution creation code
    /// cannot descend to construct the whole distribution.
    ///
    /// This is different from the behavior of Glob Patterns themself, because Glob Patterns
    /// are intended for matching on existing paths, whereas we construct new paths.
    #[test]
    fn should_match_parent_directories() -> cicero_core::Result<()> {
        let testee = DistributionFilter::Enabled(vec![
            glob::Pattern::new("a/b*")?,
            glob::Pattern::new("w/**/z")?,
        ]);
        assert!(testee.matches_dir(Path::new("a")));
        assert!(testee.matches_dir(Path::new("a/b")));

        assert!(testee.matches_dir(Path::new("w")));
        assert!(testee.matches_dir(Path::new("w/x")));
        assert!(testee.matches_dir(Path::new("w/x/y")));
        assert!(testee.matches_dir(Path::new("w/x/y/z")));

        assert!(!testee.matches_dir(Path::new("other")));
        assert!(!testee.matches_dir(Path::new("b")));

        Ok(())
    }

    #[test]
    fn should_format_filter_as_debug() -> cicero_core::Result<()> {
        let testee = DistributionFilter::Disabled;
        assert_eq!(format!("{testee:?}"), r#"Disabled()"#);

        let testee = DistributionFilter::Enabled(vec![]);
        assert_eq!(format!("{testee:?}"), r#"Enabled([])"#);

        let testee = DistributionFilter::Enabled(vec![
            glob::Pattern::new("a/b*")?,
        ]);
        assert_eq!(format!("{testee:?}"), r#"Enabled(["a/b*"])"#);

        let testee = DistributionFilter::Enabled(vec![
            glob::Pattern::new("a/b*")?,
            glob::Pattern::new("c/**")?,
        ]);
        assert_eq!(format!("{testee:?}"), r#"Enabled(["a/b*", "c/**"])"#);

        let testee = DistributionFilter::Enabled(vec![
            glob::Pattern::new("a/b*")?,
            glob::Pattern::new("c/**")?,
            glob::Pattern::new("d")?,
        ]);
        assert_eq!(format!("{testee:?}"), r#"Enabled(["a/b*", "c/**", "d"])"#);

        Ok(())
    }
}
