use core::ffi::c_void;
use core::ptr;

use doom_fish_utils::panic_safe::catch_user_panic;

#[allow(clippy::wildcard_imports)]
use super::*;
use crate::ffi_impl as ffi;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ManagerDeviceCallbackKind {
    Matching,
    Removal,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ManagerReportCallbackKind {
    Untimestamped,
    Timestamped,
}

#[allow(clippy::type_complexity)]
struct ManagerDeviceContext {
    callback: *mut Box<dyn Fn(HidDevice) + Send + Sync + 'static>,
}

#[allow(clippy::type_complexity)]
struct ManagerReportContext {
    callback: *mut Box<dyn Fn(HidDevice, HidInputReport) + Send + Sync + 'static>,
}

#[allow(clippy::type_complexity)]
struct ManagerValueContext {
    callback: *mut Box<dyn Fn(HidDevice, &HidValue) + Send + Sync + 'static>,
}

unsafe extern "C" fn manager_device_trampoline(
    context: *mut c_void,
    result: ffi::IOReturn,
    _sender: *mut c_void,
    device: ffi::IOHIDDeviceRef,
) {
    if context.is_null() || device.is_null() || result != ffi::kIOReturnSuccess {
        return;
    }
    unsafe { ffi::CFRetain(device) };
    // SAFETY: context is non-null (checked above) and points to a
    // `ManagerDeviceContext` whose `callback` field was `Box::into_raw`'d
    // when the subscription was created.
    let callback = unsafe { &*(*context.cast::<ManagerDeviceContext>()).callback };
    catch_user_panic("manager_device_trampoline", || {
        callback(HidDevice { raw: device });
    });
}

unsafe extern "C" fn manager_report_trampoline(
    context: *mut c_void,
    result: ffi::IOReturn,
    sender: *mut c_void,
    report_type: ffi::IOHIDReportType,
    report_id: u32,
    report: *mut u8,
    report_length: ffi::CFIndex,
) {
    if context.is_null() || sender.is_null() || report.is_null() || result != ffi::kIOReturnSuccess
    {
        return;
    }
    let Some(device) = retained_device_from_sender(sender) else {
        return;
    };
    let length = usize::try_from(report_length).unwrap_or(0);
    let bytes = unsafe { core::slice::from_raw_parts(report.cast_const(), length) }.to_vec();
    // SAFETY: context is non-null (checked above) and points to a
    // `ManagerReportContext` whose `callback` field was `Box::into_raw`'d
    // when the subscription was created.
    let callback = unsafe { &*(*context.cast::<ManagerReportContext>()).callback };
    catch_user_panic("manager_report_trampoline", || {
        callback(
            device,
            HidInputReport {
                report_type: HidReportType::from_raw(report_type),
                report_id,
                bytes,
                timestamp: 0,
            },
        );
    });
}

unsafe extern "C" fn manager_timestamped_report_trampoline(
    context: *mut c_void,
    result: ffi::IOReturn,
    sender: *mut c_void,
    report_type: ffi::IOHIDReportType,
    report_id: u32,
    report: *mut u8,
    report_length: ffi::CFIndex,
    timestamp: u64,
) {
    if context.is_null() || sender.is_null() || report.is_null() || result != ffi::kIOReturnSuccess
    {
        return;
    }
    let Some(device) = retained_device_from_sender(sender) else {
        return;
    };
    let length = usize::try_from(report_length).unwrap_or(0);
    let bytes = unsafe { core::slice::from_raw_parts(report.cast_const(), length) }.to_vec();
    // SAFETY: context is non-null (checked above) and points to a
    // `ManagerReportContext` whose `callback` field was `Box::into_raw`'d
    // when the subscription was created.
    let callback = unsafe { &*(*context.cast::<ManagerReportContext>()).callback };
    catch_user_panic("manager_timestamped_report_trampoline", || {
        callback(
            device,
            HidInputReport {
                report_type: HidReportType::from_raw(report_type),
                report_id,
                bytes,
                timestamp,
            },
        );
    });
}

unsafe extern "C" fn manager_value_trampoline(
    context: *mut c_void,
    result: ffi::IOReturn,
    sender: *mut c_void,
    value: ffi::IOHIDValueRef,
) {
    if context.is_null() || sender.is_null() || value.is_null() || result != ffi::kIOReturnSuccess {
        return;
    }
    let Some(device) = retained_device_from_sender(sender) else {
        return;
    };
    let Some(value) = clone_value_ref(value) else {
        return;
    };
    // SAFETY: context is non-null (checked above) and points to a
    // `ManagerValueContext` whose `callback` field was `Box::into_raw`'d
    // when the subscription was created.
    let callback = unsafe { &*(*context.cast::<ManagerValueContext>()).callback };
    catch_user_panic("manager_value_trampoline", || {
        callback(device, &value);
    });
}

fn retained_device_from_sender(sender: *mut c_void) -> Option<HidDevice> {
    let device = sender.cast::<c_void>();
    if device.is_null() {
        return None;
    }
    unsafe { ffi::CFRetain(device) };
    Some(HidDevice { raw: device.cast() })
}

fn schedule_manager(manager: ffi::IOHIDManagerRef) -> ffi::CFRunLoopRef {
    let run_loop = unsafe { ffi::CFRunLoopGetCurrent() };
    unsafe {
        ffi::IOHIDManagerScheduleWithRunLoop(manager, run_loop, ffi::kCFRunLoopDefaultMode);
    }
    run_loop
}

unsafe fn unschedule_manager(manager: ffi::IOHIDManagerRef, run_loop: ffi::CFRunLoopRef) {
    ffi::IOHIDManagerUnscheduleFromRunLoop(manager, run_loop, ffi::kCFRunLoopDefaultMode);
}

/// Owns a registration from `IOHIDManagerRegisterDeviceMatchingCallback` or `IOHIDManagerRegisterDeviceRemovalCallback`.
pub struct ManagerDeviceSubscription {
    manager: ffi::IOHIDManagerRef,
    run_loop: ffi::CFRunLoopRef,
    context: *mut ManagerDeviceContext,
    kind: ManagerDeviceCallbackKind,
}

unsafe impl Send for ManagerDeviceSubscription {}

impl Drop for ManagerDeviceSubscription {
    fn drop(&mut self) {
        if self.manager.is_null() || self.context.is_null() {
            return;
        }
        unsafe {
            match self.kind {
                ManagerDeviceCallbackKind::Matching => {
                    ffi::IOHIDManagerRegisterDeviceMatchingCallback(
                        self.manager,
                        None,
                        ptr::null_mut(),
                    );
                }
                ManagerDeviceCallbackKind::Removal => {
                    ffi::IOHIDManagerRegisterDeviceRemovalCallback(
                        self.manager,
                        None,
                        ptr::null_mut(),
                    );
                }
            }
            unschedule_manager(self.manager, self.run_loop);
            ffi::CFRelease(self.manager);
            let context = Box::from_raw(self.context);
            let _ = Box::from_raw(context.callback);
        }
        self.manager = ptr::null();
        self.context = ptr::null_mut();
    }
}

/// Owns a registration from `IOHIDManagerRegisterInputReportCallback` or `IOHIDManagerRegisterInputReportWithTimeStampCallback`.
pub struct ManagerReportSubscription {
    manager: ffi::IOHIDManagerRef,
    run_loop: ffi::CFRunLoopRef,
    context: *mut ManagerReportContext,
    kind: ManagerReportCallbackKind,
}

unsafe impl Send for ManagerReportSubscription {}

impl Drop for ManagerReportSubscription {
    fn drop(&mut self) {
        if self.manager.is_null() || self.context.is_null() {
            return;
        }
        unsafe {
            match self.kind {
                ManagerReportCallbackKind::Untimestamped => {
                    ffi::IOHIDManagerRegisterInputReportCallback(
                        self.manager,
                        None,
                        ptr::null_mut(),
                    );
                }
                ManagerReportCallbackKind::Timestamped => {
                    ffi::IOHIDManagerRegisterInputReportWithTimeStampCallback(
                        self.manager,
                        None,
                        ptr::null_mut(),
                    );
                }
            }
            unschedule_manager(self.manager, self.run_loop);
            ffi::CFRelease(self.manager);
            let context = Box::from_raw(self.context);
            let _ = Box::from_raw(context.callback);
        }
        self.manager = ptr::null();
        self.context = ptr::null_mut();
    }
}

/// Owns a registration from `IOHIDManagerRegisterInputValueCallback`.
pub struct ManagerValueSubscription {
    manager: ffi::IOHIDManagerRef,
    run_loop: ffi::CFRunLoopRef,
    context: *mut ManagerValueContext,
}

unsafe impl Send for ManagerValueSubscription {}

impl Drop for ManagerValueSubscription {
    fn drop(&mut self) {
        if self.manager.is_null() || self.context.is_null() {
            return;
        }
        unsafe {
            ffi::IOHIDManagerRegisterInputValueCallback(self.manager, None, ptr::null_mut());
            unschedule_manager(self.manager, self.run_loop);
            ffi::CFRelease(self.manager);
            let context = Box::from_raw(self.context);
            let _ = Box::from_raw(context.callback);
        }
        self.manager = ptr::null();
        self.context = ptr::null_mut();
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
/// Wraps the `IOHIDManagerOptions` bitfield used by `IOHIDManagerCreate`.
pub struct HidManagerOptions(u32);

impl HidManagerOptions {
    /// Mirrors `kIOHIDManagerOptionNone`.
    pub const NONE: Self = Self(ffi::kIOHIDManagerOptionNone);
    /// Mirrors `kIOHIDManagerOptionUsePersistentProperties`.
    pub const USE_PERSISTENT_PROPERTIES: Self =
        Self(ffi::kIOHIDManagerOptionUsePersistentProperties);
    /// Mirrors `kIOHIDManagerOptionDoNotLoadProperties`.
    pub const DO_NOT_LOAD_PROPERTIES: Self = Self(ffi::kIOHIDManagerOptionDoNotLoadProperties);
    /// Mirrors `kIOHIDManagerOptionDoNotSaveProperties`.
    pub const DO_NOT_SAVE_PROPERTIES: Self = Self(ffi::kIOHIDManagerOptionDoNotSaveProperties);
    /// Mirrors `kIOHIDManagerOptionIndependentDevices`.
    pub const INDEPENDENT_DEVICES: Self = Self(ffi::kIOHIDManagerOptionIndependentDevices);

    #[must_use]
    /// Mirrors `IOHIDManagerOptions`.
    pub const fn bits(self) -> ffi::IOHIDManagerOptions {
        self.0
    }

    #[must_use]
    /// Mirrors `IOHIDManagerOptions`.
    pub const fn from_bits(bits: ffi::IOHIDManagerOptions) -> Self {
        Self(bits)
    }
}

impl core::ops::BitOr for HidManagerOptions {
    type Output = Self;

    fn bitor(self, rhs: Self) -> Self::Output {
        Self(self.0 | rhs.0)
    }
}

impl core::ops::BitOrAssign for HidManagerOptions {
    fn bitor_assign(&mut self, rhs: Self) {
        self.0 |= rhs.0;
    }
}

#[allow(clippy::missing_errors_doc, clippy::type_complexity)]
impl HidManager {
    /// Wraps `IOHIDManagerCreate` and `IOHIDManagerOpen`.
    pub fn with_options(options: HidManagerOptions) -> Result<Self, HidError> {
        let bits = options.bits();
        let raw = unsafe { ffi::IOHIDManagerCreate(ffi::kCFAllocatorDefault, bits) };
        if raw.is_null() {
            return Err(HidError::ManagerCreateFailed);
        }
        let status = unsafe { ffi::IOHIDManagerOpen(raw, bits) };
        if status != ffi::kIOReturnSuccess {
            unsafe { ffi::CFRelease(raw) };
            return Err(HidError::ManagerOpenFailed(status));
        }
        Ok(Self { raw })
    }

    /// Wraps `IOHIDManagerSaveToPropertyDomain` with `HidManagerOptions`.
    pub fn save_to_property_domain_with_options(
        &self,
        application_id: &str,
        user_name: &str,
        host_name: &str,
        options: HidManagerOptions,
    ) -> Result<(), HidError> {
        self.save_to_property_domain(application_id, user_name, host_name, options.bits())
    }
    /// Wraps `IOHIDManagerActivate`.
    pub fn activate(&self) {
        unsafe { ffi::IOHIDManagerActivate(self.raw) };
    }

    /// Wraps `IOHIDManagerCancel`.
    pub fn cancel(&self) {
        unsafe { ffi::IOHIDManagerCancel(self.raw) };
    }

    /// Wraps `IOHIDManagerSetInputValueMatching`.
    pub fn set_input_value_matching(
        &self,
        matching: Option<&ElementMatch>,
    ) -> Result<(), HidError> {
        match matching {
            None => unsafe {
                ffi::IOHIDManagerSetInputValueMatching(self.raw, ptr::null());
                Ok(())
            },
            Some(matching) => {
                let dict = matching.to_cf_dictionary()?;
                unsafe {
                    ffi::IOHIDManagerSetInputValueMatching(self.raw, dict);
                    ffi::CFRelease(dict.cast());
                }
                Ok(())
            }
        }
    }

    /// Wraps `IOHIDManagerSetInputValueMatchingMultiple`.
    pub fn set_input_value_matching_multiple(
        &self,
        matches: &[ElementMatch],
    ) -> Result<(), HidError> {
        if matches.is_empty() {
            unsafe {
                ffi::IOHIDManagerSetInputValueMatchingMultiple(self.raw, ptr::null());
            }
            return Ok(());
        }
        let array = build_cf_dictionary_array(matches, ElementMatch::to_cf_dictionary)?;
        unsafe {
            ffi::IOHIDManagerSetInputValueMatchingMultiple(self.raw, array);
            ffi::CFRelease(array.cast());
        }
        Ok(())
    }

    /// Wraps `IOHIDManagerRegisterDeviceMatchingCallback`.
    pub fn on_device_matching<F>(&self, callback: F) -> Result<ManagerDeviceSubscription, HidError>
    where
        F: Fn(HidDevice) + Send + Sync + 'static,
    {
        let run_loop = schedule_manager(self.raw);
        let callback: Box<dyn Fn(HidDevice) + Send + Sync + 'static> = Box::new(callback);
        let callback_ptr = Box::into_raw(Box::new(callback));
        let context_ptr = Box::into_raw(Box::new(ManagerDeviceContext {
            callback: callback_ptr,
        }));
        unsafe {
            ffi::IOHIDManagerRegisterDeviceMatchingCallback(
                self.raw,
                Some(manager_device_trampoline),
                context_ptr.cast(),
            );
            ffi::CFRetain(self.raw);
        }
        Ok(ManagerDeviceSubscription {
            manager: self.raw,
            run_loop,
            context: context_ptr,
            kind: ManagerDeviceCallbackKind::Matching,
        })
    }

    /// Wraps `IOHIDManagerRegisterDeviceRemovalCallback`.
    pub fn on_device_removal<F>(&self, callback: F) -> Result<ManagerDeviceSubscription, HidError>
    where
        F: Fn(HidDevice) + Send + Sync + 'static,
    {
        let run_loop = schedule_manager(self.raw);
        let callback: Box<dyn Fn(HidDevice) + Send + Sync + 'static> = Box::new(callback);
        let callback_ptr = Box::into_raw(Box::new(callback));
        let context_ptr = Box::into_raw(Box::new(ManagerDeviceContext {
            callback: callback_ptr,
        }));
        unsafe {
            ffi::IOHIDManagerRegisterDeviceRemovalCallback(
                self.raw,
                Some(manager_device_trampoline),
                context_ptr.cast(),
            );
            ffi::CFRetain(self.raw);
        }
        Ok(ManagerDeviceSubscription {
            manager: self.raw,
            run_loop,
            context: context_ptr,
            kind: ManagerDeviceCallbackKind::Removal,
        })
    }

    /// Wraps `IOHIDManagerRegisterInputReportCallback`.
    pub fn on_input_report<F>(&self, callback: F) -> Result<ManagerReportSubscription, HidError>
    where
        F: Fn(HidDevice, HidInputReport) + Send + Sync + 'static,
    {
        let run_loop = schedule_manager(self.raw);
        let callback: Box<dyn Fn(HidDevice, HidInputReport) + Send + Sync + 'static> =
            Box::new(callback);
        let callback_ptr = Box::into_raw(Box::new(callback));
        let context_ptr = Box::into_raw(Box::new(ManagerReportContext {
            callback: callback_ptr,
        }));
        unsafe {
            ffi::IOHIDManagerRegisterInputReportCallback(
                self.raw,
                Some(manager_report_trampoline),
                context_ptr.cast(),
            );
            ffi::CFRetain(self.raw);
        }
        Ok(ManagerReportSubscription {
            manager: self.raw,
            run_loop,
            context: context_ptr,
            kind: ManagerReportCallbackKind::Untimestamped,
        })
    }

    /// Wraps `IOHIDManagerRegisterInputReportWithTimeStampCallback`.
    pub fn on_input_report_with_timestamp<F>(
        &self,
        callback: F,
    ) -> Result<ManagerReportSubscription, HidError>
    where
        F: Fn(HidDevice, HidInputReport) + Send + Sync + 'static,
    {
        let run_loop = schedule_manager(self.raw);
        let callback: Box<dyn Fn(HidDevice, HidInputReport) + Send + Sync + 'static> =
            Box::new(callback);
        let callback_ptr = Box::into_raw(Box::new(callback));
        let context_ptr = Box::into_raw(Box::new(ManagerReportContext {
            callback: callback_ptr,
        }));
        unsafe {
            ffi::IOHIDManagerRegisterInputReportWithTimeStampCallback(
                self.raw,
                Some(manager_timestamped_report_trampoline),
                context_ptr.cast(),
            );
            ffi::CFRetain(self.raw);
        }
        Ok(ManagerReportSubscription {
            manager: self.raw,
            run_loop,
            context: context_ptr,
            kind: ManagerReportCallbackKind::Timestamped,
        })
    }

    /// Wraps `IOHIDManagerRegisterInputValueCallback`.
    pub fn on_input_value<F>(&self, callback: F) -> Result<ManagerValueSubscription, HidError>
    where
        F: Fn(HidDevice, &HidValue) + Send + Sync + 'static,
    {
        let run_loop = schedule_manager(self.raw);
        let callback: Box<dyn Fn(HidDevice, &HidValue) + Send + Sync + 'static> =
            Box::new(callback);
        let callback_ptr = Box::into_raw(Box::new(callback));
        let context_ptr = Box::into_raw(Box::new(ManagerValueContext {
            callback: callback_ptr,
        }));
        unsafe {
            ffi::IOHIDManagerRegisterInputValueCallback(
                self.raw,
                Some(manager_value_trampoline),
                context_ptr.cast(),
            );
            ffi::CFRetain(self.raw);
        }
        Ok(ManagerValueSubscription {
            manager: self.raw,
            run_loop,
            context: context_ptr,
        })
    }
}
