use std::cell::Cell as StdCell;
use std::cell::RefCell;
use std::iter;
use std::rc::Rc;
use std::time::Instant;

use ruin_reactivity::{current as current_reactor, effect};
#[cfg(target_os = "macos")]
use ruin_runtime::{queue_future, spawn_worker};
use ruin_ui::{
    Color, Element, InteractionTree, KeyboardEvent, LayoutCache, LayoutSnapshot, PlatformEvent,
    PointerButton, PointerEvent, PointerEventKind, PointerRouter, RoutedPointerEvent,
    RoutedPointerEventKind, TextSystem, UiSize, WindowController, WindowSpec, WindowUpdate,
    layout_snapshot_with_cache,
};
#[cfg(target_os = "macos")]
use ruin_ui_platform_macos::{run_macos_app_event_loop, start_macos_ui};
#[cfg(target_os = "linux")]
use ruin_ui_platform_wayland::start_wayland_ui;

use crate::Result;
use crate::context::{
    InteractionSnapshot, RenderState, render_with_context, render_with_context_and_interaction,
};
use crate::input::{
    EventBindings, InputState, ShortcutBinding, TextSelection, TextSelectionDrag,
    TextSelectionState, apply_text_selection_overlay, focused_element_for_pointer,
    sync_primary_selection,
};
use crate::view::View;

#[cfg(target_os = "linux")]
fn start_platform_ui() -> ruin_ui::UiRuntime {
    start_wayland_ui()
}

#[cfg(target_os = "macos")]
fn start_platform_ui() -> ruin_ui::UiRuntime {
    start_macos_ui()
}

#[derive(Clone, Debug)]
pub struct Window {
    pub(crate) spec: WindowSpec,
}

impl Window {
    pub fn new() -> Self {
        Self {
            spec: WindowSpec::new("RUIN App"),
        }
    }

    pub fn title(mut self, title: impl Into<String>) -> Self {
        self.spec.title = title.into();
        self
    }

    pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
        self.spec = self.spec.app_id(app_id);
        self
    }

    pub fn size(mut self, width: f32, height: f32) -> Self {
        self.spec = self.spec.requested_inner_size(UiSize::new(width, height));
        self
    }

    pub fn min_size(mut self, min_width: f32, min_height: f32) -> Self {
        self.spec = self.spec.min_inner_size(UiSize::new(min_width, min_height));
        self
    }

    pub fn base_color(mut self, color: Color) -> Self {
        self.spec = self.spec.base_color(color);
        self
    }

    pub fn resizable(mut self, resizable: bool) -> Self {
        self.spec.resizable = resizable;
        self
    }
}

impl Default for Window {
    fn default() -> Self {
        Self::new()
    }
}

pub struct App {
    window: Window,
}

impl App {
    pub fn new() -> Self {
        Self {
            window: Window::new(),
        }
    }

    pub fn window(mut self, window: Window) -> Self {
        self.window = window;
        self
    }

    pub fn mount<M: Mountable>(self, root: M) -> MountedApp<M> {
        MountedApp {
            window: self.window,
            root,
        }
    }
}

impl Default for App {
    fn default() -> Self {
        Self::new()
    }
}

pub trait Mountable: 'static {
    fn render(&self) -> View;
}

pub trait Component: 'static {
    type Builder;

    fn builder() -> Self::Builder;
    fn render(&self) -> View;
}

pub trait ContextKey: 'static {
    type Value: Clone + 'static;
}

impl<T: Component> Mountable for T {
    fn render(&self) -> View {
        Component::render(self)
    }
}

impl Mountable for View {
    fn render(&self) -> View {
        self.clone()
    }
}

impl Mountable for Element {
    fn render(&self) -> View {
        View::from_element(self.clone())
    }
}

pub struct MountedApp<M: Mountable> {
    pub(crate) window: Window,
    pub(crate) root: M,
}

impl<M: Mountable> MountedApp<M> {
    async fn run_with_ui_runtime(
        app_window: Window,
        root: M,
        mut ui: ruin_ui::UiRuntime,
    ) -> Result<()> {
        let root = Rc::new(root);
        let render_state = Rc::new(RenderState::default());
        let window = ui.create_window(app_window.spec.clone())?;
        let initial_viewport = app_window
            .spec
            .requested_inner_size
            .unwrap_or(UiSize::new(960.0, 640.0));

        let viewport = ruin_reactivity::cell(initial_viewport);
        let configure_serial = ruin_reactivity::cell(0_u64);
        let scene_version = StdCell::new(0_u64);
        let text_system = Rc::new(RefCell::new(TextSystem::new()));
        let layout_cache = Rc::new(RefCell::new(LayoutCache::new()));
        let interaction_tree = Rc::new(RefCell::new(None::<InteractionTree>));
        let bindings = Rc::new(RefCell::new(EventBindings::default()));
        let shortcuts = Rc::new(RefCell::new(Vec::<ShortcutBinding>::new()));
        let current_title = Rc::new(RefCell::new(None::<String>));
        let last_min_size = Rc::new(RefCell::new(None::<UiSize>));
        let last_fixed_size = Rc::new(RefCell::new(None::<UiSize>));
        let explicit_min_size = app_window.spec.min_inner_size;
        let explicit_max_size = app_window.spec.max_inner_size;
        let window_resizable = app_window.spec.resizable;
        let mut input_state = InputState::new();
        let mut pointer_router = PointerRouter::new();

        let _scene_effect = effect({
            let window = window.clone();
            let viewport = viewport.clone();
            let configure_serial = configure_serial.clone();
            let text_system = Rc::clone(&text_system);
            let layout_cache = Rc::clone(&layout_cache);
            let interaction_tree = Rc::clone(&interaction_tree);
            let bindings = Rc::clone(&bindings);
            let shortcuts = Rc::clone(&shortcuts);
            let current_title = Rc::clone(&current_title);
            let last_min_size = Rc::clone(&last_min_size);
            let last_fixed_size = Rc::clone(&last_fixed_size);
            let root = Rc::clone(&root);
            let render_state = Rc::clone(&render_state);
            let text_selection = Rc::clone(&input_state.text_selection);
            let hovered_elements = input_state.hovered_elements.clone();
            let pressed_elements = input_state.pressed_elements.clone();
            let interaction_version = input_state.interaction_version.clone();
            move || {
                let viewport = viewport.get();
                let _ = configure_serial.get();
                let version = scene_version.get().wrapping_add(1);
                scene_version.set(version);
                let _ = text_selection.version.get();
                let _ = interaction_version.get();

                let t_effect = Instant::now();
                let interaction_snapshot = Rc::new(InteractionSnapshot {
                    hovered: hovered_elements.get(),
                    pressed: pressed_elements.get(),
                });

                let render_output = render_with_context_and_interaction(
                    Rc::clone(&render_state),
                    interaction_snapshot,
                    || root.render(),
                );

                if render_output.side_effects.window_title != *current_title.borrow() {
                    if let Some(title) = &render_output.side_effects.window_title {
                        window
                            .update(WindowUpdate::new().title(title.clone()))
                            .expect("window should remain alive while the app is running");
                    }
                    *current_title.borrow_mut() = render_output.side_effects.window_title.clone();
                }

                let render_us = t_effect.elapsed().as_micros();
                let t_layout = Instant::now();
                let LayoutSnapshot {
                    mut scene,
                    interaction_tree: next_interaction_tree,
                    root_min_size,
                } = layout_snapshot_with_cache(
                    version,
                    viewport,
                    render_output.view.element(),
                    &mut text_system.borrow_mut(),
                    &mut layout_cache.borrow_mut(),
                );
                let layout_us = t_layout.elapsed().as_micros();

                let desired_min = UiSize::new(
                    root_min_size
                        .width
                        .max(explicit_min_size.map_or(0.0, |s| s.width)),
                    root_min_size
                        .height
                        .max(explicit_min_size.map_or(0.0, |s| s.height)),
                );
                if window_resizable {
                    let current_min = *last_min_size.borrow();
                    if current_min != Some(desired_min) {
                        let _ =
                            window.update(WindowUpdate::new().min_inner_size(Some(desired_min)));
                        *last_min_size.borrow_mut() = Some(desired_min);
                    }
                } else {
                    let desired_fixed = UiSize::new(
                        desired_min
                            .width
                            .min(explicit_max_size.map_or(f32::INFINITY, |s| s.width)),
                        desired_min
                            .height
                            .min(explicit_max_size.map_or(f32::INFINITY, |s| s.height)),
                    );
                    let current_fixed = *last_fixed_size.borrow();
                    if current_fixed != Some(desired_fixed) {
                        let _ = window.update(
                            WindowUpdate::new()
                                .requested_inner_size(Some(desired_fixed))
                                .min_inner_size(Some(desired_fixed))
                                .max_inner_size(Some(desired_fixed)),
                        );
                        *last_fixed_size.borrow_mut() = Some(desired_fixed);
                    }
                }
                let effect_us = t_effect.elapsed().as_micros();

                tracing::debug!(
                    target: "ruin_app::resize",
                    version,
                    width = viewport.width,
                    height = viewport.height,
                    render_us,
                    layout_us,
                    effect_us,
                    "scene effect complete, sending ReplaceScene"
                );

                apply_text_selection_overlay(&mut scene, *text_selection.selection.borrow());

                *interaction_tree.borrow_mut() = Some(next_interaction_tree);
                *bindings.borrow_mut() = render_output.view.bindings;
                *shortcuts.borrow_mut() = render_output.side_effects.shortcuts.clone();
                window
                    .replace_scene(scene)
                    .expect("window should remain alive while the app is running");
            }
        });

        loop {
            let Some(event) = ui.next_event().await else {
                break;
            };

            let mut should_flush_reactive_work = false;
            for event in iter::once(event).chain(ui.take_pending_events()) {
                match event {
                    PlatformEvent::Configured {
                        window_id,
                        configuration,
                    } if window_id == window.id() => {
                        tracing::debug!(
                            target: "ruin_app::resize",
                            width = configuration.actual_inner_size.width,
                            height = configuration.actual_inner_size.height,
                            "app received Configured, queuing layout effect"
                        );
                        let _ = viewport.set(configuration.actual_inner_size);
                        configure_serial.update(|value| *value = value.wrapping_add(1));
                        should_flush_reactive_work = true;
                    }
                    PlatformEvent::Pointer { window_id, event } if window_id == window.id() => {
                        Self::handle_pointer_event(
                            &window,
                            &interaction_tree,
                            &bindings,
                            &mut pointer_router,
                            &mut input_state,
                            event,
                        )?;
                        should_flush_reactive_work = true;
                    }
                    PlatformEvent::Keyboard { window_id, event } if window_id == window.id() => {
                        Self::handle_keyboard_event(
                            &interaction_tree,
                            &bindings,
                            &shortcuts,
                            &input_state,
                            event,
                        )?;
                        should_flush_reactive_work = true;
                    }
                    PlatformEvent::CloseRequested { window_id } if window_id == window.id() => {
                        tracing::debug!(
                            target: "ruin_app::lifecycle",
                            ?window_id,
                            "app received CloseRequested, issuing open(false)"
                        );
                        let _ = window.update(WindowUpdate::new().open(false));
                    }
                    PlatformEvent::Closed { window_id } if window_id == window.id() => {
                        tracing::debug!(
                            target: "ruin_app::lifecycle",
                            ?window_id,
                            "app received Closed, shutting down ui runtime"
                        );
                        ui.shutdown()?;
                        return Ok(());
                    }
                    _ => {}
                }
            }
            if should_flush_reactive_work {
                current_reactor().flush_now();
            }
        }

        ui.shutdown()?;
        Ok(())
    }
}

impl<M: Mountable> MountedApp<M> {
    #[cfg(not(target_os = "macos"))]
    pub async fn run(self) -> Result<()> {
        let ui = start_platform_ui();
        Self::run_with_ui_runtime(self.window, self.root, ui).await
    }

    #[cfg(target_os = "macos")]
    pub async fn run(self) -> Result<()>
    where
        M: Send + 'static,
    {
        let ui = start_platform_ui();
        let (done_tx, done_rx) = std::sync::mpsc::channel::<std::result::Result<(), String>>();
        let window = self.window;
        let root = self.root;
        let _worker = spawn_worker(
            move || {
                queue_future(async move {
                    let result = Self::run_with_ui_runtime(window, root, ui)
                        .await
                        .map_err(|err| err.to_string());
                    let _ = done_tx.send(result);
                });
            },
            || {},
        );

        run_macos_app_event_loop();

        match done_rx.recv() {
            Ok(Ok(())) => Ok(()),
            Ok(Err(err)) => Err(err.into()),
            Err(_) => Err("ui worker terminated before completion".into()),
        }
    }

    pub(crate) fn handle_pointer_event(
        window: &WindowController,
        interaction_tree: &RefCell<Option<InteractionTree>>,
        bindings: &RefCell<EventBindings>,
        pointer_router: &mut PointerRouter,
        input_state: &mut InputState,
        event: PointerEvent,
    ) -> Result<()> {
        if matches!(
            event.kind,
            PointerEventKind::Down {
                button: PointerButton::Primary | PointerButton::Middle,
            }
        ) {
            let interaction_tree = interaction_tree.borrow();
            if let Some(interaction_tree) = interaction_tree.as_ref() {
                input_state.focused_element = focused_element_for_pointer(interaction_tree, &event);
            }
        }

        let routed = {
            let interaction_tree = interaction_tree.borrow();
            let Some(interaction_tree) = interaction_tree.as_ref() else {
                return Ok(());
            };
            pointer_router.route(interaction_tree, event)
        };

        {
            let interaction_tree = interaction_tree.borrow();
            let Some(interaction_tree) = interaction_tree.as_ref() else {
                return Ok(());
            };
            let hovered_targets = pointer_router.hovered_targets();
            for routed_event in &routed {
                bindings
                    .borrow()
                    .dispatch(routed_event, interaction_tree, hovered_targets);
                Self::handle_text_selection_event(
                    window,
                    interaction_tree,
                    routed_event,
                    &input_state.text_selection,
                )?;
            }
        }

        let next_cursor = pointer_router
            .hovered_targets()
            .last()
            .map(|target| target.cursor)
            .unwrap_or(ruin_ui::CursorIcon::Default);
        let hovered_ids = Self::interaction_ids(pointer_router.hovered_targets());
        let pressed_ids = Self::interaction_ids(pointer_router.pressed_targets());
        if next_cursor != input_state.current_cursor {
            input_state.current_cursor = next_cursor;
            window.set_cursor_icon(next_cursor)?;
        }
        if Self::update_interaction_state(input_state, hovered_ids, pressed_ids) {
            input_state.interaction_version.update(|value| *value += 1);
        }

        Ok(())
    }

    pub(crate) fn handle_keyboard_event(
        interaction_tree: &RefCell<Option<InteractionTree>>,
        bindings: &RefCell<EventBindings>,
        shortcuts: &RefCell<Vec<ShortcutBinding>>,
        input_state: &InputState,
        event: KeyboardEvent,
    ) -> Result<()> {
        let interaction_tree = interaction_tree.borrow();
        let Some(interaction_tree) = interaction_tree.as_ref() else {
            return Ok(());
        };
        let shortcut_bindings = shortcuts.borrow().clone();
        let mut consumed = false;
        for shortcut in shortcut_bindings {
            if shortcut.matches(&event, input_state.focused_element, interaction_tree) {
                shortcut.trigger(interaction_tree);
                consumed = true;
            }
        }
        if consumed {
            return Ok(());
        }
        bindings
            .borrow()
            .dispatch_key(input_state.focused_element, &event, interaction_tree);
        Ok(())
    }

    fn handle_text_selection_event(
        window: &WindowController,
        interaction_tree: &InteractionTree,
        event: &RoutedPointerEvent,
        text_selection: &TextSelectionState,
    ) -> Result<()> {
        let mut selection_changed = false;

        match event.kind {
            RoutedPointerEventKind::Down {
                button: PointerButton::Primary,
            } => {
                if let Some(hit) = interaction_tree.text_hit_test(event.position)
                    && let Some(element_id) = hit.target.element_id
                {
                    let next = Some(TextSelection {
                        element_id,
                        anchor: hit.byte_offset,
                        focus: hit.byte_offset,
                    });
                    if *text_selection.selection.borrow() != next {
                        *text_selection.selection.borrow_mut() = next;
                        selection_changed = true;
                    }
                    *text_selection.drag.borrow_mut() = Some(TextSelectionDrag {
                        element_id,
                        anchor: hit.byte_offset,
                    });
                } else {
                    selection_changed = text_selection.selection.borrow_mut().take().is_some();
                    let _ = text_selection.drag.borrow_mut().take();
                }
            }
            RoutedPointerEventKind::Move => {
                let Some(drag) = *text_selection.drag.borrow() else {
                    return Ok(());
                };
                let Some(text) = interaction_tree.text_for_element(drag.element_id) else {
                    return Ok(());
                };
                let next = Some(TextSelection {
                    element_id: drag.element_id,
                    anchor: drag.anchor,
                    focus: text.byte_offset_for_position(event.position),
                });
                if *text_selection.selection.borrow() != next {
                    *text_selection.selection.borrow_mut() = next;
                    selection_changed = true;
                }
            }
            RoutedPointerEventKind::Up {
                button: PointerButton::Primary,
            } => {
                if text_selection.drag.borrow_mut().take().is_some() {
                    selection_changed = true;
                }
            }
            _ => {}
        }

        if selection_changed {
            text_selection.version.update(|value| *value += 1);
            sync_primary_selection(window, interaction_tree, *text_selection.selection.borrow())?;
        }

        Ok(())
    }

    fn interaction_ids(targets: &[ruin_ui::HitTarget]) -> Vec<ruin_ui::ElementId> {
        targets
            .iter()
            .filter_map(|target| target.element_id)
            .collect()
    }

    fn update_interaction_state(
        input_state: &mut InputState,
        hovered_ids: Vec<ruin_ui::ElementId>,
        pressed_ids: Vec<ruin_ui::ElementId>,
    ) -> bool {
        let previous_hovered = input_state.hovered_elements.get();
        let previous_pressed = input_state.pressed_elements.get();
        let hovered_changed = input_state
            .hovered_elements
            .set(hovered_ids.clone())
            .is_some();
        let pressed_changed = input_state
            .pressed_elements
            .set(pressed_ids.clone())
            .is_some();
        if hovered_changed || pressed_changed {
            tracing::trace!(
                target: "ruin_app::interaction",
                hovered_before = ?previous_hovered,
                hovered_after = ?hovered_ids,
                pressed_before = ?previous_pressed,
                pressed_after = ?pressed_ids,
                "interaction state changed"
            );
        }
        hovered_changed || pressed_changed
    }
}

#[doc(hidden)]
pub fn __render_mountable_for_test<M: Mountable>(mountable: &M) -> View {
    render_with_context(Rc::new(RenderState::default()), || mountable.render()).view
}
