//! Explicit-construction UI runtime helpers.

use ruin_reactivity::{EffectHandle, effect};

use crate::keyboard::KeyboardEvent;
use crate::platform::{PlatformClosed, PlatformEvent, PlatformProxy, PlatformRuntime};
use crate::scene::SceneSnapshot;
use crate::tree::CursorIcon;
use crate::window::{WindowId, WindowSpec, WindowUpdate};

/// High-level UI-side owner of platform event consumption.
///
/// The headless backend currently co-locates platform and rendering on one worker thread, but this
/// runtime type deliberately treats them as logical subsystems behind an event/snapshot boundary so
/// future backends can split them without rewriting the app-facing control flow.
pub struct UiRuntime {
    platform: PlatformRuntime,
}

/// Explicit handle for one declarative window instance.
///
/// The controller owns no native resources directly. Instead, it issues commands to the platform
/// runtime and can host a reactive scene effect that pushes immutable scene snapshots whenever UI
/// state changes.
#[derive(Clone)]
pub struct WindowController {
    id: WindowId,
    proxy: PlatformProxy,
}

/// Returned when the platform event stream closes before a matching event is observed.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct EventStreamClosed;

impl std::fmt::Display for EventStreamClosed {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("platform event stream closed")
    }
}

impl std::error::Error for EventStreamClosed {}

impl UiRuntime {
    pub fn from_platform(platform: PlatformRuntime) -> Self {
        Self { platform }
    }

    /// Creates a UI runtime backed by the headless prototype backend.
    pub fn headless() -> Self {
        Self::from_platform(PlatformRuntime::headless())
    }

    /// Returns a cloneable proxy for low-level platform commands.
    pub fn proxy(&self) -> PlatformProxy {
        self.platform.proxy()
    }

    /// Creates a new declarative window and returns a controller for it.
    pub fn create_window(&self, spec: WindowSpec) -> Result<WindowController, PlatformClosed> {
        let id = self.proxy().create_window(spec)?;
        Ok(WindowController {
            id,
            proxy: self.proxy(),
        })
    }

    /// Waits for the next platform event.
    pub async fn next_event(&mut self) -> Option<PlatformEvent> {
        self.platform.next_event().await
    }

    /// Drains any platform events that are already queued locally.
    pub fn take_pending_events(&mut self) -> Vec<PlatformEvent> {
        self.platform.take_pending_events()
    }

    /// Waits until an event matches `predicate`.
    pub async fn wait_for_event_matching(
        &mut self,
        mut predicate: impl FnMut(&PlatformEvent) -> bool,
    ) -> Result<PlatformEvent, EventStreamClosed> {
        while let Some(event) = self.next_event().await {
            if predicate(&event) {
                return Ok(event);
            }
        }
        Err(EventStreamClosed)
    }

    /// Requests shutdown of the platform runtime.
    pub fn shutdown(&self) -> Result<(), PlatformClosed> {
        self.proxy().shutdown()
    }
}

impl WindowController {
    /// Returns the logical window identifier.
    pub const fn id(&self) -> WindowId {
        self.id
    }

    /// Applies a window update.
    pub fn update(&self, update: WindowUpdate) -> Result<(), PlatformClosed> {
        self.proxy.update_window(self.id, update)
    }

    /// Replaces the latest retained scene for this window.
    pub fn replace_scene(&self, scene: SceneSnapshot) -> Result<(), PlatformClosed> {
        self.proxy.replace_scene(self.id, scene)
    }

    /// Copies plain text to the platform primary-selection buffer for this window.
    #[cfg(target_os = "linux")]
    pub fn set_primary_selection_text(
        &self,
        text: impl Into<String>,
    ) -> Result<(), PlatformClosed> {
        self.proxy.set_primary_selection_text(self.id, text)
    }

    /// Copies plain text to the platform clipboard for this window.
    pub fn set_clipboard_text(&self, text: impl Into<String>) -> Result<(), PlatformClosed> {
        self.proxy.set_clipboard_text(self.id, text)
    }

    /// Requests the current plain-text clipboard contents from the platform.
    pub fn request_clipboard_text(&self) -> Result<(), PlatformClosed> {
        self.proxy.request_clipboard_text(self.id)
    }

    /// Requests the current plain-text primary selection contents from the platform.
    #[cfg(target_os = "linux")]
    pub fn request_primary_selection_text(&self) -> Result<(), PlatformClosed> {
        self.proxy.request_primary_selection_text(self.id)
    }

    /// Updates the platform cursor icon for this window.
    pub fn set_cursor_icon(&self, cursor: CursorIcon) -> Result<(), PlatformClosed> {
        self.proxy.set_cursor_icon(self.id, cursor)
    }

    /// Emits a close-request event for this window.
    pub fn emit_close_requested(&self) -> Result<(), PlatformClosed> {
        self.proxy.emit_close_requested(self.id)
    }

    /// Delivers a pointer event for this window through the platform event stream.
    pub fn emit_pointer_event(
        &self,
        event: crate::interaction::PointerEvent,
    ) -> Result<(), PlatformClosed> {
        self.proxy.emit_pointer_event(self.id, event)
    }

    /// Delivers a keyboard event for this window through the platform event stream.
    pub fn emit_keyboard_event(&self, event: KeyboardEvent) -> Result<(), PlatformClosed> {
        self.proxy.emit_keyboard_event(self.id, event)
    }

    /// Delivers an internal wake event for this window through the platform event stream.
    pub fn emit_wake(&self, token: u64) -> Result<(), PlatformClosed> {
        self.proxy.emit_wake(self.id, token)
    }

    /// Attaches a reactive effect that rebuilds and replaces the window scene whenever dependent UI
    /// state changes.
    pub fn attach_scene_effect(
        &self,
        build_scene: impl Fn() -> SceneSnapshot + 'static,
    ) -> EffectHandle {
        let controller = self.clone();
        effect(move || {
            let scene = build_scene();
            controller
                .replace_scene(scene)
                .expect("window controller should remain alive while scene effect is attached");
        })
    }
}

#[cfg(test)]
mod tests {
    use super::UiRuntime;
    use crate::platform::PlatformEvent;
    use crate::scene::{Color, Point, PreparedText, Rect, SceneSnapshot, UiSize};
    use crate::window::{WindowSpec, WindowUpdate};
    use crate::{
        KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers, PointerEvent,
        PointerEventKind,
    };
    use ruin_reactivity::cell;
    use ruin_runtime::{current_thread_handle, queue_future, run};
    use std::future::Future;

    fn run_async_test(future: impl Future<Output = ()> + 'static) {
        let main = current_thread_handle();
        queue_future(async move {
            future.await;
            let _ = main.queue_task(|| {});
        });
        run();
    }

    #[test]
    fn visibility_restore_re_presents_latest_retained_scene() {
        run_async_test(async move {
            let mut ui = UiRuntime::headless();
            let window = ui
                .create_window(WindowSpec::new("visibility-policy").visible(true))
                .expect("window should be created");

            let mut scene = SceneSnapshot::new(1, UiSize::new(320.0, 180.0));
            scene
                .push_quad(
                    Rect::new(0.0, 0.0, 320.0, 180.0),
                    Color::rgb(0x20, 0x22, 0x35),
                )
                .push_text(PreparedText::monospace(
                    "retained scene",
                    Point::new(16.0, 28.0),
                    16.0,
                    8.0,
                    Color::rgb(0xFF, 0xFF, 0xFF),
                ));
            window
                .replace_scene(scene)
                .expect("scene replacement should queue");

            let _ = ui
                .wait_for_event_matching(|event| {
                    matches!(
                        event,
                        PlatformEvent::FramePresented {
                            window_id,
                            scene_version: 1,
                            ..
                        } if *window_id == window.id()
                    )
                })
                .await
                .expect("initial present should occur");

            window
                .update(WindowUpdate::new().visible(false))
                .expect("hide should queue");
            let _ = ui
                .wait_for_event_matching(|event| {
                    matches!(
                        event,
                        PlatformEvent::VisibilityChanged {
                            window_id,
                            visible: false,
                        } if *window_id == window.id()
                    )
                })
                .await
                .expect("hide event should occur");

            window
                .update(WindowUpdate::new().visible(true))
                .expect("show should queue");
            let _ = ui
                .wait_for_event_matching(|event| {
                    matches!(
                        event,
                        PlatformEvent::VisibilityChanged {
                            window_id,
                            visible: true,
                        } if *window_id == window.id()
                    )
                })
                .await
                .expect("show event should occur");

            let event = ui
                .wait_for_event_matching(|event| {
                    matches!(
                        event,
                        PlatformEvent::FramePresented {
                            window_id,
                            scene_version: 1,
                            ..
                        } if *window_id == window.id()
                    )
                })
                .await
                .expect("visibility restore should re-present latest retained scene");

            assert!(matches!(
                event,
                PlatformEvent::FramePresented {
                    scene_version: 1,
                    ..
                }
            ));

            ui.shutdown().expect("shutdown should queue");
        });
    }

    #[test]
    fn window_controller_emits_pointer_events_through_runtime() {
        run_async_test(async move {
            let mut ui = UiRuntime::headless();
            let window = ui
                .create_window(WindowSpec::new("pointer-events").visible(true))
                .expect("window should be created");

            let _ = ui
                .wait_for_event_matching(|event| {
                    matches!(event, PlatformEvent::Opened { window_id } if *window_id == window.id())
                })
                .await
                .expect("window should open before pointer delivery");

            window
                .emit_pointer_event(PointerEvent::new(
                    0,
                    Point::new(24.0, 32.0),
                    PointerEventKind::Move,
                ))
                .expect("pointer event should queue");

            let event = ui
                .wait_for_event_matching(|event| {
                    matches!(
                        event,
                        PlatformEvent::Pointer { window_id, .. } if *window_id == window.id()
                    )
                })
                .await
                .expect("pointer event should be delivered");

            assert_eq!(
                event,
                PlatformEvent::Pointer {
                    window_id: window.id(),
                    event: PointerEvent::new(0, Point::new(24.0, 32.0), PointerEventKind::Move),
                }
            );

            ui.shutdown().expect("shutdown should queue");
        });
    }

    #[test]
    fn window_controller_emits_keyboard_events_through_runtime() {
        run_async_test(async move {
            let mut ui = UiRuntime::headless();
            let window = ui
                .create_window(WindowSpec::new("keyboard-events").visible(true))
                .expect("window should be created");

            let _ = ui
                .wait_for_event_matching(|event| {
                    matches!(event, PlatformEvent::Opened { window_id } if *window_id == window.id())
                })
                .await
                .expect("window should open before keyboard delivery");

            let keyboard_event = KeyboardEvent::new(
                30,
                KeyboardEventKind::Pressed,
                KeyboardKey::Character("a".to_owned()),
                KeyboardModifiers::default(),
                Some("a".to_owned()),
            );
            window
                .emit_keyboard_event(keyboard_event.clone())
                .expect("keyboard event should queue");

            let event = ui
                .wait_for_event_matching(|event| {
                    matches!(
                        event,
                        PlatformEvent::Keyboard { window_id, .. } if *window_id == window.id()
                    )
                })
                .await
                .expect("keyboard event should be delivered");

            assert_eq!(
                event,
                PlatformEvent::Keyboard {
                    window_id: window.id(),
                    event: keyboard_event,
                }
            );

            ui.shutdown().expect("shutdown should queue");
        });
    }

    #[test]
    fn async_signal_updates_represent_scene_without_external_events() {
        run_async_test(async move {
            let mut ui = UiRuntime::headless();
            let window = ui
                .create_window(WindowSpec::new("async-scene-effect").visible(true))
                .expect("window should be created");
            let scene_version = std::rc::Rc::new(std::cell::Cell::new(0_u64));
            let value = cell(0_u32);
            let _scene_effect = window.attach_scene_effect({
                let scene_version = std::rc::Rc::clone(&scene_version);
                let value = value.clone();
                move || {
                    let next_version = scene_version.get() + 1;
                    scene_version.set(next_version);
                    let mut scene = SceneSnapshot::new(next_version, UiSize::new(320.0, 180.0));
                    scene.push_text(PreparedText::monospace(
                        format!("value={}", value.get()),
                        Point::new(16.0, 28.0),
                        16.0,
                        8.0,
                        Color::rgb(0xFF, 0xFF, 0xFF),
                    ));
                    scene
                }
            });

            let _ = ui
                .wait_for_event_matching(|event| {
                    matches!(
                        event,
                        PlatformEvent::FramePresented {
                            window_id,
                            scene_version: 1,
                            ..
                        } if *window_id == window.id()
                    )
                })
                .await
                .expect("initial scene should present");

            queue_future({
                let value = value.clone();
                async move {
                    ruin_runtime::yield_now().await;
                    let _ = value.set(1);
                }
            });

            let _ = ui
                .wait_for_event_matching(|event| {
                    matches!(
                        event,
                        PlatformEvent::FramePresented {
                            window_id,
                            scene_version: 2,
                            ..
                        } if *window_id == window.id()
                    )
                })
                .await
                .expect("async signal update should re-present the scene");

            ui.shutdown().expect("shutdown should queue");
        });
    }
}
