//! Text rendering (v0.2 step 11): a font is an atlas of glyphs.
//!
//! Strip the mystique off text rendering and this is what remains: every
//! character is pre-rasterized into one image, a table records where each
//! landed, and at runtime you draw little textured quads. A line of text is
//! a run of sprites that happen to spell something, and because they all
//! share the font's texture, the step 10 batcher draws the whole string in
//! ONE draw call.
//!
//! v0.2 ships a BUILT-IN 8x8 pixel font (public-domain bitmaps, see the
//! provenance note below) so text works with zero asset files: HUDs, FPS
//! counters, menus, debug text. The decision record: own bitmap-font atlas
//! now because text is HUDs and the atlas machinery is already paid for;
//! glyphon (+ cosmic-text) is the documented off-ramp the day text becomes
//! UI (shaping, CJK, RTL, dynamic sizes). Do not extend this design past
//! its cliff; take the off-ramp.

use crate::content::{ContentManager, SpriteSheet, TextureData};

/// 8x8 glyph bitmaps for printable ASCII (codes 32..=127), one byte per
/// row, bit x = column x (LSB = leftmost pixel).
///
/// PROVENANCE: public-domain bitmap data from font8x8 by Daniel Hepper
/// (github.com/dhepper/font8x8), itself based on Marcel Sondaar's and
/// IBM's public-domain VGA fonts. Public domain: no license obligation;
/// cited per this project's borrowing policy.
pub(crate) const FONT8X8_BASIC: [[u8; 8]; 96] = [
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // U+0020 (space)
    [0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00], // U+0021 (!)
    [0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // U+0022 (")
    [0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00], // U+0023 (#)
    [0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00], // U+0024 ($)
    [0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00], // U+0025 (%)
    [0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00], // U+0026 (&)
    [0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00], // U+0027 (')
    [0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00], // U+0028 (()
    [0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00], // U+0029 ())
    [0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00], // U+002A (*)
    [0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00], // U+002B (+)
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06], // U+002C (,)
    [0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00], // U+002D (-)
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00], // U+002E (.)
    [0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00], // U+002F (/)
    [0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00], // U+0030 (0)
    [0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00], // U+0031 (1)
    [0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00], // U+0032 (2)
    [0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00], // U+0033 (3)
    [0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00], // U+0034 (4)
    [0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00], // U+0035 (5)
    [0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00], // U+0036 (6)
    [0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00], // U+0037 (7)
    [0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00], // U+0038 (8)
    [0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00], // U+0039 (9)
    [0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00], // U+003A (:)
    [0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06], // U+003B (;)
    [0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00], // U+003C (<)
    [0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00], // U+003D (=)
    [0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00], // U+003E (>)
    [0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00], // U+003F (?)
    [0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00], // U+0040 (@)
    [0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00], // U+0041 (A)
    [0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00], // U+0042 (B)
    [0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00], // U+0043 (C)
    [0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00], // U+0044 (D)
    [0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00], // U+0045 (E)
    [0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00], // U+0046 (F)
    [0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00], // U+0047 (G)
    [0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00], // U+0048 (H)
    [0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00], // U+0049 (I)
    [0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00], // U+004A (J)
    [0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00], // U+004B (K)
    [0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00], // U+004C (L)
    [0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00], // U+004D (M)
    [0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00], // U+004E (N)
    [0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00], // U+004F (O)
    [0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00], // U+0050 (P)
    [0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00], // U+0051 (Q)
    [0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00], // U+0052 (R)
    [0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00], // U+0053 (S)
    [0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00], // U+0054 (T)
    [0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00], // U+0055 (U)
    [0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00], // U+0056 (V)
    [0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00], // U+0057 (W)
    [0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00], // U+0058 (X)
    [0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00], // U+0059 (Y)
    [0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00], // U+005A (Z)
    [0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00], // U+005B ([)
    [0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00], // U+005C (\)
    [0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00], // U+005D (])
    [0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00], // U+005E (^)
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF], // U+005F (_)
    [0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00], // U+0060 (`)
    [0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00], // U+0061 (a)
    [0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00], // U+0062 (b)
    [0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00], // U+0063 (c)
    [0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6E, 0x00], // U+0064 (d)
    [0x00, 0x00, 0x1E, 0x33, 0x3f, 0x03, 0x1E, 0x00], // U+0065 (e)
    [0x1C, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0F, 0x00], // U+0066 (f)
    [0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F], // U+0067 (g)
    [0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00], // U+0068 (h)
    [0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00], // U+0069 (i)
    [0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E], // U+006A (j)
    [0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00], // U+006B (k)
    [0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00], // U+006C (l)
    [0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00], // U+006D (m)
    [0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00], // U+006E (n)
    [0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00], // U+006F (o)
    [0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F], // U+0070 (p)
    [0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78], // U+0071 (q)
    [0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00], // U+0072 (r)
    [0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00], // U+0073 (s)
    [0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00], // U+0074 (t)
    [0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00], // U+0075 (u)
    [0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00], // U+0076 (v)
    [0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00], // U+0077 (w)
    [0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00], // U+0078 (x)
    [0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F], // U+0079 (y)
    [0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00], // U+007A (z)
    [0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00], // U+007B ({)
    [0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00], // U+007C (|)
    [0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00], // U+007D (})
    [0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // U+007E (~)
    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // U+007F 
];

/// The atlas grid: 16 columns x 6 rows of fixed cells covers printable
/// ASCII, codes 32..=127 (code 32 is the space; 127, DEL, stays blank).
const COLS: u32 = 16;
const ROWS: u32 = 6;
const GLYPH: u32 = 8; // the bitmaps are 8x8
const PAD: u32 = 1; // the gutter rule from step 10 applies to fonts too

/// A fixed-cell bitmap font: the insight that makes this tier nearly free
/// is that such a font IS a [`SpriteSheet`], and the frame index IS the
/// character code minus the first code.
#[derive(Debug, Clone)]
pub struct MonoFont {
    /// 96 frames, row-major, ASCII order.
    pub sheet: SpriteSheet,
    /// The character at frame 0: `' '` (code 32).
    pub first: char,
    /// Pixels from one line's top to the next.
    pub line_height: f32,
}

impl MonoFont {
    /// Rasterize the built-in 8x8 ASCII font into a fresh atlas texture
    /// registered on `content` (white on transparent, so tinting works).
    /// Upload it before drawing: `ctx.upload_texture(font.sheet.texture)`.
    pub fn builtin(content: &mut ContentManager) -> MonoFont {
        let data = builtin_font_data();
        let (w, h) = (data.width, data.height);
        let texture = content.insert_rgba(w, h, data.rgba);
        MonoFont {
            sheet: SpriteSheet {
                texture,
                sheet_size: (w, h),
                frame_size: (GLYPH, GLYPH),
                columns: COLS,
                frames: 96,
                pad: PAD,
            },
            first: ' ',
            line_height: (GLYPH + 2) as f32,
        }
    }

    /// The uv rect for one character, or `None` if this font cannot draw
    /// it. Two lines of validation, one line of reuse: `checked_sub`
    /// returns None for control characters below `' '` (a stray `'\t'`
    /// cannot underflow and panic), the range check catches everything past
    /// ASCII (`'e'` with an accent, CJK, emoji). The caller decides what
    /// "cannot draw" means: skip it, draw a box, log it.
    pub fn glyph_rect(&self, c: char) -> Option<[f32; 4]> {
        let i = (c as u32).checked_sub(self.first as u32)?; // below ' ': None
        if i >= self.sheet.frames {
            return None;
        }
        Some(self.sheet.frame_rect(i))
    }
}

/// Render the 96 glyph bitmaps onto one RGBA8 canvas in the 16x6 grid, with
/// the gutter, white on transparent. Pure byte shuffling: headless-testable.
pub fn builtin_font_data() -> TextureData {
    let width = PAD + COLS * (GLYPH + PAD); // 1 + 16*9 = 145
    let height = PAD + ROWS * (GLYPH + PAD); // 1 + 6*9 = 55
    let mut rgba = vec![0u8; (width * height * 4) as usize];
    for (i, bitmap) in FONT8X8_BASIC.iter().enumerate() {
        let (col, row) = (i as u32 % COLS, i as u32 / COLS);
        let (x0, y0) = (PAD + col * (GLYPH + PAD), PAD + row * (GLYPH + PAD));
        for (y, rowbits) in bitmap.iter().enumerate() {
            for x in 0..8u32 {
                if rowbits & (1 << x) != 0 {
                    // bit x = column x (LSB = leftmost pixel)
                    let px = (((y0 + y as u32) * width + x0 + x) * 4) as usize;
                    rgba[px..px + 4].copy_from_slice(&[255, 255, 255, 255]);
                }
            }
        }
    }
    TextureData { width, height, rgba }
}

/// The entire text renderer: a pen, a loop, one quad per visible character.
/// The pen starts at `pos` (top-left, y-down: Mawida's one 2D convention,
/// which a fixed-cell font keeps because the baseline is baked into each
/// cell's pixels); every character advances one cell width (monospace);
/// newline resets x and drops by `line_height`. The seams hold: each glyph
/// is an ordinary drawable through a camera, so text works in world space
/// (damage numbers) and UI space (a separate ortho camera) with no extra
/// code - and one string is ONE draw call, because every glyph shares the
/// font's texture and the batcher makes the string a single run.
///
/// `scale` multiplies the 8 px cell (2.0 = 16 px tall glyphs).
#[cfg(feature = "runtime")]
pub fn draw_text(
    ctx: &mut crate::app::Context,
    font: &MonoFont,
    text: &str,
    pos: crate::math::Vec2,
    scale: f32,
    tint: crate::color::Color,
    camera: &dyn crate::camera::Camera,
) -> crate::error::Result<()> {
    use crate::drawable::Sprite;
    use crate::transform::Transform;

    let cw = font.sheet.frame_size.0 as f32 * scale;
    let ch = font.sheet.frame_size.1 as f32 * scale;
    // ONE Sprite, reused: mutate its rect + transform per glyph. No
    // per-glyph allocation; each draw() records a 96-byte instance.
    let mut glyph = Sprite::new(font.sheet.texture, 0.0, 0.0, cw, ch).with_tint(tint);
    let mut pen = pos; // top-left of the next cell, y-down
    for c in text.chars() {
        match c {
            '\n' => {
                pen.x = pos.x;
                pen.y += font.line_height * scale;
            }
            ' ' => pen.x += cw, // advance, draw nothing
            c => {
                if let Some(rect) = font.glyph_rect(c) {
                    glyph.set_uv_rect(rect);
                    glyph.transform = Transform::from_xy_size(pen.x, pen.y, cw, ch);
                    ctx.draw(&glyph, camera)?;
                }
                pen.x += cw; // monospace: advance even for unknowns,
            } // so columns stay aligned
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn builtin_font_canvas_shape() {
        let data = builtin_font_data();
        assert_eq!((data.width, data.height), (145, 55));
        // The space glyph (frame 0) is blank: its cell has zero lit pixels.
        // 'A' is not blank: find at least one lit pixel in its cell.
        let mut cm = ContentManager::new();
        let font = MonoFont::builtin(&mut cm);
        let a_rect = font.glyph_rect('A').unwrap();
        let x0 = (a_rect[0] * data.width as f32) as u32;
        let y0 = (a_rect[1] * data.height as f32) as u32;
        let mut lit = 0;
        for y in 0..8 {
            for x in 0..8 {
                let i = (((y0 + y) * data.width + x0 + x) * 4) as usize;
                if data.rgba[i + 3] != 0 {
                    lit += 1;
                }
            }
        }
        assert!(lit > 8, "'A' should have a real shape, found {lit} pixels");
    }

    #[test]
    fn glyph_rect_validates() {
        let mut cm = ContentManager::new();
        let font = MonoFont::builtin(&mut cm);
        assert!(font.glyph_rect('A').is_some());
        assert!(font.glyph_rect('~').is_some());
        assert!(font.glyph_rect('\t').is_none()); // below space: checked_sub
        assert!(font.glyph_rect('\u{e9}').is_none()); // past ASCII: range check
        assert!(font.glyph_rect('\u{4e2d}').is_none()); // CJK: same
    }

    #[test]
    fn distinct_glyphs_get_distinct_rects() {
        let mut cm = ContentManager::new();
        let font = MonoFont::builtin(&mut cm);
        assert_ne!(font.glyph_rect('A'), font.glyph_rect('B'));
        // '0' (code 48) starts row 1 of the grid: 48-32=16, columns=16.
        let zero = font.glyph_rect('0').unwrap();
        assert!(zero[1] > font.glyph_rect(' ').unwrap()[1]);
    }
}

// ---------------------------------------------------------------------------
// The ECS door's text path (item 6 of the v0.4 sweep)
// ---------------------------------------------------------------------------

/// Text as a component: pair it with a `Transform` (the pen start, top-left)
/// and register [`render_text`] and every entity carrying both draws its
/// string. The font comes from a `MonoFont` World RESOURCE (load one with
/// `Assets::builtin_font` in a `load_with` closure and insert it).
#[derive(Debug, Clone)]
pub struct Text {
    pub content: String,
    /// Multiplies the 8 px cell (3.0 = 24 px tall glyphs).
    pub scale: f32,
    pub color: crate::color::Color,
}

impl Text {
    pub fn new(content: impl Into<String>) -> Self {
        Self {
            content: content.into(),
            scale: 2.0,
            color: crate::color::Color::WHITE,
        }
    }
}

/// The engine-provided text render system (ECS door): the same pen loop as
/// [`draw_text`], recording glyph quads into the RenderQueue. It EXTENDS
/// the queue (does not replace it), so register it AFTER your sprite
/// render system, which conventionally assigns the frame's Vec.
#[cfg(feature = "runtime")]
pub fn render_text(world: &mut crate::world::World, _dt: f32) {
    use crate::app::RenderQueue;
    use crate::camera::OrthographicCamera;
    use crate::drawable::Sprite;
    use crate::transform::Transform;

    let Some(font) = world.try_resource::<MonoFont>().cloned() else {
        return; // no font resource: nothing to draw (insert one in load_with)
    };
    let camera = *world.resource::<OrthographicCamera>();

    let mut glyphs = Vec::new();
    {
        let (xforms, texts) = world.columns_mut::<Transform, Text>();
        for (t, txt) in xforms.iter().zip(texts.iter()) {
            let (Some(t), Some(txt)) = (t, txt) else { continue };
            let cw = font.sheet.frame_size.0 as f32 * txt.scale;
            let ch = font.sheet.frame_size.1 as f32 * txt.scale;
            let origin = t.translation;
            let mut glyph =
                Sprite::new(font.sheet.texture, 0.0, 0.0, cw, ch).with_tint(txt.color);
            let (mut pen_x, mut pen_y) = (origin.x, origin.y);
            for c in txt.content.chars() {
                match c {
                    '\n' => {
                        pen_x = origin.x;
                        pen_y += font.line_height * txt.scale;
                    }
                    ' ' => pen_x += cw,
                    c => {
                        if let Some(rect) = font.glyph_rect(c) {
                            glyph.set_uv_rect(rect);
                            glyph.transform = Transform::from_xy_size(pen_x, pen_y, cw, ch);
                            let mut s = glyph.clone();
                            s.transform.translation.z = origin.z; // keep the entity's layer
                            glyphs.push((s, camera));
                        }
                        pen_x += cw;
                    }
                }
            }
        }
    }
    world
        .resource_mut::<RenderQueue>()
        .sprites
        .extend(glyphs);
}

// ---------------------------------------------------------------------------
// Tier 2: variable width + kerning, fed by BMFont (item 10)
// ---------------------------------------------------------------------------

/// One glyph's four numbers, the vocabulary every text system shares
/// (FreeType's metrics, pre-folded into top-left y-down by BMFont):
/// where in the atlas, how big a quad, where relative to the pen, and how
/// far the pen moves after.
#[derive(Debug, Clone, Copy)]
pub struct Glyph {
    /// Where in the atlas (normalized, as always).
    pub uv_rect: [f32; 4],
    /// Quad size, pixels (glyphs are cropped tight).
    pub size: crate::math::Vec2,
    /// Draw at pen + offset (BMFont x/yoffset, y-down).
    pub offset: crate::math::Vec2,
    /// Pen movement after this glyph (BMFont xadvance).
    pub advance: f32,
}

/// A variable-width bitmap font: the sparse glyph table (a HashMap earns
/// its keep here, unlike the mono grid's arithmetic lookup) plus the
/// kerning table: a per-PAIR advance correction ("AV" tucks the V under
/// the A). Parse one from BMFont's text .fnt format with [`Font::parse_fnt`].
#[derive(Debug, Clone)]
pub struct Font {
    pub texture: crate::content::TextureId,
    pub glyphs: std::collections::HashMap<char, Glyph>,
    pub kerning: std::collections::HashMap<(char, char), f32>,
    pub line_height: f32,
    pub space_advance: f32,
}

impl Font {
    /// Parse AngelCode BMFont's TEXT format (.fnt): line-oriented
    /// `key=value` pairs. Only the fields the pen loop needs are read;
    /// everything else is skipped, so exporter variations do not break it.
    /// `texture` is the font atlas page you loaded through the normal
    /// texture path; (tex_w, tex_h) are its pixel dimensions, used to
    /// normalize the rects.
    pub fn parse_fnt(
        source: &str,
        texture: crate::content::TextureId,
        tex_w: f32,
        tex_h: f32,
    ) -> crate::error::Result<Font> {
        fn num(field: &str) -> f32 {
            field.split('=').nth(1).and_then(|v| v.parse().ok()).unwrap_or(0.0)
        }

        let mut font = Font {
            texture,
            glyphs: std::collections::HashMap::new(),
            kerning: std::collections::HashMap::new(),
            line_height: 0.0,
            space_advance: 0.0,
        };

        for line in source.lines() {
            let mut parts = line.split_whitespace();
            match parts.next() {
                Some("common") => {
                    for p in parts {
                        if p.starts_with("lineHeight=") {
                            font.line_height = num(p);
                        }
                    }
                }
                Some("char") => {
                    let (mut id, mut x, mut y, mut w, mut h) = (0u32, 0.0, 0.0, 0.0, 0.0);
                    let (mut xo, mut yo, mut adv) = (0.0, 0.0, 0.0);
                    for p in parts {
                        let v = num(p);
                        if p.starts_with("id=") { id = v as u32; }
                        else if p.starts_with("x=") { x = v; }
                        else if p.starts_with("y=") { y = v; }
                        else if p.starts_with("width=") { w = v; }
                        else if p.starts_with("height=") { h = v; }
                        else if p.starts_with("xoffset=") { xo = v; }
                        else if p.starts_with("yoffset=") { yo = v; }
                        else if p.starts_with("xadvance=") { adv = v; }
                    }
                    if let Some(c) = char::from_u32(id) {
                        if c == ' ' {
                            font.space_advance = adv;
                        }
                        font.glyphs.insert(
                            c,
                            Glyph {
                                uv_rect: [x / tex_w, y / tex_h, w / tex_w, h / tex_h],
                                size: crate::math::Vec2::new(w, h),
                                offset: crate::math::Vec2::new(xo, yo),
                                advance: adv,
                            },
                        );
                    }
                }
                Some("kerning") => {
                    let (mut first, mut second, mut amount) = (0u32, 0u32, 0.0);
                    for p in parts {
                        let v = num(p);
                        if p.starts_with("first=") { first = v as u32; }
                        else if p.starts_with("second=") { second = v as u32; }
                        else if p.starts_with("amount=") { amount = v; }
                    }
                    if let (Some(a), Some(b)) = (char::from_u32(first), char::from_u32(second)) {
                        font.kerning.insert((a, b), amount);
                    }
                }
                _ => {} // info/page/chars/kernings counts: skipped on purpose
            }
        }
        if font.line_height == 0.0 {
            return Err(crate::error::MawidaError::Other(
                "not a BMFont .fnt: no common lineHeight line".into(),
            ));
        }
        Ok(font)
    }
}

/// The variable-width pen loop: draw_text's shape with the two changed
/// lines the guide promised (quad at pen + offset sized per glyph; pen
/// advances per glyph) PLUS the kerning lookup: one HashMap read per
/// adjacent pair, added to the advance.
#[cfg(feature = "runtime")]
#[allow(clippy::too_many_arguments)]
pub fn draw_text_variable(
    ctx: &mut crate::app::Context,
    font: &Font,
    text: &str,
    pos: crate::math::Vec2,
    scale: f32,
    tint: crate::color::Color,
    camera: &dyn crate::camera::Camera,
) -> crate::error::Result<()> {
    use crate::drawable::Sprite;
    use crate::transform::Transform;

    let mut glyph_sprite = Sprite::new(font.texture, 0.0, 0.0, 1.0, 1.0).with_tint(tint);
    let mut pen = pos;
    let mut prev: Option<char> = None;
    for c in text.chars() {
        match c {
            '\n' => {
                pen.x = pos.x;
                pen.y += font.line_height * scale;
                prev = None;
            }
            ' ' => {
                pen.x += font.space_advance * scale;
                prev = None;
            }
            c => {
                if let Some(g) = font.glyphs.get(&c) {
                    // kerning: the pair correction BEFORE placing this glyph
                    if let Some(p) = prev {
                        pen.x += font.kerning.get(&(p, c)).copied().unwrap_or(0.0) * scale;
                    }
                    glyph_sprite.set_uv_rect(g.uv_rect);
                    glyph_sprite.transform = Transform::from_xy_size(
                        pen.x + g.offset.x * scale,
                        pen.y + g.offset.y * scale,
                        g.size.x * scale,
                        g.size.y * scale,
                    );
                    ctx.draw(&glyph_sprite, camera)?;
                    pen.x += g.advance * scale;
                }
                prev = Some(c);
            }
        }
    }
    Ok(())
}

#[cfg(test)]
mod tier2_tests {
    use super::*;
    use crate::content::TextureId;

    /// A tiny hand-written .fnt: enough for the parser and the pen math.
    const FNT: &str = "\
info face=\"Test\" size=16
common lineHeight=18 base=14 scaleW=64 scaleH=64 pages=1
page id=0 file=\"test_0.png\"
chars count=3
char id=65 x=0 y=0 width=10 height=12 xoffset=1 yoffset=2 xadvance=11 page=0
char id=86 x=12 y=0 width=10 height=12 xoffset=0 yoffset=2 xadvance=10 page=0
char id=32 x=0 y=0 width=0 height=0 xoffset=0 yoffset=0 xadvance=5 page=0
kernings count=1
kerning first=65 second=86 amount=-2
";

    #[test]
    fn parses_glyphs_metrics_and_kerning() {
        let font = Font::parse_fnt(FNT, TextureId(7), 64.0, 64.0).unwrap();
        assert_eq!(font.line_height, 18.0);
        assert_eq!(font.space_advance, 5.0);

        let a = font.glyphs[&'A'];
        assert_eq!(a.uv_rect, [0.0, 0.0, 10.0 / 64.0, 12.0 / 64.0]);
        assert_eq!(a.advance, 11.0);
        assert_eq!(a.offset.x, 1.0);

        // the AV pair tucks the V under the A by 2 pixels
        assert_eq!(font.kerning[&('A', 'V')], -2.0);
        assert!(!font.kerning.contains_key(&('V', 'A'))); // pairs are ordered
    }

    #[test]
    fn rejects_non_fnt_input() {
        assert!(Font::parse_fnt("hello world", TextureId(0), 64.0, 64.0).is_err());
    }
}
