//! Filesytem functionality.
//!
//! This module contains several extensions to [std::fs].

use std::{
    fs::{File, OpenOptions},
    path::{Path, PathBuf},
};

/// A std::fs::File wrapper that removes the file when dropped.
#[derive(Debug)]
pub struct TmpFile {
    path: PathBuf,
    file: File,
}

impl TmpFile {
    /// Opens a new temporary file at the given path.
    pub fn open<P: AsRef<Path>>(path: P) -> Result<TmpFile, std::io::Error> {
        let mut options = OpenOptions::new();
        options.create_new(true).write(true);
        #[cfg(unix)]
        {
            use std::os::unix::fs::OpenOptionsExt;
            options.mode(0o600);
        }
        let file = options.open(&path)?;
        Ok(TmpFile {
            path: path.as_ref().into(),
            file,
        })
    }

    pub fn file(&self) -> &File {
        &self.file
    }
}

impl Drop for TmpFile {
    fn drop(&mut self) {
        if let Err(err) = remove_file_if_exists(&self.path) {
            eprintln!(
                "Failed to remove temporary file {}: {}",
                &self.path.display(),
                err
            );
        }
    }
}

/// Removes a file from the filesystem, if it exists.
pub fn remove_file_if_exists<P: AsRef<Path>>(path: P) -> Result<(), std::io::Error> {
    std::fs::remove_file(path).or_else(|err| {
        if err.kind() == std::io::ErrorKind::NotFound {
            Ok(())
        } else {
            Err(err)
        }
    })
}

/// Returns the path without it's final component, if there is one.
///
/// In contrast to [Path::parent], this function returns the [CurDir][std::path::Component::CurDir]
/// instead of the empty string for the current directory.
pub fn parent_dir<P: AsRef<Path>>(path: &P) -> Option<&Path> {
    path.as_ref().parent().map(|p| {
        if p.as_os_str().is_empty() {
            Path::new(std::path::Component::CurDir.as_os_str())
        } else {
            p
        }
    })
}

/// Rename a file.
///
/// This function should be atomic on POSIX-conform systems.
///
/// Both paths must be located on the same filesystem.
pub fn rename_file<P, Q>(from: P, to: Q) -> Result<(), std::io::Error>
where
    P: AsRef<Path>,
    Q: AsRef<Path>,
{
    File::open(&from)?.sync_all()?;
    std::fs::rename(&from, &to)?;

    let dest_dir = parent_dir(&to).ok_or_else(|| std::io::Error::other("is root directory"))?;
    File::open(dest_dir)?.sync_all()?;

    let src_dir = parent_dir(&from).ok_or_else(|| std::io::Error::other("is root directory"))?;
    if src_dir != dest_dir {
        File::open(src_dir)?.sync_all()?;
    }

    Ok(())
}

/// Copy a file.
///
/// This functions hard links `from` to `tmp`, and then renames the latter to `to`. The renaming
/// should be atomic on POSIX-conform systems.
///
/// All three paths must be located on the same filesystem.
// Didn't use https://crates.io/crates/atomic-write-file or https://crates.io/crates/atomicwrites
// because they create randomly named temporary files, which I wanted to unveil(2) on OpenBSD, and
// remove when receiving a termination signal.
pub fn copy_file<P, Q, R>(from: P, tmp: Q, to: R) -> Result<(), std::io::Error>
where
    P: AsRef<Path>,
    Q: AsRef<Path>,
    R: AsRef<Path>,
{
    fn inner<P, Q, R>(from: P, tmp: Q, to: R) -> Result<(), std::io::Error>
    where
        P: AsRef<Path>,
        Q: AsRef<Path>,
        R: AsRef<Path>,
    {
        let tmp_dir =
            parent_dir(&from).ok_or_else(|| std::io::Error::other("is root directory"))?;
        File::open(tmp_dir)?.sync_all()?;

        std::fs::rename(&tmp, &to)?;

        let to_dir = parent_dir(&from).ok_or_else(|| std::io::Error::other("is root directory"))?;
        File::open(to_dir)?.sync_all()?;

        Ok(())
    }

    File::open(&from)?.sync_all()?;
    std::fs::hard_link(&from, &tmp)?;
    inner(&from, &tmp, &to).inspect_err(|_| {
        if let Err(err) = remove_file_if_exists(&tmp) {
            eprintln!(
                "Failed to remove temporary file {}: {}",
                tmp.as_ref().display(),
                err
            );
        }
    })
}
