//! Stage 8 example: a game HUD drawn on top of a live 3D scene.
//!
//! Run with:
//!   cargo run -p oxide-examples --bin ui_hud
//!
//! Composites two Stage-8 ingredients in one frame:
//!
//! - The Stage-4 [`ForwardPass`] renders a spinning cube + sphere + ground
//!   plane (the same scene as `hello_mesh`).
//! - A screen-space [`UiOverlayPass`] then draws a HUD *on top* of it. Both
//!   passes load (never clear) the surface the window runner already cleared,
//!   so the overlay composites over the 3D image with alpha blending.
//!
//! HUD layout (each corner is an [`UiAnchor`]-pinned widget):
//!
//! - **Top-left**  — `HP: NN`, oscillating down then back up; turns red when low.
//! - **Top-right** — `Ammo: NN`, counting down as if firing, reloading at 0.
//! - **Bottom-left** — a 100×100 minimap stand-in with a dot orbiting in sync
//!   with the camera.
//! - **Centre** — a crosshair (two thin bars) — the "target indicator".
//!
//! The animated digits exist to demonstrate the glyph atlas's dirty-flag
//! caching: once each of `0`–`9` (plus the static label text) has been
//! rasterized once, the atlas stops growing and every later frame is a 100%
//! cache hit. The example logs each atlas growth and the moment it reaches
//! steady state. Esc quits.

#![deny(warnings)]

use oxide_engine::math::Quat;
use oxide_engine::prelude::*;
use oxide_engine::ui::text::{common_system_font_paths, Font};
use oxide_engine::window::event::{ElementState, Key, NamedKey, WindowEvent};
use oxide_engine::window::RenderCtx;

/// Frames the atlas must stay the same size before we declare steady state.
const STEADY_FRAMES: u32 = 60;

struct UiHud {
    /// Drives both the camera orbit and the minimap dot.
    angle: f32,
    /// Seconds elapsed — drives the HP / Ammo animations.
    clock: f32,
    theme: UiTheme,
    font_descriptor: UiFontRef,
    gpu: Option<GpuState>,

    // --- atlas diagnostics ---
    /// Glyph count observed on the previous frame; a change means the atlas grew.
    prev_glyph_count: usize,
    /// Consecutive frames the glyph count has held steady.
    steady_frames: u32,
    /// Set once we have logged the steady-state message, so it logs only once.
    steady_logged: bool,
}

struct GpuState {
    /// Forward 3D pass + its depth buffer, wrapped in a pipeline.
    pipeline: RenderPipeline,
    ui_pass: UiOverlayPass,
    cube: GpuMesh,
    sphere: GpuMesh,
    plane: GpuMesh,
}

impl Default for UiHud {
    fn default() -> Self {
        Self {
            angle: 0.0,
            clock: 0.0,
            theme: build_theme(),
            font_descriptor: UiFontRef::regular("System"),
            gpu: None,
            prev_glyph_count: 0,
            steady_frames: 0,
            steady_logged: false,
        }
    }
}

fn build_theme() -> UiTheme {
    let descriptor = UiFontRef::regular("System");
    UiTheme::new()
        .with_default(UiVisualStyle {
            foreground: Some(Color::WHITE),
            font: Some(descriptor.clone()),
            font_size: Some(20.0),
            ..UiVisualStyle::EMPTY
        })
        // Semi-opaque chips behind the text so it stays legible over any part
        // of the 3D scene.
        .with_style(
            "chip",
            UiVisualStyle {
                background: Some(Color::rgba(0.05, 0.06, 0.08, 0.55)),
                ..UiVisualStyle::EMPTY
            },
        )
        .with_style(
            "minimap",
            UiVisualStyle {
                background: Some(Color::rgba(0.05, 0.07, 0.10, 0.65)),
                ..UiVisualStyle::EMPTY
            },
        )
        .with_style(
            "minimap-dot",
            UiVisualStyle {
                background: Some(Color::rgb(0.30, 0.85, 0.45)),
                ..UiVisualStyle::EMPTY
            },
        )
        .with_style(
            "crosshair",
            UiVisualStyle {
                background: Some(Color::rgba(0.95, 0.95, 0.95, 0.85)),
                ..UiVisualStyle::EMPTY
            },
        )
}

/// A corner-pinned label chip of fixed size, holding a single line of text.
fn corner_chip(text: &str, low: bool, anchor: UiAnchor) -> Widget {
    let mut leaf = Widget::leaf(Vec2::ZERO)
        .with_text(text)
        .with_theme_style("chip")
        .with_style(UiLayoutStyle {
            anchor,
            padding: UiInsets::symmetric(12.0, 8.0),
            align_horizontal: UiAlign::Start,
            align_vertical: UiAlign::Center,
            ..Default::default()
        });
    if low {
        leaf = leaf.with_visual(UiVisualStyle {
            foreground: Some(Color::rgb(0.95, 0.30, 0.25)),
            ..UiVisualStyle::EMPTY
        });
    }
    leaf
}

/// The full HUD document for the given gameplay values.
///
/// `margin` insets every corner from the screen edge. `dot` is the minimap
/// dot's position as a 0..1 fraction of the minimap panel.
fn build_hud(hp: i32, ammo: i32, dot: Vec2) -> Widget {
    const M: f32 = 16.0; // edge margin
    const CHIP_W: f32 = 150.0;
    const CHIP_H: f32 = 36.0;
    const MAP: f32 = 100.0;
    const CROSS: f32 = 22.0;
    const BAR: f32 = 2.0;

    // Top-left HP chip pinned to the top-left corner.
    let hp_chip = corner_chip(
        &format!("HP: {hp}"),
        hp < 30,
        UiAnchor::TOP_LEFT.with_offsets(Vec2::new(M, M), Vec2::new(M + CHIP_W, M + CHIP_H)),
    );

    // Top-right Ammo chip pinned to the top-right corner.
    let ammo_chip = corner_chip(
        &format!("Ammo: {ammo}"),
        ammo == 0,
        UiAnchor::TOP_RIGHT.with_offsets(Vec2::new(-M - CHIP_W, M), Vec2::new(-M, M + CHIP_H)),
    );

    // Bottom-left minimap: a solid panel with a single orbiting dot.
    let minimap = Widget::anchor()
        .with_theme_style("minimap")
        .with_style(UiLayoutStyle {
            anchor: UiAnchor::BOTTOM_LEFT
                .with_offsets(Vec2::new(M, -M - MAP), Vec2::new(M + MAP, -M)),
            ..Default::default()
        })
        .with_child(
            Widget::leaf(Vec2::ZERO)
                .with_theme_style("minimap-dot")
                .with_style(UiLayoutStyle {
                    anchor: UiAnchor::between(dot, dot)
                        .with_offsets(Vec2::splat(-4.0), Vec2::splat(4.0)),
                    ..Default::default()
                }),
        );

    // Centre crosshair: a horizontal + vertical bar crossing at screen centre.
    let crosshair = Widget::anchor()
        .with_style(UiLayoutStyle {
            anchor: UiAnchor::between(Vec2::splat(0.5), Vec2::splat(0.5))
                .with_offsets(Vec2::splat(-CROSS * 0.5), Vec2::splat(CROSS * 0.5)),
            ..Default::default()
        })
        .with_child(
            // Horizontal bar.
            Widget::leaf(Vec2::ZERO)
                .with_theme_style("crosshair")
                .with_style(UiLayoutStyle {
                    anchor: UiAnchor::between(Vec2::new(0.0, 0.5), Vec2::new(1.0, 0.5))
                        .with_offsets(Vec2::new(0.0, -BAR * 0.5), Vec2::new(0.0, BAR * 0.5)),
                    ..Default::default()
                }),
        )
        .with_child(
            // Vertical bar.
            Widget::leaf(Vec2::ZERO)
                .with_theme_style("crosshair")
                .with_style(UiLayoutStyle {
                    anchor: UiAnchor::between(Vec2::new(0.5, 0.0), Vec2::new(0.5, 1.0))
                        .with_offsets(Vec2::new(-BAR * 0.5, 0.0), Vec2::new(BAR * 0.5, 0.0)),
                    ..Default::default()
                }),
        );

    Widget::anchor()
        .with_style(UiLayoutStyle {
            width: UiSizing::Grow(1.0),
            height: UiSizing::Grow(1.0),
            ..Default::default()
        })
        .with_child(hp_chip)
        .with_child(ammo_chip)
        .with_child(minimap)
        .with_child(crosshair)
}

impl UiHud {
    /// Health oscillates 10..100 (down then up), as if taking damage and healing.
    fn hp(&self) -> i32 {
        (55.0 + 45.0 * (self.clock * 0.8).sin()).round() as i32
    }

    /// Ammo counts 30 → 0 (one round every 0.25 s), reloading back to 30.
    fn ammo(&self) -> i32 {
        let fired = (self.clock / 0.25) as i32 % 31;
        30 - fired
    }

    /// Minimap dot position as a 0..1 fraction, orbiting with the camera.
    fn minimap_dot(&self) -> Vec2 {
        Vec2::new(0.5 + 0.34 * self.angle.cos(), 0.5 + 0.34 * self.angle.sin())
    }

    fn hud_document(&self) -> Widget {
        build_hud(self.hp(), self.ammo(), self.minimap_dot())
    }
}

impl WindowApp for UiHud {
    fn init(&mut self, ctx: &mut AppCtx<'_>) {
        ctx.set_clear_color(Color::rgb(0.05, 0.06, 0.09));
        log::info!("ui_hud: 3D scene + HUD overlay (Esc quits)");
    }

    fn event(&mut self, ctx: &mut AppCtx<'_>, event: &WindowEvent) {
        if let WindowEvent::KeyboardInput { event: key, .. } = event {
            if key.state == ElementState::Pressed && key.logical_key == Key::Named(NamedKey::Escape)
            {
                ctx.request_exit();
            }
        }
    }

    fn update(&mut self, ctx: &mut AppCtx<'_>) {
        self.angle += ctx.dt * 0.6;
        self.clock += ctx.dt;
    }

    fn render(&mut self, ctx: &RenderCtx<'_>) {
        let device = ctx.gpu.device();
        let queue = ctx.gpu.queue();

        // Lazy-init GPU resources once the surface format is known.
        if self.gpu.is_none() {
            let mut pipeline = RenderPipeline::new();
            pipeline.add_pass("forward", ForwardPass::new(device, ctx.surface_format));

            let mut ui_pass = UiOverlayPass::new(device, ctx.surface_format);
            match common_system_font_paths()
                .iter()
                .find_map(|p| Font::from_path(p).ok())
            {
                Some(f) => {
                    ui_pass
                        .fonts_mut()
                        .insert_with_descriptor(self.font_descriptor.clone(), f);
                }
                None => log::error!(
                    "No system sans-serif font found in any of {:?}. Install \
                     'liberation-fonts' or 'dejavu-sans' and re-run.",
                    common_system_font_paths()
                ),
            }

            self.gpu = Some(GpuState {
                pipeline,
                ui_pass,
                cube: Mesh::cube().upload(device, "cube"),
                sphere: Mesh::uv_sphere(0.8, 32, 16).upload(device, "sphere"),
                plane: Mesh::plane(12.0).upload(device, "plane"),
            });
        }

        let (w, h) = ctx.size;

        // Build the HUD (needs &self) and paint it (needs the pass's fonts via
        // an immutable borrow) before taking the &mut borrow on `self.gpu` —
        // the same ordering dance `ui_menu` uses.
        let viewport =
            oxide_engine::math::Rect::from_min_size(Vec2::ZERO, Vec2::new(w as f32, h as f32));
        let document = self.hud_document();
        let tree = ui_layout(&document, viewport, 1.0);
        let painted = ui_paint(
            &document,
            &tree,
            &self.theme,
            self.gpu.as_ref().unwrap().ui_pass.fonts(),
            1.0,
        );

        let gpu = self.gpu.as_mut().unwrap();
        gpu.ui_pass
            .set_batches(vec![UiBatch::screen_space(painted, (w, h))]);

        // Orbit the camera around the scene (same as hello_mesh).
        let eye = Vec3::new(4.0 * self.angle.cos(), 2.6, 4.0 * self.angle.sin());
        let view = Transform::looking_at(eye, Vec3::new(0.0, 0.2, 0.0), Vec3::Y);
        let camera = Camera::default();

        let objects = [
            RenderObject {
                mesh: &gpu.plane,
                material: Material::diffuse(Color::rgb(0.25, 0.27, 0.30)),
                transform: Transform::from_translation(Vec3::new(0.0, -1.0, 0.0)),
            },
            RenderObject {
                mesh: &gpu.cube,
                material: Material::diffuse(Color::rgb(0.85, 0.20, 0.15)),
                transform: Transform::from_trs(
                    Vec3::new(-1.3, 0.0, 0.0),
                    Quat::from_euler(
                        oxide_engine::math::EulerRot::YXZ,
                        self.angle * 2.0,
                        0.4,
                        0.0,
                    ),
                    Vec3::ONE,
                ),
            },
            RenderObject {
                mesh: &gpu.sphere,
                material: Material::metal(Color::rgb(0.55, 0.65, 0.95), 0.35),
                transform: Transform::from_translation(Vec3::new(1.3, 0.2, 0.0)),
            },
        ];

        let lighting = Lighting::default();
        let mut frame = FrameContext {
            device,
            queue,
            color: ctx.view,
            size: ctx.size,
            viewport_rect: None,
            clear_color: Color::rgb(0.05, 0.06, 0.09),
            camera: &camera,
            view_transform: &view,
            lighting: &lighting,
            objects: &objects,
        };

        // 3D first, then the HUD overlay on top — both load the surface the
        // window runner already cleared.
        gpu.pipeline.render(&mut frame);
        gpu.ui_pass.run(&mut frame);

        let glyph_count = gpu.ui_pass.atlas_glyph_count();
        // End the &mut borrow on self.gpu before touching the diagnostics fields.
        let _ = gpu;
        self.report_atlas(glyph_count);
    }
}

impl UiHud {
    /// Log atlas growth and the moment it reaches steady state, proving the
    /// HUD's animated digits become 100% cache hits.
    fn report_atlas(&mut self, glyph_count: usize) {
        if glyph_count != self.prev_glyph_count {
            log::info!(
                "ui_hud: glyph atlas grew to {glyph_count} glyphs (new digit/char rasterized)"
            );
            self.prev_glyph_count = glyph_count;
            self.steady_frames = 0;
            self.steady_logged = false;
            return;
        }
        self.steady_frames += 1;
        if self.steady_frames == STEADY_FRAMES && !self.steady_logged {
            log::info!(
                "ui_hud: atlas steady at {glyph_count} glyphs for {STEADY_FRAMES} frames — \
                 every subsequent frame is a 100% cache hit (no rasterize, no re-upload)"
            );
            self.steady_logged = true;
        }
    }
}

fn main() -> anyhow::Result<()> {
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
    let config = WindowConfig {
        title: "Oxide — ui_hud".to_string(),
        width: 960,
        height: 540,
        ..Default::default()
    };
    run(config, UiHud::default())
}
