use std::ffi::CString;
use std::io;
use std::path::PathBuf;
use std::time::{Duration, SystemTime};

use crate::fs::{
    FileType, Metadata, MetadataInterest, Permissions, RemoveFlag, SyncDataFlag, path_from_cstring,
};
use crate::kqueue::op::{DirectFdOp, DirectOp, DirectOpExtract, impl_fd_op};
use crate::{AsyncFd, SubmissionQueue, fd, syscall};

pub(crate) struct OpenOp;

impl DirectOp for OpenOp {
    type Output = AsyncFd;
    type Resources = (CString, fd::Kind); // path.
    type Args = (libc::c_int, libc::mode_t); // flags, mode.

    fn run(
        sq: &SubmissionQueue,
        resources: Self::Resources,
        args: Self::Args,
    ) -> io::Result<Self::Output> {
        Ok(Self::run_extract(sq, resources, args)?.0)
    }
}

impl DirectOpExtract for OpenOp {
    type ExtractOutput = (AsyncFd, PathBuf);

    #[allow(clippy::cast_lossless)]
    fn run_extract(
        sq: &SubmissionQueue,
        (path, kind): Self::Resources,
        (mut flags, mode): Self::Args,
    ) -> io::Result<Self::ExtractOutput> {
        let fd::Kind::File = kind;

        flags |= libc::O_NONBLOCK; // NOTE: O_CLOEXEC is already set.
        let fd = syscall!(open(path.as_ptr(), flags, mode as libc::c_int))?;
        // SAFETY: just created the fd above.
        let fd = unsafe { AsyncFd::from_raw(fd, kind, sq.clone()) };
        Ok((fd, path_from_cstring(path)))
    }
}

pub(crate) struct CreateDirOp;

impl DirectOp for CreateDirOp {
    type Output = ();
    type Resources = CString; // path.
    type Args = ();

    fn run(
        sq: &SubmissionQueue,
        resources: Self::Resources,
        args: Self::Args,
    ) -> io::Result<Self::Output> {
        Self::run_extract(sq, resources, args)?;
        Ok(())
    }
}

impl DirectOpExtract for CreateDirOp {
    type ExtractOutput = PathBuf;

    fn run_extract(
        _: &SubmissionQueue,
        path: Self::Resources,
        (): Self::Args,
    ) -> io::Result<Self::ExtractOutput> {
        let mode = 0o777; // Same as used by the standard library.
        syscall!(mkdirat(libc::AT_FDCWD, path.as_ptr(), mode))?;
        Ok(path_from_cstring(path))
    }
}

pub(crate) struct RenameOp;

impl DirectOp for RenameOp {
    type Output = ();
    type Resources = (CString, CString); // from path, to path
    type Args = ();

    fn run(
        sq: &SubmissionQueue,
        resources: Self::Resources,
        args: Self::Args,
    ) -> io::Result<Self::Output> {
        Self::run_extract(sq, resources, args)?;
        Ok(())
    }
}

impl DirectOpExtract for RenameOp {
    type ExtractOutput = (PathBuf, PathBuf);

    fn run_extract(
        _: &SubmissionQueue,
        (from, to): Self::Resources,
        (): Self::Args,
    ) -> io::Result<Self::ExtractOutput> {
        syscall!(renameat(
            libc::AT_FDCWD,
            from.as_ptr(),
            libc::AT_FDCWD,
            to.as_ptr()
        ))?;
        Ok((path_from_cstring(from), path_from_cstring(to)))
    }
}

pub(crate) struct DeleteOp;

impl DirectOp for DeleteOp {
    type Output = ();
    type Resources = CString; // path
    type Args = RemoveFlag;

    fn run(
        sq: &SubmissionQueue,
        resources: Self::Resources,
        args: Self::Args,
    ) -> io::Result<Self::Output> {
        Self::run_extract(sq, resources, args)?;
        Ok(())
    }
}

impl DirectOpExtract for DeleteOp {
    type ExtractOutput = PathBuf;

    fn run_extract(
        _: &SubmissionQueue,
        path: Self::Resources,
        flags: Self::Args,
    ) -> io::Result<Self::ExtractOutput> {
        let flags = match flags {
            RemoveFlag::File => 0,
            RemoveFlag::Directory => libc::AT_REMOVEDIR,
        };
        syscall!(unlinkat(libc::AT_FDCWD, path.as_ptr(), flags))?;
        Ok(path_from_cstring(path))
    }
}

pub(crate) struct SyncDataOp;

impl DirectFdOp for SyncDataOp {
    type Output = ();
    type Resources = ();
    type Args = SyncDataFlag;

    fn run(fd: &AsyncFd, (): Self::Resources, flag: Self::Args) -> io::Result<Self::Output> {
        match flag {
            #[cfg(not(any(
                target_os = "ios",
                target_os = "macos",
                target_os = "tvos",
                target_os = "visionos",
                target_os = "watchos",
            )))]
            SyncDataFlag::All => syscall!(fsync(fd.fd()))?,
            #[cfg(not(any(
                target_os = "ios",
                target_os = "macos",
                target_os = "tvos",
                target_os = "visionos",
                target_os = "watchos",
            )))]
            SyncDataFlag::Data => syscall!(fdatasync(fd.fd()))?,
            #[cfg(any(
                target_os = "ios",
                target_os = "macos",
                target_os = "tvos",
                target_os = "visionos",
                target_os = "watchos",
            ))]
            SyncDataFlag::All | SyncDataFlag::Data => {
                syscall!(fcntl(fd.fd(), libc::F_FULLFSYNC, 0))?
            }
        };
        Ok(())
    }
}

impl_fd_op!(SyncDataOp);

pub(crate) const fn default_metadata_interest() -> MetadataInterest {
    MetadataInterest(0)
}

pub(crate) struct StatOp;

impl DirectFdOp for StatOp {
    type Output = Metadata;
    type Resources = Metadata;
    type Args = MetadataInterest;

    fn run(fd: &AsyncFd, mut metadata: Self::Resources, _: Self::Args) -> io::Result<Self::Output> {
        syscall!(fstat(fd.fd(), &raw mut metadata.0))?;
        Ok(metadata)
    }
}

impl_fd_op!(StatOp);

#[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))]
pub(crate) struct AdviseOp;

#[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))]
impl DirectFdOp for AdviseOp {
    type Output = ();
    type Resources = ();
    type Args = (u64, u32, crate::fs::AdviseFlag); // offset, length, advice

    fn run(
        fd: &AsyncFd,
        (): Self::Resources,
        (offset, length, advise): Self::Args,
    ) -> io::Result<Self::Output> {
        syscall!(posix_fadvise(
            fd.fd(),
            offset.cast_signed(),
            length.into(),
            advise.0.cast_signed()
        ))?;
        Ok(())
    }
}

#[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))]
impl_fd_op!(AdviseOp);

#[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))]
pub(crate) struct AllocateOp;

#[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))]
impl DirectFdOp for AllocateOp {
    type Output = ();
    type Resources = ();
    type Args = (u64, u32, crate::fs::AllocateMode); // offset, length, mode

    fn run(
        fd: &AsyncFd,
        (): Self::Resources,
        (offset, length, _): Self::Args,
    ) -> io::Result<Self::Output> {
        syscall!(posix_fallocate(
            fd.fd(),
            offset.cast_signed(),
            length.into()
        ))?;
        Ok(())
    }
}

#[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))]
impl_fd_op!(AllocateOp);

pub(crate) struct TruncateOp;

impl DirectFdOp for TruncateOp {
    type Output = ();
    type Resources = ();
    type Args = u64; // length

    fn run(fd: &AsyncFd, (): Self::Resources, length: Self::Args) -> io::Result<Self::Output> {
        syscall!(ftruncate(fd.fd(), length.cast_signed()))?;
        Ok(())
    }
}

impl_fd_op!(TruncateOp);

pub(crate) use libc::stat as Stat;

pub(crate) const fn file_type(stat: &Stat) -> FileType {
    FileType(stat.st_mode)
}

pub(crate) const fn len(stat: &Stat) -> u64 {
    stat.st_size.cast_unsigned()
}

pub(crate) const fn block_size(stat: &Stat) -> u32 {
    stat.st_blksize.cast_unsigned()
}

pub(crate) const fn permissions(stat: &Stat) -> Permissions {
    Permissions(stat.st_mode)
}

pub(crate) fn modified(stat: &Stat) -> SystemTime {
    timestamp(stat.st_mtime, stat.st_mtime_nsec)
}

pub(crate) fn accessed(stat: &Stat) -> SystemTime {
    timestamp(stat.st_atime, stat.st_atime_nsec)
}

pub(crate) fn created(stat: &Stat) -> SystemTime {
    timestamp(stat.st_birthtime, stat.st_birthtime_nsec)
}

#[allow(clippy::cast_sign_loss)] // Checked.
fn timestamp(secs: libc::time_t, nsec: libc::c_long) -> SystemTime {
    let mut time = SystemTime::UNIX_EPOCH;
    let dur = Duration::from_secs(secs as u64);
    let nanos = Duration::from_nanos(nsec as u64);
    if secs.is_positive() {
        time += dur;
    } else {
        time -= dur;
    }
    if nsec.is_positive() {
        time += nanos;
    } else {
        time -= nanos;
    }
    time
}
