//! Non-blocking bridge between native dialogs and main-thread editor workflows.

use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{
    atomic::{AtomicUsize, Ordering},
    Arc, Mutex,
};

use bevy::prelude::*;

type MainThreadCompletion = Box<dyn FnOnce(&mut World) + Send + 'static>;

#[derive(Resource)]
pub struct NativeDialogBroker {
    sender: Sender<MainThreadCompletion>,
    receiver: Mutex<Receiver<MainThreadCompletion>>,
    pending: Arc<AtomicUsize>,
}

impl Default for NativeDialogBroker {
    fn default() -> Self {
        let (sender, receiver) = mpsc::channel();
        Self {
            sender,
            receiver: Mutex::new(receiver),
            pending: Arc::new(AtomicUsize::new(0)),
        }
    }
}

impl NativeDialogBroker {
    pub fn is_pending(&self) -> bool {
        self.pending.load(Ordering::Acquire) > 0
    }

    pub fn request<T, D, C>(&self, dialog: D, completion: C) -> Result<(), &'static str>
    where
        T: Send + 'static,
        D: FnOnce() -> T + Send + 'static,
        C: FnOnce(&mut World, T) + Send + 'static,
    {
        if self
            .pending
            .compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire)
            .is_err()
        {
            return Err("another native dialog is already open");
        }

        let sender = self.sender.clone();
        let pending = Arc::clone(&self.pending);
        std::thread::spawn(move || {
            let result = dialog();
            let callback: MainThreadCompletion = Box::new(move |world| completion(world, result));
            if sender.send(callback).is_err() {
                pending.store(0, Ordering::Release);
            }
        });
        Ok(())
    }
}

pub struct NativeDialogPlugin;

impl Plugin for NativeDialogPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<NativeDialogBroker>()
            .add_systems(Update, drain_native_dialog_results);
    }
}

fn drain_native_dialog_results(world: &mut World) {
    let callbacks: Vec<_> = {
        let broker = world.resource::<NativeDialogBroker>();
        broker
            .receiver
            .lock()
            .expect("native dialog receiver poisoned")
            .try_iter()
            .collect()
    };
    for callback in callbacks {
        world
            .resource::<NativeDialogBroker>()
            .pending
            .store(0, Ordering::Release);
        callback(world);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Resource, Default)]
    struct ResultValue(Option<u32>);

    #[derive(Resource, Default)]
    struct FrameCount(u32);

    #[test]
    fn request_is_exclusive_and_completion_runs_once() {
        let mut app = App::new();
        app.add_plugins(NativeDialogPlugin)
            .init_resource::<ResultValue>();
        app.world()
            .resource::<NativeDialogBroker>()
            .request(
                || 42,
                |world, value| world.resource_mut::<ResultValue>().0 = Some(value),
            )
            .unwrap();
        assert!(app
            .world()
            .resource::<NativeDialogBroker>()
            .request(|| 7, |_, _| {})
            .is_err());

        for _ in 0..100 {
            app.update();
            if app.world().resource::<ResultValue>().0.is_some() {
                break;
            }
            std::thread::yield_now();
        }
        assert_eq!(app.world().resource::<ResultValue>().0, Some(42));
        app.update();
        assert_eq!(app.world().resource::<ResultValue>().0, Some(42));
        assert!(!app.world().resource::<NativeDialogBroker>().is_pending());
    }

    #[test]
    fn blocked_dialog_worker_does_not_block_app_frames() {
        let mut app = App::new();
        app.add_plugins(NativeDialogPlugin)
            .init_resource::<ResultValue>()
            .init_resource::<FrameCount>()
            .add_systems(Update, |mut frames: ResMut<FrameCount>| frames.0 += 1);
        let (release_sender, release_receiver) = mpsc::channel();
        app.world()
            .resource::<NativeDialogBroker>()
            .request(
                move || {
                    release_receiver
                        .recv()
                        .expect("test release sender dropped")
                },
                |world, value| world.resource_mut::<ResultValue>().0 = Some(value),
            )
            .unwrap();

        for _ in 0..8 {
            app.update();
        }
        assert_eq!(app.world().resource::<FrameCount>().0, 8);
        assert_eq!(app.world().resource::<ResultValue>().0, None);
        assert!(app.world().resource::<NativeDialogBroker>().is_pending());

        release_sender.send(73).unwrap();
        for _ in 0..100 {
            app.update();
            if app.world().resource::<ResultValue>().0.is_some() {
                break;
            }
            std::thread::yield_now();
        }
        assert_eq!(app.world().resource::<ResultValue>().0, Some(73));
        assert!(app.world().resource::<FrameCount>().0 > 8);
        assert!(!app.world().resource::<NativeDialogBroker>().is_pending());
    }
}
