//! 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

use std::collections::HashMap;

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

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

use super::chat_types::{emotion_bubble_colors, ActiveBubble};

/// Parse bounds WKT to extract width and height.
///
/// Expected format: "POLYGON((0 0, WIDTH 0, WIDTH HEIGHT, 0 HEIGHT, 0 0))"
fn parse_bounds_dimensions(bounds_wkt: &str) -> Option<(u32, u32)> {
    let trimmed = bounds_wkt.trim();
    let coords_str = trimmed
        .strip_prefix("POLYGON((")
        .and_then(|s| s.strip_suffix("))"))?;

    let points: Vec<&str> = coords_str.split(',').collect();
    if points.len() < 4 {
        return None;
    }

    let mut max_x: f64 = 0.0;
    let mut max_y: f64 = 0.0;

    for point in points.iter() {
        let coords: Vec<&str> = point.trim().split_whitespace().collect();
        if coords.len() >= 2 {
            if let (Ok(x), Ok(y)) = (coords[0].parse::<f64>(), coords[1].parse::<f64>()) {
                if x > max_x {
                    max_x = x;
                }
                if y > max_y {
                    max_y = y;
                }
            }
        }
    }

    if max_x > 0.0 && max_y > 0.0 {
        Some((max_x as u32, max_y as u32))
    } else {
        None
    }
}

/// 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,
    #[allow(unused)]
    realm_slug: String,
    #[prop(into)]
    members: Signal<Vec<ChannelMemberWithAvatar>>,
    #[prop(into)]
    active_bubbles: Signal<HashMap<(Option<Uuid>, Option<Uuid>), ActiveBubble>>,
    #[prop(into)]
    loose_props: Signal<Vec<LooseProp>>,
    #[prop(into)]
    on_move: Callback<(f64, f64)>,
    #[prop(into)]
    on_prop_click: Callback<Uuid>,
) -> impl IntoView {
    let dimensions = parse_bounds_dimensions(&scene.bounds_wkt);
    let (scene_width, scene_height) = dimensions.unwrap_or((800, 600));

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

    #[allow(unused_variables)]
    let has_background_image = scene.background_image_path.is_some();
    #[allow(unused_variables)]
    let image_path = scene.background_image_path.clone().unwrap_or_default();

    // Three separate canvas refs for layered rendering
    let bg_canvas_ref = NodeRef::<leptos::html::Canvas>::new();
    let props_canvas_ref = NodeRef::<leptos::html::Canvas>::new();
    let avatar_canvas_ref = NodeRef::<leptos::html::Canvas>::new();

    // Store scale factors for coordinate conversion (shared between both canvases)
    let scale_x = StoredValue::new(1.0_f64);
    let scale_y = StoredValue::new(1.0_f64);
    let offset_x = StoredValue::new(0.0_f64);
    let offset_y = StoredValue::new(0.0_f64);

    // Handle canvas click for movement or prop pickup (on avatar canvas - topmost layer)
    #[cfg(feature = "hydrate")]
    let on_canvas_click = {
        let on_move = on_move.clone();
        let on_prop_click = on_prop_click.clone();
        move |ev: web_sys::MouseEvent| {
            let Some(canvas) = avatar_canvas_ref.get() else {
                return;
            };

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

            let canvas_x = ev.client_x() as f64 - rect.left();
            let canvas_y = ev.client_y() as f64 - rect.top();

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

            if sx > 0.0 && sy > 0.0 {
                let scene_x = (canvas_x - ox) / sx;
                let scene_y = (canvas_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 32px of any loose prop
                let current_props = loose_props.get();
                let prop_click_radius = 32.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));
                }
            }
        }
    };

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

        let image_path_clone = image_path.clone();
        let bg_color_clone = bg_color.clone();
        let scene_width_f = scene_width as f64;
        let scene_height_f = scene_height as f64;

        // Flag to track if background has been drawn
        let bg_drawn = Rc::new(RefCell::new(false));

        // =========================================================
        // Background Effect - runs once on mount, draws static background
        // =========================================================
        Effect::new(move |_| {
            // Don't track any reactive signals - this should only run once
            let Some(canvas) = bg_canvas_ref.get() else {
                return;
            };

            // Skip if already drawn
            if *bg_drawn.borrow() {
                return;
            }

            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();
            let bg_drawn_inner = bg_drawn.clone();

            // Use setTimeout to ensure DOM is ready before drawing
            let draw_bg = Closure::once(Box::new(move || {
                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;
                scale_x.set_value(sx);
                scale_y.set_value(sy);
                offset_x.set_value(draw_x);
                offset_y.set_value(draw_y);

                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);
                    }

                    // Mark background as drawn
                    *bg_drawn_inner.borrow_mut() = true;
                }
            }) 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();
        });

        // =========================================================
        // Avatar Effect - runs when members or bubbles change
        // =========================================================
        Effect::new(move |_| {
            // Track both signals - this Effect reruns when either changes
            let current_members = members.get();
            let current_bubbles = active_bubbles.get();

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

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

            let draw_avatars_closure = Closure::once(Box::new(move || {
                let display_width = canvas_el.client_width() as u32;
                let display_height = canvas_el.client_height() as u32;

                if display_width == 0 || display_height == 0 {
                    return;
                }

                // Resize avatar canvas to match (if needed)
                if canvas_el.width() != display_width || canvas_el.height() != display_height {
                    canvas_el.set_width(display_width);
                    canvas_el.set_height(display_height);
                }

                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 (not fill - keeps canvas transparent)
                    ctx.clear_rect(0.0, 0.0, display_width as f64, display_height as f64);

                    // Get stored scale factors
                    let sx = scale_x.get_value();
                    let sy = scale_y.get_value();
                    let ox = offset_x.get_value();
                    let oy = offset_y.get_value();

                    // Draw avatars first
                    draw_avatars(&ctx, &current_members, sx, sy, ox, oy);

                    // Draw speech bubbles on top
                    let current_time = js_sys::Date::now() as i64;
                    draw_speech_bubbles(
                        &ctx,
                        &current_members,
                        &current_bubbles,
                        sx,
                        sy,
                        ox,
                        oy,
                        current_time,
                    );
                }
            }) as Box<dyn FnOnce()>);

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

        // =========================================================
        // Props Effect - runs when loose_props changes
        // =========================================================
        Effect::new(move |_| {
            // Track loose_props signal
            let current_props = loose_props.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 display_width = canvas_el.client_width() as u32;
                let display_height = canvas_el.client_height() as u32;

                if display_width == 0 || display_height == 0 {
                    return;
                }

                // Resize props canvas to match (if needed)
                if canvas_el.width() != display_width || canvas_el.height() != display_height {
                    canvas_el.set_width(display_width);
                    canvas_el.set_height(display_height);
                }

                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, display_width as f64, display_height as f64);

                    // Get stored scale factors
                    let sx = scale_x.get_value();
                    let sy = scale_y.get_value();
                    let ox = offset_x.get_value();
                    let oy = offset_y.get_value();

                    // Draw loose props
                    draw_loose_props(&ctx, &current_props, sx, sy, ox, oy);
                }
            }) 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();
        });
    }

    let aspect_ratio = scene_width as f64 / scene_height as f64;

    view! {
        <div class="scene-container w-full h-full flex justify-center items-center">
            <div
                class="scene-canvas relative overflow-hidden cursor-pointer"
                style:background-color=bg_color.clone()
                style:aspect-ratio=format!("{} / {}", scene_width, scene_height)
                style:width=format!("min(100%, calc((100vh - 64px) * {}))", aspect_ratio)
                style:max-height="calc(100vh - 64px)"
            >
                // Background layer - static, drawn once
                <canvas
                    node_ref=bg_canvas_ref
                    class="absolute inset-0 w-full h-full"
                    style="z-index: 0"
                    aria-hidden="true"
                />
                // Props layer - loose props, redrawn on drop/pickup
                <canvas
                    node_ref=props_canvas_ref
                    class="absolute inset-0 w-full h-full"
                    style="z-index: 1"
                    aria-hidden="true"
                />
                // Avatar layer - dynamic, transparent background
                <canvas
                    node_ref=avatar_canvas_ref
                    class="absolute inset-0 w-full h-full"
                    style="z-index: 2"
                    aria-label=format!("Scene: {}", scene.name)
                    role="img"
                    on:click=move |ev| {
                        #[cfg(feature = "hydrate")]
                        on_canvas_click(ev);
                        #[cfg(not(feature = "hydrate"))]
                        let _ = ev;
                    }
                >
                    {format!("Scene: {}", scene.name)}
                </canvas>
            </div>
        </div>
    }
}

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

/// 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)
    }
}

#[cfg(feature = "hydrate")]
fn draw_avatars(
    ctx: &web_sys::CanvasRenderingContext2d,
    members: &[ChannelMemberWithAvatar],
    scale_x: f64,
    scale_y: f64,
    offset_x: f64,
    offset_y: f64,
) {
    for member in members {
        let x = member.member.position_x * scale_x + offset_x;
        let y = member.member.position_y * scale_y + offset_y;

        let avatar_size = 48.0 * scale_x.min(scale_y);

        // Draw avatar placeholder circle
        ctx.begin_path();
        let _ = ctx.arc(x, y - avatar_size / 2.0, avatar_size / 2.0, 0.0, std::f64::consts::PI * 2.0);
        ctx.set_fill_style_str("#6366f1");
        ctx.fill();

        // Draw skin layer sprite if available
        if let Some(ref skin_path) = member.avatar.skin_layer[4] {
            let img = web_sys::HtmlImageElement::new().unwrap();
            let img_clone = img.clone();
            let ctx_clone = ctx.clone();
            let draw_x = x;
            let draw_y = y - avatar_size;
            let size = avatar_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 - size / 2.0, 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(skin_path));
        }

        // Draw emotion overlay if available
        if let Some(ref emotion_path) = member.avatar.emotion_layer[4] {
            let img = web_sys::HtmlImageElement::new().unwrap();
            let img_clone = img.clone();
            let ctx_clone = ctx.clone();
            let draw_x = x;
            let draw_y = y - avatar_size;
            let size = avatar_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 - size / 2.0, 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(emotion_path));
        }

        // Draw emotion indicator on avatar
        let emotion = member.member.current_emotion;
        if emotion > 0 {
            // Draw emotion number in a small badge
            let badge_size = 16.0 * scale_x.min(scale_y);
            let badge_x = x + avatar_size / 2.0 - badge_size / 2.0;
            let badge_y = y - avatar_size - badge_size / 2.0;

            // Badge background
            ctx.begin_path();
            let _ = ctx.arc(badge_x, badge_y, badge_size / 2.0, 0.0, std::f64::consts::PI * 2.0);
            ctx.set_fill_style_str("#f59e0b"); // Amber color for emotion badge
            ctx.fill();

            // Emotion number
            ctx.set_fill_style_str("#000");
            ctx.set_font(&format!("bold {}px sans-serif", 10.0 * scale_x.min(scale_y)));
            ctx.set_text_align("center");
            ctx.set_text_baseline("middle");
            let _ = ctx.fill_text(&format!("{}", emotion), badge_x, badge_y);
        }

        // Draw display name
        ctx.set_fill_style_str("#fff");
        ctx.set_font(&format!("{}px sans-serif", 12.0 * scale_x.min(scale_y)));
        ctx.set_text_align("center");
        ctx.set_text_baseline("alphabetic");
        let _ = ctx.fill_text(&member.member.display_name, x, y + 10.0 * scale_y);
    }
}

/// Draw speech bubbles above avatars.
#[cfg(feature = "hydrate")]
fn draw_speech_bubbles(
    ctx: &web_sys::CanvasRenderingContext2d,
    members: &[ChannelMemberWithAvatar],
    bubbles: &HashMap<(Option<Uuid>, Option<Uuid>), ActiveBubble>,
    scale_x: f64,
    scale_y: f64,
    offset_x: f64,
    offset_y: f64,
    current_time_ms: i64,
) {
    let scale = scale_x.min(scale_y);
    let avatar_size = 48.0 * scale;
    let max_bubble_width = 200.0 * scale;
    let padding = 8.0 * scale;
    let font_size = 12.0 * scale;
    let line_height = 16.0 * scale;
    let tail_size = 8.0 * scale;
    let border_radius = 8.0 * scale;

    for member in members {
        let key = (member.member.user_id, member.member.guest_session_id);

        if let Some(bubble) = bubbles.get(&key) {
            // Skip expired bubbles
            if bubble.expires_at < current_time_ms {
                continue;
            }

            // Use member's CURRENT position, not message position
            let x = member.member.position_x * scale_x + offset_x;
            let y = member.member.position_y * scale_y + offset_y;

            // Get emotion colors
            let (bg_color, border_color, text_color) =
                emotion_bubble_colors(&bubble.message.emotion);

            // Measure and wrap text
            ctx.set_font(&format!("{}px sans-serif", font_size));
            let lines = wrap_text(ctx, &bubble.message.content, max_bubble_width - padding * 2.0);

            // Calculate bubble dimensions
            let bubble_width = lines
                .iter()
                .map(|line: &String| -> f64 {
                    ctx.measure_text(line)
                        .map(|m: web_sys::TextMetrics| m.width())
                        .unwrap_or(0.0)
                })
                .fold(0.0_f64, |a: f64, b: f64| a.max(b))
                + padding * 2.0;
            let bubble_width = bubble_width.max(60.0 * scale); // Minimum width
            let bubble_height = (lines.len() as f64) * line_height + padding * 2.0;

            // Position bubble above avatar
            let bubble_x = x - bubble_width / 2.0;
            let bubble_y = y - avatar_size - bubble_height - tail_size - 5.0 * scale;

            // Draw bubble background with rounded corners
            draw_rounded_rect(ctx, bubble_x, bubble_y, bubble_width, bubble_height, border_radius);
            ctx.set_fill_style_str(bg_color);
            ctx.fill();
            ctx.set_stroke_style_str(border_color);
            ctx.set_line_width(2.0);
            ctx.stroke();

            // Draw tail (triangle pointing down)
            ctx.begin_path();
            ctx.move_to(x - tail_size, bubble_y + bubble_height);
            ctx.line_to(x, bubble_y + bubble_height + tail_size);
            ctx.line_to(x + tail_size, bubble_y + bubble_height);
            ctx.close_path();
            ctx.set_fill_style_str(bg_color);
            ctx.fill();
            ctx.set_stroke_style_str(border_color);
            ctx.stroke();

            // Draw text
            ctx.set_fill_style_str(text_color);
            ctx.set_text_align("left");
            ctx.set_text_baseline("top");
            for (i, line) in lines.iter().enumerate() {
                let _ = ctx.fill_text(
                    line,
                    bubble_x + padding,
                    bubble_y + padding + (i as f64) * line_height,
                );
            }
        }
    }
}

/// Wrap text to fit within max_width.
#[cfg(feature = "hydrate")]
fn wrap_text(ctx: &web_sys::CanvasRenderingContext2d, text: &str, max_width: f64) -> Vec<String> {
    let words: Vec<&str> = text.split_whitespace().collect();
    let mut lines = Vec::new();
    let mut current_line = String::new();

    for word in words {
        let test_line = if current_line.is_empty() {
            word.to_string()
        } else {
            format!("{} {}", current_line, word)
        };

        let width = ctx
            .measure_text(&test_line)
            .map(|m: web_sys::TextMetrics| m.width())
            .unwrap_or(0.0);

        if width > max_width && !current_line.is_empty() {
            lines.push(current_line);
            current_line = word.to_string();
        } else {
            current_line = test_line;
        }
    }

    if !current_line.is_empty() {
        lines.push(current_line);
    }

    // Limit to 4 lines max
    if lines.len() > 4 {
        lines.truncate(3);
        if let Some(last) = lines.last_mut() {
            last.push_str("...");
        }
    }

    // Handle empty text
    if lines.is_empty() {
        lines.push(text.to_string());
    }

    lines
}

/// Draw a rounded rectangle path.
#[cfg(feature = "hydrate")]
fn draw_rounded_rect(
    ctx: &web_sys::CanvasRenderingContext2d,
    x: f64,
    y: f64,
    width: f64,
    height: f64,
    radius: f64,
) {
    ctx.begin_path();
    ctx.move_to(x + radius, y);
    ctx.line_to(x + width - radius, y);
    ctx.arc_to(x + width, y, x + width, y + radius, radius).ok();
    ctx.line_to(x + width, y + height - radius);
    ctx.arc_to(x + width, y + height, x + width - radius, y + height, radius).ok();
    ctx.line_to(x + radius, y + height);
    ctx.arc_to(x, y + height, x, y + height - radius, radius).ok();
    ctx.line_to(x, y + radius);
    ctx.arc_to(x, y, x + radius, y, radius).ok();
    ctx.close_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,
) {
    let prop_size = 48.0 * scale_x.min(scale_y);

    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
            ctx.set_fill_style_str("#fff");
            ctx.set_font(&format!("{}px sans-serif", 10.0 * scale_x.min(scale_y)));
            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);
        }
    }
}
