//! World-space UI panels — a [`Widget`] tree rendered onto a quad in 3D.
//!
//! Stage 8's UI is "in-game UI" — what the exported game uses to draw
//! menus and HUDs. Most of the time those are **screen-space**: pixel-
//! anchored, drawn over the 3D scene by piece 4a's
//! [`UiOverlayPass`](super::super::render::UiOverlayPass) using an
//! orthographic projection. A [`UiPanel`] is the world-space alternative —
//! the same `Widget` tree, but laid out on a flat panel that sits in the
//! 3D world at some [`Transform`].
//!
//! This is what gives game projects:
//!
//! - **Diegetic UI** — terminal screens, signs, dashboards, control
//!   panels — the player sees them rendered inside the world rather than
//!   pasted over it.
//! - **Editor previews** — the UI canvas (piece 9) can drop a panel into
//!   the scene to preview a document at scale, on the same hardware path
//!   the shipped game uses.
//! - **VR / room-scale UI** later — once Stage-13 head-mounted display
//!   support lands, world-space panels are the only sensible way to
//!   present interactive UI.
//!
//! # How the math works
//!
//! A panel describes itself in two coordinate spaces:
//!
//! - **Pixel space** — where the layout algorithm operates. `pixel_size`
//!   is the resolution the `Widget` tree is laid out at (e.g.,
//!   `Vec2::new(1024.0, 768.0)`). Glyphs are rasterized at this scale.
//! - **World space** — where the panel sits in 3D. `world_size` is its
//!   physical size in world units (e.g., `Vec2::new(2.0, 1.5)` for a
//!   2 m × 1.5 m monitor).
//!
//! The piece-4 vertex format carries 2D pixel-space positions. To draw
//! that on a 3D quad, [`UiBatch::world_space`](super::super::render::UiBatch::world_space)
//! builds a single MVP that composes:
//!
//! ```text
//! mvp = camera_view_projection
//!     * panel_transform                              // world placement
//!     * scale(world.x / pixel.x, -world.y / pixel.y, 1)  // pixels → world (and flip y, since UI is y-down)
//!     * translate(-pixel.x / 2, -pixel.y / 2, 0)     // recentre pixel origin
//! ```
//!
//! The same `UiOverlayPass` then draws the panel using the same shader
//! and the same R8 atlas — the only thing that distinguishes a screen-
//! space batch from a world-space one is which constructor built it.
//!
//! # Overlay semantics for piece 4b
//!
//! World-space panels in piece 4b render as **overlays**: no depth test,
//! no depth write — they draw on top of whatever's already in the color
//! target. That keeps the implementation simple and matches the common
//! "always-visible" use case (player nameplates, mission markers,
//! editor canvas previews).
//!
//! A future depth-aware mode (where a panel behind a wall is properly
//! hidden) is in [`PLAN.md`](../../../../PLAN.md)'s Stage-8 backlog and
//! slots in by attaching a depth attachment to a second pass of the
//! same pipeline.

use glam::Mat4;
use serde::{Deserialize, Serialize};

use super::layout::layout;
use super::paint::paint;
use super::text::FontStore;
use super::theme::Theme;
use super::widget::Widget;
use crate::math::{Rect, Transform, Vec2};

/// A widget tree placed on a 3D quad.
///
/// `UiPanel` carries pure data: the document, its pixel resolution, and
/// its world size. The host owns the panel's [`Transform`] separately
/// (typically as an ECS component on the same entity), the active
/// [`Theme`], and the [`FontStore`] — all three are needed at render
/// time to build the panel's [`UiBatch`](super::super::render::UiBatch).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UiPanel {
    /// The UI document on this panel.
    pub root: Widget,
    /// Resolution to lay out the UI at, in logical pixels. Drives the
    /// pixel size of every glyph rasterization (so a higher
    /// `pixel_size.x` on the same `world_size.x` produces a crisper
    /// panel at a cost of more atlas memory).
    pub pixel_size: Vec2,
    /// Panel dimensions in world units. Together with `pixel_size` this
    /// gives the pixels-per-world-unit ratio the MVP uses.
    pub world_size: Vec2,
}

impl UiPanel {
    /// Build a panel with the given UI document and dimensions. Equivalent
    /// to the struct literal; kept as a function so the API can grow
    /// validation later without breaking callers.
    pub fn new(root: Widget, pixel_size: Vec2, world_size: Vec2) -> Self {
        Self {
            root,
            pixel_size,
            world_size,
        }
    }

    /// Convenience: lay out + paint this panel and build the
    /// [`UiBatch`](super::super::render::UiBatch) the
    /// [`UiOverlayPass`](super::super::render::UiOverlayPass) consumes.
    ///
    /// Returns `None` if the panel's `pixel_size` is non-positive — the
    /// caller didn't configure the panel and there's no meaningful
    /// rendering to do.
    pub fn build_batch(
        &self,
        theme: &Theme,
        fonts: &FontStore,
        panel_transform: &Transform,
        view_projection: Mat4,
    ) -> Option<super::super::render::UiBatch> {
        if self.pixel_size.x <= 0.0 || self.pixel_size.y <= 0.0 {
            return None;
        }
        let viewport = Rect::from_min_size(Vec2::ZERO, self.pixel_size);
        let tree = layout(&self.root, viewport, 1.0);
        let painted = paint(&self.root, &tree, theme, fonts, 1.0);
        Some(super::super::render::UiBatch::world_space(
            painted,
            self.pixel_size,
            self.world_size,
            panel_transform,
            view_projection,
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::super::style::{LayoutStyle, Sizing};
    use super::super::visual::VisualStyle;
    use super::*;
    use crate::math::{Color, Vec3};

    #[test]
    fn build_batch_returns_none_on_zero_pixel_size() {
        let panel = UiPanel::new(Widget::default(), Vec2::ZERO, Vec2::new(2.0, 2.0));
        let theme = Theme::new();
        let fonts = FontStore::new();
        let result = panel.build_batch(&theme, &fonts, &Transform::default(), Mat4::IDENTITY);
        assert!(result.is_none());
    }

    #[test]
    fn build_batch_succeeds_with_valid_panel() {
        let root = Widget::default()
            .with_style(LayoutStyle {
                width: Sizing::Grow(1.0),
                height: Sizing::Grow(1.0),
                ..Default::default()
            })
            .with_visual(VisualStyle {
                background: Some(Color::RED),
                ..VisualStyle::EMPTY
            });
        let panel = UiPanel::new(root, Vec2::new(256.0, 128.0), Vec2::new(2.0, 1.0));
        let theme = Theme::new();
        let fonts = FontStore::new();
        let batch = panel
            .build_batch(&theme, &fonts, &Transform::default(), Mat4::IDENTITY)
            .expect("valid panel should build a batch");
        // The batch's painted frame matches the panel's pixel size and has
        // one Quad command (the red background).
        assert_eq!(batch.frame.size, Vec2::new(256.0, 128.0));
        assert_eq!(batch.frame.commands.len(), 1);
    }

    #[test]
    fn panel_round_trips_through_ron() {
        let panel = UiPanel::new(
            Widget::row().with_id("hud").with_visual(VisualStyle {
                background: Some(Color::WHITE),
                ..VisualStyle::EMPTY
            }),
            Vec2::new(1024.0, 768.0),
            Vec2::new(4.0, 3.0),
        );
        let text = ron::ser::to_string_pretty(&panel, ron::ser::PrettyConfig::default()).unwrap();
        let decoded: UiPanel = ron::de::from_str(&text).unwrap();
        assert_eq!(panel, decoded);
    }

    #[test]
    fn identity_mvp_keeps_pixel_origin_at_panel_centre() {
        // Sanity: with an identity view-projection and default panel
        // transform, a vertex at (0, 0) in pixel space lands at the top-
        // left of the panel in world space, which under our MVP becomes
        // (-world.x/2, +world.y/2, 0) (y-down → y-up).
        let panel = UiPanel::new(
            Widget::default()
                .with_style(LayoutStyle {
                    width: Sizing::Grow(1.0),
                    height: Sizing::Grow(1.0),
                    ..Default::default()
                })
                .with_visual(VisualStyle {
                    background: Some(Color::RED),
                    ..VisualStyle::EMPTY
                }),
            Vec2::new(2.0, 2.0),
            Vec2::new(2.0, 2.0),
        );
        let batch = panel
            .build_batch(
                &Theme::new(),
                &FontStore::new(),
                &Transform::default(),
                Mat4::IDENTITY,
            )
            .unwrap();
        // Apply the MVP to the pixel-space top-left (0, 0, 0, 1).
        let top_left = batch.mvp * Vec3::new(0.0, 0.0, 0.0).extend(1.0);
        assert!(
            (top_left.x - -1.0).abs() < 1e-5 && (top_left.y - 1.0).abs() < 1e-5,
            "top-left should map to (-1, 1) under identity MVP, got {top_left:?}"
        );
        // Bottom-right pixel maps to (+world.x/2, -world.y/2).
        let bottom_right = batch.mvp * Vec3::new(2.0, 2.0, 0.0).extend(1.0);
        assert!(
            (bottom_right.x - 1.0).abs() < 1e-5 && (bottom_right.y - -1.0).abs() < 1e-5,
            "bottom-right should map to (1, -1), got {bottom_right:?}"
        );
    }
}
