use dioxus::prelude::*;
#[cfg(test)]
use std::cell::Cell;
#[cfg(test)]
use std::rc::Rc;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DemoModel {
    pub id: &'static str,
    pub name: &'static str,
    pub generation: u64,
    pub vertices: usize,
    pub triangles: usize,
}

const DEMO_MODELS: [DemoModel; 2] = [
    DemoModel {
        id: "calibration-cube",
        name: "Calibration cube",
        generation: 1,
        vertices: 8,
        triangles: 12,
    },
    DemoModel {
        id: "benchy-hull",
        name: "Benchy hull stub",
        generation: 2,
        vertices: 42,
        triangles: 80,
    },
];

pub fn demo_models() -> &'static [DemoModel] {
    &DEMO_MODELS
}

pub fn demo_model_for_route(model_id: &str) -> DemoModel {
    demo_models()
        .iter()
        .copied()
        .find(|model| model.id == model_id)
        .unwrap_or(DEMO_MODELS[0])
}

#[derive(Clone, Debug)]
pub struct ViewerMeshHandle {
    pub key: SessionKey,
    pub summary: MeshSummary,
}

impl ViewerMeshHandle {
    pub fn from_demo_model(model: DemoModel) -> Self {
        Self {
            key: SessionKey::new(model.id, model.generation),
            summary: MeshSummary {
                display_name: model.name,
                vertices: model.vertices,
                triangles: model.triangles,
            },
        }
    }
}

impl PartialEq for ViewerMeshHandle {
    fn eq(&self, other: &Self) -> bool {
        self.key == other.key
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MeshSummary {
    pub display_name: &'static str,
    pub vertices: usize,
    pub triangles: usize,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SessionKey {
    model_id: String,
    generation: u64,
}

impl SessionKey {
    pub fn new(model_id: impl Into<String>, generation: u64) -> Self {
        Self {
            model_id: model_id.into(),
            generation,
        }
    }

    pub fn model_id(&self) -> &str {
        &self.model_id
    }

    pub fn generation(&self) -> u64 {
        self.generation
    }

    #[cfg(test)]
    pub fn switch_to(&self, next_model_id: impl Into<String>) -> Self {
        Self {
            model_id: next_model_id.into(),
            generation: self.generation + 1,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ViewerIssue {
    Unsupported,
    Corrupt,
    TooLarge,
    RendererInit,
}

impl ViewerIssue {
    pub fn message(self) -> &'static str {
        match self {
            Self::Unsupported => "This model format is not supported yet.",
            Self::Corrupt => "This model could not be read. Try another file.",
            Self::TooLarge => "This model is too large for the browser viewer.",
            Self::RendererInit => "The viewer could not start on this canvas.",
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ViewerStatus {
    NoMesh,
    Loading,
    Ready(SessionKey),
    MeshError(ViewerIssue),
    RendererError(ViewerIssue),
}

impl ViewerStatus {
    pub fn label(&self) -> &'static str {
        match self {
            Self::NoMesh => "No mesh selected",
            Self::Loading => "Loading mesh",
            Self::Ready(_) => "Stub renderer ready",
            Self::MeshError(_) => "Mesh unavailable",
            Self::RendererError(_) => "Renderer unavailable",
        }
    }

    pub fn message(&self) -> &'static str {
        match self {
            Self::NoMesh => "Choose a model to start the viewer.",
            Self::Loading => "Preparing the mesh contract for the canvas boundary…",
            Self::Ready(_) => {
                "Stub rendering is active. A real renderer can attach behind this canvas boundary later."
            }
            Self::MeshError(issue) | Self::RendererError(issue) => issue.message(),
        }
    }

    pub fn should_have_session(&self) -> bool {
        matches!(self, Self::Ready(_))
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DemoMode {
    NoMesh,
    Loading,
    Ready,
    Unsupported,
    Corrupt,
    TooLarge,
    RendererError,
}

impl DemoMode {
    fn status(self, key: &SessionKey) -> ViewerStatus {
        match self {
            Self::NoMesh => ViewerStatus::NoMesh,
            Self::Loading => ViewerStatus::Loading,
            Self::Ready => ViewerStatus::Ready(key.clone()),
            Self::Unsupported => ViewerStatus::MeshError(ViewerIssue::Unsupported),
            Self::Corrupt => ViewerStatus::MeshError(ViewerIssue::Corrupt),
            Self::TooLarge => ViewerStatus::MeshError(ViewerIssue::TooLarge),
            Self::RendererError => ViewerStatus::RendererError(ViewerIssue::RendererInit),
        }
    }
}

#[cfg(test)]
pub struct SessionLifecycle {
    active: Option<TrackedSession>,
    cleanups: Rc<Cell<usize>>,
}

#[cfg(test)]
impl SessionLifecycle {
    pub fn new(cleanups: Rc<Cell<usize>>) -> Self {
        Self {
            active: None,
            cleanups,
        }
    }

    pub fn reconcile(&mut self, status: &ViewerStatus) {
        match status {
            ViewerStatus::Ready(key) => {
                let same_session = self
                    .active
                    .as_ref()
                    .is_some_and(|session| session.key == *key);
                if !same_session {
                    self.active = Some(TrackedSession::new(key.clone(), Rc::clone(&self.cleanups)));
                }
            }
            _ => {
                self.active = None;
            }
        }
    }

    pub fn has_active_session(&self) -> bool {
        self.active.is_some()
    }
}

#[cfg(test)]
struct TrackedSession {
    key: SessionKey,
    cleanups: Rc<Cell<usize>>,
}

#[cfg(test)]
impl TrackedSession {
    fn new(key: SessionKey, cleanups: Rc<Cell<usize>>) -> Self {
        Self { key, cleanups }
    }
}

#[cfg(test)]
impl Drop for TrackedSession {
    fn drop(&mut self) {
        self.cleanups.set(self.cleanups.get() + 1);
    }
}

#[component]
pub fn ViewerPage(initial_model: DemoModel) -> Element {
    let mut selected_model = use_signal(|| initial_model);
    let mut mode = use_signal(|| DemoMode::Ready);
    let canvas_mounted = use_signal(|| false);
    let resize_count = use_signal(|| 0_u64);

    let model = selected_model();
    let mesh = ViewerMeshHandle::from_demo_model(model);
    let status = mode().status(&mesh.key);
    let visible_mesh = status.should_have_session().then_some(mesh.clone());
    let session_label = format!("{}#{}", mesh.key.model_id(), mesh.key.generation());
    rsx! {
        section { class: "model-viewer-shell", aria_label: "Dioxus canvas-isolated model viewer scaffold",
            header { class: "model-viewer-header",
                div {
                    p { class: "viewer-eyebrow", "Canvas viewer boundary" }
                    h1 { "Model viewer scaffold" }
                    p { "Renderer-agnostic Dioxus route with one owned canvas and explicit lifecycle states." }
                }
                div { class: "viewer-status-pill", "{status.label()}" }
            }

            div { class: "viewer-demo-controls", aria_label: "Demo model controls",
                for model_option in demo_models() {
                    button {
                        class: if *model_option == model { "viewer-demo-button is-active" } else { "viewer-demo-button" },
                        r#type: "button",
                        onclick: move |_| {
                            selected_model.set(*model_option);
                            mode.set(DemoMode::Ready);
                        },
                        "{model_option.name}"
                    }
                }
            }

            div { class: "viewer-demo-controls", aria_label: "Viewer state controls",
                button { class: "viewer-demo-button", r#type: "button", onclick: move |_| mode.set(DemoMode::NoMesh), "No mesh" } button { class: "viewer-demo-button", r#type: "button", onclick: move |_| mode.set(DemoMode::Loading), "Loading" }
                button { class: "viewer-demo-button", r#type: "button", onclick: move |_| mode.set(DemoMode::Ready), "Ready" }
                button { class: "viewer-demo-button", r#type: "button", onclick: move |_| mode.set(DemoMode::Unsupported), "Unsupported" }
                button { class: "viewer-demo-button", r#type: "button", onclick: move |_| mode.set(DemoMode::Corrupt), "Corrupt" }
                button { class: "viewer-demo-button", r#type: "button", onclick: move |_| mode.set(DemoMode::TooLarge), "Too large" }
                button { class: "viewer-demo-button", r#type: "button", onclick: move |_| mode.set(DemoMode::RendererError), "Renderer error" }
            }

            ModelViewer {
                mesh: visible_mesh,
                status,
                canvas_mounted,
                resize_count,
                session_label,
            }
        }
    }
}

#[derive(Props, Clone, PartialEq)]
pub struct ModelViewerProps {
    mesh: Option<ViewerMeshHandle>,
    status: ViewerStatus,
    canvas_mounted: Signal<bool>,
    resize_count: Signal<u64>,
    session_label: String,
}

#[component]
pub fn ModelViewer(props: ModelViewerProps) -> Element {
    let canvas_mounted = props.canvas_mounted;
    let resize_count = props.resize_count;
    let status = props.status.clone();
    let mesh = props.mesh.clone();

    rsx! {
        div { class: "model-viewer-boundary", id: "polymodel-viewer-boundary", "data-viewer-status": "{status.label()}", "data-session-wanted": "{status.should_have_session()}", "data-resize-count": "{resize_count()}", "data-session-label": "{props.session_label}",

            div { class: "model-viewer-stage",
                canvas {
                    id: "polymodel-viewer-canvas", "data-mounted": "{canvas_mounted()}",
                    class: "model-viewer-canvas",
                    aria_label: "Polymodel model viewer canvas",
                    onmounted: move |event| mark_canvas_mounted(event, canvas_mounted),
                    onwheel: move |event| event.stop_propagation(),
                    onpointerdown: move |event| event.stop_propagation(),
                }
                div { class: "model-viewer-overlay", aria_live: "polite",
                    h2 { "{status.label()}" }
                    p { "{status.message()}" }
                    if let Some(mesh) = mesh {
                        dl { class: "viewer-mesh-summary",
                            div { dt { "Model" } dd { "{mesh.summary.display_name}" } }
                            div { dt { "Session" } dd { "{props.session_label}" } }
                            div { dt { "Vertices" } dd { "{mesh.summary.vertices}" } }
                            div { dt { "Triangles" } dd { "{mesh.summary.triangles}" } }
                        }
                    }
                    p { class: "viewer-lifecycle-note",
                        "Canvas mounted: {canvas_mounted()} · resize callbacks: {resize_count()} · session wanted: {status.should_have_session()}"
                    }
                }
            }
        }
        StubCanvasSession {
            status: status.clone(),
            canvas_mounted,
            resize_count,
        }
    }
}

#[component]
fn StubCanvasSession(
    status: ViewerStatus,
    canvas_mounted: Signal<bool>,
    resize_count: Signal<u64>,
) -> Element {
    let session_key = match status {
        ViewerStatus::Ready(ref key) if canvas_mounted() => Some(key.clone()),
        _ => None,
    };

    rsx! {
        if let Some(key) = session_key {
            StubCanvasSessionInner { key: "{key.model_id()}-{key.generation()}", session_key: key, resize_count }
        }
    }
}

#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
#[component]
fn StubCanvasSessionInner(session_key: SessionKey, resize_count: Signal<u64>) -> Element {
    let _ = session_key;
    let _ = resize_count;
    rsx! {}
}

#[cfg(all(target_family = "wasm", target_os = "unknown"))]
#[component]
fn StubCanvasSessionInner(session_key: SessionKey, resize_count: Signal<u64>) -> Element {
    use wasm_bindgen::JsCast;
    use wasm_bindgen::closure::Closure;
    use web_sys::Event;

    let _session = use_hook(move || {
        let mut cleanup: Option<Box<dyn FnOnce()>> = None;

        if let Some(window) = web_sys::window() {
            let mut resize_count = resize_count;
            let resize_closure = Closure::<dyn FnMut(Event)>::wrap(Box::new(move |_| {
                resize_count.set(resize_count() + 1);
            }));

            if window
                .add_event_listener_with_callback("resize", resize_closure.as_ref().unchecked_ref())
                .is_ok()
            {
                cleanup = Some(Box::new(move || {
                    if let Some(window) = web_sys::window() {
                        let _ = window.remove_event_listener_with_callback(
                            "resize",
                            resize_closure.as_ref().unchecked_ref(),
                        );
                    }
                }));
            }
        }

        std::rc::Rc::new(StubBrowserSession {
            key: session_key,
            cleanup,
        })
    });

    rsx! {}
}

#[cfg(all(target_family = "wasm", target_os = "unknown"))]
struct StubBrowserSession {
    key: SessionKey,
    cleanup: Option<Box<dyn FnOnce()>>,
}

#[cfg(all(target_family = "wasm", target_os = "unknown"))]
impl Drop for StubBrowserSession {
    fn drop(&mut self) {
        tracing::debug!(
            model_id = self.key.model_id(),
            generation = self.key.generation(),
            "dropping stub canvas session"
        );
        if let Some(cleanup) = self.cleanup.take() {
            cleanup();
        }
    }
}
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
fn mark_canvas_mounted(_: MountedEvent, mut mounted: Signal<bool>) {
    mounted.set(true);
}
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
fn mark_canvas_mounted(event: MountedEvent, mut mounted: Signal<bool>) {
    use dioxus::web::WebEventExt;
    use wasm_bindgen::JsCast;
    let element = event.as_web_event();
    mounted.set(element.dyn_into::<web_sys::HtmlCanvasElement>().is_ok());
}
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn switching_models_advances_session_generation() {
        let first = SessionKey::new("calibration-cube", 7);
        let second = first.switch_to("benchy-hull");

        assert_eq!(second.model_id(), "benchy-hull");
        assert_eq!(second.generation(), 8);
    }

    #[test]
    fn status_labels_are_user_facing() {
        let ready = ViewerStatus::Ready(SessionKey::new("calibration-cube", 1));

        assert_eq!(ViewerStatus::Loading.label(), "Loading mesh");
        assert_eq!(ready.label(), "Stub renderer ready");
        assert_eq!(
            ViewerStatus::MeshError(ViewerIssue::Corrupt).label(),
            "Mesh unavailable"
        );
    }

    #[test]
    fn modeled_errors_have_product_messages() {
        assert_eq!(
            ViewerIssue::Unsupported.message(),
            "This model format is not supported yet."
        );
        assert_eq!(
            ViewerIssue::Corrupt.message(),
            "This model could not be read. Try another file."
        );
        assert_eq!(
            ViewerIssue::TooLarge.message(),
            "This model is too large for the browser viewer."
        );
        assert_eq!(
            ViewerIssue::RendererInit.message(),
            "The viewer could not start on this canvas."
        );
    }

    #[test]
    fn demo_models_are_switchable() {
        let models = demo_models();

        assert!(models.len() >= 2);
        assert_ne!(models[0].id, models[1].id);
    }

    #[test]
    fn mesh_handle_equality_uses_only_session_key() {
        let mut cube = ViewerMeshHandle::from_demo_model(DEMO_MODELS[0]);
        let same_key = cube.clone();
        cube.summary.vertices += 10;

        assert_eq!(cube, same_key);
    }

    #[test]
    fn lifecycle_drops_replaced_and_error_sessions_once() {
        let cleanups = Rc::new(Cell::new(0));
        let mut lifecycle = SessionLifecycle::new(Rc::clone(&cleanups));

        lifecycle.reconcile(&ViewerStatus::Ready(SessionKey::new("a", 1)));
        assert!(lifecycle.has_active_session());
        assert_eq!(cleanups.get(), 0);

        lifecycle.reconcile(&ViewerStatus::Ready(SessionKey::new("b", 2)));
        assert!(lifecycle.has_active_session());
        assert_eq!(cleanups.get(), 1);

        lifecycle.reconcile(&ViewerStatus::RendererError(ViewerIssue::RendererInit));
        assert!(!lifecycle.has_active_session());
        assert_eq!(cleanups.get(), 2);
    }
}
