//! Types for working with processes.
//!
//! See also [`Ruby`](Ruby#process) for functions for working with processes.

#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
#[cfg(windows)]
use std::os::windows::process::ExitStatusExt;
use std::{num::NonZeroU32, os::raw::c_int, process::ExitStatus, ptr::null};

use rb_sys::{rb_sys_fail, rb_waitpid};

use crate::{
    api::Ruby,
    error::{protect, Error},
};

/// # Process
///
/// Functions for working with processes.
impl Ruby {
    /// Wait for a process.
    ///
    /// This function releases Ruby's Global VM Lock (GVL), so while it will
    /// block the current thread, other Ruby threads can be scheduled.
    ///
    /// Returns the Process ID (PID) of the process waited, and its exit status.
    ///
    /// If the `NOHANG` flag is passed this function will not block, instead it
    /// will clean up an exited child process if there is one, or returns
    /// `None` if there is no exited child process.
    ///
    /// If the `UNTRACED` flag is passed, this function will also return
    /// stopped processes (e.g. that can be resumed), not only exited processes.
    /// For these stopped processes the exit status will be reported as
    /// successful, although they have not yet exited.
    ///
    /// # Examples
    ///
    /// ```
    /// use std::process::Command;
    ///
    /// use magnus::{process::WaitTarget, Error, Ruby};
    ///
    /// fn example(ruby: &Ruby) -> Result<(), Error> {
    ///     let child = Command::new("ls").spawn().unwrap();
    ///     let (pid, status) = ruby
    ///         .waitpid(WaitTarget::ChildPid(child.id()), Default::default())?
    ///         .unwrap();
    ///     assert_eq!(child.id(), pid.get());
    ///     assert!(status.success());
    ///
    ///     Ok(())
    /// }
    /// # #[cfg(unix)]
    /// # Ruby::init(example).unwrap()
    /// ```
    pub fn waitpid(
        &self,
        pid: WaitTarget,
        flags: Flags,
    ) -> Result<Option<(NonZeroU32, ExitStatus)>, Error> {
        let mut out_pid = 0;
        let mut status: c_int = 0;
        protect(|| unsafe {
            out_pid = rb_waitpid(pid.to_i32() as _, &mut status as *mut c_int, flags.0);
            if out_pid < 0 {
                rb_sys_fail(null());
            }
            self.qnil()
        })?;
        Ok(NonZeroU32::new(out_pid as u32).map(|pid| (pid, ExitStatus::from_raw(status as _))))
    }
}

#[cfg(not(any(target_os = "solaris", target_os = "illumos")))]
const WNOHANG: c_int = 0x00000001;
#[cfg(any(target_os = "solaris", target_os = "illumos"))]
const WNOHANG: c_int = 0x40;

#[cfg(not(any(target_os = "solaris", target_os = "illumos")))]
const WUNTRACED: c_int = 0x00000002;
#[cfg(any(target_os = "solaris", target_os = "illumos"))]
const WUNTRACED: c_int = 0x04;

/// Argument type for [`Ruby::waitpid`].
#[derive(Clone, Copy)]
pub enum WaitTarget {
    /// Wait for the given child process
    ChildPid(u32),
    /// Wait for any child process with the process group of the current
    /// process.
    ProcessGroup, // 0
    /// Wait for any child process.
    AnyChild, // -1
    /// Wait for any child process with the given process group.
    ChildProcessGroup(u32), // negative
}

impl WaitTarget {
    fn to_i32(self) -> i32 {
        match self {
            Self::ChildPid(pid) => pid as i32,
            Self::ProcessGroup => 0,
            Self::AnyChild => -1,
            Self::ChildProcessGroup(pid) => -(pid as i32),
        }
    }
}

impl Default for WaitTarget {
    fn default() -> Self {
        Self::AnyChild
    }
}

/// Argument type for [`Ruby::waitpid`].
#[derive(Clone, Copy)]
pub struct Flags(c_int);

impl Flags {
    /// An instance of `Flags` with only `NOHANG` set.
    pub const NOHANG: Self = Self::new().nohang();
    /// An instance of `Flags` with only `UNTRACED` set.
    pub const UNTRACED: Self = Self::new().untraced();

    /// Create a new `Flags` with no flags set.
    pub const fn new() -> Self {
        Self(0)
    }

    /// Set the `NOHANG` flag.
    pub const fn nohang(self) -> Self {
        Self(self.0 | WNOHANG)
    }

    /// Set the `UNTRACED` flag.
    pub const fn untraced(self) -> Self {
        Self(self.0 | WUNTRACED)
    }
}

impl Default for Flags {
    fn default() -> Self {
        Self::new()
    }
}
