//! Effect functions for scene viewer background drawing and pan handling.

use leptos::prelude::*;

/// Set up viewport dimension tracking effect.
///
/// Tracks the outer container size and updates the provided signal.
/// Also listens for window resize events.
#[cfg(feature = "hydrate")]
pub fn setup_viewport_tracking(
    outer_container_ref: NodeRef<leptos::html::Div>,
    is_pan_mode: Signal<bool>,
    set_viewport_dimensions: WriteSignal<(f64, f64)>,
) {
    use wasm_bindgen::{JsCast, closure::Closure};

    Effect::new(move |_| {
        // Track pan mode to re-run when it changes
        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();
    });
}

/// Set up middle mouse button drag-to-pan effect.
#[cfg(feature = "hydrate")]
pub fn setup_middle_mouse_pan(
    outer_container_ref: NodeRef<leptos::html::Div>,
    is_pan_mode: Signal<bool>,
) {
    use std::cell::Cell;
    use std::rc::Rc;
    use wasm_bindgen::{JsCast, closure::Closure};

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

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

        // Keep closures alive
        onmousedown.forget();
        onmousemove.forget();
        onmouseup.forget();
        oncontextmenu.forget();
    });
}

/// Set up wheel zoom effect for pan mode.
#[cfg(feature = "hydrate")]
pub fn setup_wheel_zoom(
    outer_container_ref: NodeRef<leptos::html::Div>,
    is_pan_mode: Signal<bool>,
    on_zoom_change: Option<Callback<f64>>,
) {
    use std::cell::RefCell;
    use std::rc::Rc;
    use wasm_bindgen::{JsCast, closure::Closure};

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

/// Draw background to canvas (handles both pan and fit modes).
#[cfg(feature = "hydrate")]
pub fn draw_background(
    canvas_el: &web_sys::HtmlCanvasElement,
    bg_color: &str,
    image_path: &str,
    has_background_image: bool,
    scene_width: f64,
    scene_height: f64,
    is_pan_mode: bool,
    zoom: f64,
    set_scale_x: WriteSignal<f64>,
    set_scale_y: WriteSignal<f64>,
    set_offset_x: WriteSignal<f64>,
    set_offset_y: WriteSignal<f64>,
    set_scales_ready: WriteSignal<bool>,
) {
    use wasm_bindgen::{JsCast, closure::Closure};

    let canvas_el = canvas_el.clone();
    let bg_color = bg_color.to_string();
    let image_path = image_path.to_string();

    let draw_bg = Closure::once(Box::new(move || {
        if is_pan_mode {
            // Pan mode: canvas at native resolution * zoom
            let canvas_width = (scene_width * zoom) as u32;
            let canvas_height = (scene_height * zoom) as u32;

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

            set_scale_x.set(zoom);
            set_scale_y.set(zoom);
            set_offset_x.set(0.0);
            set_offset_y.set(0.0);
            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();

                ctx.set_fill_style_str(&bg_color);
                ctx.fill_rect(0.0, 0.0, canvas_width as f64, canvas_height as f64);

                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 display_width == 0 || display_height == 0 {
                return;
            }

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

            let canvas_aspect = display_width as f64 / display_height as f64;
            let scene_aspect = scene_width / scene_height;

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

            let sx = draw_width / scene_width;
            let sy = draw_height / scene_height;
            set_scale_x.set(sx);
            set_scale_y.set(sy);
            set_offset_x.set(draw_x);
            set_offset_y.set(draw_y);
            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);

                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
    let window = web_sys::window().unwrap();
    let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
        draw_bg.as_ref().unchecked_ref(),
        100,
    );
    draw_bg.forget();
}
