use std::io;

use nix::{
    errno::{Errno, Errno::EINTR},
    libc::pid_t,
    sys::{
        signal::{kill, Signal, SIGKILL},
        wait::waitpid,
    },
    unistd::Pid,
};

use crate::unshare::{Child, ExitStatus};

impl Child {
    /// Returns pid of the process (a mirror of std method)
    #[allow(clippy::cast_sign_loss)]
    pub fn id(&self) -> u32 {
        self.pid as u32
    }

    /// Returns pid of process with correct `pid_t` type
    pub fn pid(&self) -> pid_t {
        self.pid
    }

    /// Synchronously wait for child to complete and return exit status
    pub fn wait(&mut self) -> Result<ExitStatus, io::Error> {
        if let Some(x) = self.status {
            return Ok(x);
        }
        let status = self._wait()?;
        self.status = Some(status);
        Ok(status)
    }

    fn _wait(&mut self) -> Result<ExitStatus, io::Error> {
        use nix::sys::wait::WaitStatus::*;
        loop {
            match waitpid(Some(Pid::from_raw(self.pid)), None) {
                Ok(PtraceEvent(..)) => {}
                Ok(PtraceSyscall(..)) => {}
                Ok(Exited(x, status)) => {
                    assert!(i32::from(x) == self.pid);
                    #[allow(clippy::cast_possible_truncation)]
                    return Ok(ExitStatus::Exited(status as i8));
                }
                Ok(Signaled(x, sig, core)) => {
                    assert!(i32::from(x) == self.pid);
                    return Ok(ExitStatus::Signaled(sig, core));
                }
                Ok(Stopped(_, _)) => unreachable!(),
                Ok(Continued(_)) => unreachable!(),
                Ok(StillAlive) => unreachable!(),
                Err(EINTR) => continue,
                Err(errno) => return Err(io::Error::from_raw_os_error(errno as i32)),
            }
        }
    }

    /// Send arbitrary unix signal to the process
    pub fn signal(&self, signal: Signal) -> Result<(), Errno> {
        // This prevents (somewhat not-reliable) killing some other process
        // with same pid.
        if self.status.is_none() {
            kill(Pid::from_raw(self.pid), signal)
        } else {
            // Invalid argument: Can't kill an exited process.
            Err(Errno::ESRCH)
        }
    }

    /// Kill process with SIGKILL signal
    pub fn kill(&self) -> Result<(), Errno> {
        self.signal(SIGKILL)
    }
}
