//! Utility functions.

use std::{
    fs::{copy, create_dir_all, metadata, read, set_permissions, write, File},
    os::unix::fs::PermissionsExt,
    path::{Path, PathBuf},
    time::SystemTime,
};

use clingwrap::runner::CommandError;
use time::{macros::format_description, OffsetDateTime};

/// Create an empty file.
pub fn create_file(filename: &Path) -> Result<PathBuf, UtilError> {
    File::create(filename).map_err(|e| UtilError::CreateFile(filename.into(), e))?;
    Ok(filename.into())
}

/// Create a directory.
pub fn mkdir(dirname: &Path) -> Result<(), UtilError> {
    create_dir_all(dirname).map_err(|e| UtilError::CreateDir(dirname.into(), e))?;
    Ok(())
}

/// Create a subdirectory.
pub fn mkdir_child(parent: &Path, subdir: &str) -> Result<PathBuf, UtilError> {
    let pathname = parent.join(subdir);
    create_dir_all(&pathname).map_err(|e| UtilError::CreateDir(pathname.clone(), e))?;
    Ok(pathname)
}

/// Make sure a directory exists and is empty.
pub fn recreate_dir(dirname: &Path) -> Result<(), UtilError> {
    if dirname.exists() {
        std::fs::remove_dir_all(dirname).map_err(|e| UtilError::RemoveDir(dirname.into(), e))?;
    }
    mkdir(dirname)?;
    Ok(())
}

/// Read a text file.
pub fn cat_text_file(filename: &Path) -> Result<String, UtilError> {
    let data = read(filename).map_err(|err| UtilError::Read(filename.into(), err))?;
    let text = String::from_utf8(data).map_err(|err| UtilError::Utf8(filename.into(), err))?;
    Ok(text)
}

/// Write a file.
pub fn write_file(filename: &Path, data: &[u8]) -> Result<(), UtilError> {
    write(filename, data).map_err(|err| UtilError::WriteFile(filename.into(), err))
}

/// Copy a file.
pub fn copy_file(src: &Path, dst: &Path) -> Result<(), UtilError> {
    copy(src, dst).map_err(|err| UtilError::Copy(src.into(), dst.into(), err))?;
    Ok(())
}

/// Copy a file, make sure user has read and write permission to the copy.
pub fn copy_file_rw(src: &Path, dst: &Path) -> Result<(), UtilError> {
    copy_file(src, dst)?;
    let mut perms = std::fs::metadata(dst)
        .map_err(|err| UtilError::GetMetadata(dst.into(), err))?
        .permissions();
    perms.set_mode(0o644);
    std::fs::set_permissions(dst, perms)
        .map_err(|err| UtilError::SetPermissions(dst.into(), err))?;
    Ok(())
}

/// Write a file, make it executable.
pub fn write_executable(filename: &Path, data: &[u8]) -> Result<(), UtilError> {
    // Unix mode bits for an executable file: read/write/exec for
    // owner, read/exec for group and others
    const EXECUTABLE: u32 = 0o755;

    write(filename, data).map_err(|err| UtilError::WriteFile(filename.into(), err))?;
    let meta = metadata(filename).map_err(|err| UtilError::GetMetadata(filename.into(), err))?;
    let mut perm = meta.permissions();
    perm.set_mode(EXECUTABLE);
    set_permissions(filename, perm).map_err(|err| UtilError::MakeExec(filename.into(), err))?;
    Ok(())
}

/// Current time as a string.
pub fn now() -> Result<String, UtilError> {
    let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
    OffsetDateTime::now_utc()
        .format(fmt)
        .map_err(UtilError::TimeFormat)
}

/// Format a time stamp.
pub fn format_timestamp(time: SystemTime) -> Result<String, UtilError> {
    let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
    OffsetDateTime::from(time)
        .format(fmt)
        .map_err(UtilError::TimeFormat)
}

/// Errors from utility functions.
#[derive(Debug, thiserror::Error)]
pub enum UtilError {
    /// Can't create directory.
    #[error("failed to create directory {0}")]
    CreateDir(PathBuf, #[source] std::io::Error),

    /// Can't remove directory.
    #[error("failed to remove directory {0}")]
    RemoveDir(PathBuf, #[source] std::io::Error),

    /// Can't write file.
    #[error("failed to write file {0}")]
    WriteFile(PathBuf, #[source] std::io::Error),

    /// Can't get file metadata.
    #[error("failed to get metadata for file: {0}")]
    GetMetadata(PathBuf, #[source] std::io::Error),

    /// Can't make executable.
    #[error("failed to make a file executable: {0}")]
    MakeExec(PathBuf, #[source] std::io::Error),

    /// Can't copy file.
    #[error("failed to copy file {0} to {1}")]
    Copy(PathBuf, PathBuf, #[source] std::io::Error),

    /// Can't set file permissions.
    #[error("failed to set permissions for file {0}")]
    SetPermissions(PathBuf, #[source] std::io::Error),

    /// Can't read file.
    #[error("failed to read file {0}")]
    Read(PathBuf, #[source] std::io::Error),

    /// Can't read file as UTF-8.
    #[error("failed to understand file {0} into UTF8")]
    Utf8(PathBuf, #[source] std::string::FromUtf8Error),

    /// Can't format time as string.
    #[error("failed to format time stamp")]
    TimeFormat(#[source] time::error::Format),

    /// Can't create file.
    #[error("failed to create file {0}")]
    CreateFile(PathBuf, #[source] std::io::Error),

    /// Can't run program.
    #[error("failed to run program {0}")]
    Execute(&'static str, #[source] CommandError),
}
