//! Minimal app/runtime glue for RUIN application experiments.
//!
//! This crate is intentionally low-level. It is the substrate that a future proc-macro-driven
//! component system can expand to, not the final ergonomic authoring API.

extern crate self as ruin_app;

use std::error::Error;

pub mod app;
pub mod colors;
mod context;
pub mod converters;
mod hooks;
mod input;
pub mod primitives;
pub mod surfaces;
pub mod text_style;
pub mod view;

// Re-export public items from each module.

pub use app::{
    __render_mountable_for_test, App, Component, ContextKey, Mountable, MountedApp, Window,
};
pub use converters::{IntoBorder, IntoEdges, IntoShadow};
pub use hooks::{
    BlockWidget, FocusScope, InteractionState, Key, Memo, Resource, ResourceState, ScrollBoxWidget,
    Shortcut, ShortcutScope, Signal, WidgetRef, provide, use_context, use_effect,
    use_interaction_state, use_memo, use_resource, use_shortcut, use_shortcut_with_context,
    use_signal, use_widget_ref, use_window_title,
};
pub use primitives::{
    ButtonBuilder, ContainerBuilder, ContainerProps, FontWeight, ScrollBoxBuilder, TextBuilder,
    TextRole, block, button, column, row, scroll_box, text,
};
pub use text_style::PartialTextStyle;
pub use view::{ChildViews, Children, IntoView, LazyChildren, TextChildren, TextValue, View};

// Bare string literals as container children produce a text element with context-aware styling.
// These impls live here (not in view.rs) so they can call primitives::text().
impl Children for &str {
    fn into_views(self) -> Vec<View> {
        let s = self.to_string();
        vec![primitives::text().children(s)]
    }
}

impl Children for String {
    fn into_views(self) -> Vec<View> {
        vec![primitives::text().children(self)]
    }
}

pub use ResourceState::{Pending, Ready};
pub use ruin_app_proc_macros::{component, context_provider, view};

pub type Result<T> = std::result::Result<T, Box<dyn Error>>;

pub mod prelude {
    pub use crate::PartialTextStyle;
    pub use crate::{
        App, BlockWidget, ButtonBuilder, ChildViews, Children, Component, ContainerBuilder,
        ContextKey, FocusScope, FontWeight, InteractionState, IntoBorder, IntoEdges, IntoView, Key,
        Memo, Mountable, Pending, Ready, Resource, ResourceState, Result, ScrollBoxBuilder,
        ScrollBoxWidget, Shortcut, ShortcutScope, Signal, TextBuilder, TextChildren, TextRole,
        TextValue, View, WidgetRef, Window, block, button, colors, column, component,
        context_provider, provide, row, scroll_box, surfaces, text, use_context, use_effect,
        use_interaction_state, use_memo, use_resource, use_shortcut, use_shortcut_with_context,
        use_signal, use_widget_ref, use_window_title, view,
    };
    pub use ruin_ui::{
        AlignItems, Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree,
        JustifyContent, PointerButton, PointerEventKind, RoutedPointerEvent,
        RoutedPointerEventKind, ScrollbarStyle, TextFontFamily, TextStyle, TextWrap, UiSize,
    };
}

#[cfg(test)]
mod tests {
    use std::cell::Cell as StdCell;
    use std::cell::RefCell;
    use std::collections::HashMap;
    use std::rc::Rc;

    use ruin_ui::{KeyboardModifiers, Point, WindowSpec};

    use super::*;
    use crate::context::{
        InteractionSnapshot, RenderState, render_with_context, render_with_context_and_interaction,
    };
    use crate::input::{
        InputState, KeyHandler, focused_element_for_pointer, scroll_handler_for_event,
    };

    #[derive(Clone, Debug, PartialEq, Eq)]
    struct NamedValue(&'static str);

    struct OuterContext;
    struct InnerContext;

    impl ContextKey for OuterContext {
        type Value = NamedValue;
    }

    impl ContextKey for InnerContext {
        type Value = NamedValue;
    }

    #[component]
    fn ContractProbe(label: TextValue, actions: ChildViews, children: ChildViews) -> impl IntoView {
        column().children((
            text().children(label),
            row().children(actions),
            block().children(children),
        ))
    }

    #[test]
    fn use_context_distinguishes_marker_types_for_same_value_type() {
        let seen_outer = Rc::new(RefCell::new(None::<NamedValue>));
        let seen_inner = Rc::new(RefCell::new(None::<NamedValue>));

        let _ = render_with_context(Rc::new(RenderState::default()), {
            let seen_outer = Rc::clone(&seen_outer);
            let seen_inner = Rc::clone(&seen_inner);
            move || {
                provide::<OuterContext>(NamedValue("outer"), || {
                    provide::<InnerContext>(NamedValue("inner"), || {
                        *seen_outer.borrow_mut() = Some(use_context::<OuterContext>());
                        *seen_inner.borrow_mut() = Some(use_context::<InnerContext>());
                        View::from_element(ruin_ui::Element::column())
                    })
                })
            }
        });

        assert_eq!(*seen_outer.borrow(), Some(NamedValue("outer")));
        assert_eq!(*seen_inner.borrow(), Some(NamedValue("inner")));
    }

    #[test]
    fn nearer_provider_shadows_outer_provider_of_same_marker() {
        let seen_value = Rc::new(RefCell::new(None::<NamedValue>));

        let _ = render_with_context(Rc::new(RenderState::default()), {
            let seen_value = Rc::clone(&seen_value);
            move || {
                provide::<OuterContext>(NamedValue("outer"), || {
                    provide::<OuterContext>(NamedValue("inner"), || {
                        *seen_value.borrow_mut() = Some(use_context::<OuterContext>());
                        View::from_element(ruin_ui::Element::column())
                    })
                })
            }
        });

        assert_eq!(*seen_value.borrow(), Some(NamedValue("inner")));
    }

    #[test]
    fn components_accept_child_contracts_and_child_like_slot_props() {
        let render = render_with_context(Rc::new(RenderState::default()), || {
            IntoView::into_view(view! {
                ContractProbe(
                    label = "slot label",
                    actions = (
                        text().children("action a"),
                        text().children("action b"),
                    )
                ) {
                    text() { "body child" }
                }
            })
        });
        let debug = format!("{:?}", render.view.element());

        assert!(debug.contains("slot label"), "{debug}");
        assert!(debug.contains("action a"), "{debug}");
        assert!(debug.contains("action b"), "{debug}");
        assert!(debug.contains("body child"), "{debug}");
    }

    #[test]
    fn interaction_state_tracks_hover_and_press_for_widget_refs() {
        let render_state = Rc::new(RenderState::default());
        let hovered = Rc::new(RefCell::new(None::<bool>));
        let pressed = Rc::new(RefCell::new(None::<bool>));
        let widget_ref_slot = Rc::new(RefCell::new(None::<WidgetRef<BlockWidget>>));

        let _ = render_with_context(Rc::clone(&render_state), {
            let hovered = Rc::clone(&hovered);
            let pressed = Rc::clone(&pressed);
            let widget_ref_slot = Rc::clone(&widget_ref_slot);
            move || {
                let widget_ref = use_widget_ref::<BlockWidget>();
                *hovered.borrow_mut() = Some(use_interaction_state(widget_ref.clone()).hovered);
                *pressed.borrow_mut() = Some(use_interaction_state(widget_ref.clone()).pressed);
                *widget_ref_slot.borrow_mut() = Some(widget_ref.clone());
                block().widget_ref(widget_ref).children(())
            }
        });

        let widget_ref = widget_ref_slot
            .borrow()
            .clone()
            .expect("widget ref should be set");
        let element_id = widget_ref
            .element_id()
            .expect("widget ref should have an element id after render");
        let _ = render_with_context_and_interaction(
            render_state,
            Rc::new(InteractionSnapshot {
                hovered: vec![element_id],
                pressed: vec![element_id],
            }),
            {
                let hovered = Rc::clone(&hovered);
                let pressed = Rc::clone(&pressed);
                let widget_ref_slot = Rc::clone(&widget_ref_slot);
                move || {
                    let widget_ref = widget_ref_slot
                        .borrow()
                        .clone()
                        .expect("widget ref should persist");
                    *hovered.borrow_mut() = Some(use_interaction_state(widget_ref.clone()).hovered);
                    *pressed.borrow_mut() = Some(use_interaction_state(widget_ref.clone()).pressed);
                    block().widget_ref(widget_ref).children(())
                }
            },
        );

        assert_eq!(*hovered.borrow(), Some(true));
        assert_eq!(*pressed.borrow(), Some(true));
    }

    #[test]
    fn key_dispatch_prefers_the_nearest_focused_ancestor_handler() {
        use ruin_ui::{ElementId, KeyboardEvent, KeyboardEventKind, KeyboardKey};

        let outer_id = ElementId::new(41);
        let inner_id = ElementId::new(42);
        let root = ruin_ui::Element::column().pointer_events(false).child(
            ruin_ui::Element::column()
                .id(outer_id)
                .width(160.0)
                .height(120.0)
                .focusable(true)
                .child(
                    ruin_ui::Element::column()
                        .id(inner_id)
                        .width(120.0)
                        .height(80.0)
                        .focusable(true),
                ),
        );
        let snapshot = ruin_ui::layout_snapshot(1, ruin_ui::UiSize::new(200.0, 120.0), &root);
        let outer_hits = Rc::new(StdCell::new(0usize));
        let inner_hits = Rc::new(StdCell::new(0usize));

        let mut handlers = HashMap::<ElementId, KeyHandler>::new();
        handlers.insert(
            outer_id,
            Rc::new({
                let outer_hits = Rc::clone(&outer_hits);
                move |_, _| {
                    outer_hits.set(outer_hits.get() + 1);
                    true
                }
            }),
        );
        handlers.insert(
            inner_id,
            Rc::new({
                let inner_hits = Rc::clone(&inner_hits);
                move |_, _| {
                    inner_hits.set(inner_hits.get() + 1);
                    true
                }
            }),
        );

        crate::input::key_handler_for_focus(&handlers, Some(inner_id), &snapshot.interaction_tree)
            .unwrap()(
            &KeyboardEvent::new(
                0,
                KeyboardEventKind::Pressed,
                KeyboardKey::ArrowDown,
                KeyboardModifiers::default(),
                None,
            ),
            &snapshot.interaction_tree,
        );

        assert_eq!(inner_hits.get(), 1);
        assert_eq!(outer_hits.get(), 0);
    }

    #[test]
    fn scroll_box_arrow_keys_work_after_clicking_text_content() {
        use ruin_ui::{
            KeyboardEvent, KeyboardEventKind, KeyboardKey, PointerButton, PointerEvent,
            PointerEventKind,
        };

        let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
        let render = render_with_context(Rc::new(RenderState::default()), {
            let offset_slot = Rc::clone(&offset_slot);
            move || {
                let offset = use_signal(|| 0.0_f32);
                *offset_slot.borrow_mut() = Some(offset.clone());
                scroll_box()
                    .height(120.0)
                    .offset_y(offset)
                    .children(text().children(
                        "line 01\nline 02\nline 03\nline 04\nline 05\nline 06\nline 07\nline 08\nline 09\nline 10\nline 11\nline 12",
                    ))
            }
        });
        let offset = offset_slot
            .borrow()
            .clone()
            .expect("scroll signal should have been captured");
        let scrollbox_id = render
            .view
            .element
            .id
            .expect("scroll box should receive an element id");
        let snapshot =
            ruin_ui::layout_snapshot(1, ruin_ui::UiSize::new(260.0, 160.0), render.view.element());
        let focused = focused_element_for_pointer(
            &snapshot.interaction_tree,
            &PointerEvent::new(
                1,
                Point::new(12.0, 12.0),
                PointerEventKind::Down {
                    button: PointerButton::Primary,
                },
            ),
        );

        assert_eq!(focused, Some(scrollbox_id));

        render.view.bindings.dispatch_key(
            focused,
            &KeyboardEvent::new(
                0,
                KeyboardEventKind::Pressed,
                KeyboardKey::ArrowDown,
                KeyboardModifiers::default(),
                None,
            ),
            &snapshot.interaction_tree,
        );

        assert!(offset.get() > 0.0);
    }

    #[test]
    fn scroll_box_thumb_drag_updates_offset_signal() {
        use ruin_ui::{PointerButton, RoutedPointerEvent, RoutedPointerEventKind};

        let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
        let render = render_with_context(Rc::new(RenderState::default()), {
            let offset_slot = Rc::clone(&offset_slot);
            move || {
                let offset = use_signal(|| 0.0_f32);
                *offset_slot.borrow_mut() = Some(offset.clone());
                scroll_box()
                    .height(120.0)
                    .offset_y(offset)
                    .children(text().children(
                        "line 01\nline 02\nline 03\nline 04\nline 05\nline 06\nline 07\nline 08\nline 09\nline 10\nline 11\nline 12",
                    ))
            }
        });
        let offset = offset_slot
            .borrow()
            .clone()
            .expect("scroll signal should have been captured");
        let scrollbox_id = render
            .view
            .element
            .id
            .expect("scroll box should receive an element id");
        let snapshot =
            ruin_ui::layout_snapshot(1, ruin_ui::UiSize::new(260.0, 160.0), render.view.element());
        let metrics = snapshot
            .interaction_tree
            .scroll_metrics_for_element(scrollbox_id)
            .expect("scroll metrics should exist for the scroll box");
        let thumb = metrics
            .scrollbar_thumb
            .expect("overflowing scroll box should expose a scrollbar thumb");
        let thumb_center = Point::new(
            thumb.origin.x + (thumb.size.width * 0.5),
            thumb.origin.y + (thumb.size.height * 0.5),
        );
        let hovered_targets = snapshot.interaction_tree.hit_path(thumb_center);
        let handler = scroll_handler_for_event(
            &render.view.bindings.on_scroll,
            Some(scrollbox_id),
            &hovered_targets,
        )
        .expect("scroll box should resolve its scroll handler")
        .clone();

        handler(
            &RoutedPointerEvent {
                kind: RoutedPointerEventKind::Down {
                    button: PointerButton::Primary,
                },
                target: hovered_targets
                    .last()
                    .cloned()
                    .expect("thumb center should hit the scroll box"),
                pointer_id: 1,
                position: thumb_center,
            },
            &snapshot.interaction_tree,
        );
        handler(
            &RoutedPointerEvent {
                kind: RoutedPointerEventKind::Move,
                target: hovered_targets
                    .last()
                    .cloned()
                    .expect("thumb center should hit the scroll box"),
                pointer_id: 1,
                position: Point::new(thumb_center.x, thumb_center.y + 24.0),
            },
            &snapshot.interaction_tree,
        );

        assert!(offset.get() > 0.0);
    }

    #[test]
    fn live_input_path_scrolls_a_scroll_box_rendered_inside_a_branch() {
        use ruin_ui::{PointerButton, PointerEvent, PointerEventKind};

        let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
        let render = render_with_context(Rc::new(RenderState::default()), {
            let offset_slot = Rc::clone(&offset_slot);
            move || match true {
                true => {
                    let offset = use_signal(|| 0.0_f32);
                    *offset_slot.borrow_mut() = Some(offset.clone());
                    column()
                        .background(surfaces::raised())
                        .gap(10.0)
                        .children((
                            text().children(("bytes = ", 4096)),
                            scroll_box()
                                .height(420.0)
                                .offset_y(offset.clone())
                                .padding(12.0)
                                .background(surfaces::canvas())
                                .border_radius(10.0)
                                .border((2.0, colors::muted()))
                                .children(
                                    text()
                                        .color(colors::muted())
                                        .font_family(ruin_ui::TextFontFamily::Monospace)
                                        .children(
                                            "line 01\nline 02\nline 03\nline 04\nline 05\nline 06\nline 07\nline 08\nline 09\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24",
                                        ),
                                ),
                        ))
                }
                false => View::from_element(ruin_ui::Element::column()),
            }
        });
        let offset = offset_slot
            .borrow()
            .clone()
            .expect("scroll signal should have been captured");
        let snapshot = ruin_ui::layout_snapshot(
            1,
            ruin_ui::UiSize::new(1080.0, 760.0),
            render.view.element(),
        );
        let interaction_tree = RefCell::new(Some(snapshot.interaction_tree.clone()));
        let bindings = RefCell::new(render.view.bindings.clone());
        let mut pointer_router = ruin_ui::PointerRouter::new();
        let mut input_state = InputState::new();
        let window = ruin_ui::UiRuntime::headless()
            .create_window(WindowSpec::new("scrollbox-test"))
            .expect("headless window should be created");

        MountedApp::<View>::handle_pointer_event(
            &window,
            &interaction_tree,
            &bindings,
            &mut pointer_router,
            &mut input_state,
            PointerEvent::new(
                1,
                Point::new(24.0, 64.0),
                PointerEventKind::Down {
                    button: PointerButton::Primary,
                },
            ),
        )
        .expect("pointer down should succeed");
        MountedApp::<View>::handle_keyboard_event(
            &interaction_tree,
            &bindings,
            &RefCell::new(Vec::new()),
            &input_state,
            ruin_ui::KeyboardEvent::new(
                0,
                ruin_ui::KeyboardEventKind::Pressed,
                ruin_ui::KeyboardKey::ArrowDown,
                KeyboardModifiers::default(),
                None,
            ),
        )
        .expect("keyboard event should succeed");

        assert!(offset.get() > 0.0);
    }

    #[test]
    fn scroll_box_stays_interactive_when_it_appears_on_a_later_render() {
        use ruin_ui::{PointerButton, PointerEvent, PointerEventKind};

        let state = Rc::new(RenderState::default());
        let ready_slot = Rc::new(RefCell::new(None::<Signal<bool>>));
        let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));

        let render_once =
            |state: Rc<RenderState>,
             ready_slot: Rc<RefCell<Option<Signal<bool>>>>,
             offset_slot: Rc<RefCell<Option<Signal<f32>>>>| {
                render_with_context(state, move || {
                    let ready = use_signal(|| false);
                    let offset = use_signal(|| 0.0_f32);
                    *ready_slot.borrow_mut() = Some(ready.clone());
                    *offset_slot.borrow_mut() = Some(offset.clone());

                    if ready.get() {
                        column()
                        .background(surfaces::raised())
                        .gap(10.0)
                        .children((
                            text().children(("bytes = ", 4096)),
                            scroll_box()
                                .height(420.0)
                                .offset_y(offset.clone())
                                .padding(12.0)
                                .background(surfaces::canvas())
                                .border_radius(10.0)
                                .border((2.0, colors::muted()))
                                .children(
                                    text()
                                        .color(colors::muted())
                                        .font_family(ruin_ui::TextFontFamily::Monospace)
                                        .children(
                                            "line 01\nline 02\nline 03\nline 04\nline 05\nline 06\nline 07\nline 08\nline 09\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24",
                                        ),
                                ),
                        ))
                    } else {
                        text().children("Loading file contents...")
                    }
                })
            };

        let _initial = render_once(
            Rc::clone(&state),
            Rc::clone(&ready_slot),
            Rc::clone(&offset_slot),
        );
        let ready = ready_slot
            .borrow()
            .clone()
            .expect("ready signal should have been captured");
        let offset = offset_slot
            .borrow()
            .clone()
            .expect("offset signal should have been captured");
        let _ = ready.set(true);

        let render = render_once(state, ready_slot, Rc::clone(&offset_slot));
        let snapshot = ruin_ui::layout_snapshot(
            1,
            ruin_ui::UiSize::new(1080.0, 760.0),
            render.view.element(),
        );
        let interaction_tree = RefCell::new(Some(snapshot.interaction_tree.clone()));
        let bindings = RefCell::new(render.view.bindings.clone());
        let mut pointer_router = ruin_ui::PointerRouter::new();
        let mut input_state = InputState::new();
        let window = ruin_ui::UiRuntime::headless()
            .create_window(WindowSpec::new("scrollbox-transition-test"))
            .expect("headless window should be created");

        MountedApp::<View>::handle_pointer_event(
            &window,
            &interaction_tree,
            &bindings,
            &mut pointer_router,
            &mut input_state,
            PointerEvent::new(
                1,
                Point::new(24.0, 64.0),
                PointerEventKind::Down {
                    button: PointerButton::Primary,
                },
            ),
        )
        .expect("pointer down should succeed after branch switch");
        MountedApp::<View>::handle_keyboard_event(
            &interaction_tree,
            &bindings,
            &RefCell::new(Vec::new()),
            &input_state,
            ruin_ui::KeyboardEvent::new(
                0,
                ruin_ui::KeyboardEventKind::Pressed,
                ruin_ui::KeyboardKey::ArrowDown,
                KeyboardModifiers::default(),
                None,
            ),
        )
        .expect("keyboard event should succeed after branch switch");

        assert!(offset.get() > 0.0);
    }

    #[test]
    fn live_input_path_scrolls_with_real_cargo_lock_contents() {
        use ruin_ui::{PointerEvent, PointerEventKind};

        let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
        let render = render_with_context(Rc::new(RenderState::default()), {
            let offset_slot = Rc::clone(&offset_slot);
            move || {
                let offset = use_signal(|| 0.0_f32);
                *offset_slot.borrow_mut() = Some(offset.clone());
                column().background(surfaces::raised()).gap(10.0).children((
                    text().children(("bytes = ", include_str!("../../../Cargo.lock").len())),
                    scroll_box()
                        .height(420.0)
                        .offset_y(offset.clone())
                        .padding(12.0)
                        .background(surfaces::canvas())
                        .border_radius(10.0)
                        .border((2.0, colors::muted()))
                        .children(
                            text()
                                .color(colors::muted())
                                .font_family(ruin_ui::TextFontFamily::Monospace)
                                .children(include_str!("../../../Cargo.lock")),
                        ),
                ))
            }
        });
        let offset = offset_slot
            .borrow()
            .clone()
            .expect("scroll signal should have been captured");
        let snapshot = ruin_ui::layout_snapshot(
            1,
            ruin_ui::UiSize::new(1080.0, 760.0),
            render.view.element(),
        );
        let interaction_tree = RefCell::new(Some(snapshot.interaction_tree.clone()));
        let bindings = RefCell::new(render.view.bindings.clone());
        let mut pointer_router = ruin_ui::PointerRouter::new();
        let mut input_state = InputState::new();
        let window = ruin_ui::UiRuntime::headless()
            .create_window(WindowSpec::new("scrollbox-cargo-lock-test"))
            .expect("headless window should be created");

        MountedApp::<View>::handle_pointer_event(
            &window,
            &interaction_tree,
            &bindings,
            &mut pointer_router,
            &mut input_state,
            PointerEvent::new(
                1,
                Point::new(24.0, 64.0),
                PointerEventKind::Scroll {
                    delta: Point::new(0.0, 48.0),
                },
            ),
        )
        .expect("wheel event should succeed");

        assert!(offset.get() > 0.0);
    }

    #[test]
    fn rerendered_scroll_box_element_carries_the_updated_offset() {
        let state = Rc::new(RenderState::default());
        let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
        let render_once =
            |state: Rc<RenderState>, offset_slot: Rc<RefCell<Option<Signal<f32>>>>| {
                render_with_context(state, move || {
                    let offset = use_signal(|| 0.0_f32);
                    *offset_slot.borrow_mut() = Some(offset.clone());
                    scroll_box()
                        .height(120.0)
                        .offset_y(offset.clone())
                        .children(
                            text().children("line 01\nline 02\nline 03\nline 04\nline 05\nline 06"),
                        )
                })
            };

        let _initial = render_once(Rc::clone(&state), Rc::clone(&offset_slot));
        let offset = offset_slot
            .borrow()
            .clone()
            .expect("offset signal should have been captured");
        let _ = offset.set(96.0);
        let render = render_once(state, offset_slot);
        let debug = format!("{:?}", render.view.element());

        assert!(debug.contains("offset_y: 96.0"), "{debug}");
    }
}
