use crate::sendable::Sendable;
use std::{
    collections::{btree_map::Entry, BTreeMap},
    ffi::{c_void, CStr},
    sync::Mutex,
};

// Helper function for adding the entry `(key, (observer, action))` to `map`.
fn register<K: Eq + Ord>(
    map: &mut BTreeMap<K, Vec<(Option<Sendable>, Action)>>,
    key: K,
    observer: Option<Sendable>,
    action: Action,
) {
    match map.entry(key) {
        Entry::Occupied(mut o) => {
            o.get_mut().push((observer, action));
        }
        Entry::Vacant(v) => {
            v.insert(vec![(observer, action)]);
        }
    }
}

/// Lightweight message from object to another. When a notification is sent,
/// registered [`Action`]s are called.
///
/// Use [`ffi::WMAddNotificationObserver`] or [`NotificationCenter::register`]
/// to request notifications.
///
/// ## Safety
///
/// `Notification` encapsulates two data pointers. The Rust implementation
/// explicitly supports notifications across threads. To uphold Rust's safety
/// rules, `Notification`s must only be created with pointers that can be sent
/// across threads. The [`Sendable`] struct is provided to guarantee this.
///
/// ## Rust rewrite notes
///
/// This was originally a reference-counted structure, but it is so lightweight
/// (consisting of three pointers) that the Rust version is `Copy`. This
/// simplifies things --- each Rust recipient of a `Notification` owns it, and
/// there is no need to coordinate how it is cleaned up.
///
/// In unported C code, a notificaton's `name` may be compared against a string
/// constant using pointer equality rather than string equality. This has
/// negative implications for sending notifications across the Rust/C
/// boundary. For the time being, notifications are generated and received by
/// code written in C, but care should be taken that notification names are
/// checked properly when porting notification registration in the future. (We
/// will ideally move to a different data type for identifying notifications,
/// too.)
#[derive(Clone, Copy)]
pub struct Notification {
    name: &'static CStr,
    /// The object that generated the notification. This may be `None` for
    /// notifications that are about global state.
    source: Option<Sendable>,
    /// Optional side-channel data provided to notification listeners.
    client_data: Option<Sendable>,
}

/// Callback that notifies `observer` (which may be null) of `notification` (which won't be).
pub type Action = unsafe extern "C" fn(observer: *mut c_void, notification: *const Notification);

pub struct NotificationCenter {
    /// Notification subscriptions that match on name and source.
    exact: BTreeMap<(&'static CStr, Sendable), Vec<(Option<Sendable>, Action)>>,
    /// Notification subscriptions that match on name.
    by_name: BTreeMap<&'static CStr, Vec<(Option<Sendable>, Action)>>,
    /// Notification subscriptions that match on source.
    by_source: BTreeMap<Sendable, Vec<(Option<Sendable>, Action)>>,
    /// Notification subscriptions that match all notifications.
    universal: Vec<(Option<Sendable>, Action)>,
}

// It is safe to send NotificationCenter across threads as long as the contract
// on Sendable is respected.
unsafe impl Send for NotificationCenter {}

impl NotificationCenter {
    /// Creates a new `NotificationCenter`.
    pub const fn new() -> Self {
        NotificationCenter {
            exact: BTreeMap::new(),
            by_name: BTreeMap::new(),
            by_source: BTreeMap::new(),
            universal: Vec::new(),
        }
    }

    /// Provides access to the default, process-wide notification center.
    ///
    /// The FFI C API uses this notification center. This is protected behind a
    /// mutex that is held while `f` is run, so:
    ///
    /// * Panicking inside of `f` should be avoided.
    /// * Triggering notification dispatch recursively (as when a notification
    ///   callback dispatches another notification) must be done with care. This
    ///   is handled correctly by [`ffi::WMPostNotificationName`], which does not
    ///   hold the mutex during callback execution.
    pub fn with_global_default<R>(f: impl FnOnce(&mut Self) -> R) -> R {
        static INSTANCE: Mutex<NotificationCenter> = Mutex::new(NotificationCenter::new());
        f(&mut INSTANCE
            .try_lock()
            .expect("cannot unlock notification center (recursive notification dispatch?)"))
    }

    /// Registers `action` to be invoked and invoked on `observer` when
    /// notifications named `name` are fired from `source`.
    pub fn register_exact(
        &mut self,
        name: &'static CStr,
        source: Sendable,
        observer: Option<Sendable>,
        action: Action,
    ) {
        register(&mut self.exact, (name, source), observer, action);
    }

    /// Registers `action` to be invoked on `observer` when notifications are
    /// fired by `source` (regardless of the notification name).
    pub fn register_by_source(
        &mut self,
        source: Sendable,
        observer: Option<Sendable>,
        action: Action,
    ) {
        register(&mut self.by_source, source, observer, action);
    }

    /// Registers `action` to be invoked on `observer` for all notifications
    /// named `name`.
    pub fn register_by_name(
        &mut self,
        name: &'static CStr,
        observer: Option<Sendable>,
        action: Action,
    ) {
        register(&mut self.by_name, name, observer, action);
    }

    /// Registers `action` to be invoked on `observer` for all notifications,
    /// regardless of the notification's name or source.
    pub fn register_universal(&mut self, observer: Option<Sendable>, action: Action) {
        self.universal.push((observer, action));
    }

    /// Dispatches `notification` with registered actions.
    pub fn dispatch(&mut self, notification: Notification) {
        if let Some(observers) = self.by_name.get_mut(notification.name) {
            for (observer, action) in observers {
                let observer = Sendable::as_ptr(*observer);
                unsafe {
                    (action)(observer, &notification);
                }
            }
        }

        if let Some(source) = notification.source {
            if let Some(observers) = self.exact.get_mut(&(notification.name, source)) {
                for (observer, action) in observers {
                    let observer = Sendable::as_ptr(*observer);
                    unsafe {
                        (action)(observer, &notification);
                    }
                }
            }
            if let Some(observers) = self.by_source.get_mut(&source) {
                for (observer, action) in observers {
                    let observer = Sendable::as_ptr(*observer);
                    unsafe {
                        (action)(observer, &notification);
                    }
                }
            }
        }

        for (observer, action) in &mut self.universal {
            let observer = Sendable::as_ptr(*observer);
            unsafe {
                (action)(observer, &notification);
            }
        }
    }

    /// Returns an independent callback that dispatches `notification` when
    /// invoked.
    ///
    /// This should be used when `self` is under a non-reentrant lock (such as a
    /// mutex) which renders it inaccessible when dispatching
    /// `notification`. Callback pointers are copied into the return value,
    /// after which the lock on `self` may be relinquished, and then the return
    /// value may be invoked, and notification dispatch will actually happen.
    fn dispatch_actions(&self, notification: Notification) -> Box<dyn FnOnce()> {
        let mut actions: Vec<(Option<Sendable>, Action)> = Vec::new();

        if let Some(observers) = self.by_name.get(notification.name) {
            for x in observers.iter().copied() {
                actions.push(x);
            }
        }

        if let Some(source) = notification.source {
            if let Some(observers) = self.exact.get(&(notification.name, source)) {
                for x in observers.iter().copied() {
                    actions.push(x);
                }
            }
            if let Some(observers) = self.by_source.get(&source) {
                for x in observers.iter().copied() {
                    actions.push(x);
                }
            }
        }

        for x in self.universal.iter().copied() {
            actions.push(x);
        }

        Box::new(move || {
            for (observer, action) in actions.into_iter() {
                let observer = Sendable::as_ptr(observer);
                unsafe {
                    (action)(observer, &notification);
                }
            }
        })
    }

    /// Removes all notification subscriptions that would notify `observer` if they fired.
    pub fn remove_observer(&mut self, observer: Sendable) {
        self.exact.retain(|_, values| {
            values.retain(|(o, _)| *o != Some(observer));
            !values.is_empty()
        });
        self.by_name.retain(|_, values| {
            values.retain(|(o, _)| *o != Some(observer));
            !values.is_empty()
        });
        self.by_source.retain(|_, values| {
            values.retain(|(o, _)| *o != Some(observer));
            !values.is_empty()
        });
        self.universal.retain(|(o, _)| *o != Some(observer));
    }

    /// Clears all registered notification listeners and resets `self` to its
    /// default state.
    pub fn clear(&mut self) {
        *self = Self::new();
    }
}

pub mod ffi {
    use super::{Action, Notification, NotificationCenter, Sendable};

    use std::{
        ffi::{c_char, c_void, CStr},
        ptr::{self, NonNull},
    };

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetNotificationClientData(
        notification: *mut Notification,
    ) -> *mut c_void {
        if notification.is_null() {
            return ptr::null_mut();
        }
        unsafe {
            (*notification)
                .client_data
                .map(|x| x.ptr.as_ptr())
                .unwrap_or(ptr::null_mut())
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetNotificationObject(
        notification: *mut Notification,
    ) -> *mut c_void {
        if notification.is_null() {
            return ptr::null_mut();
        }
        unsafe {
            (*notification)
                .source
                .map(|x| x.ptr.as_ptr())
                .unwrap_or(ptr::null_mut())
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetNotificationName(
        notification: *mut Notification,
    ) -> *const c_char {
        if notification.is_null() {
            return ptr::null_mut();
        }
        unsafe { (*notification).name.as_ptr() }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMAddNotificationObserver(
        action: Option<Action>,
        observer: *mut c_void,
        name: *const c_char,
        object: *mut c_void,
    ) {
        let Some(action) = action else {
            return;
        };
        let observer = unsafe { Sendable::from_nullable(observer) };
        let source = NonNull::new(object);
        NotificationCenter::with_global_default(|c| {
            if name.is_null() {
                match source {
                    Some(source) => {
                        c.register_by_source(unsafe { Sendable::new(source) }, observer, action);
                    }
                    None => c.register_universal(observer, action),
                }
            } else {
                let name = unsafe { CStr::from_ptr(name) };
                match source {
                    Some(source) => {
                        c.register_exact(name, unsafe { Sendable::new(source) }, observer, action);
                    }
                    None => c.register_by_name(name, observer, action),
                }
            }
        });
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMRemoveNotificationObserver(observer: *mut c_void) {
        let Some(observer) = NonNull::new(observer) else {
            return;
        };

        NotificationCenter::with_global_default(|c| {
            c.remove_observer(unsafe { Sendable::new(observer) })
        });
    }

    /// Posts a notification from `object` with the given `name` and `client_data`.
    ///
    /// ## Safety
    ///
    /// `name` must be a non-null string constant or some other pointer with a
    /// static lifetime.
    ///
    /// `object` and `client_data` must be safe to send across threads (per the
    /// contract of [`Sendable`]).
    ///
    /// ## Rust rewrite notes
    ///
    /// This originally took a heap-allocated `*mut Notification`, but now the
    /// constructed `Notification` parameters are passed directly.
    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMPostNotificationName(
        name: *const c_char,
        object: *mut c_void,
        client_data: *mut c_void,
    ) {
        if name.is_null() {
            return;
        }
        let name = unsafe { CStr::from_ptr(name) };
        let source = unsafe { Sendable::from_nullable(object) };
        let client_data = unsafe { Sendable::from_nullable(client_data) };

        let todo = NotificationCenter::with_global_default(|c| {
            c.dispatch_actions(Notification {
                name,
                source,
                client_data,
            })
        });
        (todo)();
    }

    /// Resets all notifications for the global notification center. Used by
    /// `WApplication` teardown code. This is a private, WINGs-internal API.
    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn W_ClearNotificationCenter() {
        NotificationCenter::with_global_default(|c| c.clear());
    }
}

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

    #[test]
    fn reentrant_global_notification() {
        static NOTIFICATION_A_NAME: &'static CStr = c"MyNotificationA";
        static NOTIFICATION_B_NAME: &'static CStr = c"MyNotificationB";
        static DATA_A: atomic::AtomicU16 = atomic::AtomicU16::new(0);
        static DATA_B: atomic::AtomicU16 = atomic::AtomicU16::new(0);

        unsafe extern "C" fn action_a(observer: *mut c_void, notification: *const Notification) {
            assert!(observer.is_null());
            assert!(!notification.is_null());
            assert_eq!(DATA_A.fetch_add(1, atomic::Ordering::SeqCst), 0);
            unsafe {
                // Trigger action_b.
                WMPostNotificationName(
                    NOTIFICATION_B_NAME.as_ptr(),
                    ptr::null_mut(),
                    ptr::null_mut(),
                );
            }
        }

        unsafe extern "C" fn action_b(observer: *mut c_void, notification: *const Notification) {
            assert!(observer.is_null());
            assert!(!notification.is_null());
            assert_eq!(DATA_A.load(atomic::Ordering::SeqCst), 1);
            assert_eq!(DATA_B.fetch_add(1, atomic::Ordering::SeqCst), 0);
        }

        assert_eq!(DATA_A.load(atomic::Ordering::SeqCst), 0);
        assert_eq!(DATA_B.load(atomic::Ordering::SeqCst), 0);

        unsafe {
            WMAddNotificationObserver(
                Some(action_a),
                ptr::null_mut(),
                NOTIFICATION_A_NAME.as_ptr(),
                ptr::null_mut(),
            );
            WMAddNotificationObserver(
                Some(action_b),
                ptr::null_mut(),
                NOTIFICATION_B_NAME.as_ptr(),
                ptr::null_mut(),
            );
            WMPostNotificationName(
                NOTIFICATION_A_NAME.as_ptr(),
                ptr::null_mut(),
                ptr::null_mut(),
            );
        }

        assert_eq!(DATA_A.load(atomic::Ordering::SeqCst), 1);
        assert_eq!(DATA_B.load(atomic::Ordering::SeqCst), 1);
    }
}
