use crate::{GCAbiConversion, GCDatapointID};
use std::{
    ffi::{CStr, CString, c_char},
    fmt::Debug,
};

/// Safe zero cost abstraction over the [gc_abi_sys::GCDatapoint]
/// This struct is meant to be initialized only by the gateway or another internal module.
#[repr(transparent)]
pub struct GCDatapoint(gc_abi_sys::GCDatapoint);

impl GCDatapoint {
    pub fn new(id: GCDatapointID, name: &str, description: &str, unit: &str) -> GCDatapoint {
        GCDatapoint(gc_abi_sys::GCDatapoint {
            id,
            name: CString::new(name).unwrap().into_raw(),
            description: CString::new(description).unwrap().into_raw(),
            unit: CString::new(unit).unwrap().into_raw(),
        })
    }

    /// Get the CStr representation of the name
    #[inline]
    pub fn get_c_name(&self) -> &CStr {
        // Safety:
        // The pointer is guaranteed to be valid by the gateway core
        unsafe { CStr::from_ptr(self.0.name) }
    }

    /// Get the CStr representation of the description
    #[inline]
    pub fn get_c_description(&self) -> &CStr {
        // Safety:
        // The pointer is guaranteed to be valid by the gateway core
        unsafe { CStr::from_ptr(self.0.description) }
    }

    /// Get the CStr representation of the unit
    #[inline]
    pub fn get_c_unit(&self) -> &CStr {
        // Safety:
        // The pointer is guaranteed to be valid by the gateway core
        unsafe { CStr::from_ptr(self.0.unit) }
    }

    /// Get the name as a native String
    #[inline]
    pub fn get_name(&self) -> String {
        self.get_c_name().to_string_lossy().into_owned()
    }

    /// Get the description as a native String
    #[inline]
    pub fn get_description(&self) -> String {
        self.get_c_description().to_string_lossy().into_owned()
    }

    /// Get the unit as a native String
    #[inline]
    pub fn get_unit(&self) -> String {
        self.get_c_unit().to_string_lossy().into_owned()
    }

    /// Get the ID of the datapoint
    #[inline]
    pub fn get_id(&self) -> GCDatapointID {
        self.0.id
    }
}

impl Debug for GCDatapoint {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("GCDatapoint")
            .field("id", &self.get_id())
            .field("name", &self.get_name())
            .field("description", &self.get_description())
            .field("unit", &self.get_unit())
            .finish()
    }
}

impl Drop for GCDatapoint {
    fn drop(&mut self) {
        unsafe {
            drop(CString::from_raw(self.0.name as *mut c_char));
            drop(CString::from_raw(self.0.description as *mut c_char));
            drop(CString::from_raw(self.0.unit as *mut c_char));
        }
    }
}

unsafe impl GCAbiConversion for GCDatapoint {
    type CType = gc_abi_sys::GCDatapoint;

    fn from_c(c_type: Self::CType) -> Self {
        GCDatapoint(c_type)
    }

    fn into_c(self) -> Self::CType {
        self.0
    }
}

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

    #[test]
    fn test_c_conversion() {
        let c_datapoint = gc_abi_sys::GCDatapoint {
            id: 0,
            name: CString::new("test").unwrap().into_raw(),
            description: CString::new("test").unwrap().into_raw(),
            unit: CString::new("test").unwrap().into_raw(),
        };

        let datapoint: GCDatapoint = GCDatapoint::from_c(c_datapoint);

        assert_eq!(datapoint.get_id(), 0);
        assert_eq!(datapoint.get_c_name().to_str().unwrap(), "test");
        assert_eq!(datapoint.get_c_description().to_str().unwrap(), "test");
        assert_eq!(datapoint.get_c_unit().to_str().unwrap(), "test");

        assert_eq!(datapoint.get_name(), "test");
        assert_eq!(datapoint.get_description(), "test");
        assert_eq!(datapoint.get_unit(), "test");
    }

    #[test]
    fn test_data_point_create() {
        let datapoint = GCDatapoint::new(0, "test", "test", "test");

        assert_eq!(datapoint.get_id(), 0);
        assert_eq!(datapoint.get_c_name().to_str().unwrap(), "test");
        assert_eq!(datapoint.get_c_description().to_str().unwrap(), "test");
        assert_eq!(datapoint.get_c_unit().to_str().unwrap(), "test");

        assert_eq!(datapoint.get_name(), "test");
        assert_eq!(datapoint.get_description(), "test");
        assert_eq!(datapoint.get_unit(), "test");
    }
}
