// Server-side window decorations rendered through ltk's runtime-free
// `UiSurface`. Draws a title bar with the standard minimize / maximize
// (or restore) / close controls, styled from the active ltk theme so
// forge automatically follows the compositor-wide light/dark preference
// and the tokens under `[window_controls]` in `theme.toml`.
//
// The rendering path is software: ltk lays out and rasterises the
// title bar into a tiny-skia pixmap, we copy the pixels out and upload
// them as a GL texture for smithay's render loop. A per-bar cache
// skips re-rasterisation when nothing that would change the pixels has
// changed. The hit-test is geometric (not routed through ltk's
// widget_rects) because it runs on every pointer event and we want
// zero heap work on that path.

use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Point, Rectangle};

use ltk::core::{RenderOptions, UiSurface};
use ltk::{
    container, row, spacer, text, window_controls, Color, Corners, Element, TextAlign,
    WindowButtonKind,
};

use crate::drawing::{ForgeElement, RenderTexture, Texture};
use crate::output::Canvas;
use crate::text::TextRenderer;

// Title-bar geometry — logical pixels.
const BAR_HEIGHT: i32 = 40;
const PAD_H: i32 = 6;
// Rounded top corner radius for the title-bar strip, matching the figma.
// Bottom corners stay square because the bar abuts the client surface.
const BAR_RADIUS: f32 = 10.0;
// These must stay in sync with ltk's window_button theme SIZE (36px)
// and window_controls row spacing (4px) so the geometric hit-test
// matches what was laid out by ltk.
const BUTTON_SIZE: i32 = 36;
const BUTTON_SPACING: i32 = 4;

// Action emitted by a window-controls button.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SsdMsg {
    Close,
    Maximize,
    Minimize,
}

/// One shared ltk surface + cached GL texture for every visible SSD
/// title bar in the active frame. The cache is invalidated on any
/// pixel-affecting change (title, width, scale, maximized state, theme
/// generation) so we only rebuild when something actually changed.
pub struct SsdDecoration {
    ui: UiSurface<SsdMsg>,
    cached_title: String,
    cached_scale: f64,
    cached_size_px: (u32, u32),
    cached_max: bool,
    cached_theme_id: String,
    /// Index of the WindowButton currently painted with `hovered=true`.
    /// Computed by hit-testing the pointer against the **previous**
    /// frame's layout — so on size/theme changes the hover state lags
    /// by one frame, which is fine because the hover transition itself
    /// triggers a rebuild on the next motion event.
    cached_hover_idx: Option<usize>,
    texture: Option<RenderTexture>,
}

impl std::fmt::Debug for SsdDecoration {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SsdDecoration").finish()
    }
}

impl SsdDecoration {
    pub fn new() -> Self {
        Self {
            ui: UiSurface::new(1, 1),
            cached_title: String::new(),
            cached_scale: 0.0,
            cached_size_px: (0, 0),
            cached_max: false,
            cached_theme_id: String::new(),
            cached_hover_idx: None,
            texture: None,
        }
    }

    pub fn bar_height() -> i32 {
        BAR_HEIGHT
    }

    pub fn bar_radius() -> f32 {
        BAR_RADIUS
    }

    /// Draw the decoration for one window into the ForgeElement buffer.
    ///
    /// `pointer_logical` is the global compositor-space pointer position
    /// (or `None` when nothing is tracking a pointer for this output).
    /// The hover-state of the window-control buttons is derived from it
    /// by hit-testing against ltk's last laid-out widget rects — see the
    /// `cached_hover_idx` doc-comment on the struct for the lag note.
    pub fn render(
        &mut self,
        textures: &mut Vec<ForgeElement>,
        canvas: &Canvas,
        renderer: &mut GlesRenderer,
        _text_renderer: &mut TextRenderer,
        title: &str,
        window_rect: Rectangle<i32, Logical>,
        maximized: bool,
        pointer_logical: Option<Point<f64, Logical>>,
    ) {
        let scale = canvas.scale();
        let wx = window_rect.loc.x;
        let wy = window_rect.loc.y;
        let ww = window_rect.size.w.max(1);
        let bar_y = wy - BAR_HEIGHT;

        let bar_w_phys = ((ww as f64) * scale).round().max(1.0) as u32;
        let bar_h_phys = ((BAR_HEIGHT as f64) * scale).round().max(1.0) as u32;

        // Translate the global logical pointer into bar-local physical
        // pixels (ltk's coordinate space), and ask ltk's last layout
        // which widget index sits there. `UiSurface::hit_test` reads
        // the cached `widget_rects` from the previous render of this
        // same UiSurface, which is fine because forge re-renders on
        // every hover transition (see `Windows::pointer_moved`) — a
        // 1-frame stale layout is acceptable.
        let hover_idx = pointer_logical.and_then(|p| {
            let on_bar = (p.x as i32) >= wx
                && (p.x as i32) < wx + ww
                && (p.y as i32) >= bar_y
                && (p.y as i32) < wy;
            if !on_bar {
                return None;
            }
            let lx = ((p.x - wx as f64) * scale) as f32;
            let ly = ((p.y - bar_y as f64) * scale) as f32;
            self.ui.hit_test(ltk::Point { x: lx, y: ly })
        });

        let theme_id = format!(
            "{}:{:?}",
            ltk::theme::active_theme_id(),
            ltk::theme::active_mode(),
        );

        let needs_rebuild = title != self.cached_title
            || (scale - self.cached_scale).abs() > 0.01
            || (bar_w_phys, bar_h_phys) != self.cached_size_px
            || maximized != self.cached_max
            || theme_id != self.cached_theme_id
            || hover_idx != self.cached_hover_idx;

        if needs_rebuild {
            // `set_hovered` writes into UiSurface state; the next
            // `ui.render` call inside `rebuild_texture` picks it up
            // and dispatches `hovered=true` to the matching widget's
            // `draw` method.
            self.ui.set_hovered(hover_idx);
            self.rebuild_texture(
                renderer, scale, bar_w_phys, bar_h_phys, title, maximized, &theme_id,
            );
            self.cached_hover_idx = hover_idx;
        }

        if let Some(tex) = &self.texture {
            let bar_logical =
                Rectangle::<i32, Logical>::new((wx, bar_y).into(), (ww, BAR_HEIGHT).into());
            let bar_phys = bar_logical.to_physical_precise_round(scale);
            ForgeElement::add_element(textures, tex.clone(), bar_phys.loc, bar_phys, None, scale);
        }
    }

    fn rebuild_texture(
        &mut self,
        renderer: &mut GlesRenderer,
        scale: f64,
        w_phys: u32,
        h_phys: u32,
        title: &str,
        maximized: bool,
        theme_id: &str,
    ) {
        if (w_phys, h_phys) != self.cached_size_px {
            self.ui.resize(w_phys, h_phys);
            self.cached_size_px = (w_phys, h_phys);
        }
        self.ui.set_dpi_scale(scale as f32);

        let palette = ltk::theme_palette();
        let title_color = palette.text_primary;
        let bg_color = ltk::theme::window_controls().bar_bg;

        let tree = Self::build_tree(title, maximized, title_color, bg_color);

        // The container in `build_tree` paints the rounded-top bar fill, so
        // the underlying canvas must stay transparent — otherwise the
        // background would also tile into the corner cut-outs and the
        // rounding would be invisible.
        let opts = RenderOptions::full_canvas(w_phys, h_phys).background(Color::TRANSPARENT);
        let _ = self.ui.render(&tree, opts);

        // Copy the CPU pixmap out into an owned RGBA buffer and upload.
        let mut rgba = vec![0u8; (w_phys as usize) * (h_phys as usize) * 4];
        if let ltk::core::Canvas::Software(sc) = self.ui.canvas() {
            sc.write_to_wayland_buf(&mut rgba, false);
        }

        let texture =
            Texture::from_buffer(renderer, scale, &rgba, w_phys as i32, h_phys as i32, false);
        self.texture = Some(RenderTexture::new(texture));

        self.cached_title = title.to_string();
        self.cached_scale = scale;
        self.cached_max = maximized;
        self.cached_theme_id = theme_id.to_string();
    }

    fn build_tree(
        title: &str,
        maximized: bool,
        text_color: Color,
        bar_bg: Color,
    ) -> Element<SsdMsg> {
        let max_kind = if maximized {
            WindowButtonKind::Restore
        } else {
            WindowButtonKind::Maximize
        };

        let bar = row::<SsdMsg>()
            .padding(PAD_H as f32)
            .spacing(8.0)
            .push(
                text(title)
                    .size(14.0)
                    .color(text_color)
                    .align(TextAlign::Left),
            )
            .push(spacer())
            .push(window_controls(
                Some(SsdMsg::Minimize),
                max_kind,
                Some(SsdMsg::Maximize),
                Some(SsdMsg::Close),
            ));

        container(bar)
            .background(bar_bg)
            .radius(Corners::top(BAR_RADIUS))
            .into()
    }
}

/// Hit-test a logical-space point against a window's SSD. Matches the
/// button geometry produced by `build_tree` above: three 36 px buttons
/// with 4 px gaps, right-aligned with 6 px padding.
pub fn hit_test(
    window_rect: Rectangle<i32, Logical>,
    point: Point<f64, Logical>,
    bar_height: i32,
) -> Option<SsdAction> {
    if bar_height <= 0 {
        return None;
    }

    let wx = window_rect.loc.x;
    let wy = window_rect.loc.y;
    let ww = window_rect.size.w;
    let bar_y = wy - bar_height;
    let px = point.x as i32;
    let py = point.y as i32;

    if py < bar_y || py >= wy || px < wx || px >= wx + ww {
        return None;
    }

    let btn_y = bar_y + (bar_height - BUTTON_SIZE) / 2;
    if py >= btn_y && py < btn_y + BUTTON_SIZE {
        let close_right = wx + ww - PAD_H;
        let close_left = close_right - BUTTON_SIZE;
        let max_right = close_left - BUTTON_SPACING;
        let max_left = max_right - BUTTON_SIZE;
        let min_right = max_left - BUTTON_SPACING;
        let min_left = min_right - BUTTON_SIZE;

        if px >= close_left && px < close_right {
            return Some(SsdAction::Close);
        }
        if px >= max_left && px < max_right {
            return Some(SsdAction::Maximize);
        }
        if px >= min_left && px < min_right {
            return Some(SsdAction::Minimize);
        }
    }

    Some(SsdAction::Move)
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SsdAction {
    Close,
    Maximize,
    Minimize,
    Move,
}

/// Thickness of the resize grab band, measured in logical pixels outside
/// the visible window frame.
pub const RESIZE_GRAB: i32 = 8;

/// Size of the square corner grab zone (per side).
pub const RESIZE_CORNER: i32 = 16;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResizeEdge {
    N,
    S,
    E,
    W,
    NE,
    NW,
    SE,
    SW,
}

/// Returns `true` when `point` falls inside the window's visible footprint —
/// the client body plus the SSD title bar. Used by resize hit-testing to
/// decide whether a pointer position is blocked by a higher-stacked window
/// (and therefore must not arm a resize on anything below it).
pub fn body_hit_test(
    window_rect: Rectangle<i32, Logical>,
    point: Point<f64, Logical>,
    bar_height: i32,
) -> bool {
    let fx = window_rect.loc.x;
    let fy = window_rect.loc.y - bar_height;
    let fw = window_rect.size.w;
    let fh = window_rect.size.h + bar_height;
    let px = point.x as i32;
    let py = point.y as i32;
    px >= fx && px < fx + fw && py >= fy && py < fy + fh
}

/// Returns `Some(edge)` when the pointer is inside the outer resize grab
/// band around a window frame (including the SSD title bar). The outer
/// band is `RESIZE_GRAB` pixels wide; corners are `RESIZE_CORNER` squares
/// at each outer corner of that band.
pub fn resize_hit_test(
    window_rect: Rectangle<i32, Logical>,
    point: Point<f64, Logical>,
    bar_height: i32,
) -> Option<ResizeEdge> {
    let wx = window_rect.loc.x;
    let wy = window_rect.loc.y;
    let ww = window_rect.size.w;
    let wh = window_rect.size.h;

    let fx = wx;
    let fy = wy - bar_height;
    let fw = ww;
    let fh = wh + bar_height;

    let px = point.x as i32;
    let py = point.y as i32;

    let outer_x0 = fx - RESIZE_GRAB;
    let outer_y0 = fy - RESIZE_GRAB;
    let outer_x1 = fx + fw + RESIZE_GRAB;
    let outer_y1 = fy + fh + RESIZE_GRAB;

    if px < outer_x0 || px >= outer_x1 || py < outer_y0 || py >= outer_y1 {
        return None;
    }
    if px >= fx && px < fx + fw && py >= fy && py < fy + fh {
        return None;
    }

    let on_left = px < fx;
    let on_right = px >= fx + fw;
    let on_top = py < fy;
    let on_bottom = py >= fy + fh;

    let near_corner = |horiz, vert| {
        let near_h = match horiz {
            Some(true) => px < fx + RESIZE_CORNER,
            Some(false) => px >= fx + fw - RESIZE_CORNER,
            None => false,
        };
        let near_v = match vert {
            Some(true) => py < fy + RESIZE_CORNER,
            Some(false) => py >= fy + fh - RESIZE_CORNER,
            None => false,
        };
        near_h || near_v
    };

    Some(match (on_top, on_bottom, on_left, on_right) {
        (true, _, true, _) => ResizeEdge::NW,
        (true, _, _, true) => ResizeEdge::NE,
        (_, true, true, _) => ResizeEdge::SW,
        (_, true, _, true) => ResizeEdge::SE,
        (true, _, _, _) if near_corner(Some(true), None) => ResizeEdge::NW,
        (true, _, _, _) if near_corner(Some(false), None) => ResizeEdge::NE,
        (true, _, _, _) => ResizeEdge::N,
        (_, true, _, _) if near_corner(Some(true), None) => ResizeEdge::SW,
        (_, true, _, _) if near_corner(Some(false), None) => ResizeEdge::SE,
        (_, true, _, _) => ResizeEdge::S,
        (_, _, true, _) if near_corner(None, Some(true)) => ResizeEdge::NW,
        (_, _, true, _) if near_corner(None, Some(false)) => ResizeEdge::SW,
        (_, _, true, _) => ResizeEdge::W,
        (_, _, _, true) if near_corner(None, Some(true)) => ResizeEdge::NE,
        (_, _, _, true) if near_corner(None, Some(false)) => ResizeEdge::SE,
        (_, _, _, true) => ResizeEdge::E,
        _ => return None,
    })
}
