use std::io;
use std::marker::PhantomData;
pub(super) use std::os::fd::OwnedFd;
use std::os::raw::c_int;
use std::process::Child;
use std::time::Duration;

#[cfg(all(target_env = "gnu", target_os = "linux"))]
use libc::__rlimit_resource_t;

use super::WaitResult;

macro_rules! if_waitid {
    ( $($item:item)+ ) => {
    $(
        #[cfg(process_control_unix_waitid)]
        $item
    )+
    };
}

mod exit_status;
pub(super) use exit_status::ExitStatus;

mod read;
pub(super) use read::read2;

mod wait;

macro_rules! if_memory_limit {
    ( $($item:item)+ ) => {
    $(
        #[cfg(process_control_memory_limit)]
        $item
    )+
    };
}

if_memory_limit! {
    use std::ptr;

    use libc::rlimit;
    use libc::RLIMIT_AS;
}

macro_rules! if_raw_pid {
    ( $($item:item)+ ) => {
    $(
        #[cfg(any(process_control_memory_limit, process_control_unix_waitid))]
        $item
    )+
    };
}

if_raw_pid! {
    use libc::pid_t;
}

if_waitid! {
    use std::mem;

    use libc::id_t;
}

if_waitid! {
    macro_rules! static_assert {
        ( $condition:expr $(,)? ) => {
            const _: () = assert!($condition, "static assertion failed");
        };
    }
}

#[cfg(any(
    all(target_env = "musl", target_os = "linux"),
    target_os = "android",
))]
type LimitResource = c_int;
#[cfg(all(target_env = "gnu", target_os = "linux"))]
type LimitResource = __rlimit_resource_t;

fn check_syscall(result: c_int) -> io::Result<()> {
    if result >= 0 {
        Ok(())
    } else {
        Err(io::Error::last_os_error())
    }
}

if_raw_pid! {
    #[derive(Debug)]
    struct RawPid(pid_t);

    impl RawPid {
        fn new(process: &Child) -> Self {
            let pid: u32 = process.id();
            Self(pid.try_into().expect("process identifier is invalid"))
        }

        if_waitid! {
            const fn as_id(&self) -> id_t {
                static_assert!(pid_t::MAX == i32::MAX);
                static_assert!(
                    mem::size_of::<pid_t>() <= mem::size_of::<id_t>(),
                );

                self.0 as _
            }
        }
    }
}

#[derive(Debug)]
pub(super) struct Process<'a> {
    #[cfg(not(process_control_unix_waitid))]
    inner: &'a mut Child,
    #[cfg(any(process_control_memory_limit, process_control_unix_waitid))]
    pid: RawPid,
    _marker: PhantomData<&'a ()>,
}

impl<'a> Process<'a> {
    pub(super) fn new(process: &'a mut Child) -> Self {
        Self {
            #[cfg(any(
                process_control_memory_limit,
                process_control_unix_waitid,
            ))]
            pid: RawPid::new(process),
            #[cfg(not(process_control_unix_waitid))]
            inner: process,
            _marker: PhantomData,
        }
    }

    if_memory_limit! {
        fn set_limit(
            &mut self,
            resource: LimitResource,
            limit: usize,
        ) -> io::Result<()> {
            #[cfg(target_pointer_width = "32")]
            type PointerWidth = u32;
            #[cfg(target_pointer_width = "64")]
            type PointerWidth = u64;
            #[cfg(not(any(
                target_pointer_width = "32",
                target_pointer_width = "64",
            )))]
            compile_error!("unsupported pointer width");

            #[cfg_attr(
                not(target_os = "freebsd"),
                allow(clippy::useless_conversion)
            )]
            let limit = PointerWidth::try_from(limit)
                .expect("`usize` too large for pointer width")
                .into();

            check_syscall(unsafe {
                libc::prlimit(
                    self.pid.0,
                    resource,
                    &rlimit {
                        rlim_cur: limit,
                        rlim_max: limit,
                    },
                    ptr::null_mut(),
                )
            })
        }

        pub(super) fn set_memory_limit(
            &mut self,
            limit: usize,
        ) -> io::Result<()> {
            self.set_limit(RLIMIT_AS, limit)
        }
    }

    pub(super) fn wait(
        &mut self,
        time_limit: Option<Duration>,
    ) -> WaitResult<ExitStatus> {
        wait::wait(self, time_limit)
    }
}
