//! Scene viewer component for displaying realm scenes with avatars.
//!
//! Uses layered canvases for efficient rendering:
//! - Background canvas: Static, drawn once when scene loads
//! - Avatar canvas: Dynamic, redrawn when members change
//!
//! Supports two rendering modes:
//! - **Fit mode** (default): Background scales to fit viewport with letterboxing
//! - **Pan mode**: Canvas at native resolution with optional zoom, user can scroll

use std::collections::HashMap;

use leptos::prelude::*;
use uuid::Uuid;

use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp, Scene};

#[cfg(feature = "hydrate")]
use super::avatar_canvas::hit_test_canvas;
use super::avatar_canvas::{AvatarCanvas, member_key};
use super::chat_types::ActiveBubble;
use super::context_menu::{ContextMenu, ContextMenuItem};
use super::settings::{
    BASE_PROP_SIZE, REFERENCE_HEIGHT, REFERENCE_WIDTH, ViewerSettings, calculate_min_zoom,
};
use super::ws_client::FadingMember;
use crate::utils::parse_bounds_dimensions;

/// Scene viewer component for displaying a realm scene with avatars.
///
/// Uses three layered canvases:
/// - Background canvas (z-index 0): Static background, drawn once
/// - Props canvas (z-index 1): Loose props, redrawn on drop/pickup
/// - Avatar canvas (z-index 2): Transparent, redrawn on member updates
#[component]
pub fn RealmSceneViewer(
    scene: Scene,
    realm_slug: String,
    #[prop(into)] members: Signal<Vec<ChannelMemberWithAvatar>>,
    #[prop(into)] active_bubbles: Signal<HashMap<Uuid, ActiveBubble>>,
    #[prop(into)] loose_props: Signal<Vec<LooseProp>>,
    #[prop(into)] on_move: Callback<(f64, f64)>,
    #[prop(into)] on_prop_click: Callback<Uuid>,
    /// Viewer settings for pan/zoom/enlarge modes.
    #[prop(optional)]
    settings: Option<Signal<ViewerSettings>>,
    /// Callback for zoom changes (from mouse wheel). Receives zoom delta.
    #[prop(optional)]
    on_zoom_change: Option<Callback<f64>>,
    /// Members that are fading out after timeout disconnect.
    #[prop(optional, into)]
    fading_members: Option<Signal<Vec<FadingMember>>>,
    /// Current user's user_id (for context menu filtering).
    /// Note: Guests are now regular users with the 'guest' tag.
    #[prop(optional, into)]
    current_user_id: Option<Signal<Option<Uuid>>>,
    /// Whether the current user is a guest (guests cannot use context menu).
    #[prop(optional, into)]
    is_guest: Option<Signal<bool>>,
    /// Callback when whisper is requested on a member.
    #[prop(optional, into)]
    on_whisper_request: Option<Callback<String>>,
) -> impl IntoView {
    let is_guest = is_guest.unwrap_or_else(|| Signal::derive(|| false));
    // Use default settings if none provided
    let settings = settings.unwrap_or_else(|| Signal::derive(ViewerSettings::default));
    let dimensions = parse_bounds_dimensions(&scene.bounds_wkt);
    let (scene_width, scene_height) = dimensions.unwrap_or((800, 600));
    let scene_width_f = scene_width as f64;
    let scene_height_f = scene_height as f64;

    // Derived signals for rendering mode
    let is_pan_mode = Signal::derive(move || settings.get().panning_enabled);

    // Signal for viewport dimensions (outer container size)
    // Used to calculate effective minimum zoom in pan mode
    let (viewport_dimensions, set_viewport_dimensions) = signal((800.0_f64, 600.0_f64));

    // Calculate effective minimum zoom based on scene and viewport
    let effective_min_zoom = Signal::derive(move || {
        let (vp_w, vp_h) = viewport_dimensions.get();
        calculate_min_zoom(scene_width_f, scene_height_f, vp_w, vp_h)
    });

    // Zoom level clamped to effective minimum
    let zoom_level = Signal::derive(move || {
        let s = settings.get();
        if s.panning_enabled {
            let min_zoom = effective_min_zoom.get();
            s.zoom_level.max(min_zoom)
        } else {
            1.0
        }
    });

    let enlarge_props = Signal::derive(move || settings.get().enlarge_props);

    let bg_color = scene
        .background_color
        .clone()
        .unwrap_or_else(|| "#1a1a2e".to_string());

    let has_background_image = scene.background_image_path.is_some();
    let image_path = scene.background_image_path.clone().unwrap_or_default();

    // Canvas refs for background and props layers
    // Avatar layer now uses individual canvas elements per user
    let bg_canvas_ref = NodeRef::<leptos::html::Canvas>::new();
    let props_canvas_ref = NodeRef::<leptos::html::Canvas>::new();

    // Outer container ref for middle-mouse drag scrolling
    let outer_container_ref = NodeRef::<leptos::html::Div>::new();

    // Store scale factors for coordinate conversion (shared between both canvases)
    // Using RwSignal for reactivity - derived signals will recompute on resize
    let (scale_x, set_scale_x) = signal(1.0_f64);
    let (scale_y, set_scale_y) = signal(1.0_f64);
    let (offset_x, set_offset_x) = signal(0.0_f64);
    let (offset_y, set_offset_y) = signal(0.0_f64);

    // Signal to track when scale factors have been properly calculated
    let (scales_ready, set_scales_ready) = signal(false);

    // Context menu state
    let (context_menu_open, set_context_menu_open) = signal(false);
    let (context_menu_position, set_context_menu_position) = signal((0.0_f64, 0.0_f64));
    let (context_menu_target, set_context_menu_target) = signal(Option::<String>::None);

    // Handle overlay click for movement or prop pickup
    // TODO: Add hit-testing for avatar clicks
    #[cfg(feature = "hydrate")]
    let on_overlay_click = {
        let on_move = on_move.clone();
        let on_prop_click = on_prop_click.clone();
        move |ev: web_sys::MouseEvent| {
            // Get click position relative to the target element
            let target = ev.current_target().unwrap();
            let element: web_sys::HtmlElement = target.dyn_into().unwrap();
            let rect = element.get_bounding_client_rect();

            let click_x = ev.client_x() as f64 - rect.left();
            let click_y = ev.client_y() as f64 - rect.top();

            let sx = scale_x.get();
            let sy = scale_y.get();
            let ox = offset_x.get();
            let oy = offset_y.get();

            if sx > 0.0 && sy > 0.0 {
                let scene_x = (click_x - ox) / sx;
                let scene_y = (click_y - oy) / sy;

                let scene_x = scene_x.max(0.0).min(scene_width as f64);
                let scene_y = scene_y.max(0.0).min(scene_height as f64);

                // Check if click is within 40px of any loose prop
                let current_props = loose_props.get();
                let prop_click_radius = 40.0;
                let mut clicked_prop: Option<Uuid> = None;

                for prop in &current_props {
                    let dx = scene_x - prop.position_x;
                    let dy = scene_y - prop.position_y;
                    let distance = (dx * dx + dy * dy).sqrt();
                    if distance <= prop_click_radius {
                        clicked_prop = Some(prop.id);
                        break;
                    }
                }

                if let Some(prop_id) = clicked_prop {
                    on_prop_click.run(prop_id);
                } else {
                    on_move.run((scene_x, scene_y));
                }
            }
        }
    };

    // Handle right-click for context menu on avatars
    #[cfg(feature = "hydrate")]
    let on_overlay_contextmenu = {
        let current_user_id = current_user_id.clone();
        move |ev: web_sys::MouseEvent| {
            use wasm_bindgen::JsCast;

            // Guests cannot message other users - don't show context menu
            if is_guest.get() {
                return;
            }

            // Get current user identity for filtering
            let my_user_id = current_user_id.map(|s| s.get()).flatten();

            // Get click position
            let client_x = ev.client_x() as f64;
            let client_y = ev.client_y() as f64;

            // Query all avatar canvases and check for hit
            let document = web_sys::window().unwrap().document().unwrap();

            // Get the avatars container and find all canvas elements within it
            if let Some(container) = document.query_selector(".avatars-container").ok().flatten() {
                let canvases = container.get_elements_by_tag_name("canvas");
                let canvas_count = canvases.length();

                for i in 0..canvas_count {
                    if let Some(element) = canvases.item(i) {
                        if let Ok(canvas) = element.dyn_into::<web_sys::HtmlCanvasElement>() {
                            // Check for data-member-id attribute
                            if let Some(member_id_str) = canvas.get_attribute("data-member-id") {
                                // Check if click hits a non-transparent pixel
                                if hit_test_canvas(&canvas, client_x, client_y) {
                                    // Parse the member ID (now always user_id since guests are users)
                                    if let Ok(member_id) = member_id_str.parse::<Uuid>() {
                                        // Check if this is the current user's avatar
                                        let is_current_user = my_user_id == Some(member_id);

                                        if !is_current_user {
                                            // Find the display name for this member
                                            let display_name = members
                                                .get()
                                                .iter()
                                                .find(|m| m.member.user_id == member_id)
                                                .map(|m| m.member.display_name.clone());

                                            if let Some(name) = display_name {
                                                // Prevent default browser context menu
                                                ev.prevent_default();

                                                // Show context menu at click position
                                                set_context_menu_position.set((client_x, client_y));
                                                set_context_menu_target.set(Some(name));
                                                set_context_menu_open.set(true);
                                                return;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            // No avatar hit - allow default context menu
        }
    };

    #[cfg(feature = "hydrate")]
    {
        use std::cell::RefCell;
        use std::rc::Rc;
        use wasm_bindgen::{JsCast, closure::Closure};

        let image_path_clone = image_path.clone();
        let bg_color_clone = bg_color.clone();

        // =========================================================
        // Viewport Dimensions Effect - tracks outer container size
        // Uses window resize event to detect size changes
        // =========================================================
        Effect::new(move |_| {
            // Track pan mode to re-run when it changes (affects container layout)
            let _ = is_pan_mode.get();

            let Some(container) = outer_container_ref.get() else {
                return;
            };

            let container_el: web_sys::HtmlElement = container.into();

            // Measure and update dimensions
            let measure_container = {
                let container_el = container_el.clone();
                move || {
                    let width = container_el.client_width() as f64;
                    let height = container_el.client_height() as f64;
                    if width > 0.0 && height > 0.0 {
                        set_viewport_dimensions.set((width, height));
                    }
                }
            };

            // Measure immediately
            measure_container();

            // Also measure on window resize
            let resize_handler = Closure::wrap(Box::new({
                let container_el = container_el.clone();
                move |_: web_sys::Event| {
                    let width = container_el.client_width() as f64;
                    let height = container_el.client_height() as f64;
                    if width > 0.0 && height > 0.0 {
                        set_viewport_dimensions.set((width, height));
                    }
                }
            }) as Box<dyn Fn(web_sys::Event)>);

            let window = web_sys::window().unwrap();
            let _ = window.add_event_listener_with_callback(
                "resize",
                resize_handler.as_ref().unchecked_ref(),
            );

            // Keep the closure alive
            resize_handler.forget();
        });

        // Track the last settings to detect changes
        let last_pan_mode = Rc::new(RefCell::new(None::<bool>));
        let last_zoom = Rc::new(RefCell::new(None::<f64>));
        let last_viewport = Rc::new(RefCell::new(None::<(f64, f64)>));

        // =========================================================
        // Background Effect - redraws when settings or viewport change
        // =========================================================
        Effect::new(move |_| {
            // Track settings signals - this Effect reruns when they change
            let current_pan_mode = is_pan_mode.get();
            let current_zoom = zoom_level.get();
            let current_viewport = viewport_dimensions.get();

            let Some(canvas) = bg_canvas_ref.get() else {
                return;
            };

            // Check if we need to redraw (settings or viewport changed, or first render)
            let needs_redraw = {
                let last_pan = *last_pan_mode.borrow();
                let last_z = *last_zoom.borrow();
                let last_vp = *last_viewport.borrow();
                last_pan != Some(current_pan_mode)
                    || (current_pan_mode && last_z != Some(current_zoom))
                    || (!current_pan_mode && last_vp != Some(current_viewport))
            };

            if !needs_redraw {
                return;
            }

            // Update last values
            *last_pan_mode.borrow_mut() = Some(current_pan_mode);
            *last_zoom.borrow_mut() = Some(current_zoom);
            *last_viewport.borrow_mut() = Some(current_viewport);

            let canvas_el: &web_sys::HtmlCanvasElement = &canvas;
            let canvas_el = canvas_el.clone();
            let bg_color = bg_color_clone.clone();
            let image_path = image_path_clone.clone();

            // Use setTimeout to ensure DOM is ready before drawing
            let draw_bg = Closure::once(Box::new(move || {
                if current_pan_mode {
                    // Pan mode: canvas at native resolution * zoom
                    let canvas_width = (scene_width_f * current_zoom) as u32;
                    let canvas_height = (scene_height_f * current_zoom) as u32;

                    canvas_el.set_width(canvas_width);
                    canvas_el.set_height(canvas_height);

                    // Store scale factors (zoom level, no offset)
                    set_scale_x.set(current_zoom);
                    set_scale_y.set(current_zoom);
                    set_offset_x.set(0.0);
                    set_offset_y.set(0.0);

                    // Signal that scale factors are ready
                    set_scales_ready.set(true);

                    if let Ok(Some(ctx)) = canvas_el.get_context("2d") {
                        let ctx: web_sys::CanvasRenderingContext2d =
                            ctx.dyn_into::<web_sys::CanvasRenderingContext2d>().unwrap();

                        // Fill entire canvas with background color (no letterboxing)
                        ctx.set_fill_style_str(&bg_color);
                        ctx.fill_rect(0.0, 0.0, canvas_width as f64, canvas_height as f64);

                        // Draw background image if available
                        if has_background_image && !image_path.is_empty() {
                            let img = web_sys::HtmlImageElement::new().unwrap();
                            let img_clone = img.clone();
                            let ctx_clone = ctx.clone();

                            let onload = Closure::once(Box::new(move || {
                                let _ = ctx_clone.draw_image_with_html_image_element_and_dw_and_dh(
                                    &img_clone,
                                    0.0,
                                    0.0,
                                    canvas_width as f64,
                                    canvas_height as f64,
                                );
                            })
                                as Box<dyn FnOnce()>);

                            img.set_onload(Some(onload.as_ref().unchecked_ref()));
                            onload.forget();
                            img.set_src(&image_path);
                        }
                    }
                } else {
                    // Fit mode: scale to viewport with letterboxing
                    let display_width = canvas_el.client_width() as u32;
                    let display_height = canvas_el.client_height() as u32;

                    // If still no dimensions, the canvas likely isn't visible - skip drawing
                    if display_width == 0 || display_height == 0 {
                        return;
                    }

                    canvas_el.set_width(display_width);
                    canvas_el.set_height(display_height);

                    // Calculate scale to fit scene in canvas
                    let canvas_aspect = display_width as f64 / display_height as f64;
                    let scene_aspect = scene_width_f / scene_height_f;

                    let (draw_width, draw_height, draw_x, draw_y) = if canvas_aspect > scene_aspect
                    {
                        let h = display_height as f64;
                        let w = h * scene_aspect;
                        let x = (display_width as f64 - w) / 2.0;
                        (w, h, x, 0.0)
                    } else {
                        let w = display_width as f64;
                        let h = w / scene_aspect;
                        let y = (display_height as f64 - h) / 2.0;
                        (w, h, 0.0, y)
                    };

                    // Store scale factors
                    let sx = draw_width / scene_width_f;
                    let sy = draw_height / scene_height_f;
                    set_scale_x.set(sx);
                    set_scale_y.set(sy);
                    set_offset_x.set(draw_x);
                    set_offset_y.set(draw_y);

                    // Signal that scale factors are ready
                    set_scales_ready.set(true);

                    if let Ok(Some(ctx)) = canvas_el.get_context("2d") {
                        let ctx: web_sys::CanvasRenderingContext2d =
                            ctx.dyn_into::<web_sys::CanvasRenderingContext2d>().unwrap();

                        // Fill letterbox area with black
                        ctx.set_fill_style_str("#000");
                        ctx.fill_rect(0.0, 0.0, display_width as f64, display_height as f64);

                        // Fill scene area with background color
                        ctx.set_fill_style_str(&bg_color);
                        ctx.fill_rect(draw_x, draw_y, draw_width, draw_height);

                        // Draw background image if available
                        if has_background_image && !image_path.is_empty() {
                            let img = web_sys::HtmlImageElement::new().unwrap();
                            let img_clone = img.clone();
                            let ctx_clone = ctx.clone();

                            let onload = Closure::once(Box::new(move || {
                                let _ = ctx_clone.draw_image_with_html_image_element_and_dw_and_dh(
                                    &img_clone,
                                    draw_x,
                                    draw_y,
                                    draw_width,
                                    draw_height,
                                );
                            })
                                as Box<dyn FnOnce()>);

                            img.set_onload(Some(onload.as_ref().unchecked_ref()));
                            onload.forget();
                            img.set_src(&image_path);
                        }
                    }
                }
            }) as Box<dyn FnOnce()>);

            // Use setTimeout with small delay to ensure canvas is in DOM and has dimensions
            let window = web_sys::window().unwrap();
            let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
                draw_bg.as_ref().unchecked_ref(),
                100, // 100ms delay to allow DOM to settle
            );
            draw_bg.forget();
        });

        // =========================================================
        // Props Effect - runs when loose_props or settings change
        // =========================================================
        Effect::new(move |_| {
            // Track signals
            let current_props = loose_props.get();
            let current_pan_mode = is_pan_mode.get();
            let current_zoom = zoom_level.get();
            let current_enlarge = enlarge_props.get();

            // Skip drawing if scale factors haven't been calculated yet
            if !scales_ready.get() {
                return;
            }

            // Read scale factors inside the Effect (reactive context) before the closure
            let sx = scale_x.get();
            let sy = scale_y.get();
            let ox = offset_x.get();
            let oy = offset_y.get();

            let Some(canvas) = props_canvas_ref.get() else {
                return;
            };

            let canvas_el: &web_sys::HtmlCanvasElement = &canvas;
            let canvas_el = canvas_el.clone();

            let draw_props_closure = Closure::once(Box::new(move || {
                let canvas_width = canvas_el.width();
                let canvas_height = canvas_el.height();

                if canvas_width == 0 || canvas_height == 0 {
                    return;
                }

                if let Ok(Some(ctx)) = canvas_el.get_context("2d") {
                    let ctx: web_sys::CanvasRenderingContext2d =
                        ctx.dyn_into::<web_sys::CanvasRenderingContext2d>().unwrap();

                    // Clear with transparency
                    ctx.clear_rect(0.0, 0.0, canvas_width as f64, canvas_height as f64);

                    // Calculate prop size based on mode
                    let prop_size = calculate_prop_size(
                        current_pan_mode,
                        current_zoom,
                        current_enlarge,
                        sx,
                        sy,
                        scene_width_f,
                        scene_height_f,
                    );

                    // Draw loose props
                    draw_loose_props(&ctx, &current_props, sx, sy, ox, oy, prop_size);
                }
            }) as Box<dyn FnOnce()>);

            let window = web_sys::window().unwrap();
            let _ = window.request_animation_frame(draw_props_closure.as_ref().unchecked_ref());
            draw_props_closure.forget();
        });

        // =========================================================
        // Sync canvas sizes when mode or zoom changes
        // =========================================================
        Effect::new(move |_| {
            let current_pan_mode = is_pan_mode.get();
            let current_zoom = zoom_level.get();

            // Wait for scales to be ready (background drawn)
            if !scales_ready.get() {
                return;
            }

            if current_pan_mode {
                // Pan mode: resize props and avatar canvases to match background
                let canvas_width = (scene_width_f * current_zoom) as u32;
                let canvas_height = (scene_height_f * current_zoom) as u32;

                if let Some(canvas) = props_canvas_ref.get() {
                    let canvas_el: &web_sys::HtmlCanvasElement = &canvas;
                    if canvas_el.width() != canvas_width || canvas_el.height() != canvas_height {
                        canvas_el.set_width(canvas_width);
                        canvas_el.set_height(canvas_height);
                    }
                }
                // Note: Avatar canvases are now individual elements that manage their own sizes
            } else {
                // Fit mode: sync props and avatar canvases to background canvas size
                if let Some(bg_canvas) = bg_canvas_ref.get() {
                    let bg_el: &web_sys::HtmlCanvasElement = &bg_canvas;
                    let canvas_width = bg_el.width();
                    let canvas_height = bg_el.height();

                    if canvas_width > 0 && canvas_height > 0 {
                        if let Some(canvas) = props_canvas_ref.get() {
                            let canvas_el: &web_sys::HtmlCanvasElement = &canvas;
                            if canvas_el.width() != canvas_width
                                || canvas_el.height() != canvas_height
                            {
                                canvas_el.set_width(canvas_width);
                                canvas_el.set_height(canvas_height);
                            }
                        }
                        // Note: Avatar canvases are now individual elements that manage their own sizes
                    }
                }
            }
        });

        // =========================================================
        // Middle mouse button drag-to-pan (only in pan mode)
        // =========================================================
        Effect::new(move |_| {
            // Track pan mode - re-run when it changes
            let pan_mode_enabled = is_pan_mode.get();

            let Some(container) = outer_container_ref.get() else {
                return;
            };

            let container_el: web_sys::HtmlElement = container.into();

            if !pan_mode_enabled {
                // Reset cursor when not in pan mode
                let _ = container_el.style().set_property("cursor", "");
                return;
            }

            use std::cell::Cell;
            use std::rc::Rc;
            use wasm_bindgen::closure::Closure;

            let is_dragging = Rc::new(Cell::new(false));
            let last_x = Rc::new(Cell::new(0i32));
            let last_y = Rc::new(Cell::new(0i32));

            let container_for_move = container_el.clone();
            let is_dragging_move = is_dragging.clone();
            let last_x_move = last_x.clone();
            let last_y_move = last_y.clone();

            let container_for_down = container_el.clone();
            let is_dragging_down = is_dragging.clone();
            let last_x_down = last_x.clone();
            let last_y_down = last_y.clone();

            // Middle mouse down - start drag
            let onmousedown =
                Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
                    // Button 1 is middle mouse button
                    if ev.button() == 1 {
                        is_dragging_down.set(true);
                        last_x_down.set(ev.client_x());
                        last_y_down.set(ev.client_y());
                        let _ = container_for_down
                            .style()
                            .set_property("cursor", "grabbing");
                        ev.prevent_default();
                    }
                });

            // Mouse move - drag scroll
            let onmousemove =
                Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
                    if is_dragging_move.get() {
                        let dx = last_x_move.get() - ev.client_x();
                        let dy = last_y_move.get() - ev.client_y();
                        last_x_move.set(ev.client_x());
                        last_y_move.set(ev.client_y());
                        container_for_move.set_scroll_left(container_for_move.scroll_left() + dx);
                        container_for_move.set_scroll_top(container_for_move.scroll_top() + dy);
                    }
                });

            let container_for_up = container_el.clone();
            let is_dragging_up = is_dragging.clone();

            // Mouse up - stop drag
            let onmouseup =
                Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |_ev: web_sys::MouseEvent| {
                    if is_dragging_up.get() {
                        is_dragging_up.set(false);
                        let _ = container_for_up.style().set_property("cursor", "");
                    }
                });

            // Add event listeners
            let _ = container_el.add_event_listener_with_callback(
                "mousedown",
                onmousedown.as_ref().unchecked_ref(),
            );
            let _ = container_el.add_event_listener_with_callback(
                "mousemove",
                onmousemove.as_ref().unchecked_ref(),
            );
            let _ = container_el
                .add_event_listener_with_callback("mouseup", onmouseup.as_ref().unchecked_ref());

            // Also listen for mouseup on window (in case mouse released outside container)
            if let Some(window) = web_sys::window() {
                let is_dragging_window = is_dragging.clone();
                let container_for_window = container_el.clone();
                let onmouseup_window =
                    Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |_ev: web_sys::MouseEvent| {
                        if is_dragging_window.get() {
                            is_dragging_window.set(false);
                            let _ = container_for_window.style().set_property("cursor", "");
                        }
                    });
                let _ = window.add_event_listener_with_callback(
                    "mouseup",
                    onmouseup_window.as_ref().unchecked_ref(),
                );
                onmouseup_window.forget();
            }

            // Prevent context menu on middle click
            let oncontextmenu =
                Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
                    if ev.button() == 1 {
                        ev.prevent_default();
                    }
                });
            let _ = container_el.add_event_listener_with_callback(
                "auxclick",
                oncontextmenu.as_ref().unchecked_ref(),
            );

            // Forget closures to keep them alive
            onmousedown.forget();
            onmousemove.forget();
            onmouseup.forget();
            oncontextmenu.forget();
        });
    }

    // Dynamically add/remove wheel listener based on pan mode
    // This avoids Chrome's "non-passive wheel listener" warning when not in pan mode
    #[cfg(feature = "hydrate")]
    {
        use std::cell::RefCell;
        use std::rc::Rc;
        use wasm_bindgen::{closure::Closure, JsCast};

        let wheel_closure: Rc<RefCell<Option<Closure<dyn Fn(web_sys::WheelEvent)>>>> =
            Rc::new(RefCell::new(None));
        let wheel_closure_clone = wheel_closure.clone();

        Effect::new(move |_| {
            let pan_mode = is_pan_mode.get();

            if let Some(container) = outer_container_ref.get() {
                let element: &web_sys::Element = &container;

                // Remove existing listener if any
                if let Some(closure) = wheel_closure_clone.borrow().as_ref() {
                    let _ = element.remove_event_listener_with_callback(
                        "wheel",
                        closure.as_ref().unchecked_ref(),
                    );
                }

                if pan_mode {
                    // Add non-passive wheel listener for zoom
                    let closure = Closure::new(move |ev: web_sys::WheelEvent| {
                        if !ev.ctrl_key() {
                            if let Some(zoom_callback) = on_zoom_change {
                                let delta_y = ev.delta_y();
                                let zoom_delta = if delta_y < 0.0 { 0.1 } else { -0.1 };
                                zoom_callback.run(zoom_delta);
                                ev.prevent_default();
                            }
                        }
                    });

                    let options = web_sys::AddEventListenerOptions::new();
                    options.set_passive(false);

                    let _ = element.add_event_listener_with_callback_and_add_event_listener_options(
                        "wheel",
                        closure.as_ref().unchecked_ref(),
                        &options,
                    );

                    *wheel_closure_clone.borrow_mut() = Some(closure);
                } else {
                    *wheel_closure_clone.borrow_mut() = None;
                }
            }
        });
    }

    let aspect_ratio = scene_width as f64 / scene_height as f64;

    // Computed styles based on mode
    let container_class = move || {
        if is_pan_mode.get() {
            "scene-canvas relative cursor-pointer"
        } else {
            "scene-canvas relative overflow-hidden cursor-pointer"
        }
    };

    let outer_container_class = move || {
        if is_pan_mode.get() {
            let zoom = zoom_level.get();
            let (vp_w, vp_h) = viewport_dimensions.get();
            let canvas_w = scene_width_f * zoom;
            let canvas_h = scene_height_f * zoom;

            // Center canvas if smaller than viewport in both dimensions
            if canvas_w <= vp_w && canvas_h <= vp_h {
                "scene-container w-full overflow-auto flex justify-center items-center"
            } else {
                "scene-container w-full overflow-auto"
            }
        } else {
            "scene-container w-full h-full flex justify-center items-center"
        }
    };

    // Outer container needs max-height in pan mode to enable vertical scrolling
    let outer_container_style = move || {
        if is_pan_mode.get() {
            "max-height: calc(100vh - 64px)".to_string()
        } else {
            String::new()
        }
    };

    let container_style = move || {
        if is_pan_mode.get() {
            let zoom = zoom_level.get();
            format!(
                "width: {}px; height: {}px; background-color: {}",
                (scene_width as f64 * zoom) as u32,
                (scene_height as f64 * zoom) as u32,
                bg_color
            )
        } else {
            format!(
                "aspect-ratio: {} / {}; width: min(100%, calc((100vh - 64px) * {})); max-height: calc(100vh - 64px); background-color: {}",
                scene_width, scene_height, aspect_ratio, bg_color
            )
        }
    };

    let canvas_class = move || {
        if is_pan_mode.get() {
            "absolute inset-0"
        } else {
            "absolute inset-0 w-full h-full"
        }
    };

    let canvas_style = move |z_index: i32| {
        if is_pan_mode.get() {
            let zoom = zoom_level.get();
            format!(
                "z-index: {}; width: {}px; height: {}px",
                z_index,
                (scene_width as f64 * zoom) as u32,
                (scene_height as f64 * zoom) as u32
            )
        } else {
            format!("z-index: {}; width: 100%; height: 100%", z_index)
        }
    };

    // Sorted members signal for z-ordering (most recently joined = highest z-index)
    let sorted_members = Signal::derive(move || {
        let mut m = members.get();
        // Sort by joined_at descending - most recent joins on top
        m.sort_by(|a, b| b.member.joined_at.cmp(&a.member.joined_at));
        m
    });

    // Calculate prop size based on current settings
    let prop_size = Signal::derive(move || {
        let current_pan_mode = is_pan_mode.get();
        let current_zoom = zoom_level.get();
        let current_enlarge = enlarge_props.get();
        let sx = scale_x.get();
        let sy = scale_y.get();

        // Reference scale factor for "enlarge props" mode
        let ref_scale = (scene_width_f / REFERENCE_WIDTH).max(scene_height_f / REFERENCE_HEIGHT);

        if current_pan_mode {
            if current_enlarge {
                BASE_PROP_SIZE * ref_scale * current_zoom
            } else {
                BASE_PROP_SIZE * current_zoom
            }
        } else if current_enlarge {
            BASE_PROP_SIZE * ref_scale * sx.min(sy)
        } else {
            BASE_PROP_SIZE * sx.min(sy)
        }
    });

    // Text size multiplier from settings
    let text_em_size = Signal::derive(move || settings.get().text_em_size);

    // Create signals for scale/offset values to pass to AvatarCanvas
    let scale_x_signal = Signal::derive(move || scale_x.get());
    let scale_y_signal = Signal::derive(move || scale_y.get());
    let offset_x_signal = Signal::derive(move || offset_x.get());
    let offset_y_signal = Signal::derive(move || offset_y.get());

    // Create signals for scene dimensions to pass to AvatarCanvas for boundary awareness
    let scene_width_signal = Signal::derive(move || scene_width_f);
    let scene_height_signal = Signal::derive(move || scene_height_f);

    // Create a map of members by key for efficient lookup
    let members_by_key = Signal::derive(move || {
        use std::collections::HashMap;
        sorted_members
            .get()
            .into_iter()
            .enumerate()
            .map(|(idx, m)| (member_key(&m), (idx, m)))
            .collect::<HashMap<_, _>>()
    });

    // Get the list of member keys - use Memo so it only updates when keys actually change
    // (not when member data like position changes)
    let member_keys = Memo::new(move |_| {
        sorted_members
            .get()
            .iter()
            .map(member_key)
            .collect::<Vec<_>>()
    });

    let scene_name = scene.name.clone();

    view! {
        <div node_ref=outer_container_ref class=outer_container_class style=outer_container_style>
            <div
                class=container_class
                style=container_style
            >
                // Background layer - static, drawn once
                <canvas
                    node_ref=bg_canvas_ref
                    class=canvas_class
                    style=move || canvas_style(0)
                    aria-hidden="true"
                />
                // Props layer - loose props, redrawn on drop/pickup
                <canvas
                    node_ref=props_canvas_ref
                    class=canvas_class
                    style=move || canvas_style(1)
                    aria-hidden="true"
                />
                // Avatars container - individual canvases per user
                <div
                    class="avatars-container absolute inset-0"
                    style="z-index: 2; pointer-events: none;"
                >
                    // Wait for scale factors before rendering
                    <Show
                        when=move || scales_ready.get()
                        fallback=|| ()
                    >
                        // Use stable keys - each AvatarCanvas gets its own derived signal
                        {move || {
                            member_keys.get().into_iter().map(|key| {
                                // Create a derived signal for this specific member
                                let member_signal = Signal::derive(move || {
                                    members_by_key.get()
                                        .get(&key)
                                        .map(|(_, m)| m.clone())
                                        .expect("member key should exist")
                                });

                                // Derive z-index from position in sorted list
                                let z_index_signal = Signal::derive(move || {
                                    members_by_key.get()
                                        .get(&key)
                                        .map(|(idx, _)| (*idx as i32) + 10)
                                        .unwrap_or(10)
                                });

                                // Derive bubble for this member
                                let bubble_signal = Signal::derive(move || {
                                    active_bubbles.get().get(&key).cloned()
                                });

                                let z = z_index_signal.get_untracked();

                                view! {
                                    <AvatarCanvas
                                        member=member_signal
                                        scale_x=scale_x_signal
                                        scale_y=scale_y_signal
                                        offset_x=offset_x_signal
                                        offset_y=offset_y_signal
                                        prop_size=prop_size
                                        z_index=z
                                        active_bubble=bubble_signal
                                        text_em_size=text_em_size
                                        scene_width=scene_width_signal
                                        scene_height=scene_height_signal
                                    />
                                }
                            }).collect_view()
                        }}
                        // Fading members use closure approach (temporary, per-frame updates)
                        {move || {
                            let Some(fading_signal) = fading_members else {
                                return Vec::new().into_iter().collect_view();
                            };

                            #[cfg(feature = "hydrate")]
                            let now = js_sys::Date::now() as i64;
                            #[cfg(not(feature = "hydrate"))]
                            let now = 0i64;

                            fading_signal.get()
                                .into_iter()
                                .filter_map(|fading| {
                                    let elapsed = now - fading.fade_start;
                                    if elapsed < fading.fade_duration {
                                        let opacity = 1.0 - (elapsed as f64 / fading.fade_duration as f64);
                                        let opacity = opacity.max(0.0).min(1.0);
                                        // Fading members get static signals (they're temporary)
                                        let member_signal = Signal::derive({
                                            let m = fading.member.clone();
                                            move || m.clone()
                                        });
                                        let bubble_signal: Signal<Option<ActiveBubble>> = Signal::derive(|| None);
                                        Some(view! {
                                            <AvatarCanvas
                                                member=member_signal
                                                scale_x=scale_x_signal
                                                scale_y=scale_y_signal
                                                offset_x=offset_x_signal
                                                offset_y=offset_y_signal
                                                prop_size=prop_size
                                                z_index=5
                                                active_bubble=bubble_signal
                                                text_em_size=text_em_size
                                                opacity=opacity
                                                scene_width=scene_width_signal
                                                scene_height=scene_height_signal
                                            />
                                        })
                                    } else {
                                        None
                                    }
                                })
                                .collect_view()
                        }}
                    </Show>
                </div>
                // Click overlay - captures clicks for movement and hit-testing
                <div
                    class="click-overlay absolute inset-0"
                    style="z-index: 3; cursor: pointer;"
                    aria-label=format!("Scene: {}", scene_name)
                    role="img"
                    on:click=move |ev| {
                        #[cfg(feature = "hydrate")]
                        on_overlay_click(ev);
                        #[cfg(not(feature = "hydrate"))]
                        let _ = ev;
                    }
                    on:contextmenu=move |ev| {
                        #[cfg(feature = "hydrate")]
                        on_overlay_contextmenu(ev);
                        #[cfg(not(feature = "hydrate"))]
                        let _ = ev;
                    }
                />
                // Context menu for avatar interactions
                <ContextMenu
                    open=Signal::derive(move || context_menu_open.get())
                    position=Signal::derive(move || context_menu_position.get())
                    header=Signal::derive(move || context_menu_target.get())
                    items=Signal::derive(move || {
                        vec![
                            ContextMenuItem {
                                label: "Whisper".to_string(),
                                action: "whisper".to_string(),
                            },
                        ]
                    })
                    on_select=Callback::new({
                        let on_whisper_request = on_whisper_request.clone();
                        move |action: String| {
                            if action == "whisper" {
                                if let Some(target) = context_menu_target.get() {
                                    if let Some(ref callback) = on_whisper_request {
                                        callback.run(target);
                                    }
                                }
                            }
                        }
                    })
                    on_close=Callback::new(move |_: ()| {
                        set_context_menu_open.set(false);
                        set_context_menu_target.set(None);
                    })
                />
            </div>
        </div>
    }
}

#[cfg(feature = "hydrate")]
use wasm_bindgen::JsCast;

/// Calculate prop/avatar size based on current rendering mode.
///
/// - Pan mode without enlarge: BASE_PROP_SIZE * zoom_level
/// - Pan mode with enlarge: BASE_PROP_SIZE * reference_scale * zoom_level
/// - Fit mode with enlarge: Reference scaling based on 1920x1080
/// - Fit mode without enlarge: BASE_PROP_SIZE * min(scale_x, scale_y)
#[cfg(feature = "hydrate")]
fn calculate_prop_size(
    pan_mode: bool,
    zoom_level: f64,
    enlarge_props: bool,
    scale_x: f64,
    scale_y: f64,
    scene_width: f64,
    scene_height: f64,
) -> f64 {
    // Reference scale factor for "enlarge props" mode
    let ref_scale = (scene_width / REFERENCE_WIDTH).max(scene_height / REFERENCE_HEIGHT);

    if pan_mode {
        if enlarge_props {
            BASE_PROP_SIZE * ref_scale * zoom_level
        } else {
            BASE_PROP_SIZE * zoom_level
        }
    } else if enlarge_props {
        // Reference scaling: scale props relative to 1920x1080 reference
        BASE_PROP_SIZE * ref_scale * scale_x.min(scale_y)
    } else {
        // Default: base size scaled to viewport
        BASE_PROP_SIZE * scale_x.min(scale_y)
    }
}

/// Normalize an asset path to be absolute, prefixing with /static/ if needed.
#[cfg(feature = "hydrate")]
fn normalize_asset_path(path: &str) -> String {
    if path.starts_with('/') {
        path.to_string()
    } else {
        format!("/static/{}", path)
    }
}

/// Draw loose props on the props canvas layer.
#[cfg(feature = "hydrate")]
fn draw_loose_props(
    ctx: &web_sys::CanvasRenderingContext2d,
    props: &[LooseProp],
    scale_x: f64,
    scale_y: f64,
    offset_x: f64,
    offset_y: f64,
    prop_size: f64,
) {
    for prop in props {
        let x = prop.position_x * scale_x + offset_x;
        let y = prop.position_y * scale_y + offset_y;

        // Draw prop sprite if asset path available
        if !prop.prop_asset_path.is_empty() {
            let img = web_sys::HtmlImageElement::new().unwrap();
            let img_clone = img.clone();
            let ctx_clone = ctx.clone();
            let draw_x = x - prop_size / 2.0;
            let draw_y = y - prop_size / 2.0;
            let size = prop_size;

            let onload = wasm_bindgen::closure::Closure::once(Box::new(move || {
                let _ = ctx_clone.draw_image_with_html_image_element_and_dw_and_dh(
                    &img_clone, draw_x, draw_y, size, size,
                );
            }) as Box<dyn FnOnce()>);

            img.set_onload(Some(onload.as_ref().unchecked_ref()));
            onload.forget();
            img.set_src(&normalize_asset_path(&prop.prop_asset_path));
        } else {
            // Fallback: draw a placeholder circle with prop name
            ctx.begin_path();
            let _ = ctx.arc(x, y, prop_size / 2.0, 0.0, std::f64::consts::PI * 2.0);
            ctx.set_fill_style_str("#f59e0b"); // Amber color
            ctx.fill();
            ctx.set_stroke_style_str("#d97706");
            ctx.set_line_width(2.0);
            ctx.stroke();

            // Draw prop name below
            let text_scale = prop_size / BASE_PROP_SIZE;
            ctx.set_fill_style_str("#fff");
            ctx.set_font(&format!("{}px sans-serif", 10.0 * text_scale));
            ctx.set_text_align("center");
            ctx.set_text_baseline("top");
            let _ = ctx.fill_text(&prop.prop_name, x, y + prop_size / 2.0 + 2.0);
        }
    }
}
