/*
==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--

Dia-Files

Copyright (C) 2019-2024  Anonymous

There are several releases over multiple years,
they are listed as ranges, such as: "2019-2024".

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
*/

//! # Filter
//!
//! This module provides some common filters.

#[cfg(unix)]
use crate::Result;

#[cfg(not(feature="async-std"))]
use std::path::Path;

#[cfg(feature="async-std")]
use {
    core::future::Future,
    async_std::path::Path,
};

/// # Filter
///
/// ## Notes
///
/// - File extensions do _not_ contain leading dot `.`.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Filter<'a> {

    /// # A filter that accepts all paths
    AllPaths,

    /// # A 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.
    All {

        /// # Filters
        filters: &'a [Filter<'a>],

    },

    /// # A 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.
    Any {

        /// # Filters
        filters: &'a [Filter<'a>],

    },

    /// # A filter that ignores directories by names you provide
    ///
    /// ## Notes
    ///
    /// Providing an empty list means _no names are ignored_. So all directories will be _accepted_.
    IgnoredDirNames {

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

        /// # Case
        case: Case,

    },

    /// # A filter that ignores files by extensions you provide
    ///
    /// ## Notes
    ///
    /// If you provide an empty list, that means no extensions are ignored, so _all files having extension_ will be _accepted_.
    IgnoredFileExts {

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

        /// # Case
        case: Case,

        /// # Flag for files which have no extension
        files_without_extension: bool,

    },

    /// # A filter that ignores files by names you provide
    ///
    /// ## Notes
    ///
    /// Providing an empty list means _no names are ignored_. So all files will be _accepted_.
    IgnoredFileNames {

        /// # File names
        file_names: &'a [&'a str],

        /// # Case
        case: Case,

    },

    /// # A filter that _rejects_ symbolic link files
    NonSymlinkFiles,

    /// # Same Device filter
    ///
    /// A filter which only accepts files on a same device of a file you provide.
    #[cfg(unix)]
    #[doc(cfg(unix))]
    SameDevice {

        /// # Device ID
        device_id: u64,

    },

    /// # A 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_.
    SomeDirNames {

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

        /// # Case
        case: Case,

    },

    /// # A filter that only accepts files by extensions you provide
    ///
    /// ## Notes
    ///
    /// Providing an empty list means you have _nothing_ to pick. So _all files having extension_ will be _ignored_.
    SomeFileExts {

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

        /// # Case
        case: Case,

        /// # Flag for files which have no extension
        files_without_extension: bool,

    },

    /// # A 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_.
    SomeFileNames {

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

        /// # Case
        case: Case,

    },

    /// # A filter that only accepts symbolic link files
    SymlinkFiles,

    /// # User defined filter
    ///
    ///  [`accept()`](#variant.UserDefined.field.accept) should return `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(...) == false` is _not_ always the opposite meaning that one might expect. For instance:
    ///
    ///     + If [`SymlinkFiles`](#variant.SymlinkFiles) ignores a file, that means the file is not a symbolic link file. _However..._
    ///     + If it accepts that file, it does _not_ mean that the file is a symbolic link file. Obviously, it might be a directory.
    UserDefined {

        /// # User defined filter
        #[cfg(not(feature="async-std"))]
        #[doc(cfg(not(feature="async-std")))]
        accept: fn(&Path) -> bool,

        /// # User defined filter
        #[cfg(feature="async-std")]
        #[doc(cfg(feature="async-std"))]
        accept: fn(&Path) -> Box<dyn Future<Output=bool> + Unpin>,

    },

}

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

    /// # Sensitive
    Sensitive,

    /// # Insensitive
    Insensitive,

}

#[cfg(unix)]
macro_rules! make_same_device { ($path: ident) => {{
    use std::os::unix::fs::MetadataExt;
    Ok(Self::SameDevice {
        device_id: async_call!($path.as_ref().metadata()).map(|m| m.dev())?,
    })
}}}

/// # 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 file. _However..._
///     + `SymlinkFiles::accept(some_file) == true` does _not_ mean that `some_file` is a symbolic link file. Obviously, `some_file` might
///       be a directory.
///
/// [::SymlinkFiles]: struct.SymlinkFiles.html
macro_rules! accept { ($self: ident, $path: ident) => {{
    match $self {
        Self::AllPaths => true,
        Self::All { filters } => accept_all!(filters, $path),
        Self::Any { filters } => accept_any!(filters, $path),
        Self::IgnoredDirNames { dir_names, case } => ignored_dir_names!(dir_names, case, $path),
        Self::IgnoredFileExts { file_exts, case, files_without_extension: fwe } => ignored_file_exts!(file_exts, case, fwe, $path),
        Self::IgnoredFileNames { file_names, case } => ignored_file_names!(file_names, case, $path),
        Self::NonSymlinkFiles => non_symlink_files!($path),
        #[cfg(unix)]
        Self::SameDevice { device_id } => same_device!(device_id, $path),
        Self::SomeDirNames { names, case } => some_dir_names!(names, case, $path),
        Self::SomeFileExts { file_exts, case, files_without_extension } => some_file_exts!(file_exts, case, files_without_extension, $path),
        Self::SomeFileNames { names, case } => some_file_names!(names, case, $path),
        Self::SymlinkFiles => symlink_files!($path),
        Self::UserDefined { accept } => {
            let f = accept($path);
            #[cfg(feature="async-std")]
            let f = Box::pin(f);
            async_call!(f)
        },
    }
}}}

macro_rules! accept_all { ($filters: ident, $path: ident) => {{
    if $filters.is_empty() {
        false
    } else {
        for f in *$filters {
            let f = f.accept($path);
            #[cfg(feature="async-std")]
            let f = Box::pin(f);
            if async_call!(f) == false {
                return false
            }
        }
        true
    }
}}}

macro_rules! accept_any { ($filters: ident, $path: ident) => {{
    for f in *$filters {
        let f = f.accept($path);
        #[cfg(feature="async-std")]
        let f = Box::pin(f);
        if async_call!(f) {
            return true
        }
    }
    false
}}}

macro_rules! ignored_dir_names { ($dir_names: ident, $case: ident, $path: ident) => {{
    if $dir_names.is_empty() {
        return true;
    }

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

    true
}}}

macro_rules! ignored_file_exts { ($file_exts: ident, $case: ident, $files_without_extension: ident, $path: ident) => {{
    if async_call!($path.is_file()) {
        return match $path.extension().map(|e| e.to_str()) {
            Some(Some(ext)) => $file_exts.is_empty() || match $case {
                Case::Sensitive => $file_exts.contains(&ext) == false,
                Case::Insensitive => {
                    let ext = ext.to_lowercase();
                    $file_exts.iter().all(|e| e.to_lowercase() != ext)
                },
            },
            Some(None) => true,
            None => $files_without_extension == &false,
        };
    }

    true
}}}

macro_rules! ignored_file_names { ($file_names: ident, $case: ident, $path: ident) => {{
    if $file_names.is_empty() {
        return true;
    }

    if async_call!($path.is_file()) {
        if let Some(Some(name)) = $path.file_name().map(|n| n.to_str()) {
            return match $case {
                Case::Sensitive => $file_names.contains(&name) == false,
                Case::Insensitive => {
                    let name = name.to_lowercase();
                    $file_names.iter().all(|n| n.to_lowercase() != name)
                },
            };
        }
    }

    true
}}}

macro_rules! non_symlink_files { ($path: ident) => {{
    if async_call!($path.is_file()) {
        if let Ok(true) = async_call!($path.symlink_metadata()).map(|m| m.file_type().is_symlink()) {
            return false;
        }
    }
    true
}}}

#[cfg(unix)]
macro_rules! same_device { ($device_id: ident, $path: ident) => {{
    use std::os::unix::fs::MetadataExt;
    match async_call!($path.metadata()).map(|m| m.dev()) {
        Ok(id) => $device_id == &id,
        Err(_) => false,
    }
}}}

macro_rules! some_dir_names { ($names: ident, $case: ident, $path: ident) => {{
    if async_call!($path.is_dir()) {
        if $names.is_empty() {
            return false;
        }

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

    true
}}}

macro_rules! some_file_exts { ($file_exts: ident, $case: ident, $files_without_extension: ident, $path: ident) => {{
    if async_call!($path.is_file()) {
        return match $path.extension().map(|e| e.to_str()) {
            Some(Some(ext)) => $file_exts.is_empty() == false && match $case {
                Case::Sensitive => $file_exts.contains(&ext),
                Case::Insensitive => {
                    let ext = ext.to_lowercase();
                    $file_exts.iter().any(|e| e.to_lowercase() == ext)
                },
            },
            Some(None) => false,
            None => *$files_without_extension,
        };
    }

    true
}}}

macro_rules! some_file_names { ($names: ident, $case: ident, $path: ident) => {{
    if async_call!($path.is_file()) {
        if $names.is_empty() {
            return false;
        }

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

    true
}}}

macro_rules! symlink_files { ($path: ident) => {{
    if async_call!($path.is_file()) {
        return match async_call!($path.symlink_metadata()).map(|m| m.file_type().is_symlink()) {
            Ok(true) => true,
            _ => false,
        };
    }
    true
}}}

impl Filter<'_> {


    /// # Makes [`SameDevice`](#variant.SameDevice) from a path
    #[cfg(all(not(feature="async-std"), unix))]
    #[doc(cfg(all(not(feature="async-std"), unix)))]
    pub fn make_same_device<P>(path: P) -> Result<Self> where P: AsRef<Path> {
        make_same_device!(path)
    }

    /// # Makes [`SameDevice`](#variant.SameDevice) from a path
    #[cfg(all(feature="async-std", unix))]
    #[doc(cfg(all(feature="async-std", unix)))]
    pub async fn make_same_device<P>(path: P) -> Result<Self> where P: AsRef<Path> {
        make_same_device!(path)
    }

    #[cfg(not(feature="async-std"))]
    #[doc(cfg(not(feature="async-std")))]
    pub (crate) fn accept(&self, path: &Path) -> bool {
        accept!(self, path)
    }

    #[cfg(feature="async-std")]
    #[doc(cfg(feature="async-std"))]
    pub (crate) async fn accept(&self, path: &Path) -> bool {
        accept!(self, path)
    }

}
