// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

mod error;

use std::fs::File;
use std::fs::FileTimes;
use std::fs::OpenOptions;
use std::io::Read;
use std::io::Seek;
use std::io::Write;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;

use camino::Utf8Path;
use camino::Utf8PathBuf;
use snafu::ResultExt;
use tempfile::NamedTempFile;

pub use crate::error::FileIoError;
pub use crate::error::FileIoSnafu;
pub use crate::error::FileOp;

pub type Result<T, E = FileIoError> = std::result::Result<T, E>;

/// See [File::create].
pub fn create_file(path: impl AsRef<Path>) -> Result<File> {
    File::create(&path).context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Create,
    })
}

/// See [File::open].
pub fn open_file(path: impl AsRef<Path>) -> Result<File> {
    File::open(&path).context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Open,
    })
}

pub fn open_file_ext(path: impl AsRef<Path>, options: OpenOptions) -> Result<File> {
    options.open(&path).context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Open,
    })
}

/// See [std::fs::write].
pub fn write_file(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
    std::fs::write(&path, contents).context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Write,
    })
}

pub fn write_file_and_flush(
    path: impl AsRef<Path> + Clone,
    contents: impl AsRef<[u8]>,
) -> Result<()> {
    let mut file = create_file(path.clone())?;
    file.write_all(contents.as_ref()).context(FileIoSnafu {
        path: path.clone().as_ref(),
        op: FileOp::Write,
    })?;
    file.sync_all().context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Sync,
    })
}

/// See [File::set_times].
pub fn set_file_times(path: impl AsRef<Path>, times: FileTimes) -> Result<()> {
    #[cfg(not(windows))]
    let file = open_file(&path)?;

    #[cfg(windows)]
    let file = {
        use std::os::windows::fs::OpenOptionsExt;
        open_file_ext(
            &path,
            OpenOptions::new()
                .write(true)
                // It's required to modify the time attributes of a directory in windows system.
                .custom_flags(0x02000000) // FILE_FLAG_BACKUP_SEMANTICS
                .to_owned(),
        )?
    };

    file.set_times(times).context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::SetFileTimes,
    })
}

/// See [std::fs::remove_file].
#[allow(dead_code)]
pub fn remove_file(path: impl AsRef<Path>) -> Result<()> {
    std::fs::remove_file(&path).context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Remove,
    })
}

/// See [std::fs::remove_dir_all].
#[allow(dead_code)]
pub fn remove_dir_all(path: impl AsRef<Path>) -> Result<()> {
    std::fs::remove_dir_all(&path).context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Remove,
    })
}

/// See [std::fs::create_dir].
pub fn create_dir(path: impl AsRef<Path>) -> Result<()> {
    std::fs::create_dir(&path).context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Create,
    })
}

/// See [std::fs::create_dir_all].
pub fn create_dir_all(path: impl AsRef<Path>) -> Result<()> {
    std::fs::create_dir_all(&path).context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Create,
    })
}

/// See [std::fs::read].
pub fn read_file(path: impl AsRef<Path>) -> Result<Vec<u8>> {
    std::fs::read(&path).context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Read,
    })
}

/// See [std::fs::read_to_string].
pub fn read_to_string(path: impl AsRef<Path>) -> Result<String> {
    std::fs::read_to_string(&path).context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Read,
    })
}

/// See [std::fs::copy].
pub fn copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<u64> {
    std::fs::copy(&src, &dst).context(FileIoSnafu {
        path: dst.as_ref(),
        op: FileOp::CopyFrom(src.as_ref().to_owned()),
    })
}

/// Copy a file from src to dst if dst doesn't exist or if src is newer than
/// dst. Preserves the modification time from the source file.
pub fn copy_if_newer(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<bool> {
    let src = src.as_ref();
    let dst = dst.as_ref();

    let should_copy = if !dst.exists() {
        true
    } else {
        let src_time = modified_time(src)?;
        let dst_time = modified_time(dst)?;
        src_time > dst_time
    };

    if should_copy {
        copy_file(src, dst)?;

        // Preserve the modification time from the source file
        let src_mtime = modified_time(src)?;
        let times = FileTimes::new().set_modified(src_mtime);
        set_file_times(dst, times)?;

        Ok(true)
    } else {
        Ok(false)
    }
}

/// Like [read_file], but skips the section that is potentially locked by
/// SQLite.
pub fn read_locked_db_file(path: impl AsRef<Path>) -> Result<Vec<u8>> {
    read_locked_db_file_inner(&path).context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Read,
    })
}

const LOCKED_SECTION_START_BYTE: usize = 1024 * 1024 * 1024;
const LOCKED_SECTION_LEN_BYTES: usize = 512;
const LOCKED_SECTION_END_BYTE: usize = LOCKED_SECTION_START_BYTE + LOCKED_SECTION_LEN_BYTES;

fn read_locked_db_file_inner(path: impl AsRef<Path>) -> std::io::Result<Vec<u8>> {
    let size = std::fs::metadata(&path)?.len() as usize;
    if size < LOCKED_SECTION_END_BYTE {
        return std::fs::read(path);
    }

    let mut file = File::open(&path)?;
    let mut buf = vec![0; size];
    file.read_exact(&mut buf[..LOCKED_SECTION_START_BYTE])?;
    file.seek(std::io::SeekFrom::Current(LOCKED_SECTION_LEN_BYTES as i64))?;
    file.read_exact(&mut buf[LOCKED_SECTION_END_BYTE..])?;

    Ok(buf)
}

/// See [std::fs::metadata].
pub fn metadata(path: impl AsRef<Path>) -> Result<std::fs::Metadata> {
    std::fs::metadata(&path).context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Metadata,
    })
}

/// Get the modification time of a file.
pub fn modified_time(path: impl AsRef<Path>) -> Result<std::time::SystemTime> {
    metadata(&path)?.modified().context(FileIoSnafu {
        path: path.as_ref(),
        op: FileOp::Metadata,
    })
}

pub fn new_tempfile() -> Result<NamedTempFile> {
    NamedTempFile::new().context(FileIoSnafu {
        path: std::env::temp_dir(),
        op: FileOp::Create,
    })
}

pub fn new_tempfile_in(dir: impl AsRef<Path>) -> Result<NamedTempFile> {
    NamedTempFile::new_in(&dir).context(FileIoSnafu {
        path: dir.as_ref(),
        op: FileOp::Create,
    })
}

pub fn new_tempfile_in_parent_of(file: &Path) -> Result<NamedTempFile> {
    let dir = file.parent().unwrap_or(file);
    NamedTempFile::new_in(dir).context(FileIoSnafu {
        path: dir,
        op: FileOp::Create,
    })
}

/// Atomically replace the target path with the provided temp file.
///
/// If `fsync` is true, file data is synced to disk prior to renaming, and the
/// folder is synced on UNIX platforms after renaming. This minimizes the
/// chances of corruption if there is a crash or power loss directly after the
/// op, but it can be considerably slower.
pub fn atomic_rename(file: NamedTempFile, target: &Path, fsync: bool) -> Result<()> {
    if fsync {
        file.as_file().sync_all().context(FileIoSnafu {
            path: file.path(),
            op: FileOp::Sync,
        })?;
    }
    file.persist(target)?;
    #[cfg(not(windows))]
    if fsync {
        if let Some(parent) = target.parent() {
            open_file(parent)?.sync_all().context(FileIoSnafu {
                path: parent,
                op: FileOp::Sync,
            })?;
        }
    }
    Ok(())
}

/// Like [std::fs::read_dir], but only yielding files. [Err]s are not filtered.
pub fn read_dir_files(path: impl AsRef<Path>) -> Result<ReadDirFiles> {
    std::fs::read_dir(&path)
        .map(ReadDirFiles)
        .context(FileIoSnafu {
            path: path.as_ref(),
            op: FileOp::Read,
        })
}

/// A shortcut for gathering the utf8 paths in a folder into a vec. Will
/// abort if any dir entry is unreadable. Does not gather files from subfolders.
pub fn paths_in_dir(path: impl AsRef<Path>) -> Result<Vec<Utf8PathBuf>> {
    read_dir_files(path.as_ref())?
        .map(|entry| {
            let entry = entry.context(FileIoSnafu {
                path: path.as_ref(),
                op: FileOp::Read,
            })?;
            entry.path().utf8()
        })
        .collect()
}

/// True if name does not contain any path separators.
pub fn filename_is_safe(name: &str) -> bool {
    let mut components = Path::new(name).components();
    let first_element_normal = components
        .next()
        .map(|component| matches!(component, Component::Normal(_)))
        .unwrap_or_default();

    first_element_normal && components.next().is_none()
}

pub struct ReadDirFiles(std::fs::ReadDir);

impl Iterator for ReadDirFiles {
    type Item = std::io::Result<std::fs::DirEntry>;

    fn next(&mut self) -> Option<Self::Item> {
        let next = self.0.next();
        if let Some(Ok(entry)) = next.as_ref() {
            match entry.metadata().map(|metadata| metadata.is_file()) {
                Ok(true) => next,
                Ok(false) => self.next(),
                Err(error) => Some(Err(error)),
            }
        } else {
            next
        }
    }
}

/// True if changed.
pub fn write_file_if_changed(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<bool> {
    let path = path.as_ref();
    let contents = contents.as_ref();
    let changed = {
        read_file(path)
            .map(|existing| existing != contents)
            .unwrap_or(true)
    };
    if changed {
        write_file(path, contents)?;
        Ok(true)
    } else {
        Ok(false)
    }
}

pub trait ToUtf8PathBuf {
    fn utf8(self) -> Result<Utf8PathBuf>;
}

impl ToUtf8PathBuf for PathBuf {
    fn utf8(self) -> Result<Utf8PathBuf> {
        Utf8PathBuf::from_path_buf(self).map_err(|path| FileIoError {
            path,
            op: FileOp::DecodeUtf8Filename,
            source: std::io::Error::from(std::io::ErrorKind::InvalidData),
        })
    }
}

pub trait ToUtf8Path {
    fn utf8(&self) -> Result<&Utf8Path>;
}

impl ToUtf8Path for Path {
    fn utf8(&self) -> Result<&Utf8Path> {
        Utf8Path::from_path(self).ok_or_else(|| FileIoError {
            path: self.into(),
            op: FileOp::DecodeUtf8Filename,
            source: std::io::Error::from(std::io::ErrorKind::InvalidData),
        })
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn path_traversal() {
        assert!(filename_is_safe("foo"));

        assert!(!filename_is_safe(".."));
        assert!(!filename_is_safe("foo/bar"));
        assert!(!filename_is_safe("/foo"));
        assert!(!filename_is_safe("../foo"));

        if cfg!(windows) {
            assert!(!filename_is_safe("foo\\bar"));
            assert!(!filename_is_safe("c:\\foo"));
            assert!(!filename_is_safe("\\foo"));
        }
    }
}
