use std::{fmt, os::raw::c_void};

use crate::{Color, ConvError, ToXfconfValue, TryFromXfconfValue, XfconfGValueExt};
use glib::{Type, prelude::*, translate::*};

macro_rules! impl_try_from_xfconf_value_simple {
    ($ty:ty) => {
        impl TryFromXfconfValue for $ty {
            fn try_from_xfconf_value(value: &glib::Value) -> Result<Self, ConvError> {
                value.get().map_err(as_conv_error)
            }

            fn xfconf_display_name() -> &'static str {
                stringify!($ty)
            }
        }
    };
}

macro_rules! impl_try_from_xfconf_value_int {
    ($ty:ty) => {
        impl TryFromXfconfValue for $ty {
            fn try_from_xfconf_value(value: &glib::Value) -> Result<Self, ConvError> {
                match value.type_() {
                    Type::U8
                    | Type::I8
                    | Type::U32
                    | Type::I32
                    | Type::U64
                    | Type::I64
                    | Type::U_LONG
                    | Type::I_LONG => value
                        .transform::<$ty>()
                        .map_err(as_conv_error)
                        .and_then(|v| v.get::<$ty>().map_err(as_conv_error)),
                    Type::STRING => value
                        .get::<&str>()
                        .map_err(as_conv_error)
                        .and_then(|v| v.parse::<$ty>().map_err(as_conv_error)),
                    Type::BOOL => value
                        .get::<bool>()
                        .map_err(as_conv_error)
                        .map(|v| if v { 1 } else { 0 }),
                    Type::FLAGS => {
                        if std::mem::size_of::<$ty>() < std::mem::size_of::<u32>() {
                            Err(ConvError::new("Flags type is too small to hold value"))
                        } else {
                            value
                                .transform::<$ty>()
                                .map_err(as_conv_error)
                                .and_then(|v| v.get::<$ty>().map_err(as_conv_error))
                        }
                    }
                    x if x == crate::XfconfGType::u16() => value
                        .get_u16()
                        .ok_or_else(|| ConvError::new("Value does not fit in a u16"))
                        .and_then(|v| <$ty>::try_from(v).map_err(as_conv_error)),
                    x if x == crate::XfconfGType::i16() => value
                        .get_i16()
                        .ok_or_else(|| ConvError::new("Value does not fit in an i16"))
                        .and_then(|v| <$ty>::try_from(v).map_err(as_conv_error)),
                    x => Err(ConvError::new(format!(
                        "Value of type {} cannot be converted to a {}",
                        x.name(),
                        stringify!($ty)
                    ))),
                }
            }

            fn xfconf_display_name() -> &'static str {
                stringify!($ty)
            }
        }
    };
}

macro_rules! impl_try_from_xfconf_value_int16 {
    ($ty:ty) => {
        impl TryFromXfconfValue for $ty {
            fn try_from_xfconf_value(value: &glib::Value) -> Result<Self, ConvError> {
                match value.type_() {
                    Type::U8 | Type::U32 | Type::U64 | Type::U_LONG => value
                        .transform::<u64>()
                        .map_err(as_conv_error)
                        .and_then(|v| v.get::<u64>().map_err(as_conv_error))
                        .and_then(|v| <$ty>::try_from(v).map_err(as_conv_error)),
                    Type::I8 | Type::I32 | Type::I64 | Type::I_LONG => value
                        .transform::<i64>()
                        .map_err(as_conv_error)
                        .and_then(|v| v.get::<i64>().map_err(as_conv_error))
                        .and_then(|v| <$ty>::try_from(v).map_err(as_conv_error)),
                    Type::STRING => value
                        .get::<&str>()
                        .map_err(as_conv_error)
                        .and_then(|v| v.parse::<$ty>().map_err(as_conv_error)),
                    Type::BOOL => value
                        .get::<bool>()
                        .map_err(as_conv_error)
                        .map(|v| if v { 1 } else { 0 }),
                    Type::FLAGS => {
                        if std::mem::size_of::<$ty>() < std::mem::size_of::<u32>() {
                            Err(ConvError::new("Flags type is too small to hold value"))
                        } else {
                            value
                                .transform::<u32>()
                                .map_err(as_conv_error)
                                .and_then(|v| v.get::<u32>().map_err(as_conv_error))
                                .and_then(|v| <$ty>::try_from(v).map_err(as_conv_error))
                        }
                    }
                    x if x == crate::XfconfGType::u16() => value
                        .get_u16()
                        .ok_or_else(|| ConvError::new("Value does not fit in a u16"))
                        .and_then(|v| <$ty>::try_from(v).map_err(as_conv_error)),
                    x if x == crate::XfconfGType::i16() => value
                        .get_i16()
                        .ok_or_else(|| ConvError::new("Value does not fit in an i16"))
                        .and_then(|v| <$ty>::try_from(v).map_err(as_conv_error)),
                    x => Err(ConvError::new(format!(
                        "Value of type {} cannot be converted to a {}",
                        x.name(),
                        stringify!($ty)
                    ))),
                }
            }

            fn xfconf_display_name() -> &'static str {
                stringify!($ty)
            }
        }
    };
}

macro_rules! impl_to_xfconf_value_simple {
    ($ty:ty) => {
        impl ToXfconfValue for $ty {
            fn to_xfconf_value(&self) -> glib::Value {
                self.to_value()
            }
        }
    };
}

macro_rules! impl_to_xfconf_value_size {
    ($ty:ty, $t64:ty, $t32:ty, $e16:ident) => {
        impl ToXfconfValue for $ty {
            fn to_xfconf_value(&self) -> glib::Value {
                #[cfg(target_pointer_width = "16")]
                let value = crate::XfconfGType::$e16(*self);
                #[cfg(target_pointer_width = "32")]
                let value = (*self as $t32).to_value();
                #[cfg(target_pointer_width = "64")]
                let value = (*self as $t64).to_value();

                value
            }
        }
    };
}

macro_rules! impl_to_xfconf_value_short {
    ($ty:ty, $construct:path) => {
        impl ToXfconfValue for $ty {
            fn to_xfconf_value(&self) -> glib::Value {
                $construct(*self)
            }
        }
    };
}

impl_try_from_xfconf_value_int!(u8);
impl_try_from_xfconf_value_int16!(u16);
impl_try_from_xfconf_value_int!(u32);
impl_try_from_xfconf_value_int!(u64);
impl_try_from_xfconf_value_int!(i8);
impl_try_from_xfconf_value_int16!(i16);
impl_try_from_xfconf_value_int!(i32);
impl_try_from_xfconf_value_int!(i64);
impl_try_from_xfconf_value_simple!(String);
impl_try_from_xfconf_value_simple!(glib::GString);

impl TryFromXfconfValue for bool {
    fn try_from_xfconf_value(value: &glib::Value) -> Result<Self, ConvError> {
        match value.type_() {
            Type::U8 | Type::U32 | Type::U64 | Type::U_LONG => {
                value.get::<u64>().map_err(as_conv_error).map(|v| v != 0)
            }
            Type::I8 | Type::I32 | Type::I64 | Type::I_LONG => {
                value.get::<i64>().map_err(as_conv_error).map(|v| v != 0)
            }
            Type::STRING => value
                .get::<&str>()
                .map_err(as_conv_error)
                .and_then(|v| v.parse::<bool>().map_err(as_conv_error)),
            Type::BOOL => value.get::<bool>().map_err(as_conv_error),
            Type::F32 => value.get::<f32>().map_err(as_conv_error).map(|v| v != 0f32),
            Type::F64 => value.get::<f64>().map_err(as_conv_error).map(|v| v != 0f64),
            Type::UNIT => Ok(false),
            x => Err(ConvError::new(format!(
                "Value of type {} cannot be converted to a bool",
                x.name()
            ))),
        }
    }

    fn xfconf_display_name() -> &'static str {
        "bool"
    }
}

impl TryFromXfconfValue for f32 {
    fn try_from_xfconf_value(value: &glib::Value) -> Result<Self, ConvError> {
        match value.type_() {
            Type::BOOL => value.get::<bool>().map_err(as_conv_error).map(f32::from),
            Type::F32 => value.get::<f32>().map_err(as_conv_error),
            Type::U8 => value.get::<u8>().map_err(as_conv_error).map(f32::from),
            Type::I8 => value.get::<i8>().map_err(as_conv_error).map(f32::from),
            Type::STRING => value
                .get::<&str>()
                .map_err(as_conv_error)
                .and_then(|v| v.parse::<f32>().map_err(as_conv_error)),
            x if x == crate::XfconfGType::u16() => value
                .get_u16()
                .ok_or_else(|| ConvError::new("Value does not fit in a u16"))
                .map(f32::from),
            x if x == crate::XfconfGType::i16() => value
                .get_i16()
                .ok_or_else(|| ConvError::new("Value does not fit in an i16"))
                .map(f32::from),
            x => Err(ConvError::new(format!(
                "Value of type {} cannot be converted to a f32",
                x.name()
            ))),
        }
    }

    fn xfconf_display_name() -> &'static str {
        "f32"
    }
}

impl TryFromXfconfValue for f64 {
    fn try_from_xfconf_value(value: &glib::Value) -> Result<Self, ConvError> {
        match value.type_() {
            Type::BOOL => value.get::<bool>().map_err(as_conv_error).map(f64::from),
            Type::F32 => value.get::<f32>().map_err(as_conv_error).map(f64::from),
            Type::F64 => value.get::<f64>().map_err(as_conv_error),
            Type::U8 => value.get::<u8>().map_err(as_conv_error).map(f64::from),
            Type::U32 => value.get::<u32>().map_err(as_conv_error).map(f64::from),
            Type::I8 => value.get::<i8>().map_err(as_conv_error).map(f64::from),
            Type::I32 => value.get::<i32>().map_err(as_conv_error).map(f64::from),
            Type::STRING => value
                .get::<&str>()
                .map_err(as_conv_error)
                .and_then(|v| v.parse::<f64>().map_err(as_conv_error)),
            x if x == crate::XfconfGType::u16() => value
                .get_u16()
                .ok_or_else(|| ConvError::new("Value does not fit in a u16"))
                .map(f64::from),
            x if x == crate::XfconfGType::i16() => value
                .get_i16()
                .ok_or_else(|| ConvError::new("Value does not fit in an i16"))
                .map(f64::from),
            x => Err(ConvError::new(format!(
                "Value of type {} cannot be converted to a f64",
                x.name()
            ))),
        }
    }

    fn xfconf_display_name() -> &'static str {
        "f64"
    }
}

impl TryFromXfconfValue for Color {
    fn try_from_xfconf_value(value: &glib::Value) -> Result<Self, ConvError> {
        Color::try_from(value)
    }

    fn xfconf_display_name() -> &'static str {
        "color"
    }
}

impl TryFromXfconfValue for glib::Value {
    fn try_from_xfconf_value(value: &glib::Value) -> Result<Self, ConvError> {
        Ok(value.clone())
    }

    fn xfconf_display_name() -> &'static str {
        "value"
    }
}

impl<T: TryFromXfconfValue> TryFromXfconfValue for Vec<T> {
    fn try_from_xfconf_value(value: &glib::Value) -> Result<Self, ConvError> {
        if value.type_() == ptr_array_gtype() {
            let array_ptr = unsafe { glib::gobject_ffi::g_value_get_boxed(value.as_ptr()) }
                as *mut glib::ffi::GPtrArray;
            if !array_ptr.is_null() {
                let values: Vec<glib::Value> =
                    unsafe { FromGlibPtrArrayContainerAsVec::from_glib_none_as_vec(array_ptr) };
                values
                    .into_iter()
                    .map(|v| T::try_from_xfconf_value(&v))
                    .collect::<Result<Vec<T>, _>>()
                    .map_err(|err| {
                        ConvError::new(format!("Unable to convert all values in vec: {err}"))
                    })
            } else {
                Err(ConvError::new("Array pointer was NULL"))
            }
        } else {
            Err(ConvError::new("Array was not a GPtrArray"))
        }
    }

    fn xfconf_display_name() -> &'static str {
        "vec"
    }
}

impl_to_xfconf_value_simple!(bool);
impl_to_xfconf_value_simple!(u8);
impl_to_xfconf_value_short!(u16, crate::XfconfGType::value_from_u16);
impl_to_xfconf_value_simple!(u32);
impl_to_xfconf_value_simple!(u64);
impl_to_xfconf_value_size!(usize, u64, u32, value_from_u16);
impl_to_xfconf_value_simple!(i8);
impl_to_xfconf_value_short!(i16, crate::XfconfGType::value_from_i16);
impl_to_xfconf_value_simple!(i32);
impl_to_xfconf_value_simple!(i64);
impl_to_xfconf_value_size!(isize, i64, i32, value_from_i16);
impl_to_xfconf_value_simple!(f32);
impl_to_xfconf_value_simple!(f64);
impl_to_xfconf_value_simple!(String);
impl_to_xfconf_value_simple!(&str);
impl_to_xfconf_value_simple!(Color);

impl ToXfconfValue for glib::Value {
    fn to_xfconf_value(&self) -> glib::Value {
        self.clone()
    }
}

impl<T: ToXfconfValue> ToXfconfValue for Vec<T> {
    fn to_xfconf_value(&self) -> glib::Value {
        gvalue_slice_to_gvalue(self)
    }
}

impl<T: ToXfconfValue> ToXfconfValue for &[T] {
    fn to_xfconf_value(&self) -> glib::Value {
        gvalue_slice_to_gvalue(self)
    }
}

#[inline]
pub(crate) fn ptr_array_gtype() -> glib::Type {
    unsafe { glib::Type::from_glib(glib::ffi::g_ptr_array_get_type()) }
}

unsafe extern "C" fn gvalue_ptr_free(ptr: *mut c_void) {
    unsafe {
        if !ptr.is_null() {
            glib::gobject_ffi::g_value_unset(ptr as *mut _);
            glib::ffi::g_free(ptr);
        }
    }
}

pub(crate) fn gvalue_slice_to_gvalue<T: ToXfconfValue>(values: &[T]) -> glib::Value {
    unsafe {
        let ptrarr = glib::ffi::g_ptr_array_new_full(
            u32::try_from(values.len()).unwrap(),
            Some(gvalue_ptr_free),
        );
        assert!(!ptrarr.is_null());

        for value in values {
            glib::ffi::g_ptr_array_add(ptrarr, value.to_xfconf_value().to_glib_full() as *mut _);
        }

        let value = glib::Value::from_type(ptr_array_gtype());
        glib::gobject_ffi::g_value_take_boxed(value.as_ptr(), ptrarr as *const _);
        value
    }
}

fn as_conv_error<T: fmt::Display>(err: T) -> ConvError {
    ConvError::new(err.to_string())
}
