//! Individual avatar canvas component for per-user rendering.
//!
//! Each avatar gets its own canvas element positioned via CSS transforms.
//! This enables efficient updates: position changes only update CSS (no redraw),
//! while appearance changes (emotion, skin) redraw only that avatar's canvas.

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

use chattyness_db::models::ChannelMemberWithAvatar;

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

/// Base text size multiplier. Text at 100% slider = base_sizes * 1.4
const BASE_TEXT_SCALE: f64 = 1.4;

/// Estimate bubble height based on actual text content.
/// Returns the total height including padding, tail, and gap.
fn estimate_bubble_height(text: &str, text_scale: f64) -> f64 {
    let max_bubble_width = 200.0 * text_scale;
    let padding = 8.0 * text_scale;
    let line_height = 16.0 * text_scale;
    let tail_size = 8.0 * text_scale;
    let gap = 5.0 * text_scale;

    // Estimate chars per line: roughly 6 pixels per char at default scale
    let chars_per_line = ((max_bubble_width - padding * 2.0) / (6.0 * text_scale)).floor() as usize;
    let chars_per_line = chars_per_line.max(10); // minimum 10 chars per line
    let char_count = text.len();

    // Simple heuristic: estimate lines based on character count
    let estimated_lines = ((char_count as f64) / (chars_per_line as f64)).ceil() as usize;
    let estimated_lines = estimated_lines.clamp(1, 4); // 1-4 lines max

    let bubble_height = (estimated_lines as f64) * line_height + padding * 2.0;
    bubble_height + tail_size + gap
}

/// Content bounds for a 3x3 avatar grid.
/// Tracks which rows/columns contain actual content for centering calculations.
struct ContentBounds {
    min_col: usize,
    max_col: usize,
    min_row: usize,
    max_row: usize,
}

impl ContentBounds {
    /// Calculate content bounds from 4 layers (9 positions each).
    fn from_layers(
        skin: &[Option<String>; 9],
        clothes: &[Option<String>; 9],
        accessories: &[Option<String>; 9],
        emotion: &[Option<String>; 9],
    ) -> Self {
        let has_content_at = |pos: usize| -> bool {
            skin[pos].is_some()
                || clothes[pos].is_some()
                || accessories[pos].is_some()
                || emotion[pos].is_some()
        };

        // Columns: 0 (left), 1 (middle), 2 (right)
        let left_col = [0, 3, 6].iter().any(|&p| has_content_at(p));
        let mid_col = [1, 4, 7].iter().any(|&p| has_content_at(p));
        let right_col = [2, 5, 8].iter().any(|&p| has_content_at(p));

        let min_col = if left_col {
            0
        } else if mid_col {
            1
        } else {
            2
        };
        let max_col = if right_col {
            2
        } else if mid_col {
            1
        } else {
            0
        };

        // Rows: 0 (top), 1 (middle), 2 (bottom)
        let top_row = [0, 1, 2].iter().any(|&p| has_content_at(p));
        let mid_row = [3, 4, 5].iter().any(|&p| has_content_at(p));
        let bot_row = [6, 7, 8].iter().any(|&p| has_content_at(p));

        let min_row = if top_row {
            0
        } else if mid_row {
            1
        } else {
            2
        };
        let max_row = if bot_row {
            2
        } else if mid_row {
            1
        } else {
            0
        };

        Self {
            min_col,
            max_col,
            min_row,
            max_row,
        }
    }

    /// Content center column (0.0 to 2.0, grid center is 1.0).
    fn center_col(&self) -> f64 {
        (self.min_col + self.max_col) as f64 / 2.0
    }

    /// Content center row (0.0 to 2.0, grid center is 1.0).
    fn center_row(&self) -> f64 {
        (self.min_row + self.max_row) as f64 / 2.0
    }

    /// X offset from grid center to content center.
    fn x_offset(&self, cell_size: f64) -> f64 {
        (self.center_col() - 1.0) * cell_size
    }

    /// Y offset from grid center to content center.
    fn y_offset(&self, cell_size: f64) -> f64 {
        (self.center_row() - 1.0) * cell_size
    }

    /// Number of empty rows at the bottom (for name positioning).
    fn empty_bottom_rows(&self) -> usize {
        2 - self.max_row
    }

    /// Number of empty rows at the top (for bubble positioning).
    fn empty_top_rows(&self) -> usize {
        self.min_row
    }

    /// Width of actual content in pixels.
    fn content_width(&self, cell_size: f64) -> f64 {
        (self.max_col - self.min_col + 1) as f64 * cell_size
    }

    /// Height of actual content in pixels.
    fn content_height(&self, cell_size: f64) -> f64 {
        (self.max_row - self.min_row + 1) as f64 * cell_size
    }
}

/// Computed boundaries for visual clamping in screen space.
#[derive(Clone, Copy)]
struct ScreenBoundaries {
    /// Left edge of drawable area (= offset_x)
    min_x: f64,
    /// Right edge (= offset_x + scene_width * scale_x)
    max_x: f64,
    /// Top edge (= offset_y)
    min_y: f64,
    /// Bottom edge (= offset_y + scene_height * scale_y)
    max_y: f64,
}

impl ScreenBoundaries {
    fn from_transform(
        scene_width: f64,
        scene_height: f64,
        scale_x: f64,
        scale_y: f64,
        offset_x: f64,
        offset_y: f64,
    ) -> Self {
        Self {
            min_x: offset_x,
            max_x: offset_x + (scene_width * scale_x),
            min_y: offset_y,
            max_y: offset_y + (scene_height * scale_y),
        }
    }

    /// Clamp avatar center so visual bounds stay within screen boundaries.
    fn clamp_avatar_center(
        &self,
        center_x: f64,
        center_y: f64,
        half_width: f64,
        half_height: f64,
    ) -> (f64, f64) {
        let clamped_x = center_x
            .max(self.min_x + half_width)
            .min(self.max_x - half_width);
        let clamped_y = center_y
            .max(self.min_y + half_height)
            .min(self.max_y - half_height);
        (clamped_x, clamped_y)
    }
}

/// Position of speech bubble relative to avatar.
#[derive(Clone, Copy, PartialEq)]
enum BubblePosition {
    /// Default: bubble above avatar
    Above,
    /// When near top edge: bubble below avatar
    Below,
}

/// Determine bubble position based on available space above avatar.
fn determine_bubble_position(
    avatar_screen_y: f64,
    avatar_half_height: f64,
    bubble_height: f64,
    tail_size: f64,
    gap: f64,
    min_y: f64,
) -> BubblePosition {
    let space_needed = bubble_height + tail_size + gap;
    let avatar_top = avatar_screen_y - avatar_half_height;
    let space_available = avatar_top - min_y;

    if space_available < space_needed {
        BubblePosition::Below
    } else {
        BubblePosition::Above
    }
}

/// Unified layout context for avatar canvas rendering.
///
/// This struct computes all derived layout values once from the inputs,
/// providing a single source of truth for:
/// - Canvas dimensions and position
/// - Avatar positioning within the canvas
/// - Coordinate transformations between canvas-local and screen space
/// - Bubble positioning and clamping
///
/// By centralizing these calculations, we avoid scattered, duplicated logic
/// and ensure the style closure, Effect, and draw_bubble all use consistent values.
#[derive(Clone, Copy)]
#[allow(dead_code)] // Some fields kept for potential future use
struct CanvasLayout {
    // Core dimensions
    prop_size: f64,
    avatar_size: f64,

    // Content offset from grid center
    content_x_offset: f64,
    content_y_offset: f64,

    // Text scaling
    text_scale: f64,
    bubble_max_width: f64,

    // Canvas dimensions
    canvas_width: f64,
    canvas_height: f64,

    // Canvas position in screen space
    canvas_screen_x: f64,
    canvas_screen_y: f64,

    // Avatar center within canvas (canvas-local coordinates)
    avatar_cx: f64,
    avatar_cy: f64,

    // Scene boundaries for clamping
    boundaries: ScreenBoundaries,

    // Bubble state
    bubble_position: BubblePosition,
    bubble_height_reserved: f64,

    // Content row info for positioning
    empty_top_rows: usize,
    empty_bottom_rows: usize,
}

impl CanvasLayout {
    /// Create a new layout from all input parameters.
    fn new(
        content_bounds: &ContentBounds,
        prop_size: f64,
        text_em_size: f64,
        avatar_screen_x: f64,
        avatar_screen_y: f64,
        boundaries: ScreenBoundaries,
        has_bubble: bool,
        bubble_text: Option<&str>,
    ) -> Self {
        let avatar_size = prop_size * 3.0;
        let text_scale = text_em_size * BASE_TEXT_SCALE;
        let bubble_max_width = 200.0 * text_scale;

        // Content offsets from grid center
        let content_x_offset = content_bounds.x_offset(prop_size);
        let content_y_offset = content_bounds.y_offset(prop_size);

        // Empty rows for positioning elements relative to content
        let empty_top_rows = content_bounds.empty_top_rows();
        let empty_bottom_rows = content_bounds.empty_bottom_rows();

        // Content dimensions for clamping
        let content_half_width = content_bounds.content_width(prop_size) / 2.0;
        let content_half_height = content_bounds.content_height(prop_size) / 2.0;

        // Clamp avatar so content stays within scene
        let (clamped_x, clamped_y) = boundaries.clamp_avatar_center(
            avatar_screen_x,
            avatar_screen_y,
            content_half_width,
            content_half_height,
        );

        // Calculate bubble height and position
        let bubble_height_reserved = if has_bubble {
            (4.0 * 16.0 + 16.0 + 8.0 + 5.0) * text_scale
        } else {
            0.0
        };
        let name_height = 20.0 * text_scale;

        // Determine bubble position (above or below)
        // Use actual content height, not full 3x3 grid size
        let bubble_position = if has_bubble {
            let estimated_height = bubble_text
                .map(|t| estimate_bubble_height(t, text_scale))
                .unwrap_or(0.0);
            // clamped_y is the content center, so use content_half_height
            // to find the actual top of the visible avatar content
            determine_bubble_position(
                clamped_y,
                content_half_height,
                estimated_height,
                0.0,
                0.0,
                boundaries.min_y,
            )
        } else {
            BubblePosition::Above
        };

        // Canvas dimensions - wide enough to fit shifted bubble
        let extra_margin = if has_bubble { bubble_max_width } else { 0.0 };
        let canvas_width = avatar_size.max(bubble_max_width) + extra_margin;
        let canvas_height = avatar_size + bubble_height_reserved + name_height;

        // Canvas position in screen space
        // The avatar grid center maps to canvas_width/2, but we need to account
        // for the content offset so the visible content aligns with clamped_x/y
        let canvas_x = clamped_x - avatar_size / 2.0 - content_x_offset;
        let canvas_screen_x = canvas_x - (canvas_width - avatar_size) / 2.0;

        let canvas_y = clamped_y - avatar_size / 2.0 - content_y_offset;
        let canvas_screen_y = match bubble_position {
            BubblePosition::Above => canvas_y - bubble_height_reserved,
            BubblePosition::Below => canvas_y,
        };

        // Avatar center within canvas
        let avatar_cx = canvas_width / 2.0;
        let avatar_cy = match bubble_position {
            BubblePosition::Above => bubble_height_reserved + avatar_size / 2.0,
            BubblePosition::Below => avatar_size / 2.0,
        };

        Self {
            prop_size,
            avatar_size,
            content_x_offset,
            content_y_offset,
            text_scale,
            bubble_max_width,
            canvas_width,
            canvas_height,
            canvas_screen_x,
            canvas_screen_y,
            avatar_cx,
            avatar_cy,
            boundaries,
            bubble_position,
            bubble_height_reserved,
            empty_top_rows,
            empty_bottom_rows,
        }
    }

    /// CSS style string for positioning the canvas element.
    fn css_style(&self, z_index: i32, pointer_events: &str, opacity: f64) -> String {
        format!(
            "position: absolute; \
             left: 0; top: 0; \
             transform: translate({}px, {}px); \
             z-index: {}; \
             pointer-events: {}; \
             width: {}px; \
             height: {}px; \
             opacity: {};",
            self.canvas_screen_x,
            self.canvas_screen_y,
            z_index,
            pointer_events,
            self.canvas_width,
            self.canvas_height,
            opacity
        )
    }

    /// Content center X in canvas-local coordinates.
    fn content_center_x(&self) -> f64 {
        self.avatar_cx + self.content_x_offset
    }

    /// Top of avatar in canvas-local coordinates.
    fn avatar_top_y(&self) -> f64 {
        self.avatar_cy - self.avatar_size / 2.0
    }

    /// Bottom of avatar in canvas-local coordinates.
    fn avatar_bottom_y(&self) -> f64 {
        self.avatar_cy + self.avatar_size / 2.0
    }

    /// Convert canvas-local X to screen X.
    fn canvas_to_screen_x(&self, x: f64) -> f64 {
        self.canvas_screen_x + x
    }

    /// Clamp a bubble's X position to stay within scene boundaries.
    /// Takes and returns canvas-local coordinates.
    fn clamp_bubble_x(&self, bubble_x: f64, bubble_width: f64) -> f64 {
        // Convert to screen space
        let screen_left = self.canvas_to_screen_x(bubble_x);
        let screen_right = screen_left + bubble_width;

        // Calculate shifts needed to stay within bounds
        let shift_right = (self.boundaries.min_x - screen_left).max(0.0);
        let shift_left = (screen_right - self.boundaries.max_x).max(0.0);

        // Apply shift and clamp to canvas bounds
        let shifted = bubble_x + shift_right - shift_left;
        shifted.max(0.0).min(self.canvas_width - bubble_width)
    }

    /// Adjustment for bubble Y position based on empty rows at top.
    /// Returns the distance in pixels from grid top to content top.
    fn content_top_adjustment(&self) -> f64 {
        self.empty_top_rows as f64 * self.prop_size
    }

    /// Adjustment for name Y position based on empty rows at bottom.
    /// Returns the distance in pixels from grid bottom to content bottom.
    fn content_bottom_adjustment(&self) -> f64 {
        self.empty_bottom_rows as f64 * self.prop_size
    }
}

/// Get a unique key for a member (for Leptos For keying).
pub fn member_key(m: &ChannelMemberWithAvatar) -> (Option<Uuid>, Option<Uuid>) {
    (m.member.user_id, m.member.guest_session_id)
}

/// Individual avatar canvas component.
///
/// Renders a single avatar with:
/// - CSS transform for position (GPU-accelerated, no redraw on move)
/// - Canvas for avatar sprite (redraws only on appearance change)
/// - Optional speech bubble above the avatar
#[component]
pub fn AvatarCanvas(
    /// The member data for this avatar (as a signal for reactive updates).
    member: Signal<ChannelMemberWithAvatar>,
    /// X scale factor for coordinate conversion.
    scale_x: Signal<f64>,
    /// Y scale factor for coordinate conversion.
    scale_y: Signal<f64>,
    /// X offset for coordinate conversion.
    offset_x: Signal<f64>,
    /// Y offset for coordinate conversion.
    offset_y: Signal<f64>,
    /// Size of the avatar in pixels.
    prop_size: Signal<f64>,
    /// Z-index for stacking order (higher = on top).
    z_index: i32,
    /// Active speech bubble for this user (if any).
    active_bubble: Signal<Option<ActiveBubble>>,
    /// Text size multiplier for display names, chat bubbles, and badges.
    #[prop(default = 1.0.into())]
    text_em_size: Signal<f64>,
    /// Opacity for fade-out animation (0.0 to 1.0, default 1.0).
    #[prop(default = 1.0)]
    opacity: f64,
    /// Scene width in scene coordinates (for boundary calculations).
    #[prop(optional)]
    scene_width: Option<Signal<f64>>,
    /// Scene height in scene coordinates (for boundary calculations).
    #[prop(optional)]
    scene_height: Option<Signal<f64>>,
) -> impl IntoView {
    let canvas_ref = NodeRef::<leptos::html::Canvas>::new();

    // Disable pointer-events when fading (opacity < 1.0) so fading avatars aren't clickable
    let pointer_events = if opacity < 1.0 { "none" } else { "auto" };

    // Reactive style for CSS positioning (GPU-accelerated transforms)
    // This closure re-runs when position, scale, offset, or prop_size changes
    let style = move || {
        let m = member.get();
        let ps = prop_size.get();
        let sx = scale_x.get();
        let sy = scale_y.get();
        let ox = offset_x.get();
        let oy = offset_y.get();
        let te = text_em_size.get();
        let bubble = active_bubble.get();

        // Calculate content bounds for centering on actual content
        let content_bounds = ContentBounds::from_layers(
            &m.avatar.skin_layer,
            &m.avatar.clothes_layer,
            &m.avatar.accessories_layer,
            &m.avatar.emotion_layer,
        );

        // Get scene dimensions (use large defaults if not provided)
        let sw = scene_width.map(|s| s.get()).unwrap_or(10000.0);
        let sh = scene_height.map(|s| s.get()).unwrap_or(10000.0);

        // Compute screen boundaries and avatar screen position
        let boundaries = ScreenBoundaries::from_transform(sw, sh, sx, sy, ox, oy);
        let avatar_screen_x = m.member.position_x * sx + ox;
        let avatar_screen_y = m.member.position_y * sy + oy;

        // Create unified layout - all calculations happen in one place
        let layout = CanvasLayout::new(
            &content_bounds,
            ps,
            te,
            avatar_screen_x,
            avatar_screen_y,
            boundaries,
            bubble.is_some(),
            bubble.as_ref().map(|b| b.message.content.as_str()),
        );

        // Generate CSS style from layout
        layout.css_style(z_index, pointer_events, opacity)
    };

    // Store references for the effect
    #[cfg(feature = "hydrate")]
    {
        use std::cell::RefCell;
        use std::collections::HashMap;
        use std::rc::Rc;
        use wasm_bindgen::JsCast;
        use wasm_bindgen::closure::Closure;

        // Image cache for this avatar (persists across re-renders)
        let image_cache: Rc<RefCell<HashMap<String, web_sys::HtmlImageElement>>> =
            Rc::new(RefCell::new(HashMap::new()));

        // Redraw trigger - incremented when images load to cause Effect to re-run
        let (redraw_trigger, set_redraw_trigger) = signal(0u32);

        // Effect to draw the avatar when canvas is ready or appearance changes
        Effect::new(move |_| {
            // Subscribe to redraw trigger so this effect re-runs when images load
            let _ = redraw_trigger.get();

            // Get current values from signals
            let m = member.get();
            let ps = prop_size.get();
            let te = text_em_size.get();
            let bubble = active_bubble.get();

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

            // Calculate content bounds for the avatar
            let content_bounds = ContentBounds::from_layers(
                &m.avatar.skin_layer,
                &m.avatar.clothes_layer,
                &m.avatar.accessories_layer,
                &m.avatar.emotion_layer,
            );

            // Get scene dimensions and transform parameters
            let sx = scale_x.get();
            let sy = scale_y.get();
            let ox = offset_x.get();
            let oy = offset_y.get();
            let sw = scene_width.map(|s| s.get()).unwrap_or(10000.0);
            let sh = scene_height.map(|s| s.get()).unwrap_or(10000.0);

            // Create unified layout - same calculation as style closure
            let boundaries = ScreenBoundaries::from_transform(sw, sh, sx, sy, ox, oy);
            let avatar_screen_x = m.member.position_x * sx + ox;
            let avatar_screen_y = m.member.position_y * sy + oy;

            let layout = CanvasLayout::new(
                &content_bounds,
                ps,
                te,
                avatar_screen_x,
                avatar_screen_y,
                boundaries,
                bubble.is_some(),
                bubble.as_ref().map(|b| b.message.content.as_str()),
            );

            let canvas_el: &web_sys::HtmlCanvasElement = &canvas;

            // Set canvas resolution from layout
            canvas_el.set_width(layout.canvas_width as u32);
            canvas_el.set_height(layout.canvas_height as u32);

            let Ok(Some(ctx)) = canvas_el.get_context("2d") else {
                return;
            };
            let ctx: web_sys::CanvasRenderingContext2d = ctx.dyn_into().unwrap();

            // Clear canvas
            ctx.clear_rect(0.0, 0.0, layout.canvas_width, layout.canvas_height);

            // Helper to load and draw an image
            // Images are cached; when loaded, triggers a redraw via signal
            let draw_image = |path: &str,
                              cache: &Rc<RefCell<HashMap<String, web_sys::HtmlImageElement>>>,
                              ctx: &web_sys::CanvasRenderingContext2d,
                              x: f64,
                              y: f64,
                              size: f64| {
                let normalized_path = normalize_asset_path(path);
                let mut cache_borrow = cache.borrow_mut();

                if let Some(img) = cache_borrow.get(&normalized_path) {
                    // Image is in cache - draw if loaded
                    if img.complete() && img.natural_width() > 0 {
                        let _ = ctx.draw_image_with_html_image_element_and_dw_and_dh(
                            img,
                            x - size / 2.0,
                            y - size / 2.0,
                            size,
                            size,
                        );
                    }
                    // If not complete, onload handler will trigger redraw
                } else {
                    // Not in cache - create and start loading
                    let img = web_sys::HtmlImageElement::new().unwrap();

                    // Set onload handler to trigger redraw when image loads
                    let trigger = set_redraw_trigger;
                    let onload = Closure::once(Box::new(move || {
                        trigger.update(|v| *v += 1);
                    }) as Box<dyn FnOnce()>);
                    img.set_onload(Some(onload.as_ref().unchecked_ref()));
                    onload.forget();

                    img.set_src(&normalized_path);
                    cache_borrow.insert(normalized_path, img);
                }
            };

            // Draw all 9 positions of the avatar grid (3x3 layout)
            let cell_size = layout.prop_size;
            let grid_origin_x = layout.avatar_cx - layout.avatar_size / 2.0;
            let grid_origin_y = layout.avatar_cy - layout.avatar_size / 2.0;

            // Draw skin layer for all 9 positions
            for pos in 0..9 {
                if let Some(ref skin_path) = m.avatar.skin_layer[pos] {
                    let col = pos % 3;
                    let row = pos / 3;
                    let cell_cx = grid_origin_x + (col as f64 + 0.5) * cell_size;
                    let cell_cy = grid_origin_y + (row as f64 + 0.5) * cell_size;
                    draw_image(skin_path, &image_cache, &ctx, cell_cx, cell_cy, cell_size);
                }
            }

            // Draw clothes layer for all 9 positions
            for pos in 0..9 {
                if let Some(ref clothes_path) = m.avatar.clothes_layer[pos] {
                    let col = pos % 3;
                    let row = pos / 3;
                    let cell_cx = grid_origin_x + (col as f64 + 0.5) * cell_size;
                    let cell_cy = grid_origin_y + (row as f64 + 0.5) * cell_size;
                    draw_image(
                        clothes_path,
                        &image_cache,
                        &ctx,
                        cell_cx,
                        cell_cy,
                        cell_size,
                    );
                }
            }

            // Draw accessories layer for all 9 positions
            for pos in 0..9 {
                if let Some(ref accessories_path) = m.avatar.accessories_layer[pos] {
                    let col = pos % 3;
                    let row = pos / 3;
                    let cell_cx = grid_origin_x + (col as f64 + 0.5) * cell_size;
                    let cell_cy = grid_origin_y + (row as f64 + 0.5) * cell_size;
                    draw_image(
                        accessories_path,
                        &image_cache,
                        &ctx,
                        cell_cx,
                        cell_cy,
                        cell_size,
                    );
                }
            }

            // Draw emotion overlay for all 9 positions
            for pos in 0..9 {
                if let Some(ref emotion_path) = m.avatar.emotion_layer[pos] {
                    let col = pos % 3;
                    let row = pos / 3;
                    let cell_cx = grid_origin_x + (col as f64 + 0.5) * cell_size;
                    let cell_cy = grid_origin_y + (row as f64 + 0.5) * cell_size;
                    draw_image(
                        emotion_path,
                        &image_cache,
                        &ctx,
                        cell_cx,
                        cell_cy,
                        cell_size,
                    );
                }
            }

            // Draw emotion badge if non-neutral
            let current_emotion = m.member.current_emotion;
            if current_emotion > 0 {
                let badge_size = 16.0 * layout.text_scale;
                let badge_x = layout.avatar_cx + layout.avatar_size / 2.0 - badge_size / 2.0;
                let badge_y = layout.avatar_cy - layout.avatar_size / 2.0 - badge_size / 2.0;

                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");
                ctx.fill();

                ctx.set_fill_style_str("#000");
                ctx.set_font(&format!("bold {}px sans-serif", 10.0 * layout.text_scale));
                ctx.set_text_align("center");
                ctx.set_text_baseline("middle");
                let _ = ctx.fill_text(&format!("{}", current_emotion), badge_x, badge_y);
            }

            // Draw display name below avatar (with black outline for readability)
            let name_x = layout.content_center_x();
            let name_y = layout.avatar_bottom_y() - layout.content_bottom_adjustment()
                + 15.0 * layout.text_scale;

            let display_name = &m.member.display_name;
            ctx.set_font(&format!("{}px sans-serif", 12.0 * layout.text_scale));
            ctx.set_text_align("center");
            ctx.set_text_baseline("alphabetic");
            // Black outline
            ctx.set_stroke_style_str("#000");
            ctx.set_line_width(3.0);
            let _ = ctx.stroke_text(display_name, name_x, name_y);
            // White fill
            ctx.set_fill_style_str("#fff");
            let _ = ctx.fill_text(display_name, name_x, name_y);

            // Draw speech bubble if active
            if let Some(ref b) = bubble {
                let current_time = js_sys::Date::now() as i64;
                if b.expires_at >= current_time {
                    draw_bubble_with_layout(&ctx, b, &layout, te);
                }
            }
        });
    }

    // Compute data-member-id reactively
    let data_member_id = move || {
        let m = member.get();
        m.member
            .user_id
            .map(|u| u.to_string())
            .or_else(|| m.member.guest_session_id.map(|g| g.to_string()))
            .unwrap_or_default()
    };

    view! {
        <canvas
            node_ref=canvas_ref
            style=style
            data-member-id=data_member_id
        />
    }
}

/// 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 a speech bubble using the unified CanvasLayout.
///
/// This is the preferred method for drawing bubbles - it uses the layout's
/// coordinate transformation and clamping methods, ensuring consistency
/// with the canvas positioning.
#[cfg(feature = "hydrate")]
fn draw_bubble_with_layout(
    ctx: &web_sys::CanvasRenderingContext2d,
    bubble: &ActiveBubble,
    layout: &CanvasLayout,
    text_em_size: f64,
) {
    let text_scale = text_em_size * BASE_TEXT_SCALE;
    let max_bubble_width = 200.0 * text_scale;
    let padding = 8.0 * text_scale;
    let font_size = 12.0 * text_scale;
    let line_height = 16.0 * text_scale;
    let tail_size = 8.0 * text_scale;
    let border_radius = 8.0 * text_scale;
    let gap = 5.0 * text_scale;

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

    // Use italic font for whispers
    let font_style = if bubble.message.is_whisper {
        "italic "
    } else {
        ""
    };

    // Measure and wrap text
    ctx.set_font(&format!("{}{}px sans-serif", font_style, 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| ctx.measure_text(line).map(|m| m.width()).unwrap_or(0.0))
        .fold(0.0_f64, |a, b| a.max(b))
        + padding * 2.0;
    let bubble_width = bubble_width.max(60.0 * text_scale);
    let bubble_height = (lines.len() as f64) * line_height + padding * 2.0;

    // Get content center from layout
    let content_center_x = layout.content_center_x();

    // Calculate initial bubble X (centered on content)
    let initial_bubble_x = content_center_x - bubble_width / 2.0;

    // Use layout's clamping method - this handles coordinate transformation correctly
    let bubble_x = layout.clamp_bubble_x(initial_bubble_x, bubble_width);

    // Calculate tail center - point toward content center but stay within bubble bounds
    let tail_center_x = content_center_x
        .max(bubble_x + tail_size + border_radius)
        .min(bubble_x + bubble_width - tail_size - border_radius);

    // Calculate vertical position based on bubble position
    let bubble_y = match layout.bubble_position {
        BubblePosition::Above => {
            // Position vertically closer to content when top rows are empty
            let adjusted_top_y = layout.avatar_top_y() + layout.content_top_adjustment();
            adjusted_top_y - bubble_height - tail_size - gap
        }
        BubblePosition::Below => {
            // Position below avatar with gap for tail
            layout.avatar_bottom_y() + tail_size + gap
        }
    };

    // Draw bubble background
    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 pointing to content center
    ctx.begin_path();
    match layout.bubble_position {
        BubblePosition::Above => {
            // Tail points DOWN toward avatar
            ctx.move_to(tail_center_x - tail_size, bubble_y + bubble_height);
            ctx.line_to(tail_center_x, bubble_y + bubble_height + tail_size);
            ctx.line_to(tail_center_x + tail_size, bubble_y + bubble_height);
        }
        BubblePosition::Below => {
            // Tail points UP toward avatar
            ctx.move_to(tail_center_x - tail_size, bubble_y);
            ctx.line_to(tail_center_x, bubble_y - tail_size);
            ctx.line_to(tail_center_x + tail_size, bubble_y);
        }
    }
    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_font(&format!("{}{}px sans-serif", font_style, font_size));
    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| 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
    if lines.len() > 4 {
        lines.truncate(3);
        if let Some(last) = lines.last_mut() {
            last.push_str("...");
        }
    }

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

    lines
}

/// Test if a click at the given client coordinates hits a non-transparent pixel.
///
/// Returns true if the alpha channel at the clicked pixel is > 0.
/// This enables pixel-perfect hit detection on avatar canvases.
#[cfg(feature = "hydrate")]
pub fn hit_test_canvas(canvas: &web_sys::HtmlCanvasElement, client_x: f64, client_y: f64) -> bool {
    use wasm_bindgen::JsCast;

    // Get the canvas bounding rect to transform client coords to canvas coords
    let rect = canvas.get_bounding_client_rect();

    // Calculate click position relative to the canvas element
    let relative_x = client_x - rect.left();
    let relative_y = client_y - rect.top();

    // Check if click is within canvas bounds
    if relative_x < 0.0
        || relative_y < 0.0
        || relative_x >= rect.width()
        || relative_y >= rect.height()
    {
        return false;
    }

    // Transform to canvas pixel coordinates (accounting for CSS scaling)
    let canvas_width = canvas.width() as f64;
    let canvas_height = canvas.height() as f64;

    // Avoid division by zero
    if rect.width() == 0.0 || rect.height() == 0.0 {
        return false;
    }

    let scale_x = canvas_width / rect.width();
    let scale_y = canvas_height / rect.height();

    let pixel_x = (relative_x * scale_x) as f64;
    let pixel_y = (relative_y * scale_y) as f64;

    // Get the 2D context and read the pixel data using JavaScript interop
    if let Ok(Some(ctx)) = canvas.get_context("2d") {
        let ctx: web_sys::CanvasRenderingContext2d = ctx.dyn_into().unwrap();

        // Use web_sys::CanvasRenderingContext2d::get_image_data with proper error handling
        match ctx.get_image_data(pixel_x, pixel_y, 1.0, 1.0) {
            Ok(image_data) => {
                // Get the pixel data as Clamped<Vec<u8>>
                let data = image_data.data();
                // Alpha channel is the 4th value (index 3)
                if data.len() >= 4 {
                    return data[3] > 0;
                }
            }
            Err(_) => {
                // Security error or other issue with getImageData - assume no hit
                return false;
            }
        }
    }

    false
}

/// 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.quadratic_curve_to(x + width, y, x + width, y + radius);
    ctx.line_to(x + width, y + height - radius);
    ctx.quadratic_curve_to(x + width, y + height, x + width - radius, y + height);
    ctx.line_to(x + radius, y + height);
    ctx.quadratic_curve_to(x, y + height, x, y + height - radius);
    ctx.line_to(x, y + radius);
    ctx.quadratic_curve_to(x, y, x + radius, y);
    ctx.close_path();
}
