// License: see LICENSE file at root directory of `master` branch

//! # Filter
//!
//! This module provides [`Filter`][::Filter] trait and some common filters.
//!
//! [::Filter]: trait.Filter.html

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

/// # Constant for files which have no extensions
///
/// ## See also
///
/// - [`SomeFileExts`][::SomeFileExts]
/// - [`IgnoredFileExts`][::IgnoredFileExts]
///
/// [::SomeFileExts]: struct.SomeFileExts.html
/// [::IgnoredFileExts]: struct.IgnoredFileExts.html
pub const NO_EXT: &str = concat!();

/// # Filter
pub trait Filter {

    /// # Returns `true` to accept given path, `false` to ignore it
    ///
    /// ## Notes
    ///
    /// - Filters of _files_ should always return `true` if input path is a _directory_.
    /// - Filters of _directories_ should always return `true` if input path is a _file_.
    ///
    /// - Because of above rules, `!accept(...)` is _not_ always the opposite meaning that one might expect. For instance:
    ///
    ///     + [`SymlinkFiles::accept(some_file) == false`][::SymlinkFiles] means `some_file` is not a symbolic link. _However..._
    ///     + `SymlinkFiles::accept(some_file) == true` does _not_ mean that `some_file` is a symbolic link. Obviously, `some_file` might be a
    ///       directory.
    ///
    /// [::SymlinkFiles]: struct.SymlinkFiles.html
    fn accept(&self, path: &Path) -> bool;

}

/// # Case
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Case {

    /// # Sensitive
    Sensitive,

    /// # Insensitive
    Insensitive,

}

/// # A [filter][::Filter] that accepts all paths
///
/// [::Filter]: trait.Filter.html
#[derive(Debug, Clone)]
pub struct AllPaths {

    /// # Lock
    _lock: (),

}

impl AllPaths {

    /// # Makes new instance
    pub fn new() -> Self {
        Self {
            _lock: (),
        }
    }

}

impl Filter for AllPaths {

    fn accept(&self, _: &Path) -> bool {
        true
    }

}

/// # A [filter][::Filter] that only accepts directories by names you provide
///
/// ## Notes
///
/// Providing an empty list means you have no names to accept. So _all directories_ will be _ignored_.
///
/// [::Filter]: trait.Filter.html
#[derive(Debug, Clone)]
pub struct SomeDirNames<'a> {

    /// # Names
    names: &'a [&'a str],

    /// # Case
    case: Case,

}

impl<'a> SomeDirNames<'a> {

    /// # Makes new instance
    pub fn new(names: &'a [&'a str], case: Case) -> Self {
        Self {
            names,
            case,
        }
    }

}

impl Filter for SomeDirNames<'_> {

    fn accept(&self, p: &Path) -> bool {
        if p.is_dir() {
            if self.names.is_empty() {
                return false;
            }

            return match p.file_name().map(|n| n.to_str()) {
                Some(Some(name)) => match self.case {
                    Case::Sensitive => self.names.contains(&name),
                    Case::Insensitive => {
                        let name = name.to_lowercase();
                        self.names.iter().any(|n| n.to_lowercase() == name)
                    },
                },
                _ => false,
            };
        }

        true
    }

}

/// # A [filter][::Filter] that ignores directories by names you provide
///
/// ## Notes
///
/// Providing an empty list means _no names are ignored_. So all directories will be _accepted_.
///
/// ## Examples
///
/// ```
/// use std::path::PathBuf;
/// use dia_files::filter::{Case, IgnoredDirNames};
///
/// const IGNORED_DIR_NAMES: &[&str] = &[
///     ".git", ".hg", ".svn", ".gradle", "target", "build", "release", "debug",
/// ];
///
/// let dir = PathBuf::from(file!()).parent().unwrap().to_path_buf();
///
/// let filter = IgnoredDirNames::new(IGNORED_DIR_NAMES, Case::Sensitive);
/// assert!(dia_files::find_files(&dir, false, filter).unwrap().count() > 0);
///
/// let filter = IgnoredDirNames::new(&[], Case::Insensitive);
/// assert!(dia_files::find_files(&dir, false, filter).unwrap().count() > 0);
/// ```
///
/// [::Filter]: trait.Filter.html
#[derive(Debug, Clone)]
pub struct IgnoredDirNames<'a> {

    /// # Directory names
    dir_names: &'a [&'a str],

    /// # Case
    case: Case,

}

impl<'a> IgnoredDirNames<'a> {

    /// # Makes new instance
    pub fn new(dir_names: &'a [&'a str], case: Case) -> Self {
        Self {
            dir_names,
            case,
        }
    }

}

impl Filter for IgnoredDirNames<'_> {

    fn accept(&self, p: &Path) -> bool {
        if self.dir_names.is_empty() {
            return true;
        }

        if p.is_dir() {
            if let Some(Some(name)) = p.file_name().map(|n| n.to_str()) {
                return match self.case {
                    Case::Sensitive => self.dir_names.contains(&name) == false,
                    Case::Insensitive => {
                        let name = name.to_lowercase();
                        self.dir_names.iter().all(|n| n.to_lowercase() != name)
                    },
                };
            }
        }

        true
    }

}

/// # A [filter][::Filter] that only accepts files by extensions you provide
///
/// ## Notes
///
/// - File extensions do _not_ contain leading dot `.`.
/// - Providing an empty list means you have _nothing_ to pick. So _all files_ will be _ignored_.
/// - To pick files which have no extensions, you can provide [`NO_EXT`][::NO_EXT].
///
/// ## Examples
///
/// ```
/// use std::path::PathBuf;
/// use dia_files::filter::{NO_EXT, Case, SomeFileExts};
///
/// let dir = PathBuf::from(file!()).parent().unwrap().to_path_buf();
///
/// let filter = SomeFileExts::new(&["rs"], Case::Sensitive);
/// assert!(dia_files::find_files(&dir, false, filter).unwrap().count() > 0);
///
/// let filter = SomeFileExts::new(&[], Case::Insensitive);
/// assert!(dia_files::find_files(&dir, false, filter).unwrap().count() == 0);
///
/// let filter = SomeFileExts::new(&[NO_EXT], Case::Insensitive);
/// assert!(dia_files::find_files(&dir, false, filter).unwrap().count() == 0);
/// ```
///
/// [::Filter]: trait.Filter.html
/// [::NO_EXT]: constant.NO_EXT.html
#[derive(Debug, Clone)]
pub struct SomeFileExts<'a> {

    /// # File extensions
    file_exts: &'a [&'a str],

    /// # Case
    case: Case,

    /// # Flag for files which have no extensions
    files_without_extensions: bool,

}

impl<'a> SomeFileExts<'a> {

    /// # Makes new instance
    pub fn new(file_exts: &'a [&'a str], case: Case) -> Self {
        Self {
            file_exts,
            case,
            files_without_extensions: file_exts.iter().any(|e| e.is_empty()),
        }
    }

}

impl Filter for SomeFileExts<'_> {

    fn accept(&self, p: &Path) -> bool {
        if p.is_file() {
            if self.file_exts.is_empty() {
                return false;
            }

            return match p.extension().map(|e| e.to_str()) {
                Some(Some(ext)) => match self.case {
                    Case::Sensitive => self.file_exts.contains(&ext),
                    Case::Insensitive => {
                        let ext = ext.to_lowercase();
                        self.file_exts.iter().any(|e| e.to_lowercase() == ext)
                    },
                },
                Some(None) => false,
                None => self.files_without_extensions,
            };
        }

        true
    }

}

/// # A [filter][::Filter] that ignores files by extensions you provide
///
/// ## Notes
///
/// - File extensions do _not_ contain leading dot `.`.
/// - If you provide an empty list, that means no extensions are ignored, so all files will be _accepted_.
/// - To ignore files which have no extensions, you can provide [`NO_EXT`][::NO_EXT].
///
/// ## Examples
///
/// ```
/// use std::path::PathBuf;
/// use dia_files::filter::{Case, IgnoredFileExts};
///
/// let dir = PathBuf::from(file!()).parent().unwrap().to_path_buf();
///
/// let filter = IgnoredFileExts::new(&["rs"], Case::Sensitive);
/// assert!(dia_files::find_files(&dir, false, filter).unwrap().count() == 0);
///
/// let filter = IgnoredFileExts::new(&[], Case::Insensitive);
/// assert!(dia_files::find_files(&dir, false, filter).unwrap().count() > 0);
/// ```
///
/// [::Filter]: trait.Filter.html
/// [::NO_EXT]: constant.NO_EXT.html
#[derive(Debug, Clone)]
pub struct IgnoredFileExts<'a> {

    /// # File extensions
    file_exts: &'a [&'a str],

    /// # Case
    case: Case,

    /// # Flag for files which have no extensions
    files_without_extensions: bool,

}

impl<'a> IgnoredFileExts<'a> {

    /// # Makes new instance
    pub fn new(file_exts: &'a [&'a str], case: Case) -> Self {
        Self {
            file_exts,
            case,
            files_without_extensions: file_exts.iter().any(|e| e.is_empty()),
        }
    }

}

impl Filter for IgnoredFileExts<'_> {

    fn accept(&self, p: &Path) -> bool {
        if p.is_file() {
            if self.file_exts.is_empty() {
                return true;
            }

            return match p.extension().map(|e| e.to_str()) {
                Some(Some(ext)) => match self.case {
                    Case::Sensitive => self.file_exts.contains(&ext) == false,
                    Case::Insensitive => {
                        let ext = ext.to_lowercase();
                        self.file_exts.iter().all(|e| e.to_lowercase() != ext)
                    },
                },
                Some(None) => true,
                None => self.files_without_extensions == false,
            };
        }

        true
    }

}

/// # A [filter][::Filter] that only accepts files by names you provide
///
/// ## Notes
///
/// Providing an empty list means you have no names to accept. So _all files_ will be _ignored_.
///
/// [::Filter]: trait.Filter.html
#[derive(Debug, Clone)]
pub struct SomeFileNames<'a> {

    /// # Names
    names: &'a [&'a str],

    /// # Case
    case: Case,

}

impl<'a> SomeFileNames<'a> {

    /// # Makes new instance
    pub fn new(names: &'a [&'a str], case: Case) -> Self {
        Self {
            names,
            case,
        }
    }

}

impl Filter for SomeFileNames<'_> {

    fn accept(&self, p: &Path) -> bool {
        if p.is_file() {
            if self.names.is_empty() {
                return false;
            }

            return match p.file_name().map(|n| n.to_str()) {
                Some(Some(name)) => match self.case {
                    Case::Sensitive => self.names.contains(&name),
                    Case::Insensitive => {
                        let name = name.to_lowercase();
                        self.names.iter().any(|n| n.to_lowercase() == name)
                    },
                },
                _ => false,
            };
        }

        true
    }

}

/// # A [filter][::Filter] that only _accepts_ symbolic link files
///
/// [::Filter]: trait.Filter.html
#[derive(Debug, Clone)]
pub struct SymlinkFiles {

    /// # Lock
    _lock: (),

}

impl SymlinkFiles {

    /// # Makes new instance
    pub fn new() -> Self {
        Self {
            _lock: (),
        }
    }

}

impl Filter for SymlinkFiles {

    fn accept(&self, p: &Path) -> bool {
        if p.is_file() {
            return match p.symlink_metadata().map(|m| m.file_type().is_symlink()) {
                Ok(true) => true,
                _ => false,
            };
        }
        true
    }

}

/// # A [filter][::Filter] that _rejects_ symbolic link files
///
/// [::Filter]: trait.Filter.html
#[derive(Debug, Clone)]
pub struct NonSymlinkFiles {

    /// # Lock
    _lock: (),

}

impl NonSymlinkFiles {

    /// # Makes new instance
    pub fn new() -> Self {
        Self {
            _lock: (),
        }
    }

}

impl Filter for NonSymlinkFiles {

    fn accept(&self, p: &Path) -> bool {
        if p.is_file() {
            if let Ok(true) = p.symlink_metadata().map(|m| m.file_type().is_symlink()) {
                return false;
            }
        }
        true
    }

}

/// # A [filter][::Filter] that combines all filters you provide, using _logical conjunction_
///
/// ## Notes
///
/// - If you provide an empty list, this filter _rejects_ all paths.
/// - Files are only accepted if _all_ filters accept them.
///
/// ## Examples
///
/// To find all Rust source files, ignoring vesion control systems' and build directories:
///
/// ```
/// use std::path::PathBuf;
/// use dia_files::filter::{All, Case, Filter, IgnoredDirNames, SomeFileExts};
///
/// const IGNORED_DIR_NAMES: &[&str] = &[
///     ".git", ".hg", ".svn", ".gradle", "target", "build", "release", "debug",
/// ];
/// const FILE_EXTS: &[&str] = &["rs"];
///
/// let dir = PathBuf::from(file!()).parent().unwrap().to_path_buf();
/// let filters: Vec<Box<Filter>> = vec![
///     Box::new(IgnoredDirNames::new(IGNORED_DIR_NAMES, Case::Sensitive)),
///     Box::new(SomeFileExts::new(FILE_EXTS, Case::Sensitive)),
/// ];
/// let filter = All::new(filters);
/// assert!(dia_files::find_files(dir, false, filter).unwrap().count() > 0);
/// ```
///
/// [::Filter]: trait.Filter.html
pub struct All {

    /// # Filters
    filters: Vec<Box<Filter>>,

}

impl All {

    /// # Makes new instance
    pub fn new(filters: Vec<Box<Filter>>) -> Self {
        Self {
            filters,
        }
    }

}

impl Filter for All {

    fn accept(&self, p: &Path) -> bool {
        match self.filters.is_empty() {
            true => false,
            false => self.filters.iter().all(|f| f.accept(p)),
        }
    }

}

/// # A [filter][::Filter] that combines all filters you provide, using _logical disjunction_
///
/// ## Notes
///
/// - If you provide an empty list, this filter _rejects_ all paths.
/// - Files are accepted if _any_ of the filters accepts them.
///
/// [::Filter]: trait.Filter.html
pub struct Any {

    /// # Filters
    filters: Vec<Box<Filter>>,

}

impl Any {

    /// # Makes new instance
    pub fn new(filters: Vec<Box<Filter>>) -> Self {
        Self {
            filters,
        }
    }

}

impl Filter for Any {

    fn accept(&self, p: &Path) -> bool {
        self.filters.iter().any(|f| f.accept(p))
    }

}
