use libc::c_char;
use libc::c_void;
use std::ffi::CStr;
use std::ffi::CString;
use std::ffi::NulError;
use std::fmt;
use std::ptr;
use std::str::Utf8Error;
use sys::properties::SDL_PropertiesID;

use crate::get_error;
use crate::sys;
use crate::Error;

#[derive(Debug)]
pub enum PropertiesError {
    ArgumentError(NulError),
    StringError(Utf8Error),
    NullPointer,
    SdlError(Error),
}

#[derive(Clone)]
pub struct Properties {
    internal: sys::properties::SDL_PropertiesID,
    constant: bool,
}

macro_rules! cstring {
    ($name:ident) => {
        let $name = match CString::new($name) {
            Ok(name) => name,
            Err(error) => return Err(PropertiesError::ArgumentError(error)),
        };
    };
}

pub type EnumerateCallback = Box<dyn Fn(&Properties, Result<&str, PropertiesError>)>;
unsafe extern "C" fn enumerate(
    userdata: *mut c_void,
    props: SDL_PropertiesID,
    name: *const c_char,
) {
    let properties: &Properties = std::mem::transmute(&props);
    let callback_ptr = userdata as *mut EnumerateCallback;
    let name = CStr::from_ptr(name);
    match name.to_str() {
        Ok(name) => (*callback_ptr)(properties, Ok(name)),
        Err(error) => (*callback_ptr)(properties, Err(PropertiesError::StringError(error))),
    }
}

pub type CleanupBox = fn(*mut c_void);
unsafe extern "C" fn cleanup_box(userdata: *mut c_void, value: *mut c_void) {
    let callback_ptr = userdata as *mut CleanupBox;
    (*callback_ptr)(value);
}

pub type CleanupCallback = Box<dyn FnOnce(*mut c_void)>;
unsafe extern "C" fn cleanup_custom(userdata: *mut c_void, value: *mut c_void) {
    let callback_ptr = userdata as *mut CleanupCallback;
    let callback = Box::from_raw(callback_ptr);
    (*callback)(value);
}

pub use sys::properties::SDL_PropertyType as PropertyType;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct VersionTriple {
    pub major: i32,
    pub minor: i32,
    pub patch: i32,
}

#[derive(Clone, Copy)]
pub struct PropertyName {
    pub module: &'static str,
    pub name: &'static str,
    pub short_name: &'static str,
    pub value: &'static str,
    pub raw: *const c_char,
    pub ty: PropertyType,
    pub doc: Option<&'static str>,
    pub available_since: Option<VersionTriple>,
}

impl fmt::Debug for Properties {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Properties")
            .field("internal", &self.internal.0)
            .field("constant", &self.constant)
            .finish()
    }
}

impl fmt::Debug for PropertyName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("PropertyName")
            .field("module", &self.module)
            .field("name", &self.name)
            .field("short_name", &self.short_name)
            .field("value", &self.value)
            .field("ty", &self.ty.0)
            .field("doc", &self.doc)
            .field("available_since", &self.available_since)
            .finish()
    }
}

impl PropertyName {
    #[inline(always)]
    pub const fn as_ptr(&self) -> *const c_char {
        self.raw
    }
}

pub mod names {
    //! Auto-generated property metadata. This module is generated by `build.rs`
    //! using the metadata shipped with `sdl3-sys`.
    #![allow(dead_code)]
    include!(concat!(env!("OUT_DIR"), "/sdl3_property_names.rs"));
}

impl Properties {
    #[doc(alias = "SDL_CreateProperties")]
    pub fn new() -> Result<Self, PropertiesError> {
        let internal = unsafe { sys::properties::SDL_CreateProperties() };
        if internal == 0 {
            Err(PropertiesError::SdlError(get_error()))
        } else {
            Ok(Self {
                internal,
                constant: false,
            })
        }
    }

    pub(crate) fn raw(&self) -> sys::properties::SDL_PropertiesID {
        self.internal
    }

    pub fn from_ll(props: SDL_PropertiesID) -> Self {
        Self {
            internal: props,
            constant: false,
        }
    }

    pub fn const_from_ll(props: SDL_PropertiesID) -> Self {
        Self {
            internal: props,
            constant: true,
        }
    }

    #[doc(alias = "SDL_GetGlobalProperties")]
    pub fn global() -> Result<Self, PropertiesError> {
        let internal = unsafe { sys::properties::SDL_GetGlobalProperties() };
        if internal == 0 {
            Err(PropertiesError::SdlError(get_error()))
        } else {
            Ok(Self {
                internal,
                constant: true,
            })
        }
    }

    #[doc(alias = "SDL_LockProperties")]
    pub fn lock(&mut self) -> Result<(), PropertiesError> {
        unsafe {
            if !sys::properties::SDL_LockProperties(self.internal) {
                return Err(PropertiesError::SdlError(get_error()));
            }
        }
        Ok(())
    }

    #[doc(alias = "SDL_UnlockProperties")]
    pub fn unlock(&mut self) {
        unsafe {
            sys::properties::SDL_UnlockProperties(self.internal);
        }
    }

    #[doc(alias = "SDL_HasProperty")]
    pub fn contains(&self, name: &str) -> Result<bool, PropertiesError> {
        cstring!(name);
        unsafe {
            Ok(sys::properties::SDL_HasProperty(
                self.internal,
                name.as_ptr(),
            ))
        }
    }

    #[doc(alias = "SDL_SetPointerPropertyWithCleanup")]
    pub fn set_with_cleanup<T>(
        &self,
        name: &str,
        value: *mut T,
        cleanup: Box<dyn FnOnce(*mut T)>,
    ) -> Result<(), PropertiesError> {
        cstring!(name);
        let value_ptr = value as *mut c_void;
        let cleanup_ptr = Box::into_raw(Box::new(cleanup)) as *mut c_void;
        if unsafe {
            sys::properties::SDL_SetPointerPropertyWithCleanup(
                self.internal,
                name.as_ptr(),
                value_ptr,
                Some(cleanup_custom),
                cleanup_ptr,
            )
        } {
            Ok(())
        } else {
            Err(PropertiesError::SdlError(get_error()))
        }
    }

    #[doc(alias = "SDL_GetPropertyType")]
    pub fn get_type(&self, name: &str) -> Result<PropertyType, PropertiesError> {
        cstring!(name);
        unsafe {
            Ok(sys::properties::SDL_GetPropertyType(
                self.internal,
                name.as_ptr(),
            ))
        }
    }

    #[doc(alias = "SDL_GetStringProperty")]
    pub fn get_string(&self, name: &str, default: &str) -> Result<String, PropertiesError> {
        cstring!(name);
        cstring!(default);
        let value = unsafe {
            let value = sys::properties::SDL_GetStringProperty(
                self.internal,
                name.as_ptr(),
                default.as_ptr(),
            );
            CStr::from_ptr(value)
        };

        match value.to_str() {
            Ok(value) => Ok(String::from(value)),
            Err(error) => Err(PropertiesError::StringError(error)),
        }
    }

    #[doc(alias = "SDL_CopyProperties")]
    pub fn copy(&self, destination: &mut Self) -> Result<(), PropertiesError> {
        if unsafe { sys::properties::SDL_CopyProperties(self.internal, destination.internal) } {
            Ok(())
        } else {
            Err(PropertiesError::SdlError(get_error()))
        }
    }

    #[doc(alias = "SDL_EnumerateProperties")]
    pub fn enumerate(&self, callback: EnumerateCallback) -> Result<(), PropertiesError> {
        let callback_ptr = Box::into_raw(Box::new(callback)) as *mut c_void;
        if unsafe {
            sys::properties::SDL_EnumerateProperties(self.internal, Some(enumerate), callback_ptr)
        } {
            Ok(())
        } else {
            Err(PropertiesError::SdlError(get_error()))
        }
    }

    #[doc(alias = "SDL_ClearProperty")]
    pub fn clear(&mut self, name: &str) -> Result<(), PropertiesError> {
        cstring!(name);
        if unsafe { sys::properties::SDL_ClearProperty(self.internal, name.as_ptr()) } {
            Ok(())
        } else {
            Err(PropertiesError::SdlError(get_error()))
        }
    }

    #[doc(alias = "SDL_GetPointerProperty")]
    pub fn with<T>(&mut self, name: &str, with: fn(&T)) -> Result<(), PropertiesError> {
        self.lock()?;
        let pointer: *mut T = self.get(name, ptr::null_mut())?;
        if pointer.is_null() {
            return Err(PropertiesError::NullPointer);
        }
        let reference = unsafe { &mut *pointer };
        with(reference);
        self.unlock();
        Ok(())
    }
}

pub trait Setter<T> {
    fn set(&self, name: &str, value: T) -> Result<(), PropertiesError>;
}

impl Setter<bool> for Properties {
    #[doc(alias = "SDL_SetBooleanProperty")]
    fn set(&self, name: &str, value: bool) -> Result<(), PropertiesError> {
        cstring!(name);
        if unsafe { sys::properties::SDL_SetBooleanProperty(self.internal, name.as_ptr(), value) } {
            Ok(())
        } else {
            Err(PropertiesError::SdlError(get_error()))
        }
    }
}

impl Setter<f32> for Properties {
    #[doc(alias = "SDL_SetFloatProperty")]
    fn set(&self, name: &str, value: f32) -> Result<(), PropertiesError> {
        cstring!(name);
        if unsafe { sys::properties::SDL_SetFloatProperty(self.internal, name.as_ptr(), value) } {
            Ok(())
        } else {
            Err(PropertiesError::SdlError(get_error()))
        }
    }
}

impl Setter<i64> for Properties {
    #[doc(alias = "SDL_SetNumberProperty")]
    fn set(&self, name: &str, value: i64) -> Result<(), PropertiesError> {
        cstring!(name);
        if unsafe { sys::properties::SDL_SetNumberProperty(self.internal, name.as_ptr(), value) } {
            Ok(())
        } else {
            Err(PropertiesError::SdlError(get_error()))
        }
    }
}

impl Setter<&str> for Properties {
    #[doc(alias = "SDL_SetStringProperty")]
    fn set(&self, name: &str, value: &str) -> Result<(), PropertiesError> {
        cstring!(name);
        // Have to transform the value into a cstring, SDL makes an internal copy
        cstring!(value);
        if unsafe {
            sys::properties::SDL_SetStringProperty(self.internal, name.as_ptr(), value.as_ptr())
        } {
            Ok(())
        } else {
            Err(PropertiesError::SdlError(get_error()))
        }
    }
}

impl<T> Setter<*mut T> for Properties {
    #[doc(alias = "SDL_SetPointerProperty")]
    fn set(&self, name: &str, value: *mut T) -> Result<(), PropertiesError> {
        cstring!(name);
        if unsafe {
            sys::properties::SDL_SetPointerProperty(
                self.internal,
                name.as_ptr(),
                value as *mut c_void,
            )
        } {
            Ok(())
        } else {
            Err(PropertiesError::SdlError(get_error()))
        }
    }
}

impl<T> Setter<Box<T>> for Properties {
    #[doc(alias = "SDL_SetPointerPropertyWithCleanup")]
    fn set(&self, name: &str, value: Box<T>) -> Result<(), PropertiesError> {
        cstring!(name);
        let value_ptr: *mut c_void = Box::into_raw(value) as *mut c_void;
        let cleanup: CleanupBox = |value: *mut c_void| {
            let value = value as *mut T;
            unsafe {
                drop(Box::from_raw(value));
            }
        };
        let cleanup_ptr = Box::into_raw(Box::new(cleanup)) as *mut c_void;
        if unsafe {
            sys::properties::SDL_SetPointerPropertyWithCleanup(
                self.internal,
                name.as_ptr(),
                value_ptr,
                Some(cleanup_box),
                cleanup_ptr,
            )
        } {
            Ok(())
        } else {
            Err(PropertiesError::SdlError(get_error()))
        }
    }
}

pub trait Getter<T> {
    fn get(&self, name: &str, default: T) -> Result<T, PropertiesError>;
}

impl Getter<bool> for Properties {
    #[doc(alias = "SDL_GetBooleanProperty")]
    fn get(&self, name: &str, default: bool) -> Result<bool, PropertiesError> {
        cstring!(name);
        unsafe {
            Ok(sys::properties::SDL_GetBooleanProperty(
                self.internal,
                name.as_ptr(),
                default,
            ))
        }
    }
}

impl Getter<f32> for Properties {
    #[doc(alias = "SDL_GetFloatProperty")]
    fn get(&self, name: &str, default: f32) -> Result<f32, PropertiesError> {
        cstring!(name);
        unsafe {
            Ok(sys::properties::SDL_GetFloatProperty(
                self.internal,
                name.as_ptr(),
                default,
            ))
        }
    }
}

impl Getter<i64> for Properties {
    #[doc(alias = "SDL_GetNumberProperty")]
    fn get(&self, name: &str, default: i64) -> Result<i64, PropertiesError> {
        cstring!(name);
        unsafe {
            Ok(sys::properties::SDL_GetNumberProperty(
                self.internal,
                name.as_ptr(),
                default,
            ))
        }
    }
}

impl<T> Getter<*mut T> for Properties {
    #[doc(alias = "SDL_GetPointerProperty")]
    fn get(&self, name: &str, default: *mut T) -> Result<*mut T, PropertiesError> {
        cstring!(name);
        let pointer = unsafe {
            sys::properties::SDL_GetPointerProperty(
                self.internal,
                name.as_ptr(),
                default as *mut c_void,
            )
        };
        if pointer.is_null() {
            return Err(PropertiesError::NullPointer);
        }
        Ok(pointer as *mut T)
    }
}

impl Drop for Properties {
    fn drop(&mut self) {
        if !self.constant {
            unsafe {
                sys::properties::SDL_DestroyProperties(self.internal);
            }
        }
    }
}
