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

Dia-Files

Copyright (C) 2019-2025  Anonymous

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

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/>.

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

//! # Root

use {
    std::path::{Path, PathBuf},
    crate::{
        CODE_NAME,
        FilePermissions,
        Result,
        filter::Filter,
    },
};

#[cfg(not(feature="tokio"))]
use std::fs::{self, ReadDir};

#[cfg(feature="tokio")]
use tokio::fs::{self, ReadDir};

/// # File discovery
///
/// ## Notes
///
/// - You can make this struct by [`find_files()`][::find_files()].
/// - If sub directories are symlinks, they will be ignored.
///
/// [::find_files()]: fn.find_files.html
#[derive(Debug)]
pub struct FileDiscovery<F> {
    root: PathBuf,
    filter: F,
    recursive: bool,
    current: ReadDir,
    sub_dirs: Option<Vec<PathBuf>>,
    max_depth: Option<usize>,
}

macro_rules! make_file_discovery { ($dir: ident, $recursive: ident, $filter: ident, $max_depth: ident) => {{
    Ok(Self {
        root: async_call!(fs::canonicalize($dir.as_ref()))?,
        filter: $filter,
        recursive: $recursive,
        current: async_call!(fs::read_dir($dir))?,
        sub_dirs: None,
        max_depth: $max_depth,
    })
}}}

macro_rules! next { ($self: ident) => {{
    loop {
        #[cfg(not(feature="tokio"))]
        let next = $self.current.next();
        #[cfg(feature="tokio")]
        let next = $self.current.next_entry().await.transpose();
        match next {
            Some(Ok(entry)) => {
                match async_call!(entry.file_type()) {
                    Ok(file_type) => {
                        let path = entry.path();
                        let is_symlink = file_type.is_symlink();
                        if file_type.is_dir() || (is_symlink && path.is_dir()) {
                            // If we accept symlinks, update code that calls ancestors()!
                            if $self.recursive == false || is_symlink {
                                continue;
                            }

                            if let Some(max_depth) = $self.max_depth.as_ref() {
                                match async_call!(fs::canonicalize(&path)) {
                                    // Symlinks are ignored, so using ancestors() is safe.
                                    Ok(path) => match depth_from(&$self.root, &path) {
                                        Ok(depth) => if &depth >= max_depth {
                                            continue;
                                        },
                                        Err(err) => return Some(Err(err)),
                                    },
                                    Err(err) => return Some(Err(err)),
                                };
                            }

                            if async_call!($self.filter.accept(&path)) == false {
                                continue;
                            }

                            match $self.sub_dirs.as_mut() {
                                Some(sub_dirs) => sub_dirs.push(path),
                                None => $self.sub_dirs = Some(vec!(path)),
                            };
                        } else if file_type.is_file() || (is_symlink && path.is_file()) {
                            if async_call!($self.filter.accept(&path)) == false {
                                continue;
                            }
                            return Some(Ok(path));
                        }
                    },
                    Err(err) => return Some(Err(err)),
                };
            },
            Some(Err(err)) => return Some(Err(err)),
            None => match $self.sub_dirs.as_mut() {
                None => return None,
                Some(sub_dirs) => match sub_dirs.len() {
                    0 => return None,
                    _ => match async_call!(fs::read_dir(sub_dirs.remove(0))) {
                        Ok(new) => $self.current = new,
                        Err(err) => return Some(Err(err)),
                    },
                },
            },
        };
    }
}}}

impl<'a> FileDiscovery<Filter<'a>> {

    /// # Makes new instance
    #[cfg(not(feature="tokio"))]
    #[doc(cfg(not(feature="tokio")))]
    pub fn make<P>(dir: P, recursive: bool, filter: Filter<'a>, max_depth: Option<usize>) -> Result<Self> where P: AsRef<Path> {
        make_file_discovery!(dir, recursive, filter, max_depth)
    }

    /// # Makes new instance
    #[cfg(feature="tokio")]
    #[doc(cfg(feature="tokio"))]
    pub async fn make<P>(dir: P, recursive: bool, filter: Filter<'a>, max_depth: Option<usize>) -> Result<Self> where P: AsRef<Path> {
        make_file_discovery!(dir, recursive, filter, max_depth)
    }

}

#[cfg(feature="tokio")]
#[doc(cfg(feature="tokio"))]
impl FileDiscovery<Filter<'_>> {

    /// # Finds next path
    pub async fn next(&mut self) -> Option<Result<PathBuf>> {
        next!(self)
    }

    /// # Counts all files
    pub async fn count(mut self) -> Option<usize> {
        let mut result = usize::MIN;
        while let Some(path) = self.next().await {
            if path.is_ok() {
                result = match result.checked_add(1) {
                    None => return None,
                    Some(n) => n,
                };
            }
        }
        Some(result)
    }

}

#[cfg(not(feature="tokio"))]
#[doc(cfg(not(feature="tokio")))]
impl Iterator for FileDiscovery<Filter<'_>> {

    type Item = Result<PathBuf>;

    fn next(&mut self) -> Option<Self::Item> {
        next!(self)
    }

}

/// # Calculates depth of path from a root directory
///
/// ## Notes
///
/// - [`canonicalize()`][r://PathBuf/canonicalize()] is _not_ called on input paths.
///
/// [r://PathBuf/canonicalize()]: https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.canonicalize
fn depth_from<P, Q>(root_dir: P, path: Q) -> Result<usize> where P: AsRef<Path>, Q: AsRef<Path> {
    let root_dir = root_dir.as_ref();
    let path = path.as_ref();

    let mut depth: usize = 0;
    let mut found_root = false;
    for a in path.ancestors().skip(1) {
        if a == root_dir {
            found_root = true;
            break;
        } else {
            match depth.checked_add(1) {
                Some(new_depth) => depth = new_depth,
                None => return Err(err!("Directory level of {path:?} is too deep: {depth}")),
            };
        }
    }

    if found_root {
        Ok(depth)
    } else {
        Err(err!("{path:?} was expected to be inside of {root_dir:?}, but not"))
    }
}

/// # Finds files
///
/// This function makes new instance of [`FileDiscovery`][::FileDiscovery]. You should refer to that struct for notes on usage.
///
/// [::FileDiscovery]: struct.FileDiscovery.html
#[cfg(not(feature="tokio"))]
#[doc(cfg(not(feature="tokio")))]
pub fn find_files<'a, P>(dir: P, recursive: bool, filter: Filter<'a>) -> Result<FileDiscovery<Filter<'a>>> where P: AsRef<Path> {
    FileDiscovery::make(dir, recursive, filter, None)
}

/// # Finds files
///
/// This function makes new instance of [`FileDiscovery`][::FileDiscovery]. You should refer to that struct for notes on usage.
///
/// [::FileDiscovery]: struct.FileDiscovery.html
#[cfg(feature="tokio")]
#[doc(cfg(feature="tokio"))]
pub async fn find_files<'a, P>(dir: P, recursive: bool, filter: Filter<'a>) -> Result<FileDiscovery<Filter<'a>>> where P: AsRef<Path> {
    FileDiscovery::make(dir, recursive, filter, None).await
}

#[cfg(not(feature="tokio"))]
macro_rules! exists { ($p: expr) => { $p.exists() }}

#[cfg(feature="tokio")]
macro_rules! exists { ($p: expr) => { fs::try_exists($p).await? }}

macro_rules! on_same_unix_device { ($first: ident, $second: ident) => {{
    use std::os::unix::fs::MetadataExt;

    let first = $first.as_ref();
    let second = $second.as_ref();

    if exists!(first) == false || exists!(second) == false {
        Ok(false)
    } else {
        Ok(async_call!(fs::metadata(first))?.dev() == async_call!(fs::metadata(second))?.dev())
    }
}}}

/// # Checks if two paths are on a same Unix device
///
/// If either file does not exist, the function returns `Ok(false)`.
#[cfg(all(not(feature="tokio"), unix))]
#[doc(cfg(all(not(feature="tokio"), unix)))]
pub fn on_same_unix_device<P, Q>(first: P, second: Q) -> Result<bool> where P: AsRef<Path>, Q: AsRef<Path> {
    on_same_unix_device!(first, second)
}

/// # Checks if two paths are on a same Unix device
///
/// If either file does not exist, the function returns `Ok(false)`.
#[cfg(all(feature="tokio", unix))]
#[doc(cfg(all(feature="tokio", unix)))]
pub async fn on_same_unix_device<P, Q>(first: P, second: Q) -> Result<bool> where P: AsRef<Path>, Q: AsRef<Path> {
    on_same_unix_device!(first, second)
}

#[cfg(all(not(feature="tokio"), unix))]
#[doc(cfg(all(not(feature="tokio"), unix)))]
#[test]
fn test_on_same_unix_device() -> Result<()> {
    assert!(on_same_unix_device(file!(), PathBuf::from(file!()).parent().unwrap().join("lib.rs"))?);
    assert!(on_same_unix_device(file!(), std::env::temp_dir())? == false);

    Ok(())
}

macro_rules! write_file { ($target: ident, $file_permissions: ident, $data: ident, $tmp_file_suffix: ident) => {{
    let target = $target.as_ref();

    let tmp_file_suffix = $tmp_file_suffix.as_ref().trim();
    if tmp_file_suffix.is_empty() {
        return Err(err!("Temporary file suffix is empty"));
    }

    let tmp_file = match (target.parent(), target.file_name().map(|n| n.to_str())) {
        (Some(dir), Some(Some(file_name))) => dir.join(format!("{file_name}.{tmp_file_suffix}.{CODE_NAME}.tmp")),
        _ => return Err(err!("Failed to get host directory and/or file name of {target:?}")),
    };

    macro_rules! job { () => {{
        #[cfg(unix)]
        let file_permissions = match $file_permissions {
            Some(file_permissions) => Some(file_permissions),
            None => if exists!(target) {
                Some(async_call!(fs::metadata(target))?.permissions().try_into()?)
            } else {
                None
            },
        };

        async_call!(fs::write(&tmp_file, $data))?;

        #[cfg(unix)]
        if let Some(file_permissions) = file_permissions {
            async_call!(file_permissions.set(&tmp_file))?;
        }

        async_call!(fs::rename(&tmp_file, target))
    }}}
    match job!() {
        Ok(()) => Ok(()),
        Err(err) => {
            if exists!(&tmp_file) {
                async_call!(fs::remove_file(tmp_file))?;
            }
            Err(err)
        },
    }
}}}

/// # Writes data to target file via a temporary file
///
/// ## Steps
///
/// - Make a temporary file in the same directory as your target file, then write to it.
/// - On success, rename that file to your target file.
///
/// ## Notes
///
/// - Currently, file permissions are only supported on Unix. If you don't provide permissions:
///
///     + If target file exists, its permissions will be used.
///     + If target file does not exist, default permissions when creating new files will be used. Normally, this is system-wide setting.
///
/// - The function returns an error if your temporary file suffix is either empty or just contains white spaces.
/// - If the temporary file exists, it will be overwritten.
/// - On failure, the function tries to delete the temporary file, then returns the error.
#[cfg(not(feature="tokio"))]
#[doc(cfg(not(feature="tokio")))]
pub fn write_file<P, B, S>(target: P, file_permissions: Option<FilePermissions>, data: B, tmp_file_suffix: S) -> Result<()>
where P: AsRef<Path>, B: AsRef<[u8]>, S: AsRef<str> {
    write_file!(target, file_permissions, data, tmp_file_suffix)
}

/// # Writes data to target file via a temporary file
///
/// ## Steps
///
/// - Make a temporary file in the same directory as your target file, then write to it.
/// - On success, rename that file to your target file.
///
/// ## Notes
///
/// - Currently, file permissions are only supported on Unix. If you don't provide permissions:
///
///     + If target file exists, its permissions will be used.
///     + If target file does not exist, default permissions when creating new files will be used. Normally, this is system-wide setting.
///
/// - The function returns an error if your temporary file suffix is either empty or just contains white spaces.
/// - If the temporary file exists, it will be overwritten.
/// - On failure, the function tries to delete the temporary file, then returns the error.
#[cfg(feature="tokio")]
#[doc(cfg(feature="tokio"))]
pub async fn write_file<P, B, S>(target: P, file_permissions: Option<FilePermissions>, data: B, tmp_file_suffix: S) -> Result<()>
where P: AsRef<Path>, B: AsRef<[u8]>, S: AsRef<str> {
    write_file!(target, file_permissions, data, tmp_file_suffix)
}