use std::ffi::{CString, NulError, c_char, c_void};
use std::marker::PhantomData;
use std::sync::Arc;

use libloading::Library;
use libloading::os::unix::Symbol as UnixSymbol;

type NotifyInit = unsafe extern "C" fn(*const c_char) -> bool;
type NotifyUninit = unsafe extern "C" fn();
type NotifyNotificationNew =
    unsafe extern "C" fn(*const c_char, *const c_char, *const c_char) -> *mut c_void;
type NotifyNotificationShow = unsafe extern "C" fn(*mut c_void, *mut *mut c_void) -> i32;
type NotifyNotificationSetTimeout = unsafe extern "C" fn(*mut c_void, i32);

/// Represents an uninitialized notification system
struct NotificationSystem {
    _lib: Library,
    notify_init: UnixSymbol<NotifyInit>,
    notify_uninit: UnixSymbol<NotifyUninit>,
    notify_notification_new: UnixSymbol<NotifyNotificationNew>,
    notify_notification_show: UnixSymbol<NotifyNotificationShow>,
    notify_notification_set_timeout: UnixSymbol<NotifyNotificationSetTimeout>,
}

/// Represents an initialized notification system with working notifications
pub struct InitializedNotificationSystem {
    inner: Arc<NotificationSystem>,
    _marker: PhantomData<()>,
}

impl NotificationSystem {
    pub fn new() -> Result<Self, libloading::Error> {
        unsafe {
            let lib = Library::new("libnotify.so.4")?;

            Ok(Self {
                notify_init: lib.get::<NotifyInit>(b"notify_init\0")?.into_raw(),
                notify_uninit: lib.get::<NotifyUninit>(b"notify_uninit\0")?.into_raw(),
                notify_notification_new: lib
                    .get::<NotifyNotificationNew>(b"notify_notification_new\0")?
                    .into_raw(),
                notify_notification_show: lib
                    .get::<NotifyNotificationShow>(b"notify_notification_show\0")?
                    .into_raw(),
                notify_notification_set_timeout: lib
                    .get::<NotifyNotificationSetTimeout>(b"notify_notification_set_timeout\0")?
                    .into_raw(),
                _lib: lib,
            })
        }
    }

    /// Initialize the notification system. Consumes the uninitialized version
    /// and returns an initialized version.
    pub fn init(self, app_name: &str) -> Result<InitializedNotificationSystem, NulError> {
        let app_name = CString::new(app_name)?;
        unsafe {
            if (self.notify_init)(app_name.as_ptr()) {
                Ok(InitializedNotificationSystem {
                    inner: Arc::new(self), // Wrap self in Arc
                    _marker: PhantomData,
                })
            } else {
                panic!("notify_init failed") // Convert to proper error type
            }
        }
    }
}

impl Drop for NotificationSystem {
    fn drop(&mut self) {
        // Safety: Last reference to the library is being dropped
        unsafe { (self.notify_uninit)() };
    }
}

impl InitializedNotificationSystem {
    pub fn new(app_name: &str) -> Result<Self, Box<dyn std::error::Error>> {
        let lib = NotificationSystem::new()?;
        Ok(lib.init(app_name)?)
    }
    pub fn show_notification(
        &self,
        summary: &str,
        body: &str,
        icon: &str,
        timeout_ms: i32,
    ) -> Result<(), NotificationError> {
        let summary = CString::new(summary)?;
        let body = CString::new(body)?;
        let icon = CString::new(icon)?;

        unsafe {
            let notification = (self.inner.notify_notification_new)(
                summary.as_ptr(),
                body.as_ptr(),
                icon.as_ptr(),
            );

            (self.inner.notify_notification_set_timeout)(notification, timeout_ms);

            // Handle potential errors from the C library
            let mut error_ptr: *mut c_void = std::ptr::null_mut();
            let success = (self.inner.notify_notification_show)(notification, &mut error_ptr);

            if success != 0 {
                Ok(())
            } else {
                Err(NotificationError::NativeError(error_ptr))
            }
        }
    }
}

#[derive(Debug)]
pub enum NotificationError {
    NullByteInString(NulError),
    NativeError(*mut c_void),
}

impl From<NulError> for NotificationError {
    fn from(e: NulError) -> Self {
        NotificationError::NullByteInString(e)
    }
}
