//! Safe native replacements for the CLR delegate types in the Vivox API.

use crate::Error;
use libremetaverse_types::compat::{Object, SocketException};
use std::any::Any;
use std::fmt;
use std::sync::Arc;

type AsyncCallback = Box<dyn Fn(&dyn Any) + Send + Sync>;
type AsyncResult = Box<dyn Any + Send + Sync>;

macro_rules! callback_type {
    ($name:ident ( $($argument:ident : $type:ty),* $(,)? )) => {
        #[derive(Clone)]
        pub struct $name {
            handler: Arc<dyn Fn($($type),*) -> Result<(), Error> + Send + Sync>,
        }

        impl fmt::Debug for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter
                    .debug_struct(stringify!($name))
                    .finish_non_exhaustive()
            }
        }

        impl $name {
            /// Creates a native callback without exposing CLR method pointers.
            pub fn from_handler<F>(handler: F) -> Self
            where
                F: Fn($($type),*) -> Result<(), Error> + Send + Sync + 'static,
            {
                Self { handler: Arc::new(handler) }
            }

            /// Rehydrates a callback previously stored in a mapped `Object`.
            /// A non-zero CLR method pointer cannot be invoked safely in native
            /// Rust and is therefore rejected explicitly.
            pub fn new(object: Object, method: isize) -> Result<Self, Error> {
                if method != 0 {
                    return Err(Error::Argument);
                }
                object.downcast_ref::<Self>().cloned().ok_or(Error::Argument)
            }

            pub fn invoke(&self, $($argument: $type),*) -> Result<(), Error> {
                (self.handler)($($argument),*)
            }

            pub fn begin_invoke(
                &self,
                $($argument: $type,)*
                callback: AsyncCallback,
                object: Object,
            ) -> Result<AsyncResult, Error> {
                self.invoke($($argument),*)?;
                callback(&object);
                Ok(Box::new(()))
            }

            pub fn end_invoke(&self, result: AsyncResult) -> Result<(), Error> {
                result.downcast::<()>().map(|_| ()).map_err(|_| Error::Argument)
            }
        }
    };
}

callback_type!(TCPPipeOnDisconnectedCallback(se: SocketException));
callback_type!(TCPPipeOnReceiveLineCallback(line: String));
callback_type!(VoiceGatewayDaemonConnectedCallback());
callback_type!(VoiceGatewayDaemonCouldntConnectCallback());
callback_type!(VoiceGatewayDaemonCouldntRunCallback());
callback_type!(VoiceGatewayDaemonDisconnectedCallback());
callback_type!(VoiceGatewayDaemonExitedCallback());
callback_type!(VoiceGatewayDaemonRunningCallback());
callback_type!(VoiceGatewayVoiceConnectionChangeCallback(
    state: crate::VoiceGatewayConnectionState,
));
callback_type!(VoiceGatewayVoiceMicTestCallback(level: f32));
callback_type!(VoiceManagerAuxAudioPropertiesCallback(cookie: i32, energy: f32));
callback_type!(VoiceManagerBasicActionCallback(
    cookie: i32,
    status_code: i32,
    status_string: String,
));
callback_type!(VoiceManagerConnectorCreatedCallback(
    cookie: i32,
    status_code: i32,
    status_string: String,
    connector_handle: String,
));
callback_type!(VoiceManagerDevicesCallback(
    cookie: i32,
    status_code: i32,
    status_string: String,
    current_device: String,
));
callback_type!(VoiceManagerLoginCallback(
    cookie: i32,
    status_code: i32,
    status_string: String,
    account_handle: String,
));
callback_type!(VoiceManagerLoginStateChangeCallback(
    cookie: i32,
    account_handle: String,
    status_code: i32,
    status_string: String,
    state: i32,
));
callback_type!(VoiceManagerNewSessionCallback(
    cookie: i32,
    account_handle: String,
    event_session_handle: String,
    state: i32,
    name_string: String,
    uri_string: String,
));
callback_type!(VoiceManagerParcelVoiceInfoCallback(
    region_name: String,
    local_id: i32,
    channel_uri: Option<String>,
));
callback_type!(VoiceManagerParticipantPropertiesCallback(
    cookie: i32,
    uri_string: String,
    status_code: i32,
    status_string: String,
    is_locally_muted: bool,
    is_moderator_muted: bool,
    is_speaking: bool,
    volume: i32,
    energy: f32,
));
callback_type!(VoiceManagerParticipantStateChangeCallback(
    cookie: i32,
    uri_string: String,
    status_code: i32,
    status_string: String,
    state: i32,
    name_string: String,
    display_name_string: String,
    participant_type: i32,
));
callback_type!(VoiceManagerProvisionAccountCallback(
    username: String,
    password: String,
));
callback_type!(VoiceManagerSessionCreatedCallback(
    cookie: i32,
    status_code: i32,
    status_string: String,
    session_handle: String,
));
callback_type!(VoiceManagerSessionStateChangeCallback(
    cookie: i32,
    uri_string: String,
    status_code: i32,
    status_string: String,
    event_session_handle: String,
    state: i32,
    is_channel: bool,
    name_string: String,
));

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicI32, Ordering};

    #[test]
    fn callback_invocation_and_apm_completion_execute_once() {
        let total = Arc::new(AtomicI32::new(0));
        let observed = Arc::clone(&total);
        let callback =
            VoiceManagerAuxAudioPropertiesCallback::from_handler(move |cookie, energy| {
                observed.fetch_add(cookie + energy as i32, Ordering::SeqCst);
                Ok(())
            });
        callback.invoke(2, 3.0).unwrap();
        let completion_count = Arc::new(AtomicI32::new(0));
        let completed = Arc::clone(&completion_count);
        let result = callback
            .begin_invoke(
                4,
                5.0,
                Box::new(move |_| {
                    completed.fetch_add(1, Ordering::SeqCst);
                }),
                Object::Undefined,
            )
            .unwrap();
        callback.end_invoke(result).unwrap();
        assert_eq!(total.load(Ordering::SeqCst), 14);
        assert_eq!(completion_count.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn callback_round_trips_through_mapped_object_without_raw_pointer() {
        let callback = TCPPipeOnReceiveLineCallback::from_handler(|_| Ok(()));
        let restored = TCPPipeOnReceiveLineCallback::new(Object::opaque(callback), 0).unwrap();
        restored.invoke("line".into()).unwrap();
        assert!(TCPPipeOnReceiveLineCallback::new(Object::Undefined, 0).is_err());
        assert!(TCPPipeOnReceiveLineCallback::new(Object::Undefined, 1).is_err());
    }
}
