///|
/// Renderer-local glyph atlas: shelf packing over a lazily-grown set of RGBA8
/// coverage pages.
///
/// It is local because P14 (`docs/invariants.md`) forbids a renderer importing
/// another renderer, so the WGPU atlas cannot be reused. The shared-atlas
/// question belongs to plan stage L3, which decides the text architecture for
/// every non-Skia route; what lives here is the residency policy that question
/// has to be answered against — a page is cleared and repacked when the whole
/// page set fills, the same recovery `moui_wgpu_renderer/native_visual_plan.mbt`
/// runs, so a document longer than the atlas scrolls instead of losing its text
/// for the rest of the session.
///
/// The side is 1024 to match that route's measured budget, not this file's
/// convenience: at 512 a Retina frame of 14pt Chinese text ran out after ~200
/// distinct glyphs and dropped the rest of the page every frame (`docs/plans/
/// active/no-third-party-gpu-route.md`, L3c).
///
/// Growth is what fixes the case one page cannot hold. Clear-and-repack recovers
/// a *document* that outruns the atlas across frames, but a single frame whose
/// own glyph set exceeds an empty 1024×1024 page (~818 distinct 14pt hanzi at
/// device scale 2.0, ~347 at 24pt) was repacked into the same shortfall and lost
/// its overflow every frame. Such a frame now takes a second page. Page 0 exists
/// from construction and costs exactly what the single atlas costed; a page after
/// that is only allocated by a frame that needs it, and `METAL_ATLAS_MAX_PAGES`
/// bounds the worst case at 4 pages — 16 MB of CPU staging plus 16 MB of texture
/// — which is why the cap is part of the design rather than an afterthought.
///
/// The page an item samples is carried in the word the wire already had: a glyph
/// run that spills becomes one draw per page, each with `texture_key` naming its
/// page, so the 76-byte vertex, the 9-int32 item and the MSL are untouched.
priv struct MetalGlyphAtlas {
  mut pages : Array[MetalAtlasPage]
  mut placements : Map[String, MetalGlyphPlacement]
  mut full_events : Int32
  mut missing_glyphs : Int32
  mut resets : Int32
}

///|
/// One shelf-packed coverage page and its GPU mirror. `dirty` is per page so a
/// frame that only touched page 0 does not re-upload every other page.
priv struct MetalAtlasPage {
  pixels : Array[Byte]
  mut next_x : Int32
  mut next_y : Int32
  mut shelf_height : Int32
  mut dirty : Bool
}

///|
priv struct MetalGlyphPlacement {
  page : Int32
  x : Int32
  y : Int32
  width : Int32
  height : Int32
  bearing_x : Double
  bearing_y : Double
  coverage_only : Bool
}

///|
priv enum MetalGlyphLookup {
  Ready(MetalGlyphPlacement)
  Missing
  Full
}

///|
const METAL_ATLAS_SIZE : Int32 = 1024

///|
const METAL_ATLAS_PADDING : Int32 = 1

///|
const METAL_ATLAS_MAX_PAGES : Int32 = 4

///|
fn new_metal_atlas_page() -> MetalAtlasPage {
  MetalAtlasPage::{
    pixels: Array::make(METAL_ATLAS_SIZE * METAL_ATLAS_SIZE * 4, b'\x00'),
    next_x: 0,
    next_y: 0,
    shelf_height: 0,
    dirty: true,
  }
}

///|
fn new_metal_glyph_atlas() -> MetalGlyphAtlas {
  MetalGlyphAtlas::{
    pages: [new_metal_atlas_page()],
    placements: Map([]),
    full_events: 0,
    missing_glyphs: 0,
    resets: 0,
  }
}

///|
/// Empty every page: cursors back at the origin, placements dropped, pixels
/// zeroed, page 0 marked for re-upload. `cache_key` is documented to carry every
/// raster-affecting field (`moui/render/native_text/text_model.mbt:22-24`), so a
/// placement that is dropped here is re-rasterized on demand rather than reused
/// at the wrong size.
///
/// Only page 0 survives the clear. The host's textures for pages beyond it stay
/// allocated until the context is released — bounded by the same
/// `METAL_ATLAS_MAX_PAGES` — because a route that freed them here would rebuild a
/// 4 MB texture on every frame of a document that keeps crossing the cap.
fn clear_metal_glyph_atlas(atlas : MetalGlyphAtlas) -> Unit {
  let first = atlas.pages[0]
  first.next_x = 0
  first.next_y = 0
  first.shelf_height = 0
  first.pixels.fill(b'\x00')
  first.dirty = true
  atlas.pages = [first]
  atlas.placements = Map([])
}

///|
/// The pages this atlas has grown, in upload order.
fn metal_glyph_atlas_page_count(atlas : MetalGlyphAtlas) -> Int32 {
  atlas.pages.length()
}

///|
fn metal_glyph_atlas_page_dirty(atlas : MetalGlyphAtlas, page : Int32) -> Bool {
  atlas.pages[page].dirty
}

///|
fn metal_glyph_atlas_page_clean(atlas : MetalGlyphAtlas, page : Int32) -> Unit {
  atlas.pages[page].dirty = false
}

///|
fn metal_glyph_atlas_page_bytes(atlas : MetalGlyphAtlas, page : Int32) -> Bytes {
  Bytes::from_array(atlas.pages[page].pixels)
}

///|
fn ensure_metal_glyph(
  atlas : MetalGlyphAtlas,
  cache : @native_text.NativeTextCache,
  key : @native_text.NativeGlyphKey,
) -> MetalGlyphLookup {
  let cache_key = key.cache_key()
  match atlas.placements.get(cache_key) {
    Some(placement) => Ready(placement)
    None =>
      match cache.raster_glyph(key) {
        None => {
          atlas.missing_glyphs = atlas.missing_glyphs + 1
          Missing
        }
        Some(raster) =>
          match place_metal_glyph(atlas, raster.width(), raster.height()) {
            None => {
              atlas.full_events = atlas.full_events + 1
              Full
            }
            Some((page, x, y)) => {
              blit_metal_glyph(atlas.pages[page].pixels, x, y, raster)
              let placement = MetalGlyphPlacement::{
                page,
                x,
                y,
                width: raster.width(),
                height: raster.height(),
                bearing_x: raster.bearing_x().to_double(),
                bearing_y: raster.bearing_y().to_double(),
                coverage_only: raster.format() ==
                @native_text.NativeRasterGlyphFormat::AlphaMask,
              }
              atlas.placements.set(cache_key, placement)
              atlas.pages[page].dirty = true
              Ready(placement)
            }
          }
      }
  }
}

///|
/// Reserve a box, growing the atlas by one page when the last page has no room.
/// `None` means either a box no page can ever hold (wider or taller than the
/// side) or a page set at its cap — the caller's clear-and-rebuild retry is the
/// recovery for the second, and there is no recovery for the first.
fn place_metal_glyph(
  atlas : MetalGlyphAtlas,
  width : Int32,
  height : Int32,
) -> (Int32, Int32, Int32)? {
  if width + METAL_ATLAS_PADDING > METAL_ATLAS_SIZE || height > METAL_ATLAS_SIZE {
    return None
  }
  let last = atlas.pages[atlas.pages.length() - 1]
  match place_metal_glyph_on_page(last, width, height) {
    Some((x, y)) => Some((atlas.pages.length() - 1, x, y))
    None =>
      if atlas.pages.length() >= METAL_ATLAS_MAX_PAGES {
        None
      } else {
        let page = new_metal_atlas_page()
        atlas.pages.push(page)
        // An empty page takes any box the size guard above accepts, so the
        // second arm cannot be reached; it keeps the match total.
        match place_metal_glyph_on_page(page, width, height) {
          Some((x, y)) => Some((atlas.pages.length() - 1, x, y))
          None => None
        }
      }
  }
}

///|
/// Shelf packing inside one page: a box that leaves the row starts a new row at
/// the tallest height seen, and a row that leaves the page is `None`.
fn place_metal_glyph_on_page(
  page : MetalAtlasPage,
  width : Int32,
  height : Int32,
) -> (Int32, Int32)? {
  if width + METAL_ATLAS_PADDING > METAL_ATLAS_SIZE || height > METAL_ATLAS_SIZE {
    return None
  }
  if page.next_x + width + METAL_ATLAS_PADDING > METAL_ATLAS_SIZE {
    page.next_x = 0
    page.next_y = page.next_y + page.shelf_height + METAL_ATLAS_PADDING
    page.shelf_height = 0
  }
  if page.next_y + height > METAL_ATLAS_SIZE {
    return None
  }
  let x = page.next_x
  let y = page.next_y
  page.next_x = page.next_x + width + METAL_ATLAS_PADDING
  page.shelf_height = page.shelf_height.max(height)
  Some((x, y))
}

///|
fn blit_metal_glyph(
  pixels : Array[Byte],
  origin_x : Int32,
  origin_y : Int32,
  raster : @native_text.NativeRasterGlyph,
) -> Unit {
  let width = raster.width()
  let height = raster.height()
  let source = raster.pixels()
  let coverage_only = raster.format() ==
    @native_text.NativeRasterGlyphFormat::AlphaMask
  for row in 0..