use std::ffi::{CStr, CString, c_char};

use crate::GCAbiConversion;

pub type GCPluginFeatures = u64;

/// A struct that represents the information of a plugin.
/// This struct is meant to be initialized by the plugin and send to the gateway. It also the owner of the data it represents.
#[repr(transparent)]
pub struct GCPluginInfo(gc_abi_sys::sGCPluginInfo);

impl GCPluginInfo {
    /// Create a new plugin info object
    ///
    /// This creates 3 additional allocations
    pub fn new(name: &str, version: &str, plugin_ifc_version: &str, features: GCPluginFeatures) -> Self {
        GCPluginInfo(gc_abi_sys::sGCPluginInfo {
            name: CString::new(name).unwrap().into_raw(),
            version: CString::new(version).unwrap().into_raw(),
            pluginIfcVersion: CString::new(plugin_ifc_version).unwrap().into_raw(),
            features,
        })
    }

    /// Create a new plugin info object from C strings
    pub fn new_from_c(name: CString, version: CString, plugin_ifc_version: CString, features: GCPluginFeatures) -> Self {
        GCPluginInfo(gc_abi_sys::sGCPluginInfo {
            name: name.into_raw(),
            version: version.into_raw(),
            pluginIfcVersion: plugin_ifc_version.into_raw(),
            features,
        })
    }

    /// Get the name of the plugin as a Rust string
    ///
    /// This method allocates a new string
    #[inline]
    pub fn name(&self) -> String {
        self.c_name().to_string_lossy().to_string()
    }

    /// Get the version of the plugin as a Rust string
    ///
    /// This method allocates a new string
    #[inline]
    pub fn version(&self) -> String {
        self.c_version().to_string_lossy().to_string()
    }

    /// Get the plugin interface version of the plugin as a Rust string
    ///
    /// This method allocates a new string
    #[inline]
    pub fn plugin_ifc_version(&self) -> String {
        self.c_plugin_ifc_version().to_string_lossy().to_string()
    }

    /// Get the name of the plugin as a C string
    #[inline]
    pub fn c_name(&self) -> &CStr {
        unsafe { std::ffi::CStr::from_ptr(self.0.name) }
    }

    /// Get the version of the plugin as a C string
    #[inline]
    pub fn c_version(&self) -> &CStr {
        unsafe { std::ffi::CStr::from_ptr(self.0.version) }
    }

    /// Get the plugin interface version of the plugin as a C string
    #[inline]
    pub fn c_plugin_ifc_version(&self) -> &CStr {
        unsafe { std::ffi::CStr::from_ptr(self.0.pluginIfcVersion) }
    }

    /// Get the features of the plugin as a C string
    #[inline]
    pub fn features(&self) -> GCPluginFeatures {
        self.0.features
    }
}

impl Drop for GCPluginInfo {
    fn drop(&mut self) {
        // Safety:
        // All the string are owned by the current object and are guaranteed to be valid
        unsafe {
            drop(CString::from_raw(self.0.name as *mut c_char));
            drop(CString::from_raw(self.0.version as *mut c_char));
            drop(CString::from_raw(self.0.pluginIfcVersion as *mut c_char));
        }
    }
}

impl Clone for GCPluginInfo {
    fn clone(&self) -> Self {
        GCPluginInfo::new_from_c(
            CString::clone(&self.c_name().to_owned()),
            CString::clone(&self.c_version().to_owned()),
            CString::clone(&self.c_plugin_ifc_version().to_owned()),
            self.features(),
        )
    }
}

unsafe impl GCAbiConversion for GCPluginInfo {
    type CType = gc_abi_sys::sGCPluginInfo;

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

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

unsafe impl Send for GCPluginInfo {}
unsafe impl Sync for GCPluginInfo {}

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

    #[test]
    fn test_store_and_access() {
        let info = allocation_counter::measure(|| {
            let plugin_info = GCPluginInfo::new_from_c(
                CString::new("test").unwrap(),
                CString::new("test").unwrap(),
                CString::new("test").unwrap(),
                0,
            );

            assert_eq!(plugin_info.c_name().to_str().unwrap(), "test");
            assert_eq!(plugin_info.c_version().to_str().unwrap(), "test");
            assert_eq!(plugin_info.c_plugin_ifc_version().to_str().unwrap(), "test");
            assert_eq!(plugin_info.features(), 0);
        });

        assert_eq!(info.bytes_current, 0, "Memory leak detected");
    }

    #[test]
    fn test_conversion_to_c() {
        let info = allocation_counter::measure(|| {
            let plugin_info = GCPluginInfo::new("test", "test", "test", 0);
            let c_info: gc_abi_sys::GCPluginInfo = unsafe { (plugin_info).as_ptr() };

            assert!(
                ptr::addr_eq(ptr::addr_of!(plugin_info), c_info),
                "The rust abstraction should be zero cost"
            );

            unsafe {
                assert_eq!(plugin_info.c_name().to_str().unwrap(), CStr::from_ptr((*c_info).name).to_str().unwrap());
                assert_eq!(
                    plugin_info.c_version().to_str().unwrap(),
                    CStr::from_ptr((*c_info).version).to_str().unwrap()
                );
                assert_eq!(
                    plugin_info.c_plugin_ifc_version().to_str().unwrap(),
                    CStr::from_ptr((*c_info).pluginIfcVersion).to_str().unwrap()
                );
                assert_eq!(plugin_info.features(), (*c_info).features);
            }
        });
        assert_eq!(info.bytes_current, 0, "Memory leak detected");
    }

    #[test]
    fn test_plugin_info_clone() {
        let info = allocation_counter::measure(|| {
            let plugin_info = GCPluginInfo::new("test", "test", "test", 0);
            let plugin_info_clone = plugin_info.clone();

            drop(plugin_info);
            assert_eq!(plugin_info_clone.c_name().to_str().unwrap(), "test");
            assert_eq!(plugin_info_clone.c_version().to_str().unwrap(), "test");
            assert_eq!(plugin_info_clone.c_plugin_ifc_version().to_str().unwrap(), "test");
            assert_eq!(plugin_info_clone.features(), 0);
        });

        assert_eq!(info.bytes_current, 0, "Memory leak detected");
    }
}
