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 asset_path: &'static str,
    pub generation: u64,
    pub vertices: usize,
    pub triangles: usize,
}

const DEMO_MODELS: [DemoModel; 2] = [
    DemoModel {
        id: "body-f-chest-v4",
        name: "Body F chest v4",
        asset_path: "/assets/models/body_f_chest-v4.stl",
        generation: 1,
        vertices: 41051,
        triangles: 82122,
    },
    DemoModel {
        id: "body-f-chest-v4-detail",
        name: "Body F chest detail",
        asset_path: "/assets/models/body_f_chest-v4.stl",
        generation: 2,
        vertices: 41051,
        triangles: 82122,
    },
];

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,
                asset_path: model.asset_path,
                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 asset_path: &'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,
        }
    }
}

#[cfg_attr(not(test), allow(dead_code))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ViewerIssue {
    MeshUnavailable,
    RendererInit,
}

impl ViewerIssue {
    pub fn message(self) -> &'static str {
        match self {
            Self::MeshUnavailable => "This model could not be loaded for the viewer.",
            Self::RendererInit => "The viewer could not start on this canvas.",
        }
    }
}

#[cfg_attr(not(test), allow(dead_code))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ViewerStatus {
    Ready(SessionKey),
    MeshError(ViewerIssue),
    RendererError(ViewerIssue),
}

impl ViewerStatus {
    pub fn label(&self) -> &'static str {
        match self {
            Self::Ready(_) => "Viewer ready",
            Self::MeshError(_) => "Mesh unavailable",
            Self::RendererError(_) => "Renderer unavailable",
        }
    }

    pub fn message(&self) -> &'static str {
        match self {
            Self::Ready(_) => "STL asset is loaded and ready for the canvas renderer.",
            Self::MeshError(issue) | Self::RendererError(issue) => issue.message(),
        }
    }

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

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RendererMount {
    pub key: SessionKey,
    pub mesh: MeshSummary,
}

impl RendererMount {
    pub fn new(key: SessionKey, mesh: MeshSummary) -> Self {
        Self { key, mesh }
    }
}

#[cfg_attr(not(test), allow(dead_code))]
pub trait ViewerRenderer: Sized {
    type Canvas;
    type Error;

    fn mount(canvas: Self::Canvas, mount: RendererMount) -> Result<Self, Self::Error>;
    fn replace_mesh(&mut self, mount: RendererMount) -> Result<(), Self::Error>;
    fn resize(&mut self) -> Result<(), Self::Error>;
}

#[cfg(test)]
pub struct RendererLifecycle<R: ViewerRenderer> {
    active: Option<R>,
}

#[cfg(test)]
impl<R: ViewerRenderer> RendererLifecycle<R>
where
    R::Canvas: Clone,
{
    pub fn new() -> Self {
        Self { active: None }
    }

    pub fn reconcile(
        &mut self,
        canvas: Option<R::Canvas>,
        status: &ViewerStatus,
        mesh: Option<MeshSummary>,
    ) -> Result<(), R::Error> {
        let Some(canvas) = canvas else {
            self.active = None;
            return Ok(());
        };

        let (ViewerStatus::Ready(key), Some(mesh)) = (status, mesh) else {
            self.active = None;
            return Ok(());
        };

        let mount = RendererMount::new(key.clone(), mesh);
        match self.active.as_mut() {
            Some(renderer) => renderer.replace_mesh(mount),
            None => {
                self.active = Some(R::mount(canvas, mount)?);
                Ok(())
            }
        }
    }

    pub fn resize(&mut self) -> Result<(), R::Error> {
        if let Some(renderer) = self.active.as_mut() {
            renderer.resize()?;
        }
        Ok(())
    }

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

#[component]
pub fn ViewerPage(initial_model: DemoModel) -> Element {
    let mut selected_model = use_signal(|| initial_model);
    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 = ViewerStatus::Ready(mesh.key.clone());
    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: "Asset-backed STL model viewer",
            header { class: "model-viewer-header",
                div {
                    p { class: "viewer-eyebrow", "Asset-backed STL viewer" }
                    h1 { "Model viewer" }
                    p { "Loads the committed STL asset through the same metadata a renderer adapter receives." }
                }
                div { class: "viewer-status-pill", "{status.label()}" }
            }

            div { class: "viewer-demo-controls", aria_label: "Model choices",
                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),
                        "{model_option.name}"
                    }
                }
            }

            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();
    let asset_path = mesh
        .as_ref()
        .map(|mesh| mesh.summary.asset_path)
        .unwrap_or("");

    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}",
            "data-asset-path": asset_path.to_string(),

            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(),
                }
                if let Some(ref mesh) = mesh {
                    AssetPreview { mesh: mesh.clone() }
                }
                div { class: "model-viewer-overlay", aria_live: "polite",
                    h2 { "{status.label()}" }
                    p { "{status.message()}" }
                    if let Some(ref mesh) = mesh {
                        dl { class: "viewer-mesh-summary",
                            div { dt { "Model" } dd { "{mesh.summary.display_name}" } }
                            div { dt { "Asset" } dd { "{mesh.summary.asset_path}" } }
                            div { dt { "Vertices" } dd { "{mesh.summary.vertices}" } }
                            div { dt { "Triangles" } dd { "{mesh.summary.triangles}" } }
                        }
                    }
                }
            }
        }
        CanvasRendererSession {
            status: status.clone(),
            mesh,
            canvas_mounted,
            resize_count,
        }
    }
}

#[component]
fn AssetPreview(mesh: ViewerMeshHandle) -> Element {
    rsx! {
        svg {
            id: "polymodel-viewer-preview",
            class: "model-viewer-preview",
            view_box: "0 0 640 420",
            role: "img",
            "aria-label": "Preview silhouette for {mesh.summary.display_name}",
            path { class: "model-preview-shadow", d: "M158 346 C245 384 406 386 493 347 C425 330 235 329 158 346 Z" }
            path { class: "model-preview-body", d: "M320 56 C263 71 220 115 201 173 C177 247 202 312 278 337 L362 337 C438 312 463 247 439 173 C420 115 377 71 320 56 Z" }
            path { class: "model-preview-cut", d: "M249 191 C268 154 292 136 320 136 C348 136 372 154 391 191 C365 181 275 181 249 191 Z" }
            path { class: "model-preview-line", d: "M218 250 C267 276 373 276 422 250" }
            path { class: "model-preview-line", d: "M238 302 C286 322 354 322 402 302" }
            text { class: "model-preview-label", x: "320", y: "388", text_anchor: "middle", "{mesh.summary.triangles} triangles · {mesh.summary.vertices} vertices" }
        }
    }
}

#[component]
fn CanvasRendererSession(
    status: ViewerStatus,
    mesh: Option<ViewerMeshHandle>,
    canvas_mounted: Signal<bool>,
    resize_count: Signal<u64>,
) -> Element {
    let mount = match (status, mesh) {
        (ViewerStatus::Ready(key), Some(mesh)) if canvas_mounted() => {
            Some(RendererMount::new(key, mesh.summary))
        }
        _ => None,
    };

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

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

#[cfg(all(target_family = "wasm", target_os = "unknown"))]
#[component]
fn BrowserRendererSession(mount: RendererMount, 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(BrowserRendererStub { mount, cleanup })
    });

    rsx! {}
}

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

#[cfg(all(target_family = "wasm", target_os = "unknown"))]
impl Drop for BrowserRendererStub {
    fn drop(&mut self) {
        tracing::debug!(
            model_id = self.mount.key.model_id(),
            generation = self.mount.key.generation(),
            asset_path = self.mount.mesh.asset_path,
            "dropping browser renderer 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::*;
    use crate::mesh::stl::load_stl;

    #[derive(Clone)]
    struct FakeCanvas;

    struct FakeRenderer {
        events: Rc<Cell<usize>>,
        drops: Rc<Cell<usize>>,
    }

    impl ViewerRenderer for FakeRenderer {
        type Canvas = (FakeCanvas, Rc<Cell<usize>>, Rc<Cell<usize>>);
        type Error = ();

        fn mount(canvas: Self::Canvas, _mount: RendererMount) -> Result<Self, Self::Error> {
            canvas.1.set(canvas.1.get() + 1);
            Ok(Self {
                events: canvas.1,
                drops: canvas.2,
            })
        }

        fn replace_mesh(&mut self, _mount: RendererMount) -> Result<(), Self::Error> {
            self.events.set(self.events.get() + 1);
            Ok(())
        }

        fn resize(&mut self) -> Result<(), Self::Error> {
            self.events.set(self.events.get() + 1);
            Ok(())
        }
    }

    impl Drop for FakeRenderer {
        fn drop(&mut self) {
            self.drops.set(self.drops.get() + 1);
        }
    }

    fn first_mount() -> RendererMount {
        let model = demo_models()[0];
        let handle = ViewerMeshHandle::from_demo_model(model);
        RendererMount::new(handle.key, handle.summary)
    }

    #[test]
    fn demo_route_uses_committed_stl_asset() {
        let model = demo_model_for_route("body-f-chest-v4");
        assert_eq!(model.asset_path, "/assets/models/body_f_chest-v4.stl");
        assert_eq!(model.vertices, 41051);
        assert_eq!(model.triangles, 82122);
    }

    #[test]
    fn demo_model_metadata_matches_committed_fixture() {
        let model = demo_models()[0];
        let path = format!("{}{}", env!("CARGO_MANIFEST_DIR"), model.asset_path);
        let bytes = std::fs::read(path).expect("demo STL asset should be committed");
        let mesh = load_stl(&bytes).expect("demo STL asset should parse");

        assert_eq!(mesh.trimesh.positions.len(), model.vertices);
        assert_eq!(mesh.trimesh.triangle_count(), model.triangles);
    }

    #[test]
    fn switching_models_advances_session_generation() {
        let first = SessionKey::new("body-f-chest-v4", 7);
        let second = first.switch_to("body-f-chest-v4-detail");

        assert_eq!(second.model_id(), "body-f-chest-v4-detail");
        assert_eq!(second.generation(), 8);
    }

    #[test]
    fn status_labels_are_user_facing() {
        let ready = ViewerStatus::Ready(SessionKey::new("body-f-chest-v4", 1));

        assert_eq!(ready.label(), "Viewer ready");
        assert_eq!(
            ViewerStatus::MeshError(ViewerIssue::MeshUnavailable).label(),
            "Mesh unavailable"
        );
    }

    #[test]
    fn renderer_lifecycle_mounts_replaces_resizes_and_drops() {
        let events = Rc::new(Cell::new(0));
        let drops = Rc::new(Cell::new(0));
        let canvas = (FakeCanvas, Rc::clone(&events), Rc::clone(&drops));
        let mut lifecycle = RendererLifecycle::<FakeRenderer>::new();
        let mount = first_mount();
        let status = ViewerStatus::Ready(mount.key.clone());

        lifecycle
            .reconcile(Some(canvas.clone()), &status, Some(mount.mesh))
            .expect("mount should succeed");
        assert!(lifecycle.has_active_session());
        assert_eq!(events.get(), 1);

        lifecycle.resize().expect("resize should forward");
        assert_eq!(events.get(), 2);

        let replacement =
            RendererMount::new(SessionKey::new("body-f-chest-v4-detail", 2), mount.mesh);
        lifecycle
            .reconcile(
                Some(canvas),
                &ViewerStatus::Ready(replacement.key),
                Some(replacement.mesh),
            )
            .expect("replace should succeed");
        assert_eq!(events.get(), 3);
        assert_eq!(drops.get(), 0);

        lifecycle
            .reconcile(
                None,
                &ViewerStatus::RendererError(ViewerIssue::RendererInit),
                None,
            )
            .expect("drop path should succeed");
        assert!(!lifecycle.has_active_session());
        assert_eq!(drops.get(), 1);
    }

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

        assert_eq!(body, same_key);
    }
}
