use crate::sys::time::TimeSpec;
#[cfg(any(
    target_os = "freebsd",
    target_os = "dragonfly",
    target_os = "linux",
    target_os = "android",
    target_os = "emscripten",
))]
use crate::{unistd::Pid, Error};
use crate::{Errno, Result};
use libc::{self, clockid_t};
use std::mem::MaybeUninit;

/// Clock identifier
///
/// Newtype pattern around `clockid_t` (which is just alias). It pervents bugs caused by
/// accidentally passing wrong value.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ClockId(clockid_t);

impl ClockId {
    /// Creates `ClockId` from raw `clockid_t`
    pub fn from_raw(clk_id: clockid_t) -> Self {
        ClockId(clk_id)
    }

    /// Returns `ClockId` of a `pid` CPU-time clock
    #[cfg(any(
        target_os = "freebsd",
        target_os = "dragonfly",
        target_os = "linux",
        target_os = "android",
        target_os = "emscripten",
    ))]
    pub fn pid_cpu_clock_id(pid: Pid) -> Result<Self> {
        clock_getcpuclockid(pid)
    }

    /// Returns resolution of the clock id
    pub fn res(self) -> Result<TimeSpec> {
        clock_getres(self)
    }

    /// Returns the current time on the clock id
    pub fn now(self) -> Result<TimeSpec> {
        clock_gettime(self)
    }

    /// Sets time to `timespec` on the clock id
    #[cfg(not(any(
        target_os = "macos",
        target_os = "ios",
        all(
            not(any(target_env = "uclibc", target_env = "newlibc")),
            any(target_os = "redox", target_os = "hermit",),
        ),
    )))]
    pub fn set_time(self, timespec: TimeSpec) -> Result<()> {
        clock_settime(self, timespec)
    }

    /// Gets the raw `clockid_t` wrapped by `self`
    pub fn as_raw(self) -> clockid_t {
        self.0
    }

    #[cfg(any(
        target_os = "fuchsia",
        all(
            not(any(target_env = "uclibc", target_env = "newlib")),
            any(target_os = "linux", target_os = "android", target_os = "emscripten"),
        )
    ))]
    pub const CLOCK_BOOTTIME: ClockId = ClockId(libc::CLOCK_BOOTTIME);
    #[cfg(any(
        target_os = "fuchsia",
        all(
            not(any(target_env = "uclibc", target_env = "newlib")),
            any(target_os = "linux", target_os = "android", target_os = "emscripten")
        )
    ))]
    pub const CLOCK_BOOTTIME_ALARM: ClockId = ClockId(libc::CLOCK_BOOTTIME_ALARM);
    pub const CLOCK_MONOTONIC: ClockId = ClockId(libc::CLOCK_MONOTONIC);
    #[cfg(any(
        target_os = "fuchsia",
        all(
            not(any(target_env = "uclibc", target_env = "newlib")),
            any(target_os = "linux", target_os = "android", target_os = "emscripten")
        )
    ))]
    pub const CLOCK_MONOTONIC_COARSE: ClockId = ClockId(libc::CLOCK_MONOTONIC_COARSE);
    #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
    pub const CLOCK_MONOTONIC_FAST: ClockId = ClockId(libc::CLOCK_MONOTONIC_FAST);
    #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
    pub const CLOCK_MONOTONIC_PRECISE: ClockId = ClockId(libc::CLOCK_MONOTONIC_PRECISE);
    #[cfg(any(
        target_os = "fuchsia",
        all(
            not(any(target_env = "uclibc", target_env = "newlib")),
            any(target_os = "linux", target_os = "android", target_os = "emscripten")
        )
    ))]
    pub const CLOCK_MONOTONIC_RAW: ClockId = ClockId(libc::CLOCK_MONOTONIC_RAW);
    #[cfg(any(
        target_os = "fuchsia",
        target_env = "uclibc",
        target_os = "macos",
        target_os = "ios",
        target_os = "freebsd",
        target_os = "dragonfly",
        all(
            not(target_env = "newlib"),
            any(target_os = "linux", target_os = "android", target_os = "emscripten")
        )
    ))]
    pub const CLOCK_PROCESS_CPUTIME_ID: ClockId = ClockId(libc::CLOCK_PROCESS_CPUTIME_ID);
    #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
    pub const CLOCK_PROF: ClockId = ClockId(libc::CLOCK_PROF);
    pub const CLOCK_REALTIME: ClockId = ClockId(libc::CLOCK_REALTIME);
    #[cfg(any(
        target_os = "fuchsia",
        all(
            not(any(target_env = "uclibc", target_env = "newlib")),
            any(target_os = "linux", target_os = "android", target_os = "emscripten")
        )
    ))]
    pub const CLOCK_REALTIME_ALARM: ClockId = ClockId(libc::CLOCK_REALTIME_ALARM);
    #[cfg(any(
        target_os = "fuchsia",
        all(
            not(any(target_env = "uclibc", target_env = "newlib")),
            any(target_os = "linux", target_os = "android", target_os = "emscripten")
        )
    ))]
    pub const CLOCK_REALTIME_COARSE: ClockId = ClockId(libc::CLOCK_REALTIME_COARSE);
    #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
    pub const CLOCK_REALTIME_FAST: ClockId = ClockId(libc::CLOCK_REALTIME_FAST);
    #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
    pub const CLOCK_REALTIME_PRECISE: ClockId = ClockId(libc::CLOCK_REALTIME_PRECISE);
    #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
    pub const CLOCK_SECOND: ClockId = ClockId(libc::CLOCK_SECOND);
    #[cfg(any(
        target_os = "fuchsia",
        all(
            not(any(target_env = "uclibc", target_env = "newlib")),
            any(
                target_os = "emscripten",
                all(target_os = "linux", target_env = "musl")
            )
        )
    ))]
    pub const CLOCK_SGI_CYCLE: ClockId = ClockId(libc::CLOCK_SGI_CYCLE);
    #[cfg(any(
        target_os = "fuchsia",
        all(
            not(any(target_env = "uclibc", target_env = "newlib")),
            any(
                target_os = "emscripten",
                all(target_os = "linux", target_env = "musl")
            )
        )
    ))]
    pub const CLOCK_TAI: ClockId = ClockId(libc::CLOCK_TAI);
    #[cfg(any(
        target_env = "uclibc",
        target_os = "fuchsia",
        target_os = "ios",
        target_os = "macos",
        target_os = "freebsd",
        target_os = "dragonfly",
        all(
            not(target_env = "newlib"),
            any(target_os = "linux", target_os = "android", target_os = "emscripten",),
        ),
    ))]
    pub const CLOCK_THREAD_CPUTIME_ID: ClockId = ClockId(libc::CLOCK_THREAD_CPUTIME_ID);
    #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
    pub const CLOCK_UPTIME: ClockId = ClockId(libc::CLOCK_UPTIME);
    #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
    pub const CLOCK_UPTIME_FAST: ClockId = ClockId(libc::CLOCK_UPTIME_FAST);
    #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
    pub const CLOCK_UPTIME_PRECISE: ClockId = ClockId(libc::CLOCK_UPTIME_PRECISE);
    #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
    pub const CLOCK_VIRTUAL: ClockId = ClockId(libc::CLOCK_VIRTUAL);
}

impl Into<clockid_t> for ClockId {
    fn into(self) -> clockid_t {
        self.as_raw()
    }
}

impl From<clockid_t> for ClockId {
    fn from(clk_id: clockid_t) -> Self {
        ClockId::from_raw(clk_id)
    }
}

impl std::fmt::Display for ClockId {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        std::fmt::Display::fmt(&self.0, f)
    }
}

/// Get the resolution of the specified clock, (see
/// [clock_getres(2)](https://pubs.opengroup.org/onlinepubs/7908799/xsh/clock_getres.html)).
pub fn clock_getres(clock_id: ClockId) -> Result<TimeSpec> {
    let mut c_time: MaybeUninit<libc::timespec> = MaybeUninit::uninit();
    let ret = unsafe { libc::clock_getres(clock_id.as_raw(), c_time.as_mut_ptr()) };
    Errno::result(ret)?;
    let res = unsafe { c_time.assume_init() };
    Ok(TimeSpec::from(res))
}

/// Get the time of the specified clock, (see
/// [clock_gettime(2)](https://pubs.opengroup.org/onlinepubs/7908799/xsh/clock_gettime.html)).
pub fn clock_gettime(clock_id: ClockId) -> Result<TimeSpec> {
    let mut c_time: MaybeUninit<libc::timespec> = MaybeUninit::uninit();
    let ret = unsafe { libc::clock_gettime(clock_id.as_raw(), c_time.as_mut_ptr()) };
    Errno::result(ret)?;
    let res = unsafe { c_time.assume_init() };
    Ok(TimeSpec::from(res))
}

/// Set the time of the specified clock, (see
/// [clock_settime(2)](https://pubs.opengroup.org/onlinepubs/7908799/xsh/clock_settime.html)).
#[cfg(not(any(
    target_os = "macos",
    target_os = "ios",
    all(
        not(any(target_env = "uclibc", target_env = "newlibc")),
        any(target_os = "redox", target_os = "hermit",),
    ),
)))]
pub fn clock_settime(clock_id: ClockId, timespec: TimeSpec) -> Result<()> {
    let ret = unsafe { libc::clock_settime(clock_id.as_raw(), timespec.as_ref()) };
    Errno::result(ret).map(drop)
}

/// Get the clock id of the specified process id, (see
/// [clock_getcpuclockid(3)](https://pubs.opengroup.org/onlinepubs/009695399/functions/clock_getcpuclockid.html)).
#[cfg(any(
    target_os = "freebsd",
    target_os = "dragonfly",
    target_os = "linux",
    target_os = "android",
    target_os = "emscripten",
))]
pub fn clock_getcpuclockid(pid: Pid) -> Result<ClockId> {
    let mut clk_id: MaybeUninit<libc::clockid_t> = MaybeUninit::uninit();
    let ret = unsafe { libc::clock_getcpuclockid(pid.into(), clk_id.as_mut_ptr()) };
    if ret == 0 {
        let res = unsafe { clk_id.assume_init() };
        Ok(ClockId::from(res))
    } else {
        Err(Error::Sys(Errno::from_i32(ret)))
    }
}
