// Take a look at the license at the top of the repository in the LICENSE file.

use std::{
    future,
    mem::transmute,
    pin::Pin,
    sync::{Arc, Mutex},
    task::{Context, Poll},
};

use futures_channel::mpsc::{self, UnboundedReceiver};
use futures_core::Stream;
use futures_util::{StreamExt, stream::FusedStream};
use glib::{
    ControlFlow,
    ffi::{gboolean, gpointer},
    prelude::*,
    source::Priority,
    translate::*,
};

use crate::{Bus, BusSyncReply, Message, MessageType, ffi};

unsafe extern "C" fn trampoline_watch<F: FnMut(&Bus, &Message) -> ControlFlow + Send + 'static>(
    bus: *mut ffi::GstBus,
    msg: *mut ffi::GstMessage,
    func: gpointer,
) -> gboolean {
    unsafe {
        let func: &mut F = &mut *(func as *mut F);
        func(&from_glib_borrow(bus), &Message::from_glib_borrow(msg)).into_glib()
    }
}

unsafe extern "C" fn destroy_closure_watch<
    F: FnMut(&Bus, &Message) -> ControlFlow + Send + 'static,
>(
    ptr: gpointer,
) {
    unsafe {
        let _ = Box::<F>::from_raw(ptr as *mut _);
    }
}

fn into_raw_watch<F: FnMut(&Bus, &Message) -> ControlFlow + Send + 'static>(func: F) -> gpointer {
    #[allow(clippy::type_complexity)]
    let func: Box<F> = Box::new(func);
    Box::into_raw(func) as gpointer
}

unsafe extern "C" fn trampoline_watch_local<F: FnMut(&Bus, &Message) -> ControlFlow + 'static>(
    bus: *mut ffi::GstBus,
    msg: *mut ffi::GstMessage,
    func: gpointer,
) -> gboolean {
    unsafe {
        let func: &mut glib::thread_guard::ThreadGuard<F> =
            &mut *(func as *mut glib::thread_guard::ThreadGuard<F>);
        (func.get_mut())(&from_glib_borrow(bus), &Message::from_glib_borrow(msg)).into_glib()
    }
}

unsafe extern "C" fn destroy_closure_watch_local<
    F: FnMut(&Bus, &Message) -> ControlFlow + 'static,
>(
    ptr: gpointer,
) {
    unsafe {
        let _ = Box::<glib::thread_guard::ThreadGuard<F>>::from_raw(ptr as *mut _);
    }
}

fn into_raw_watch_local<F: FnMut(&Bus, &Message) -> ControlFlow + 'static>(func: F) -> gpointer {
    #[allow(clippy::type_complexity)]
    let func: Box<glib::thread_guard::ThreadGuard<F>> =
        Box::new(glib::thread_guard::ThreadGuard::new(func));
    Box::into_raw(func) as gpointer
}

unsafe extern "C" fn trampoline_sync<
    F: Fn(&Bus, &Message) -> BusSyncReply + Send + Sync + 'static,
>(
    bus: *mut ffi::GstBus,
    msg: *mut ffi::GstMessage,
    func: gpointer,
) -> ffi::GstBusSyncReply {
    unsafe {
        let f: &F = &*(func as *const F);
        let res = f(&from_glib_borrow(bus), &Message::from_glib_borrow(msg)).into_glib();

        if res == ffi::GST_BUS_DROP {
            ffi::gst_mini_object_unref(msg as *mut _);
        }

        res
    }
}

unsafe extern "C" fn destroy_closure_sync<
    F: Fn(&Bus, &Message) -> BusSyncReply + Send + Sync + 'static,
>(
    ptr: gpointer,
) {
    unsafe {
        let _ = Box::<F>::from_raw(ptr as *mut _);
    }
}

fn into_raw_sync<F: Fn(&Bus, &Message) -> BusSyncReply + Send + Sync + 'static>(
    func: F,
) -> gpointer {
    let func: Box<F> = Box::new(func);
    Box::into_raw(func) as gpointer
}

impl Bus {
    #[doc(alias = "gst_bus_add_signal_watch")]
    #[doc(alias = "gst_bus_add_signal_watch_full")]
    pub fn add_signal_watch_full(&self, priority: Priority) {
        unsafe {
            ffi::gst_bus_add_signal_watch_full(self.to_glib_none().0, priority.into_glib());
        }
    }

    #[doc(alias = "gst_bus_create_watch")]
    pub fn create_watch<F>(&self, name: Option<&str>, priority: Priority, func: F) -> glib::Source
    where
        F: FnMut(&Bus, &Message) -> ControlFlow + Send + 'static,
    {
        skip_assert_initialized!();
        unsafe {
            let source = ffi::gst_bus_create_watch(self.to_glib_none().0);
            glib::ffi::g_source_set_callback(
                source,
                Some(transmute::<
                    *mut (),
                    unsafe extern "C" fn(glib::ffi::gpointer) -> i32,
                >(trampoline_watch::<F> as *mut ())),
                into_raw_watch(func),
                Some(destroy_closure_watch::<F>),
            );
            glib::ffi::g_source_set_priority(source, priority.into_glib());

            if let Some(name) = name {
                glib::ffi::g_source_set_name(source, name.to_glib_none().0);
            }

            from_glib_full(source)
        }
    }

    #[doc(alias = "gst_bus_add_watch")]
    #[doc(alias = "gst_bus_add_watch_full")]
    pub fn add_watch<F>(&self, func: F) -> Result<BusWatchGuard, glib::BoolError>
    where
        F: FnMut(&Bus, &Message) -> ControlFlow + Send + 'static,
    {
        unsafe {
            let res = ffi::gst_bus_add_watch_full(
                self.to_glib_none().0,
                glib::ffi::G_PRIORITY_DEFAULT,
                Some(trampoline_watch::<F>),
                into_raw_watch(func),
                Some(destroy_closure_watch::<F>),
            );

            if res == 0 {
                Err(glib::bool_error!("Bus already has a watch"))
            } else {
                Ok(BusWatchGuard { bus: self.clone() })
            }
        }
    }

    #[doc(alias = "gst_bus_add_watch")]
    #[doc(alias = "gst_bus_add_watch_full")]
    pub fn add_watch_local<F>(&self, func: F) -> Result<BusWatchGuard, glib::BoolError>
    where
        F: FnMut(&Bus, &Message) -> ControlFlow + 'static,
    {
        unsafe {
            let ctx = glib::MainContext::ref_thread_default();
            let _acquire = ctx
                .acquire()
                .expect("thread default main context already acquired by another thread");

            let res = ffi::gst_bus_add_watch_full(
                self.to_glib_none().0,
                glib::ffi::G_PRIORITY_DEFAULT,
                Some(trampoline_watch_local::<F>),
                into_raw_watch_local(func),
                Some(destroy_closure_watch_local::<F>),
            );

            if res == 0 {
                Err(glib::bool_error!("Bus already has a watch"))
            } else {
                Ok(BusWatchGuard { bus: self.clone() })
            }
        }
    }

    #[doc(alias = "gst_bus_set_sync_handler")]
    pub fn set_sync_handler<F>(&self, func: F)
    where
        F: Fn(&Bus, &Message) -> BusSyncReply + Send + Sync + 'static,
    {
        unsafe {
            let bus = self.to_glib_none().0;

            #[allow(clippy::manual_dangling_ptr)]
            #[cfg(not(feature = "v1_18"))]
            {
                static SET_ONCE_QUARK: std::sync::OnceLock<glib::Quark> =
                    std::sync::OnceLock::new();

                let set_once_quark = SET_ONCE_QUARK
                    .get_or_init(|| glib::Quark::from_str("gstreamer-rs-sync-handler"));

                // This is not thread-safe before 1.16.3, see
                // https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/merge_requests/416
                if crate::version() < (1, 16, 3, 0) {
                    if !glib::gobject_ffi::g_object_get_qdata(
                        bus as *mut _,
                        set_once_quark.into_glib(),
                    )
                    .is_null()
                    {
                        panic!("Bus sync handler can only be set once");
                    }

                    glib::gobject_ffi::g_object_set_qdata(
                        bus as *mut _,
                        set_once_quark.into_glib(),
                        1 as *mut _,
                    );
                }
            }

            ffi::gst_bus_set_sync_handler(
                bus,
                Some(trampoline_sync::<F>),
                into_raw_sync(func),
                Some(destroy_closure_sync::<F>),
            )
        }
    }

    pub fn unset_sync_handler(&self) {
        #[cfg(not(feature = "v1_18"))]
        {
            // This is not thread-safe before 1.16.3, see
            // https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/merge_requests/416
            if crate::version() < (1, 16, 3, 0) {
                return;
            }
        }

        unsafe {
            use std::ptr;

            ffi::gst_bus_set_sync_handler(self.to_glib_none().0, None, ptr::null_mut(), None)
        }
    }

    #[doc(alias = "gst_bus_pop")]
    pub fn iter(&self) -> Iter<'_> {
        self.iter_timed(Some(crate::ClockTime::ZERO))
    }

    #[doc(alias = "gst_bus_timed_pop")]
    pub fn iter_timed(&self, timeout: impl Into<Option<crate::ClockTime>>) -> Iter<'_> {
        Iter {
            bus: self,
            timeout: timeout.into(),
        }
    }

    #[doc(alias = "gst_bus_pop_filtered")]
    pub fn iter_filtered<'a>(
        &'a self,
        msg_types: &'a [MessageType],
    ) -> impl Iterator<Item = Message> + 'a {
        self.iter_timed_filtered(Some(crate::ClockTime::ZERO), msg_types)
    }

    #[doc(alias = "gst_bus_timed_pop_filtered")]
    pub fn iter_timed_filtered<'a>(
        &'a self,
        timeout: impl Into<Option<crate::ClockTime>>,
        msg_types: &'a [MessageType],
    ) -> impl Iterator<Item = Message> + 'a {
        self.iter_timed(timeout)
            .filter(move |msg| msg_types.contains(&msg.type_()))
    }

    #[doc(alias = "gst_bus_timed_pop_filtered")]
    pub fn timed_pop_filtered(
        &self,
        timeout: impl Into<Option<crate::ClockTime>>,
        msg_types: &[MessageType],
    ) -> Option<Message> {
        // Infinite wait: just loop forever
        let Some(timeout) = timeout.into() else {
            loop {
                let msg = self.timed_pop(None)?;
                if msg_types.contains(&msg.type_()) {
                    return Some(msg);
                }
            }
        };

        // Finite timeout
        let total = timeout;
        let start = std::time::Instant::now();

        loop {
            let elapsed = crate::ClockTime::from_nseconds(start.elapsed().as_nanos() as u64);

            // If timeout budget is exhausted, return None
            let remaining = total.checked_sub(elapsed)?;

            let msg = self.timed_pop(Some(remaining))?;

            if msg_types.contains(&msg.type_()) {
                return Some(msg);
            }

            // Discard non-matching messages without restarting the timeout
        }
    }

    #[doc(alias = "gst_bus_pop_filtered")]
    pub fn pop_filtered(&self, msg_types: &[MessageType]) -> Option<Message> {
        loop {
            let msg = self.pop()?;
            if msg_types.contains(&msg.type_()) {
                return Some(msg);
            }
        }
    }

    pub fn stream(&self) -> BusStream {
        BusStream::new(self)
    }

    pub fn stream_filtered<'a>(
        &self,
        message_types: &'a [MessageType],
    ) -> impl FusedStream<Item = Message> + Unpin + Send + 'a + use<'a> {
        self.stream().filter(move |message| {
            let message_type = message.type_();

            future::ready(message_types.contains(&message_type))
        })
    }
}

#[must_use = "iterators are lazy and do nothing unless consumed"]
#[derive(Debug)]
pub struct Iter<'a> {
    bus: &'a Bus,
    timeout: Option<crate::ClockTime>,
}

impl Iterator for Iter<'_> {
    type Item = Message;

    fn next(&mut self) -> Option<Message> {
        self.bus.timed_pop(self.timeout)
    }
}

#[derive(Debug)]
pub struct BusStream {
    bus: glib::WeakRef<Bus>,
    receiver: UnboundedReceiver<Message>,
}

impl BusStream {
    fn new(bus: &Bus) -> Self {
        skip_assert_initialized!();

        let mutex = Arc::new(Mutex::new(()));
        let (sender, receiver) = mpsc::unbounded();

        // Use a mutex to ensure that the sync handler is not putting any messages into the sender
        // until we have removed all previously queued messages from the bus.
        // This makes sure that the messages are staying in order.
        //
        // We could use the bus' object lock here but a separate mutex seems safer.
        let _mutex_guard = mutex.lock().unwrap();
        bus.set_sync_handler({
            let sender = sender.clone();
            let mutex = mutex.clone();

            move |_bus, message| {
                let _mutex_guard = mutex.lock().unwrap();

                let _ = sender.unbounded_send(message.to_owned());

                BusSyncReply::Drop
            }
        });

        // First pop all messages that might've been previously queued before creating the bus stream.
        while let Some(message) = bus.pop() {
            let _ = sender.unbounded_send(message);
        }

        Self {
            bus: bus.downgrade(),
            receiver,
        }
    }
}

impl Drop for BusStream {
    fn drop(&mut self) {
        if let Some(bus) = self.bus.upgrade() {
            bus.unset_sync_handler();
        }
    }
}

impl Stream for BusStream {
    type Item = Message;

    fn poll_next(mut self: Pin<&mut Self>, context: &mut Context) -> Poll<Option<Self::Item>> {
        self.receiver.poll_next_unpin(context)
    }
}

impl FusedStream for BusStream {
    fn is_terminated(&self) -> bool {
        self.receiver.is_terminated()
    }
}

// rustdoc-stripper-ignore-next
/// Manages ownership of the bus watch added to a bus with [`Bus::add_watch`] or [`Bus::add_watch_local`]
///
/// When dropped the bus watch is removed from the bus.
#[derive(Debug)]
#[must_use = "if unused the bus watch will immediately be removed"]
pub struct BusWatchGuard {
    bus: Bus,
}

impl Drop for BusWatchGuard {
    fn drop(&mut self) {
        let _ = self.bus.remove_watch();
    }
}

#[cfg(test)]
mod tests {
    use std::{
        sync::{Arc, Barrier, Mutex},
        thread,
        time::{Duration, Instant},
    };

    use super::*;

    #[test]
    fn test_sync_handler() {
        crate::init().unwrap();

        let bus = Bus::new();
        let msgs = Arc::new(Mutex::new(Vec::new()));
        let msgs_clone = msgs.clone();
        bus.set_sync_handler(move |_, msg| {
            msgs_clone.lock().unwrap().push(msg.clone());
            BusSyncReply::Pass
        });

        bus.post(crate::message::Eos::new()).unwrap();

        let msgs = msgs.lock().unwrap();
        assert_eq!(msgs.len(), 1);
        match msgs[0].view() {
            crate::MessageView::Eos(_) => (),
            _ => unreachable!(),
        }
    }

    #[test]
    fn test_bus_stream() {
        crate::init().unwrap();

        let bus = Bus::new();
        let bus_stream = bus.stream();

        let eos_message = crate::message::Eos::new();
        bus.post(eos_message).unwrap();

        let bus_future = StreamExt::into_future(bus_stream);
        let (message, _) = futures_executor::block_on(bus_future);

        match message.unwrap().view() {
            crate::MessageView::Eos(_) => (),
            _ => unreachable!(),
        }
    }

    #[test]
    fn test_bus_timed_pop_filtered_does_not_restart_timeout_for_filtered_messages() {
        crate::init().unwrap();

        let bus = Bus::new();
        let bus_clone = bus.clone();

        let timeout = Duration::from_millis(100);
        let timeout_allowance = Duration::from_millis(50);

        let barrier = Arc::new(Barrier::new(2));
        let barrier_clone = barrier.clone();

        // Post non-matching messages for longer than the timeout
        let message_poster = thread::spawn(move || {
            // Signal that posting is about to start
            barrier_clone.wait();

            let start = Instant::now();
            while start.elapsed() < timeout * 3 {
                bus_clone
                    .post(crate::message::DurationChanged::new())
                    .unwrap();
                thread::sleep(Duration::from_millis(10));
            }
        });

        // Wait until the poster thread is ready
        barrier.wait();

        let start = Instant::now();

        let msg = bus.timed_pop_filtered(
            crate::ClockTime::from_mseconds(timeout.as_millis() as u64),
            &[MessageType::Eos],
        );

        let elapsed = start.elapsed();

        assert!(msg.is_none());

        // While filtering messages, `timed_pop_filtered` must not restart the
        // timeout for each discarded message. The timeout applies to the full
        // wait for a matching message.
        assert!(
            elapsed < timeout + timeout_allowance,
            "timed_pop_filtered exceeded the requested timeout while filtering"
        );

        message_poster
            .join()
            .expect("message_poster thread panicked");
    }
}
