eclisp 0.1.0

Bindings for Embeddable Common Lisp
use core::fmt;
use std::{ffi::CStr, ptr::NonNull};

use crate::{Lisp, ListError, ListIter, sys};

#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum ValueError {
    #[error("Expected {expected}, got {actual}")]
    TypeMismatch {
        expected: &'static str,
        actual: String,
    },

    #[error("Invalid UTF-8")]
    InvalidUtf8,

    #[error("Failed to convert Lisp value")]
    ConversionFailed,
}

pub struct Value<'rt> {
    runtime: &'rt Lisp,
    root: NonNull<sys::eclisp_root>,
}

impl<'rt> Value<'rt> {
    /// # Safety
    /// Root must be valid, owned, and allocated by the ECL.
    pub(crate) unsafe fn from_root(runtime: &'rt Lisp, root: NonNull<sys::eclisp_root>) -> Self {
        Self { runtime, root }
    }

    pub(crate) fn as_raw(&self) -> sys::cl_object {
        unsafe { sys::eclisp_root_get(self.root.as_ptr()) }
    }

    pub(crate) fn belongs_to(&self, runtime: &Lisp) -> bool {
        std::ptr::eq(self.runtime, runtime)
    }

    pub fn iter(&self) -> ListIter<'_, 'rt> {
        ListIter::new(self)
    }

    pub fn is_nil(&self) -> bool {
        unsafe { sys::eclisp_root_is_nil(self.root.as_ptr()) != 0 }
    }

    pub fn is_t(&self) -> bool {
        unsafe { sys::eclisp_root_is_t(self.root.as_ptr()) != 0 }
    }

    pub fn is_int(&self) -> bool {
        unsafe { sys::eclisp_root_is_int(self.root.as_ptr()) != 0 }
    }

    pub fn is_float(&self) -> bool {
        unsafe { sys::eclisp_root_is_float(self.root.as_ptr()) != 0 }
    }

    pub fn is_string(&self) -> bool {
        unsafe { sys::eclisp_root_is_string(self.root.as_ptr()) != 0 }
    }

    pub fn try_as_i64(&self) -> Result<i64, ValueError> {
        if !self.is_int() {
            return Err(ValueError::TypeMismatch {
                expected: "int",
                actual: "other".into(),
            });
        }

        let mut value = 0i64;

        let success = unsafe { sys::eclisp_root_as_int(self.root.as_ptr(), &mut value) };

        if success == 0 {
            return Err(ValueError::ConversionFailed);
        }

        Ok(value)
    }

    pub fn try_as_f64(&self) -> Result<f64, ValueError> {
        if !self.is_float() {
            return Err(ValueError::TypeMismatch {
                expected: "float",
                actual: "other".into(),
            });
        }

        let mut value = 0f64;

        let success = unsafe { sys::eclisp_root_as_float(self.root.as_ptr(), &mut value) };

        if success == 0 {
            return Err(ValueError::ConversionFailed);
        }

        Ok(value)
    }

    pub fn try_as_string(&self) -> Result<String, ValueError> {
        let mut buffer = std::ptr::null_mut();
        let mut length = 0usize;

        let success =
            unsafe { sys::eclisp_root_as_string(self.root.as_ptr(), &mut buffer, &mut length) };

        if success == 0 || buffer.is_null() {
            return Err(ValueError::ConversionFailed);
        }

        let result = unsafe {
            let bytes = std::slice::from_raw_parts(buffer.cast::<u8>(), length);

            std::str::from_utf8(bytes)
                .map(str::to_owned)
                .map_err(|_| ValueError::InvalidUtf8)
        };

        unsafe {
            sys::eclisp_string_free(buffer);
        }

        result
    }

    pub fn repr(&self) -> Result<String, ValueError> {
        let mut buffer = std::ptr::null_mut();

        let success = unsafe { sys::eclisp_root_repr(self.root.as_ptr(), &mut buffer) };

        if buffer.is_null() {
            return Err(ValueError::ConversionFailed);
        }

        let result = if success != 0 {
            unsafe { CStr::from_ptr(buffer) }
                .to_str()
                .map(str::to_owned)
                .map_err(|_| ValueError::InvalidUtf8)
        } else {
            Err(ValueError::ConversionFailed)
        };

        unsafe {
            sys::eclisp_string_free(buffer);
        }

        result
    }

    pub fn is_list(&self) -> bool {
        unsafe { sys::eclisp_root_is_list(self.root.as_ptr()) != 0 }
    }

    pub fn is_cons(&self) -> bool {
        unsafe { sys::eclisp_root_is_cons(self.root.as_ptr()) != 0 }
    }

    pub fn car(&self) -> Result<Value<'rt>, ListError> {
        if !self.is_cons() {
            return Err(ListError::NotCons);
        }

        let mut root = std::ptr::null_mut();

        let success = unsafe { sys::eclisp_root_car(self.root.as_ptr(), &mut root) };

        if success == 0 {
            return Err(ListError::AllocationFailed);
        }

        let root = NonNull::new(root).ok_or(ListError::AllocationFailed)?;

        Ok(unsafe { Value::from_root(self.runtime, root) })
    }

    pub fn cdr(&self) -> Result<Value<'rt>, ListError> {
        if !self.is_cons() {
            return Err(ListError::NotCons);
        }

        let mut root = std::ptr::null_mut();

        let success = unsafe { sys::eclisp_root_cdr(self.root.as_ptr(), &mut root) };

        if success == 0 {
            return Err(ListError::AllocationFailed);
        }

        let root = NonNull::new(root).ok_or(ListError::AllocationFailed)?;

        Ok(unsafe { Value::from_root(self.runtime, root) })
    }
}

impl Drop for Value<'_> {
    fn drop(&mut self) {
        unsafe { sys::eclisp_root_free(self.root.as_ptr()) }
    }
}

impl fmt::Debug for Value<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.repr() {
            Ok(repr) => write!(f, "Value({repr})"),
            Err(_) => write!(f, "Value(<error>)"),
        }
    }
}

impl fmt::Display for Value<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.repr() {
            Ok(repr) => write!(f, "{repr}"),
            Err(_) => write!(f, "<error>"),
        }
    }
}