//! Glyph atlas — packs rasterized glyphs into one R8 alpha texture, caches
//! them by (font, glyph, size), and exposes UV regions the renderer draws as
//! textured quads.
//!
//! The atlas **is** the cache: every glyph is rasterized exactly once per
//! `(FontId, GlyphId, size_px)` triple and reused for the rest of the
//! process's lifetime. The performance discussion in the Stage-8 design
//! notes assumes this — a HUD that repaints the same characters every frame
//! never re-rasterizes after warm-up.
//!
//! # Packer choice
//!
//! Piece 3 uses a **shelf packer**: glyphs are arranged in horizontal rows
//! ("shelves") whose height is the height of the first glyph that opened the
//! shelf. Subsequent glyphs either fit horizontally on an existing shelf
//! (height ≤ shelf height) or start a new shelf below. This is the standard
//! choice for monotonically-growing glyph atlases — simple, deterministic,
//! near-optimal density for typically-uniform glyph heights, and easy to
//! grow (later: multi-page atlases) when full.
//!
//! Piece 3 does **not** evict. With a 1024×1024 R8 atlas the typical Western
//! UI uses a single-digit-percent fraction; CJK or many-size scenarios that
//! actually run out are handled by piece-4 follow-ups (multi-page atlases
//! or LRU per page).

use std::collections::HashMap;

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

use super::font::{FontId, FontStore, GlyphId};

/// Cache key for one rasterized glyph.
///
/// `size_px` is rounded to the nearest pixel before being used as the key —
/// distinct 23.4-pixel and 23.6-pixel renderings would otherwise produce
/// different atlas entries despite being visually indistinguishable.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GlyphKey {
    pub font: FontId,
    pub glyph: GlyphId,
    pub size_px: u16,
}

impl GlyphKey {
    /// Build a key, rounding `size_px` to the nearest pixel.
    pub fn new(font: FontId, glyph: GlyphId, size_px: f32) -> Self {
        Self {
            font,
            glyph,
            size_px: size_px.round().max(1.0) as u16,
        }
    }
}

/// One glyph's packed location inside the atlas plus the metrics the
/// renderer needs to position its quad on a baseline.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AtlasEntry {
    /// Top-left UV (normalized to `[0, 1]`).
    pub uv_min: Vec2,
    /// Bottom-right UV.
    pub uv_max: Vec2,
    /// Width / height of the packed region in **pixels**, so the renderer
    /// can size the quad without re-querying the atlas dimensions.
    pub size_px: Vec2,
    /// Offset from the glyph's pen position to the top-left of the quad,
    /// in pixels (`bearing.x` left/right, `bearing.y` from the **baseline**;
    /// negative `y` means the glyph extends above the baseline).
    pub bearing: Vec2,
    /// Horizontal advance for the next glyph at this size.
    pub advance_px: f32,
}

/// CPU-side glyph atlas — owns the alpha buffer, the packer state, and the
/// `(GlyphKey -> AtlasEntry)` cache.
///
/// A piece-4 GPU follow-up will upload [`pixels`](Self::pixels) into a
/// single R8 texture and re-upload only the dirty region when new glyphs are
/// packed. Piece 3 stays pixel-buffer-only so every test runs headlessly.
#[derive(Debug)]
pub struct GlyphAtlas {
    width: u32,
    height: u32,
    pixels: Vec<u8>,
    cache: HashMap<GlyphKey, AtlasEntry>,
    packer: ShelfPacker,
    dirty: bool,
}

impl GlyphAtlas {
    /// Allocate a fresh `width × height` R8 atlas (one byte per pixel,
    /// initially zero).
    pub fn new(width: u32, height: u32) -> Self {
        Self {
            width,
            height,
            pixels: vec![0u8; (width as usize) * (height as usize)],
            cache: HashMap::new(),
            packer: ShelfPacker::new(width, height),
            dirty: false,
        }
    }

    /// `(width, height)` in pixels.
    pub fn size(&self) -> (u32, u32) {
        (self.width, self.height)
    }

    /// Raw alpha buffer (`width * height` bytes, row-major). The piece-4
    /// render pass will upload this into an R8 texture; tests assert on it
    /// directly.
    pub fn pixels(&self) -> &[u8] {
        &self.pixels
    }

    /// Look up an entry, rasterizing and packing if not yet present.
    ///
    /// Returns `None` if the glyph has no outline (e.g., a space — the
    /// shaper still positions it via the font's advance) **or** the atlas
    /// has no room for the rasterized bitmap. A space-glyph miss is
    /// indistinguishable from a packing failure by signature; in practice
    /// the shaper handles both the same way (skip the quad, keep the
    /// advance).
    pub fn get_or_rasterize(&mut self, key: GlyphKey, fonts: &FontStore) -> Option<AtlasEntry> {
        if let Some(entry) = self.cache.get(&key) {
            return Some(*entry);
        }
        let font = fonts.get(key.font)?;
        let raster = font.rasterize(key.glyph, key.size_px as f32)?;
        let (x, y) = self.packer.pack(raster.width, raster.height)?;

        // Blit the alpha mask into the atlas at (x, y).
        let aw = self.width as usize;
        for row in 0..raster.height as usize {
            let src_start = row * raster.width as usize;
            let dst_start = (y as usize + row) * aw + x as usize;
            self.pixels[dst_start..dst_start + raster.width as usize]
                .copy_from_slice(&raster.bitmap[src_start..src_start + raster.width as usize]);
        }
        self.dirty = true;

        let w = self.width as f32;
        let h = self.height as f32;
        let entry = AtlasEntry {
            uv_min: Vec2::new(x as f32 / w, y as f32 / h),
            uv_max: Vec2::new(
                (x + raster.width) as f32 / w,
                (y + raster.height) as f32 / h,
            ),
            size_px: Vec2::new(raster.width as f32, raster.height as f32),
            bearing: Vec2::new(raster.bearing_x, raster.bearing_y),
            advance_px: raster.advance_x,
        };
        self.cache.insert(key, entry);
        Some(entry)
    }

    /// Borrow an entry that's already cached, without triggering
    /// rasterization. Useful when the renderer wants to draw only glyphs the
    /// atlas already knows.
    pub fn get(&self, key: &GlyphKey) -> Option<&AtlasEntry> {
        self.cache.get(key)
    }

    /// Number of cached glyphs.
    pub fn len(&self) -> usize {
        self.cache.len()
    }

    /// `true` if no glyphs are cached.
    pub fn is_empty(&self) -> bool {
        self.cache.is_empty()
    }

    /// `true` if [`get_or_rasterize`](Self::get_or_rasterize) added at least
    /// one glyph since the last [`clear_dirty`](Self::clear_dirty). The
    /// piece-4 render pass checks this before re-uploading the texture.
    pub fn dirty(&self) -> bool {
        self.dirty
    }

    /// Clear the dirty flag. Call after uploading the texture.
    pub fn clear_dirty(&mut self) {
        self.dirty = false;
    }
}

// ---------- shelf packer ----------

#[derive(Debug)]
struct ShelfPacker {
    width: u32,
    height: u32,
    shelves: Vec<Shelf>,
    next_y: u32,
}

#[derive(Debug)]
struct Shelf {
    y: u32,
    height: u32,
    cursor_x: u32,
}

impl ShelfPacker {
    fn new(width: u32, height: u32) -> Self {
        Self {
            width,
            height,
            shelves: Vec::new(),
            next_y: 0,
        }
    }

    fn pack(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
        if w > self.width || h > self.height {
            return None;
        }
        // Prefer the tightest-fitting existing shelf that still has
        // horizontal room — keeps shelf heights stable and packs short
        // glyphs against short glyphs.
        let mut best: Option<usize> = None;
        let mut best_waste = u32::MAX;
        for (i, shelf) in self.shelves.iter().enumerate() {
            if shelf.cursor_x + w <= self.width && h <= shelf.height {
                let waste = shelf.height - h;
                if waste < best_waste {
                    best = Some(i);
                    best_waste = waste;
                }
            }
        }
        if let Some(i) = best {
            let shelf = &mut self.shelves[i];
            let x = shelf.cursor_x;
            let y = shelf.y;
            shelf.cursor_x += w;
            return Some((x, y));
        }
        // No existing shelf fits — open a new one at `next_y` if there's
        // vertical room.
        if self.next_y + h > self.height {
            return None;
        }
        let y = self.next_y;
        self.next_y += h;
        self.shelves.push(Shelf {
            y,
            height: h,
            cursor_x: w,
        });
        Some((0, y))
    }
}

#[cfg(test)]
mod tests {
    use super::super::font::try_load_system_font;
    use super::*;
    use crate::ui::visual::FontRef;

    #[test]
    fn key_rounds_size_to_nearest_pixel() {
        let k1 = GlyphKey::new(FontId(0), GlyphId(1), 23.4);
        let k2 = GlyphKey::new(FontId(0), GlyphId(1), 23.6);
        assert_eq!(k1.size_px, 23);
        assert_eq!(k2.size_px, 24);
        assert_ne!(k1, k2);
    }

    #[test]
    fn key_clamps_sub_pixel_size_to_one() {
        // A 0.4-pixel font would otherwise round to zero, producing a useless
        // key. The packer requires width ≥ 1.
        let k = GlyphKey::new(FontId(0), GlyphId(1), 0.4);
        assert_eq!(k.size_px, 1);
    }

    #[test]
    fn shelf_packer_fits_glyphs_in_order() {
        let mut p = ShelfPacker::new(64, 64);
        // First glyph opens a shelf at y=0 with height 10.
        assert_eq!(p.pack(20, 10), Some((0, 0)));
        // Second glyph fits on the same shelf — same y, advanced cursor.
        assert_eq!(p.pack(20, 10), Some((20, 0)));
        // Third glyph: doesn't fit horizontally on shelf 0; opens shelf 1
        // at y=10.
        assert_eq!(p.pack(40, 8), Some((0, 10)));
        // Tall glyph that fits horizontally on neither existing shelf opens
        // shelf 2 at y=18.
        assert_eq!(p.pack(64, 20), Some((0, 18)));
    }

    #[test]
    fn shelf_packer_prefers_tight_fit_among_existing_shelves() {
        let mut p = ShelfPacker::new(64, 64);
        // Open shelf 0 at y=0 with height 20, occupying width 50.
        assert_eq!(p.pack(50, 20), Some((0, 0)));
        // A 50-wide 8-tall glyph won't fit horizontally on shelf 0
        // (50 + 50 = 100 > 64) — that forces shelf 1 open at y=20 with
        // height 8.
        assert_eq!(p.pack(50, 8), Some((0, 20)));
        // Now pack a 10×8 glyph: shelf 0 (waste 12) and shelf 1 (waste 0)
        // both fit horizontally, so the tight-fit shelf 1 wins.
        assert_eq!(p.pack(10, 8), Some((50, 20)));
    }

    #[test]
    fn shelf_packer_rejects_overflow() {
        let mut p = ShelfPacker::new(32, 32);
        // First fills almost all the vertical room.
        assert_eq!(p.pack(32, 30), Some((0, 0)));
        // 4-tall glyph won't fit vertically.
        assert_eq!(p.pack(8, 4), None);
        // Anything wider than the atlas is also rejected.
        let mut p2 = ShelfPacker::new(32, 32);
        assert_eq!(p2.pack(40, 4), None);
    }

    #[test]
    fn empty_atlas_has_no_dirty_no_entries() {
        let atlas = GlyphAtlas::new(64, 64);
        assert_eq!(atlas.size(), (64, 64));
        assert!(!atlas.dirty());
        assert!(atlas.is_empty());
        assert!(atlas.pixels().iter().all(|&p| p == 0));
    }

    #[test]
    fn dirty_flag_lifecycle() {
        let Some(font) = try_load_system_font() else {
            return;
        };
        let mut store = FontStore::new();
        let id = store.insert(font);
        let mut atlas = GlyphAtlas::new(256, 256);
        assert!(!atlas.dirty());

        let glyph = store.get(id).unwrap().glyph_id('A');
        atlas
            .get_or_rasterize(GlyphKey::new(id, glyph, 24.0), &store)
            .unwrap();
        assert!(atlas.dirty());
        atlas.clear_dirty();
        assert!(!atlas.dirty());

        // Second lookup of the same key is a cache hit — no rasterization,
        // no new dirty.
        atlas
            .get_or_rasterize(GlyphKey::new(id, glyph, 24.0), &store)
            .unwrap();
        assert!(!atlas.dirty());
    }

    #[test]
    fn distinct_glyphs_get_distinct_regions() {
        let Some(font) = try_load_system_font() else {
            return;
        };
        let mut store = FontStore::new();
        let id = store.insert_with_descriptor(FontRef::regular("System"), font);
        let mut atlas = GlyphAtlas::new(512, 512);

        let a = store.get(id).unwrap().glyph_id('A');
        let b = store.get(id).unwrap().glyph_id('B');
        let e_a = atlas
            .get_or_rasterize(GlyphKey::new(id, a, 24.0), &store)
            .unwrap();
        let e_b = atlas
            .get_or_rasterize(GlyphKey::new(id, b, 24.0), &store)
            .unwrap();
        // Different glyphs → different UV rects.
        assert_ne!(e_a.uv_min, e_b.uv_min);
        // UV rects stay inside `[0, 1]`.
        assert!(e_a.uv_min.x >= 0.0 && e_a.uv_max.x <= 1.0);
        assert!(e_a.uv_min.y >= 0.0 && e_a.uv_max.y <= 1.0);

        assert_eq!(atlas.len(), 2);
    }

    #[test]
    fn space_glyph_returns_none_but_does_not_corrupt_atlas() {
        let Some(font) = try_load_system_font() else {
            return;
        };
        let mut store = FontStore::new();
        let id = store.insert(font);
        let space = store.get(id).unwrap().glyph_id(' ');
        let mut atlas = GlyphAtlas::new(128, 128);
        assert!(atlas
            .get_or_rasterize(GlyphKey::new(id, space, 24.0), &store)
            .is_none());
        assert!(atlas.is_empty());
        assert!(!atlas.dirty());
    }

    #[test]
    fn atlas_pixels_match_rasterized_bitmap_at_packed_region() {
        let Some(font) = try_load_system_font() else {
            return;
        };
        let mut store = FontStore::new();
        let id = store.insert(font);
        let mut atlas = GlyphAtlas::new(128, 128);
        let glyph = store.get(id).unwrap().glyph_id('A');
        let entry = atlas
            .get_or_rasterize(GlyphKey::new(id, glyph, 24.0), &store)
            .unwrap();
        // Convert the entry's UV back to a pixel rect and verify *some*
        // pixel inside it is opaque (i.e., the blit actually happened).
        let x = (entry.uv_min.x * 128.0).round() as usize;
        let y = (entry.uv_min.y * 128.0).round() as usize;
        let w = entry.size_px.x as usize;
        let h = entry.size_px.y as usize;
        let mut had_opaque = false;
        for row in 0..h {
            for col in 0..w {
                if atlas.pixels()[(y + row) * 128 + (x + col)] > 200 {
                    had_opaque = true;
                }
            }
        }
        assert!(had_opaque, "blitted region should contain opaque pixels");
    }
}
