use libc::{c_char, c_int, c_void, pid_t};

use super::kinfo_proc::kinfo_proc;
use std::ffi::CStr;
use std::io::Error;

// Executable path buffer size
const BUF_SIZE: usize = 4096;
// File info flag. Designates that file is CWD.
const PS_FST_UFLAG_CDIR: c_int = 0x0002;
const KERN_PROC_PID: c_int = 1;
const KERN_PROC_PROC: c_int = 8;
const KERN_PROC_INC_THREAD: c_int = 0x10;

#[link(name = "procstat")]
extern "C" {
    fn procstat_open_sysctl() -> *const c_void;
    fn procstat_getprocs(
        prstat: *const c_void,
        request: c_int,
        pid: pid_t,
        count: *const c_int,
    ) -> *const kinfo_proc;
    fn procstat_getpathname(
        prstat: *const c_void,
        kinfo_proc: *const kinfo_proc,
        pathname: *const c_char,
        size: c_int,
    );
    fn procstat_getfiles(
        prstat: *const c_void,
        kinfo_proc: *const kinfo_proc,
        mmap: c_int,
    ) -> *const stailq_entry;
    fn procstat_freeprocs(prstat: *const c_void, kinfo_proc: *const kinfo_proc);
    fn procstat_close(prstat: *const c_void);
}

#[derive(Debug)]
#[repr(C)]
struct Filestat {
    fs_type: c_int,
    fs_flags: c_int,
    fs_fflags: c_int,
    fs_uflags: c_int,
    fs_fd: c_int,
    fs_ref_count: c_int,
    fs_offset: c_int,
    fs_typedep: *const c_void,
    fs_path: *const c_char,
    next: stailq_entry,
}

#[derive(Debug)]
#[repr(C)]
struct stailq_entry {
    next: *const Filestat,
}

fn procstat_call<T>(
    request: c_int,
    pid: pid_t,
    count: c_int,
    call: &dyn Fn(*const c_void, *const kinfo_proc, c_int) -> T,
) -> Result<T, Error> {
    unsafe {
        let prstat = procstat_open_sysctl();

        if prstat.is_null() {
            return Err(Error::last_os_error());
        }

        let kinfo_procs = procstat_getprocs(prstat, request, pid, &count as *const _);

        if kinfo_procs.is_null() {
            return Err(Error::last_os_error());
        }

        let ret = call(prstat, kinfo_procs, count);

        procstat_freeprocs(prstat, kinfo_procs);
        procstat_close(prstat);

        Ok(ret)
    }
}

/// Retrieves process information via libprocstat
pub fn threads_info(pid: pid_t) -> Result<Vec<kinfo_proc>, Error> {
    let count: c_int = 0;

    procstat_call(
        KERN_PROC_PID | KERN_PROC_INC_THREAD,
        pid,
        count,
        &|_, kinfo, count| unsafe { Ok(std::slice::from_raw_parts(kinfo, count as usize).into()) },
    )?
}

pub fn exe(pid: pid_t) -> Result<String, Error> {
    let result: [c_char; BUF_SIZE] = [0; BUF_SIZE];

    procstat_call(
        KERN_PROC_PID | KERN_PROC_INC_THREAD,
        pid,
        BUF_SIZE as c_int,
        &|prstat, kinfo, _| unsafe {
            procstat_getpathname(prstat, kinfo, &result as _, BUF_SIZE as c_int);
            let bytes = CStr::from_ptr(&result as _).to_bytes();

            Ok(String::from_utf8_unchecked(bytes.to_vec()))
        },
    )?
}

fn get_file_with_uflag(pid: pid_t, uflag: c_int) -> Result<Option<String>, Error> {
    procstat_call(
        KERN_PROC_PID | KERN_PROC_INC_THREAD,
        pid,
        0 as c_int,
        &|prstat, kinfo, _| {
            let mut filestat = unsafe { (*procstat_getfiles(prstat, kinfo, 0)).next };

            loop {
                if filestat.is_null() {
                    return None;
                };

                unsafe {
                    let ref derefered = *filestat;

                    if derefered.fs_uflags & uflag != 0 {
                        let bytes = CStr::from_ptr(derefered.fs_path).to_bytes();
                        return Some(String::from_utf8_unchecked(bytes.to_vec()));
                    }

                    filestat = (derefered.next).next;
                };
            }
        },
    )
}

pub fn cwd(pid: pid_t) -> Result<String, Error> {
    match get_file_with_uflag(pid, PS_FST_UFLAG_CDIR)? {
        Some(string) => Ok(string),
        None => Ok("".to_owned()),
    }
}

pub fn processes() -> Result<std::collections::HashMap<pid_t, pid_t>, Error> {
    procstat_call(KERN_PROC_PROC, 0, 0, &|_, kinfo, count| {
        let mut ret = std::collections::HashMap::new();
        let proc = unsafe { std::slice::from_raw_parts(kinfo, count as usize) };
        for proc in proc {
            ret.insert(proc.ki_pid, proc.ki_ppid);
        }
        Ok(ret)
    })?
}
