//! Timer API via file descriptors.
//!
//! Timer FD is a Linux-only API to create timers and get expiration
//! notifications through file descriptors.
//!
//! For more documentation, please read [timerfd_create(2)](https://man7.org/linux/man-pages/man2/timerfd_create.2.html).
//!
//! # Examples
//!
//! Create a new one-shot timer that expires after 1 second.
//! ```
//! # use std::os::unix::io::AsRawFd;
//! # use nix::sys::timerfd::{TimerFd, ClockId, TimerFlags, TimerSetTimeFlags,
//! #    Expiration};
//! # use nix::sys::time::{TimeSpec, TimeValLike};
//! # use nix::unistd::read;
//! #
//! // We create a new monotonic timer.
//! let timer = TimerFd::new(ClockId::CLOCK_MONOTONIC, TimerFlags::empty())
//!     .unwrap();
//!
//! // We set a new one-shot timer in 1 seconds.
//! timer.set(
//!     Expiration::OneShot(TimeSpec::seconds(1)),
//!     TimerSetTimeFlags::empty()
//! ).unwrap();
//!
//! // We wait for the timer to expire.
//! timer.wait().unwrap();
//! ```
use crate::sys::time::timer::TimerSpec;
pub use crate::sys::time::timer::{Expiration, TimerSetTimeFlags};
use crate::unistd::read;
use crate::{errno::Errno, Result};
use libc::c_int;
use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};

/// A timerfd instance. This is also a file descriptor, you can feed it to
/// other interfaces taking file descriptors as arguments, [`epoll`] for example.
///
/// [`epoll`]: crate::sys::epoll
#[derive(Debug)]
pub struct TimerFd {
    fd: OwnedFd,
}

impl AsFd for TimerFd {
    fn as_fd(&self) -> BorrowedFd<'_> {
        self.fd.as_fd()
    }
}

impl FromRawFd for TimerFd {
    unsafe fn from_raw_fd(fd: RawFd) -> Self {
        TimerFd {
            fd: OwnedFd::from_raw_fd(fd),
        }
    }
}

libc_enum! {
    /// The type of the clock used to mark the progress of the timer. For more
    /// details on each kind of clock, please refer to [timerfd_create(2)](https://man7.org/linux/man-pages/man2/timerfd_create.2.html).
    #[repr(i32)]
    #[non_exhaustive]
    pub enum ClockId {
        /// A settable system-wide real-time clock.
        CLOCK_REALTIME,
        /// A non-settable monotonically increasing clock.
        ///
        /// Does not change after system startup.
        /// Does not measure time while the system is suspended.
        CLOCK_MONOTONIC,
        /// Like `CLOCK_MONOTONIC`, except that `CLOCK_BOOTTIME` includes the time
        /// that the system was suspended.
        CLOCK_BOOTTIME,
        /// Like `CLOCK_REALTIME`, but will wake the system if it is suspended.
        CLOCK_REALTIME_ALARM,
        /// Like `CLOCK_BOOTTIME`, but will wake the system if it is suspended.
        CLOCK_BOOTTIME_ALARM,
    }
}

libc_bitflags! {
    /// Additional flags to change the behaviour of the file descriptor at the
    /// time of creation.
    pub struct TimerFlags: c_int {
        /// Set the `O_NONBLOCK` flag on the open file description referred to by the new file descriptor.
        TFD_NONBLOCK;
        /// Set the `FD_CLOEXEC` flag on the file descriptor.
        TFD_CLOEXEC;
    }
}

impl TimerFd {
    /// Creates a new timer based on the clock defined by `clockid`. The
    /// underlying fd can be assigned specific flags with `flags` (CLOEXEC,
    /// NONBLOCK). The underlying fd will be closed on drop.
    #[doc(alias("timerfd_create"))]
    pub fn new(clockid: ClockId, flags: TimerFlags) -> Result<Self> {
        Errno::result(unsafe {
            libc::timerfd_create(clockid as i32, flags.bits())
        })
        .map(|fd| Self {
            fd: unsafe { OwnedFd::from_raw_fd(fd) },
        })
    }

    /// Sets a new alarm on the timer.
    ///
    /// # Types of alarm
    ///
    /// There are 3 types of alarms you can set:
    ///
    ///   - one shot: the alarm will trigger once after the specified amount of
    /// time.
    ///     Example: I want an alarm to go off in 60s and then disable itself.
    ///
    ///   - interval: the alarm will trigger every specified interval of time.
    ///     Example: I want an alarm to go off every 60s. The alarm will first
    ///     go off 60s after I set it and every 60s after that. The alarm will
    ///     not disable itself.
    ///
    ///   - interval delayed: the alarm will trigger after a certain amount of
    ///     time and then trigger at a specified interval.
    ///     Example: I want an alarm to go off every 60s but only start in 1h.
    ///     The alarm will first trigger 1h after I set it and then every 60s
    ///     after that. The alarm will not disable itself.
    ///
    /// # Relative vs absolute alarm
    ///
    /// If you do not set any `TimerSetTimeFlags`, then the `TimeSpec` you pass
    /// to the `Expiration` you want is relative. If however you want an alarm
    /// to go off at a certain point in time, you can set `TFD_TIMER_ABSTIME`.
    /// Then the one shot TimeSpec and the delay TimeSpec of the delayed
    /// interval are going to be interpreted as absolute.
    ///
    /// # Cancel on a clock change
    ///
    /// If you set a `TFD_TIMER_CANCEL_ON_SET` alongside `TFD_TIMER_ABSTIME`
    /// and the clock for this timer is `CLOCK_REALTIME` or `CLOCK_REALTIME_ALARM`,
    /// then this timer is marked as cancelable if the real-time clock undergoes
    /// a discontinuous change.
    ///
    /// # Disabling alarms
    ///
    /// Note: Only one alarm can be set for any given timer. Setting a new alarm
    /// actually removes the previous one.
    ///
    /// Note: Setting a one shot alarm with a 0s TimeSpec disables the alarm
    /// altogether.
    #[doc(alias("timerfd_settime"))]
    pub fn set(
        &self,
        expiration: Expiration,
        flags: TimerSetTimeFlags,
    ) -> Result<()> {
        let timerspec: TimerSpec = expiration.into();
        Errno::result(unsafe {
            libc::timerfd_settime(
                self.fd.as_fd().as_raw_fd(),
                flags.bits(),
                timerspec.as_ref(),
                std::ptr::null_mut(),
            )
        })
        .map(drop)
    }

    /// Get the parameters for the alarm currently set, if any.
    #[doc(alias("timerfd_gettime"))]
    pub fn get(&self) -> Result<Option<Expiration>> {
        let mut timerspec = TimerSpec::none();
        Errno::result(unsafe {
            libc::timerfd_gettime(
                self.fd.as_fd().as_raw_fd(),
                timerspec.as_mut(),
            )
        })
        .map(|_| {
            if timerspec.as_ref().it_interval.tv_sec == 0
                && timerspec.as_ref().it_interval.tv_nsec == 0
                && timerspec.as_ref().it_value.tv_sec == 0
                && timerspec.as_ref().it_value.tv_nsec == 0
            {
                None
            } else {
                Some(timerspec.into())
            }
        })
    }

    /// Remove the alarm if any is set.
    #[doc(alias("timerfd_settime"))]
    pub fn unset(&self) -> Result<()> {
        Errno::result(unsafe {
            libc::timerfd_settime(
                self.fd.as_fd().as_raw_fd(),
                TimerSetTimeFlags::empty().bits(),
                TimerSpec::none().as_ref(),
                std::ptr::null_mut(),
            )
        })
        .map(drop)
    }

    /// Wait for the configured alarm to expire.
    ///
    /// Note: If the alarm is unset, then you will wait forever.
    pub fn wait(&self) -> Result<()> {
        while let Err(e) = read(self.fd.as_fd().as_raw_fd(), &mut [0u8; 8]) {
            if e == Errno::ECANCELED {
                break;
            }
            if e != Errno::EINTR {
                return Err(e);
            }
        }

        Ok(())
    }
}
