use anyhow::{bail, Result};
use nix_bindings_util_sys as raw;
use std::ptr::null_mut;
use std::ptr::NonNull;

/// A context for error handling, when interacting directly with the generated bindings for the C API in [nix_bindings_util_sys].
///
/// The `nix-store` and `nix-expr` libraries that consume this type internally store a private context in their `EvalState` and `Store` structs to avoid allocating a new context for each operation. The state of a context is irrelevant when used correctly (e.g. with [check_call!]), so it's safe to reuse, and safe to allocate more contexts in methods such as [Clone::clone].
pub struct Context {
    inner: NonNull<raw::c_context>,
}

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

impl Context {
    pub fn new() -> Self {
        let ctx = unsafe { raw::c_context_create() };
        if ctx.is_null() {
            // We've failed to allocate a (relatively small) Context struct.
            // We're almost certainly going to crash anyways.
            panic!("nix_c_context_create returned a null pointer");
        }
        Context {
            inner: NonNull::new(ctx).unwrap(),
        }
    }

    /// Access the C context pointer.
    ///
    /// We recommend to use `check_call!` if possible.
    pub fn ptr(&mut self) -> *mut raw::c_context {
        self.inner.as_ptr()
    }

    /// Check the error code and return an error if it's not `NIX_OK`.
    ///
    /// We recommend to use `check_call!` if possible.
    pub fn check_err(&self) -> Result<()> {
        let err = unsafe { raw::err_code(self.inner.as_ptr()) };
        if err != raw::err_NIX_OK {
            // msgp is a borrowed pointer (pointing into the context), so we don't need to free it
            let msgp = unsafe { raw::err_msg(null_mut(), self.inner.as_ptr(), null_mut()) };
            // Turn the i8 pointer into a Rust string by copying
            let msg: &str = unsafe { core::ffi::CStr::from_ptr(msgp).to_str()? };
            bail!("{}", msg);
        }
        Ok(())
    }

    pub fn clear(&mut self) {
        unsafe {
            raw::set_err_msg(self.inner.as_ptr(), raw::err_NIX_OK, c"".as_ptr());
        }
    }

    pub fn check_err_and_clear(&mut self) -> Result<()> {
        let r = self.check_err();
        if r.is_err() {
            self.clear();
        }
        r
    }

    pub fn check_one_call_or_key_none<T, F: FnOnce(*mut raw::c_context) -> T>(
        &mut self,
        f: F,
    ) -> Result<Option<T>> {
        let t = f(self.ptr());
        if unsafe { raw::err_code(self.inner.as_ptr()) == raw::err_NIX_ERR_KEY } {
            self.clear();
            return Ok(None);
        }
        self.check_err_and_clear()?;
        Ok(Some(t))
    }
}

impl Drop for Context {
    fn drop(&mut self) {
        unsafe {
            raw::c_context_free(self.inner.as_ptr());
        }
    }
}

#[macro_export]
macro_rules! check_call {
    ($($f:ident)::+($ctx:expr $(, $arg:expr)*)) => {
        {
            let ctx : &mut $crate::context::Context = $ctx;
            let ret = $($f)::*(ctx.ptr() $(, $arg)*);
            match ctx.check_err() {
                Ok(_) => Ok(ret),
                Err(e) => {
                    ctx.clear();
                    Err(e)
                }
            }
        }
    }
}

pub use check_call;

// TODO: Generalize this macro to work with any error code or any error handling logic
#[macro_export]
macro_rules! check_call_opt_key {
    ($($f:ident)::+($ctx:expr, $($arg:expr),*)) => {
        {
            let ctx : &mut $crate::context::Context = $ctx;
            let ret = $($f)::*(ctx.ptr(), $($arg,)*);
            if unsafe { raw::err_code(ctx.ptr()) == raw::err_NIX_ERR_KEY } {
                ctx.clear();
                return Ok(None);
            }
            match ctx.check_err() {
                Ok(_) => Ok(Some(ret)),
                Err(e) => {
                    ctx.clear();
                    Err(e)
                }
            }
        }
    }
}

pub use check_call_opt_key;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn context_new_and_drop() {
        // don't crash
        let _c = Context::new();
    }

    fn set_dummy_err(ctx_ptr: *mut raw::c_context) {
        unsafe {
            raw::set_err_msg(
                ctx_ptr,
                raw::err_NIX_ERR_UNKNOWN,
                c"dummy error message".as_ptr(),
            );
        }
    }

    #[test]
    fn check_call_dynamic_context() {
        let r = check_call!(set_dummy_err(&mut Context::new()));
        assert!(r.is_err());
        assert_eq!(r.unwrap_err().to_string(), "dummy error message");
    }
}
