//! Kernel event notification mechanism
//!
//! # See Also
//! [kqueue(2)](https://www.freebsd.org/cgi/man.cgi?query=kqueue)

use crate::{Errno, Result};
#[cfg(not(target_os = "netbsd"))]
use libc::{c_int, c_long, intptr_t, time_t, timespec, uintptr_t};
#[cfg(target_os = "netbsd")]
use libc::{c_long, intptr_t, size_t, time_t, timespec, uintptr_t};
use std::convert::TryInto;
use std::mem;
use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd};
use std::ptr;

/// A kernel event queue.  Used to notify a process of various asynchronous
/// events.
#[repr(C)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct KEvent {
    kevent: libc::kevent,
}

/// A kernel event queue.
///
/// Used by the kernel to notify the process of various types of asynchronous
/// events.
#[repr(transparent)]
#[derive(Debug)]
pub struct Kqueue(OwnedFd);

impl Kqueue {
    /// Create a new kernel event queue.
    pub fn new() -> Result<Self> {
        let res = unsafe { libc::kqueue() };

        Errno::result(res).map(|fd| unsafe { Self(OwnedFd::from_raw_fd(fd)) })
    }

    /// Register new events with the kqueue, and return any pending events to
    /// the user.
    ///
    /// This method will block until either the timeout expires, or a registered
    /// event triggers a notification.
    ///
    /// # Arguments
    /// - `changelist` - Any new kevents to register for notifications.
    /// - `eventlist` - Storage space for the kernel to return notifications.
    /// - `timeout` - An optional timeout.
    ///
    /// # Returns
    /// Returns the number of events placed in the `eventlist`.  If an error
    /// occurs while processing an element of the `changelist` and there is
    /// enough room in the `eventlist`, then the event will be placed in the
    /// `eventlist` with `EV_ERROR` set in `flags` and the system error in
    /// `data`.
    pub fn kevent(
        &self,
        changelist: &[KEvent],
        eventlist: &mut [KEvent],
        timeout_opt: Option<timespec>,
    ) -> Result<usize> {
        let res = unsafe {
            libc::kevent(
                self.0.as_raw_fd(),
                changelist.as_ptr() as *const libc::kevent,
                changelist.len() as type_of_nchanges,
                eventlist.as_mut_ptr() as *mut libc::kevent,
                eventlist.len() as type_of_nchanges,
                if let Some(ref timeout) = timeout_opt {
                    timeout as *const timespec
                } else {
                    ptr::null()
                },
            )
        };
        Errno::result(res).map(|r| r as usize)
    }
}

#[cfg(any(
    target_os = "dragonfly",
    target_os = "freebsd",
    target_os = "ios",
    target_os = "macos",
    target_os = "openbsd"
))]
type type_of_udata = *mut libc::c_void;
#[cfg(target_os = "netbsd")]
type type_of_udata = intptr_t;

#[cfg(target_os = "netbsd")]
type type_of_event_filter = u32;
#[cfg(not(target_os = "netbsd"))]
type type_of_event_filter = i16;
libc_enum! {
    #[cfg_attr(target_os = "netbsd", repr(u32))]
    #[cfg_attr(not(target_os = "netbsd"), repr(i16))]
    #[non_exhaustive]
    /// Kqueue filter types.  These are all the different types of event that a
    /// kqueue can notify for.
    pub enum EventFilter {
        /// Notifies on the completion of a POSIX AIO operation.
        EVFILT_AIO,
        #[cfg(target_os = "freebsd")]
        /// Returns whenever there is no remaining data in the write buffer
        EVFILT_EMPTY,
        #[cfg(target_os = "dragonfly")]
        /// Takes a descriptor as the identifier, and returns whenever one of
        /// the specified exceptional conditions has occurred on the descriptor.
        EVFILT_EXCEPT,
        #[cfg(any(target_os = "dragonfly",
                  target_os = "freebsd",
                  target_os = "ios",
                  target_os = "macos"))]
        /// Establishes a file system monitor.
        EVFILT_FS,
        #[cfg(target_os = "freebsd")]
        /// Notify for completion of a list of POSIX AIO operations.
        /// # See Also
        /// [lio_listio(2)](https://www.freebsd.org/cgi/man.cgi?query=lio_listio)
        EVFILT_LIO,
        #[cfg(any(target_os = "ios", target_os = "macos"))]
        /// Mach portsets
        EVFILT_MACHPORT,
        /// Notifies when a process performs one or more of the requested
        /// events.
        EVFILT_PROC,
        /// Returns events associated with the process referenced by a given
        /// process descriptor, created by `pdfork()`. The events to monitor are:
        ///
        /// - NOTE_EXIT: the process has exited. The exit status will be stored in data.
        #[cfg(target_os = "freebsd")]
        EVFILT_PROCDESC,
        /// Takes a file descriptor as the identifier, and notifies whenever
        /// there is data available to read.
        EVFILT_READ,
        #[cfg(target_os = "freebsd")]
        #[doc(hidden)]
        #[deprecated(since = "0.27.0", note = "Never fully implemented by the OS")]
        EVFILT_SENDFILE,
        /// Takes a signal number to monitor as the identifier and notifies when
        /// the given signal is delivered to the process.
        EVFILT_SIGNAL,
        /// Establishes a timer and notifies when the timer expires.
        EVFILT_TIMER,
        #[cfg(any(target_os = "dragonfly",
                  target_os = "freebsd",
                  target_os = "ios",
                  target_os = "macos"))]
        /// Notifies only when explicitly requested by the user.
        EVFILT_USER,
        #[cfg(any(target_os = "ios", target_os = "macos"))]
        /// Virtual memory events
        EVFILT_VM,
        /// Notifies when a requested event happens on a specified file.
        EVFILT_VNODE,
        /// Takes a file descriptor as the identifier, and notifies whenever
        /// it is possible to write to the file without blocking.
        EVFILT_WRITE,
    }
    impl TryFrom<type_of_event_filter>
}

#[cfg(any(
    target_os = "dragonfly",
    target_os = "freebsd",
    target_os = "ios",
    target_os = "macos",
    target_os = "openbsd"
))]
#[doc(hidden)]
pub type type_of_event_flag = u16;
#[cfg(target_os = "netbsd")]
#[doc(hidden)]
pub type type_of_event_flag = u32;
libc_bitflags! {
    /// Event flags.  See the man page for details.
    // There's no useful documentation we can write for the individual flags
    // that wouldn't simply be repeating the man page.
    pub struct EventFlag: type_of_event_flag {
        #[allow(missing_docs)]
        EV_ADD;
        #[allow(missing_docs)]
        EV_CLEAR;
        #[allow(missing_docs)]
        EV_DELETE;
        #[allow(missing_docs)]
        EV_DISABLE;
        #[cfg(any(target_os = "dragonfly", target_os = "freebsd",
                  target_os = "ios", target_os = "macos",
                  target_os = "netbsd", target_os = "openbsd"))]
        #[allow(missing_docs)]
        EV_DISPATCH;
        #[cfg(target_os = "freebsd")]
        #[allow(missing_docs)]
        EV_DROP;
        #[allow(missing_docs)]
        EV_ENABLE;
        #[allow(missing_docs)]
        EV_EOF;
        #[allow(missing_docs)]
        EV_ERROR;
        #[cfg(any(target_os = "macos", target_os = "ios"))]
        #[allow(missing_docs)]
        EV_FLAG0;
        #[allow(missing_docs)]
        EV_FLAG1;
        #[cfg(target_os = "dragonfly")]
        #[allow(missing_docs)]
        EV_NODATA;
        #[allow(missing_docs)]
        EV_ONESHOT;
        #[cfg(any(target_os = "macos", target_os = "ios"))]
        #[allow(missing_docs)]
        EV_OOBAND;
        #[cfg(any(target_os = "macos", target_os = "ios"))]
        #[allow(missing_docs)]
        EV_POLL;
        #[cfg(any(target_os = "dragonfly", target_os = "freebsd",
                  target_os = "ios", target_os = "macos",
                  target_os = "netbsd", target_os = "openbsd"))]
        #[allow(missing_docs)]
        EV_RECEIPT;
    }
}

libc_bitflags!(
    /// Filter-specific flags.  See the man page for details.
    // There's no useful documentation we can write for the individual flags
    // that wouldn't simply be repeating the man page.
    #[allow(missing_docs)]
    pub struct FilterFlag: u32 {
        #[cfg(any(target_os = "macos", target_os = "ios"))]
        #[allow(missing_docs)]
        NOTE_ABSOLUTE;
        #[allow(missing_docs)]
        NOTE_ATTRIB;
        #[allow(missing_docs)]
        NOTE_CHILD;
        #[allow(missing_docs)]
        NOTE_DELETE;
        #[cfg(target_os = "openbsd")]
        #[allow(missing_docs)]
        NOTE_EOF;
        #[allow(missing_docs)]
        NOTE_EXEC;
        #[allow(missing_docs)]
        NOTE_EXIT;
        #[cfg(any(target_os = "macos", target_os = "ios"))]
        #[allow(missing_docs)]
        NOTE_EXITSTATUS;
        #[allow(missing_docs)]
        NOTE_EXTEND;
        #[cfg(any(target_os = "macos",
                  target_os = "ios",
                  target_os = "freebsd",
                  target_os = "dragonfly"))]
        #[allow(missing_docs)]
        NOTE_FFAND;
        #[cfg(any(target_os = "macos",
                  target_os = "ios",
                  target_os = "freebsd",
                  target_os = "dragonfly"))]
        #[allow(missing_docs)]
        NOTE_FFCOPY;
        #[cfg(any(target_os = "macos",
                  target_os = "ios",
                  target_os = "freebsd",
                  target_os = "dragonfly"))]
        #[allow(missing_docs)]
        NOTE_FFCTRLMASK;
        #[cfg(any(target_os = "macos",
                  target_os = "ios",
                  target_os = "freebsd",
                  target_os = "dragonfly"))]
        #[allow(missing_docs)]
        NOTE_FFLAGSMASK;
        #[cfg(any(target_os = "macos",
                  target_os = "ios",
                  target_os = "freebsd",
                  target_os = "dragonfly"))]
        #[allow(missing_docs)]
        NOTE_FFNOP;
        #[cfg(any(target_os = "macos",
                  target_os = "ios",
                  target_os = "freebsd",
                  target_os = "dragonfly"))]
        #[allow(missing_docs)]
        NOTE_FFOR;
        #[allow(missing_docs)]
        NOTE_FORK;
        #[allow(missing_docs)]
        NOTE_LINK;
        #[allow(missing_docs)]
        NOTE_LOWAT;
        #[cfg(target_os = "freebsd")]
        #[allow(missing_docs)]
        NOTE_MSECONDS;
        #[cfg(any(target_os = "macos", target_os = "ios"))]
        #[allow(missing_docs)]
        NOTE_NONE;
        #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
        #[allow(missing_docs)]
        NOTE_NSECONDS;
        #[cfg(target_os = "dragonfly")]
        #[allow(missing_docs)]
        NOTE_OOB;
        #[allow(missing_docs)]
        NOTE_PCTRLMASK;
        #[allow(missing_docs)]
        NOTE_PDATAMASK;
        #[allow(missing_docs)]
        NOTE_RENAME;
        #[allow(missing_docs)]
        NOTE_REVOKE;
        #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
        #[allow(missing_docs)]
        NOTE_SECONDS;
        #[cfg(any(target_os = "macos", target_os = "ios"))]
        #[allow(missing_docs)]
        NOTE_SIGNAL;
        #[allow(missing_docs)]
        NOTE_TRACK;
        #[allow(missing_docs)]
        NOTE_TRACKERR;
        #[cfg(any(target_os = "macos",
                  target_os = "ios",
                  target_os = "freebsd",
                  target_os = "dragonfly"))]
        #[allow(missing_docs)]
        NOTE_TRIGGER;
        #[cfg(target_os = "openbsd")]
        #[allow(missing_docs)]
        NOTE_TRUNCATE;
        #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
        #[allow(missing_docs)]
        NOTE_USECONDS;
        #[cfg(any(target_os = "macos", target_os = "ios"))]
        #[allow(missing_docs)]
        NOTE_VM_ERROR;
        #[cfg(any(target_os = "macos", target_os = "ios"))]
        #[allow(missing_docs)]
        NOTE_VM_PRESSURE;
        #[cfg(any(target_os = "macos", target_os = "ios"))]
        #[allow(missing_docs)]
        NOTE_VM_PRESSURE_SUDDEN_TERMINATE;
        #[cfg(any(target_os = "macos", target_os = "ios"))]
        #[allow(missing_docs)]
        NOTE_VM_PRESSURE_TERMINATE;
        #[allow(missing_docs)]
        NOTE_WRITE;
    }
);

#[allow(missing_docs)]
#[deprecated(since = "0.27.0", note = "Use KEvent::new instead")]
pub fn kqueue() -> Result<Kqueue> {
    Kqueue::new()
}

// KEvent can't derive Send because on some operating systems, udata is defined
// as a void*.  However, KEvent's public API always treats udata as an intptr_t,
// which is safe to Send.
unsafe impl Send for KEvent {}

impl KEvent {
    #[allow(clippy::needless_update)] // Not needless on all platforms.
    /// Construct a new `KEvent` suitable for submission to the kernel via the
    /// `changelist` argument of [`Kqueue::kevent`].
    pub fn new(
        ident: uintptr_t,
        filter: EventFilter,
        flags: EventFlag,
        fflags: FilterFlag,
        data: intptr_t,
        udata: intptr_t,
    ) -> KEvent {
        KEvent {
            kevent: libc::kevent {
                ident,
                filter: filter as type_of_event_filter,
                flags: flags.bits(),
                fflags: fflags.bits(),
                // data can be either i64 or intptr_t, depending on platform
                data: data as _,
                udata: udata as type_of_udata,
                ..unsafe { mem::zeroed() }
            },
        }
    }

    /// Value used to identify this event.  The exact interpretation is
    /// determined by the attached filter, but often is a raw file descriptor.
    pub fn ident(&self) -> uintptr_t {
        self.kevent.ident
    }

    /// Identifies the kernel filter used to process this event.
    ///
    /// Will only return an error if the kernel reports an event via a filter
    /// that is unknown to Nix.
    pub fn filter(&self) -> Result<EventFilter> {
        self.kevent.filter.try_into()
    }

    /// Flags control what the kernel will do when this event is added with
    /// [`Kqueue::kevent`].
    pub fn flags(&self) -> EventFlag {
        EventFlag::from_bits(self.kevent.flags).unwrap()
    }

    /// Filter-specific flags.
    pub fn fflags(&self) -> FilterFlag {
        FilterFlag::from_bits(self.kevent.fflags).unwrap()
    }

    /// Filter-specific data value.
    pub fn data(&self) -> intptr_t {
        self.kevent.data as intptr_t
    }

    /// Opaque user-defined value passed through the kernel unchanged.
    pub fn udata(&self) -> intptr_t {
        self.kevent.udata as intptr_t
    }
}

#[allow(missing_docs)]
#[deprecated(since = "0.27.0", note = "Use Kqueue::kevent instead")]
pub fn kevent(
    kq: &Kqueue,
    changelist: &[KEvent],
    eventlist: &mut [KEvent],
    timeout_ms: usize,
) -> Result<usize> {
    // Convert ms to timespec
    let timeout = timespec {
        tv_sec: (timeout_ms / 1000) as time_t,
        tv_nsec: ((timeout_ms % 1000) * 1_000_000) as c_long,
    };

    kq.kevent(changelist, eventlist, Some(timeout))
}

#[cfg(any(
    target_os = "macos",
    target_os = "ios",
    target_os = "freebsd",
    target_os = "dragonfly",
    target_os = "openbsd"
))]
type type_of_nchanges = c_int;
#[cfg(target_os = "netbsd")]
type type_of_nchanges = size_t;

#[allow(missing_docs)]
#[deprecated(since = "0.27.0", note = "Use Kqueue::kevent instead")]
pub fn kevent_ts(
    kq: &Kqueue,
    changelist: &[KEvent],
    eventlist: &mut [KEvent],
    timeout_opt: Option<timespec>,
) -> Result<usize> {
    kq.kevent(changelist, eventlist, timeout_opt)
}

/// Modify an existing [`KEvent`].
// Probably should deprecate.  Would anybody ever use it over `KEvent::new`?
#[deprecated(since = "0.27.0", note = "Use Kqueue::kevent instead")]
#[inline]
pub fn ev_set(
    ev: &mut KEvent,
    ident: usize,
    filter: EventFilter,
    flags: EventFlag,
    fflags: FilterFlag,
    udata: intptr_t,
) {
    ev.kevent.ident = ident as uintptr_t;
    ev.kevent.filter = filter as type_of_event_filter;
    ev.kevent.flags = flags.bits();
    ev.kevent.fflags = fflags.bits();
    ev.kevent.data = 0;
    ev.kevent.udata = udata as type_of_udata;
}

#[test]
fn test_struct_kevent() {
    use std::mem;

    let udata: intptr_t = 12345;

    let actual = KEvent::new(
        0xdead_beef,
        EventFilter::EVFILT_READ,
        EventFlag::EV_ONESHOT | EventFlag::EV_ADD,
        FilterFlag::NOTE_CHILD | FilterFlag::NOTE_EXIT,
        0x1337,
        udata,
    );
    assert_eq!(0xdead_beef, actual.ident());
    let filter = actual.kevent.filter;
    assert_eq!(libc::EVFILT_READ, filter);
    assert_eq!(libc::EV_ONESHOT | libc::EV_ADD, actual.flags().bits());
    assert_eq!(libc::NOTE_CHILD | libc::NOTE_EXIT, actual.fflags().bits());
    assert_eq!(0x1337, actual.data());
    assert_eq!(udata as type_of_udata, actual.udata() as type_of_udata);
    assert_eq!(mem::size_of::<libc::kevent>(), mem::size_of::<KEvent>());
}

#[test]
fn test_kevent_filter() {
    let udata: intptr_t = 12345;

    let actual = KEvent::new(
        0xdead_beef,
        EventFilter::EVFILT_READ,
        EventFlag::EV_ONESHOT | EventFlag::EV_ADD,
        FilterFlag::NOTE_CHILD | FilterFlag::NOTE_EXIT,
        0x1337,
        udata,
    );
    assert_eq!(EventFilter::EVFILT_READ, actual.filter().unwrap());
}
