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

mod coordinates;
mod effects;
mod overlays;

pub use coordinates::*;
pub use overlays::*;

use std::collections::HashMap;

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

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

use super::avatar_canvas::{AvatarCanvas, member_key};
#[cfg(feature = "hydrate")]
use super::canvas_utils::hit_test_canvas;
use super::chat_types::ActiveBubble;
use super::context_menu::{ContextMenu, ContextMenuItem};
use super::loose_prop_canvas::LoosePropCanvas;
use super::settings::{
    BASE_AVATAR_SCALE, 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,
    #[allow(unused)]
    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>,
    #[prop(optional)] settings: Option<Signal<ViewerSettings>>,
    #[prop(optional)] on_zoom_change: Option<Callback<f64>>,
    #[prop(optional, into)] fading_members: Option<Signal<Vec<FadingMember>>>,
    #[prop(optional, into)] current_user_id: Option<Signal<Option<Uuid>>>,
    #[prop(optional, into)] is_guest: Option<Signal<bool>>,
    #[prop(optional, into)] on_whisper_request: Option<Callback<String>>,
    #[prop(optional, into)] is_moderator: Option<Signal<bool>>,
    #[prop(optional, into)] on_prop_scale_update: Option<Callback<(Uuid, f32)>>,
    #[prop(optional, into)] on_prop_move: Option<Callback<(Uuid, f64, f64)>>,
    #[prop(optional, into)] on_prop_lock_toggle: Option<Callback<(Uuid, bool)>>,
) -> impl IntoView {
    let is_guest = is_guest.unwrap_or_else(|| Signal::derive(|| false));
    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);
    let (viewport_dimensions, set_viewport_dimensions) = signal((800.0_f64, 600.0_f64));

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

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

    let bg_canvas_ref = NodeRef::<leptos::html::Canvas>::new();
    let outer_container_ref = NodeRef::<leptos::html::Div>::new();

    // Scale/offset state
    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);
    let (scales_ready, set_scales_ready) = signal(false);

    // Context menu state (for avatar whisper)
    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);

    // Prop context menu state
    let (prop_context_menu_open, set_prop_context_menu_open) = signal(false);
    let (prop_context_menu_position, set_prop_context_menu_position) = signal((0.0_f64, 0.0_f64));
    let (prop_context_menu_target, set_prop_context_menu_target) = signal(Option::<Uuid>::None);

    // Scale mode state
    let (scale_mode_active, set_scale_mode_active) = signal(false);
    let (scale_mode_prop_id, set_scale_mode_prop_id) = signal(Option::<Uuid>::None);
    let (scale_mode_initial_scale, set_scale_mode_initial_scale) = signal(1.0_f32);
    let scale_mode_preview_scale = RwSignal::new(1.0_f32);
    let (scale_mode_prop_center, set_scale_mode_prop_center) = signal((0.0_f64, 0.0_f64));

    // Move mode state
    let (move_mode_active, set_move_mode_active) = signal(false);
    let (move_mode_prop_id, set_move_mode_prop_id) = signal(Option::<Uuid>::None);
    let move_mode_preview_position = RwSignal::new((0.0_f64, 0.0_f64));
    let (move_mode_prop_scale, set_move_mode_prop_scale) = signal(1.0_f32);
    let (prop_context_is_locked, set_prop_context_is_locked) = signal(false);

    // Click handler for movement (props are now handled via context menu)
    #[cfg(feature = "hydrate")]
    let on_overlay_click = {
        let on_move = on_move.clone();
        move |ev: web_sys::MouseEvent| {
            use wasm_bindgen::JsCast;

            let client_x = ev.client_x() as f64;
            let client_y = ev.client_y() as f64;

            let document = web_sys::window().unwrap().document().unwrap();
            let mut clicked_prop: Option<Uuid> = None;

            if let Some(container) = document.query_selector(".props-container").ok().flatten() {
                let canvases = container.get_elements_by_tag_name("canvas");
                for i in 0..canvases.length() {
                    if let Some(element) = canvases.item(i) {
                        if let Ok(canvas) = element.dyn_into::<web_sys::HtmlCanvasElement>() {
                            if let Some(prop_id_str) = canvas.get_attribute("data-prop-id") {
                                if hit_test_canvas(&canvas, client_x, client_y) {
                                    if let Ok(prop_id) = prop_id_str.parse::<Uuid>() {
                                        clicked_prop = Some(prop_id);
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }

            if clicked_prop.is_none() {
                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 = client_x - rect.left();
                let click_y = client_y - 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).max(0.0).min(scene_width as f64);
                    let scene_y = ((click_y - oy) / sy).max(0.0).min(scene_height as f64);
                    on_move.run((scene_x, scene_y));
                }
            }
        }
    };

    // Context menu handler
    #[cfg(feature = "hydrate")]
    let on_overlay_contextmenu = {
        let current_user_id = current_user_id.clone();
        move |ev: web_sys::MouseEvent| {
            use wasm_bindgen::JsCast;

            let client_x = ev.client_x() as f64;
            let client_y = ev.client_y() as f64;
            let document = web_sys::window().unwrap().document().unwrap();

            // Check props first
            if let Some(container) = document.query_selector(".props-container").ok().flatten() {
                let canvases = container.get_elements_by_tag_name("canvas");
                for i in 0..canvases.length() {
                    if let Some(element) = canvases.item(i) {
                        if let Ok(canvas) = element.dyn_into::<web_sys::HtmlCanvasElement>() {
                            if let Some(prop_id_str) = canvas.get_attribute("data-prop-id") {
                                if hit_test_canvas(&canvas, client_x, client_y) {
                                    if let Ok(prop_id) = prop_id_str.parse::<Uuid>() {
                                        if let Some(prop) = loose_props.get().iter().find(|p| p.id == prop_id) {
                                            let is_mod = is_moderator.map(|s| s.get()).unwrap_or(false);

                                            // Don't show menu if prop is locked and user is not a moderator
                                            if prop.is_locked && !is_mod {
                                                ev.prevent_default();
                                                return;
                                            }

                                            ev.prevent_default();
                                            set_prop_context_menu_position.set((client_x, client_y));
                                            set_prop_context_menu_target.set(Some(prop_id));
                                            set_prop_context_menu_open.set(true);

                                            set_scale_mode_initial_scale.set(prop.scale);
                                            set_prop_context_is_locked.set(prop.is_locked);
                                            set_move_mode_prop_scale.set(prop.scale);
                                            let rect = canvas.get_bounding_client_rect();
                                            set_scale_mode_prop_center.set((
                                                rect.left() + rect.width() / 2.0,
                                                rect.top() + rect.height() / 2.0,
                                            ));
                                        }
                                        return;
                                    }
                                }
                            }
                        }
                    }
                }
            }

            // Guests cannot access avatar context menu
            if is_guest.get() {
                return;
            }

            let my_user_id = current_user_id.map(|s| s.get()).flatten();

            // Check avatars
            if let Some(container) = document.query_selector(".avatars-container").ok().flatten() {
                let canvases = container.get_elements_by_tag_name("canvas");
                for i in 0..canvases.length() {
                    if let Some(element) = canvases.item(i) {
                        if let Ok(canvas) = element.dyn_into::<web_sys::HtmlCanvasElement>() {
                            if let Some(member_id_str) = canvas.get_attribute("data-member-id") {
                                if hit_test_canvas(&canvas, client_x, client_y) {
                                    if let Ok(member_id) = member_id_str.parse::<Uuid>() {
                                        if my_user_id != Some(member_id) {
                                            if let Some(name) = members.get().iter()
                                                .find(|m| m.member.user_id == member_id)
                                                .map(|m| m.member.display_name.clone())
                                            {
                                                ev.prevent_default();
                                                set_context_menu_position.set((client_x, client_y));
                                                set_context_menu_target.set(Some(name));
                                                set_context_menu_open.set(true);
                                                return;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    };

    // Set up effects
    #[cfg(feature = "hydrate")]
    {
        use std::cell::RefCell;
        use std::rc::Rc;

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

        // Viewport tracking
        effects::setup_viewport_tracking(outer_container_ref, is_pan_mode, set_viewport_dimensions);

        // Background drawing effect
        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)>));

        Effect::new(move |_| {
            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 };

            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 }

            *last_pan_mode.borrow_mut() = Some(current_pan_mode);
            *last_zoom.borrow_mut() = Some(current_zoom);
            *last_viewport.borrow_mut() = Some(current_viewport);

            effects::draw_background(
                &canvas,
                &bg_color_clone,
                &image_path_clone,
                has_background_image,
                scene_width_f,
                scene_height_f,
                current_pan_mode,
                current_zoom,
                set_scale_x,
                set_scale_y,
                set_offset_x,
                set_offset_y,
                set_scales_ready,
            );
        });

        // Middle mouse pan
        effects::setup_middle_mouse_pan(outer_container_ref, is_pan_mode);

        // Wheel zoom
        effects::setup_wheel_zoom(outer_container_ref, is_pan_mode, on_zoom_change);
    }

    let aspect_ratio = scene_width as f64 / scene_height as f64;

    // Style computations
    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;

            if canvas_w <= vp_w && canvas_h <= vp_h {
                "scene-container scene-viewer-container w-full overflow-auto flex justify-center items-center"
            } else {
                "scene-container scene-viewer-container w-full overflow-auto"
            }
        } else {
            "scene-container scene-viewer-container w-full h-full flex justify-center items-center"
        }
    };

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

    // Member sorting and derived signals
    let sorted_members = Signal::derive(move || {
        let mut m = members.get();
        m.sort_by(|a, b| b.member.joined_at.cmp(&a.member.joined_at));
        m
    });

    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();
        let ref_scale = (scene_width_f / REFERENCE_WIDTH).max(scene_height_f / REFERENCE_HEIGHT);

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

    let text_em_size = Signal::derive(move || settings.get().text_em_size);
    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());
    let scene_width_signal = Signal::derive(move || scene_width_f);
    let scene_height_signal = Signal::derive(move || scene_height_f);

    let members_by_key = Signal::derive(move || {
        sorted_members.get().into_iter().enumerate()
            .map(|(idx, m)| (member_key(&m), (idx, m)))
            .collect::<HashMap<_, _>>()
    });

    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>
                <canvas
                    node_ref=bg_canvas_ref
                    class=canvas_class
                    style=move || canvas_style(0)
                    aria-hidden="true"
                />
                <div class="props-container absolute inset-0" style="z-index: 1; pointer-events: none;">
                    <Show when=move || scales_ready.get() fallback=|| ()>
                        {move || {
                            loose_props.get().into_iter().map(|prop| {
                                let prop_id = prop.id;
                                let prop_signal = Signal::derive(move || {
                                    loose_props.get().into_iter().find(|p| p.id == prop_id).unwrap_or_else(|| prop.clone())
                                });
                                view! {
                                    <LoosePropCanvas
                                        prop=prop_signal
                                        scale_x=scale_x_signal
                                        scale_y=scale_y_signal
                                        offset_x=offset_x_signal
                                        offset_y=offset_y_signal
                                        base_prop_size=prop_size
                                        z_index=5
                                    />
                                }
                            }).collect_view()
                        }}
                    </Show>
                </div>
                <div class="avatars-container absolute inset-0" style="z-index: 2; pointer-events: none;">
                    <Show when=move || scales_ready.get() fallback=|| ()>
                        {move || {
                            member_keys.get().into_iter().map(|key| {
                                let member_signal = Signal::derive(move || {
                                    members_by_key.get().get(&key).map(|(_, m)| m.clone()).expect("member key should exist")
                                });
                                let z_index_signal = Signal::derive(move || {
                                    members_by_key.get().get(&key).map(|(idx, _)| (*idx as i32) + 10).unwrap_or(10)
                                });
                                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()
                        }}
                        {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)).max(0.0).min(1.0);
                                    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>
                <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;
                    }
                />
                <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);
                    })
                />
                <ContextMenu
                    open=Signal::derive(move || prop_context_menu_open.get())
                    position=Signal::derive(move || prop_context_menu_position.get())
                    header=Signal::derive(move || Some("Prop".to_string()))
                    items=Signal::derive(move || {
                        let is_mod = is_moderator.map(|s| s.get()).unwrap_or(false);
                        let is_locked = prop_context_is_locked.get();
                        let mut items = Vec::new();
                        if !is_locked || is_mod {
                            items.push(ContextMenuItem { label: "Pick Up".to_string(), action: "pick_up".to_string() });
                            items.push(ContextMenuItem { label: "Move".to_string(), action: "move".to_string() });
                        }
                        if is_mod {
                            items.push(ContextMenuItem { label: "Set Scale".to_string(), action: "set_scale".to_string() });
                            items.push(ContextMenuItem {
                                label: if is_locked { "Unlock" } else { "Lock" }.to_string(),
                                action: if is_locked { "unlock" } else { "lock" }.to_string(),
                            });
                        }
                        items
                    })
                    on_select=Callback::new({
                        let on_prop_lock_toggle = on_prop_lock_toggle.clone();
                        let on_prop_click = on_prop_click.clone();
                        move |action: String| {
                            match action.as_str() {
                                "pick_up" => {
                                    if let Some(prop_id) = prop_context_menu_target.get() {
                                        on_prop_click.run(prop_id);
                                    }
                                }
                                "set_scale" => {
                                    if let Some(prop_id) = prop_context_menu_target.get() {
                                        set_scale_mode_prop_id.set(Some(prop_id));
                                        scale_mode_preview_scale.set(scale_mode_initial_scale.get());
                                        set_scale_mode_active.set(true);
                                    }
                                }
                                "move" => {
                                    if let Some(prop_id) = prop_context_menu_target.get() {
                                        set_move_mode_prop_id.set(Some(prop_id));
                                        if let Some(prop) = loose_props.get().iter().find(|p| p.id == prop_id) {
                                            move_mode_preview_position.set((prop.position_x, prop.position_y));
                                        }
                                        set_move_mode_active.set(true);
                                    }
                                }
                                "lock" | "unlock" => {
                                    if let Some(prop_id) = prop_context_menu_target.get() {
                                        if let Some(ref callback) = on_prop_lock_toggle {
                                            callback.run((prop_id, action == "lock"));
                                        }
                                    }
                                }
                                _ => {}
                            }
                            set_prop_context_menu_open.set(false);
                        }
                    })
                    on_close=Callback::new(move |_: ()| {
                        set_prop_context_menu_open.set(false);
                        set_prop_context_menu_target.set(None);
                    })
                />
                <ScaleOverlay
                    active=Signal::derive(move || scale_mode_active.get())
                    prop_id=Signal::derive(move || scale_mode_prop_id.get())
                    preview_scale=scale_mode_preview_scale
                    prop_center=Signal::derive(move || scale_mode_prop_center.get())
                    loose_props=loose_props
                    prop_size=prop_size
                    on_apply=on_prop_scale_update.unwrap_or_else(|| Callback::new(|_| {}))
                    on_cancel=Callback::new(move |_: ()| {
                        set_scale_mode_active.set(false);
                        set_scale_mode_prop_id.set(None);
                    })
                />
                <MoveOverlay
                    active=Signal::derive(move || move_mode_active.get())
                    prop_id=Signal::derive(move || move_mode_prop_id.get())
                    preview_position=move_mode_preview_position
                    prop_scale=Signal::derive(move || move_mode_prop_scale.get())
                    loose_props=loose_props
                    prop_size=prop_size
                    scale_x=scale_x_signal
                    scale_y=scale_y_signal
                    offset_x=offset_x_signal
                    offset_y=offset_y_signal
                    on_apply=on_prop_move.unwrap_or_else(|| Callback::new(|_| {}))
                    on_cancel=Callback::new(move |_: ()| {
                        set_move_mode_active.set(false);
                        set_move_mode_prop_id.set(None);
                    })
                />
            </div>
        </div>
    }
}
