//! Glyph atlas generator (host, std).
//!
//! Rasterizes the SAME frozen Bravura SMuFL subset the web uses (see `tools/bravura/subset.py`'s
//! GLYPHS list, 39 names) with the pure-Rust `fontdue` rasterizer, packs the coverage into a
//! row-major 4-bit-alpha atlas, and emits the COMMITTED, generated, `no_std`-compatible
//! `rust/pm-ui/src/notation/glyphs.rs` (a `Glyph` enum + a `GlyphMeta` metrics table + the atlas
//! bytes). Run after the subset changes:
//!
//!     cargo run --manifest-path rust/glyphgen/Cargo.toml
//!
//! Device staff geometry: 1 SMuFL em = 4 staff spaces. We pick STAFF_SPACE = 10 px → EM = 40 px,
//! which fits the ST7796 320×480 staff (5 lines × 10 px = 40 px tall) and matches the web's S≈11.

use std::fmt::Write as _;
use std::path::PathBuf;

const STAFF_SPACE: f32 = 10.0;
const EM: f32 = 4.0 * STAFF_SPACE; // 40 px — SMuFL: 1 em = 4 staff spaces

/// (enum variant identifier, SMuFL glyph name) — order defines the `Glyph` enum + table index.
/// Keep in lockstep with `src/notation.js` GLYPH map and `tools/bravura/subset.py` GLYPHS.
const GLYPHS: &[(&str, char)] = &[
    // clef
    ("Clef", '\u{E069}'), // unpitchedPercussionClef1
    // noteheads
    ("NoteheadBlack", '\u{E0A4}'),
    ("NoteheadX", '\u{E0A9}'),       // noteheadXBlack
    ("NoteheadCircleX", '\u{E0B3}'), // noteheadCircleX
    ("ParenL", '\u{E0F5}'),          // noteheadParenthesisLeft
    ("ParenR", '\u{E0F6}'),          // noteheadParenthesisRight
    ("NoteheadHalf", '\u{E0A3}'),
    ("NoteheadWhole", '\u{E0A2}'),
    // flags
    ("Flag8Up", '\u{E240}'),
    ("Flag8Down", '\u{E241}'),
    ("Flag16Up", '\u{E242}'),
    ("Flag16Down", '\u{E243}'),
    // rests
    ("RestWhole", '\u{E4E3}'),
    ("RestHalf", '\u{E4E4}'),
    ("RestQuarter", '\u{E4E5}'),
    ("Rest8", '\u{E4E6}'),
    ("Rest16", '\u{E4E7}'),
    // articulations + dot
    ("AccentAbove", '\u{E4A0}'), // articAccentAbove
    ("AccentBelow", '\u{E4A1}'), // articAccentBelow
    ("Dot", '\u{E1E7}'),         // augmentationDot
    // time signature digits
    ("Sig0", '\u{E080}'),
    ("Sig1", '\u{E081}'),
    ("Sig2", '\u{E082}'),
    ("Sig3", '\u{E083}'),
    ("Sig4", '\u{E084}'),
    ("Sig5", '\u{E085}'),
    ("Sig6", '\u{E086}'),
    ("Sig7", '\u{E087}'),
    ("Sig8", '\u{E088}'),
    ("Sig9", '\u{E089}'),
    ("SigPlus", '\u{E08C}'),
    ("SigCommon", '\u{E08A}'),
    ("SigCut", '\u{E08B}'),
    // ornaments
    ("GraceAcc", '\u{E560}'),   // graceNoteAcciaccaturaStemUp
    ("GraceSlash", '\u{E564}'), // graceNoteSlashStemUp
    ("Trem1", '\u{E220}'),
    ("Trem2", '\u{E221}'),
    ("Trem3", '\u{E222}'),
    ("BuzzRoll", '\u{E22A}'),
];

struct Packed {
    ident: &'static str,
    atlas_x: u16,
    atlas_y: u16,
    w: u16,
    h: u16,
    bearing_x: i16, // left edge of bitmap relative to the glyph origin (xmin)
    bearing_y: i16, // top edge of bitmap above the baseline (ymin + height)
    advance: i16,   // horizontal advance (rounded)
}

fn main() {
    let here = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let root = here.parent().unwrap().parent().unwrap(); // repo root
    // prefer the vendored copy under rust/assets, fall back to tools/
    let font_path = {
        let a = here.parent().unwrap().join("assets/bravura/Bravura.otf");
        if a.exists() {
            a
        } else {
            root.join("tools/bravura/Bravura.otf")
        }
    };
    let font_bytes = std::fs::read(&font_path).expect("read Bravura.otf");
    let font = fontdue::Font::from_bytes(font_bytes, fontdue::FontSettings::default()).expect("parse font");

    // rasterize each glyph
    struct Raster {
        ident: &'static str,
        metrics: fontdue::Metrics,
        bitmap: Vec<u8>,
    }
    let mut rasters: Vec<Raster> = Vec::new();
    for &(ident, ch) in GLYPHS {
        let (metrics, bitmap) = font.rasterize(ch, EM);
        rasters.push(Raster { ident, metrics, bitmap });
    }

    // ---- pack into a simple shelf/grid atlas, ATLAS_W wide, 1px padding between cells ----
    const ATLAS_W: u16 = 256;
    const PAD: u16 = 1;
    let mut packed: Vec<Packed> = Vec::new();
    let mut atlas_h: u16 = 0;
    let mut cx: u16 = PAD;
    let mut cy: u16 = PAD;
    let mut shelf_h: u16 = 0;
    // pre-compute placements
    let mut placements: Vec<(u16, u16)> = Vec::with_capacity(rasters.len());
    for r in &rasters {
        let w = r.metrics.width as u16;
        let h = r.metrics.height as u16;
        if cx + w + PAD > ATLAS_W {
            cx = PAD;
            cy += shelf_h + PAD;
            shelf_h = 0;
        }
        placements.push((cx, cy));
        cx += w + PAD;
        if h > shelf_h {
            shelf_h = h;
        }
        let bottom = cy + h + PAD;
        if bottom > atlas_h {
            atlas_h = bottom;
        }
    }

    // 8-bit coverage atlas, then we 4-bit pack it row by row at emit time.
    let mut cov = vec![0u8; ATLAS_W as usize * atlas_h as usize];
    for (r, &(px, py)) in rasters.iter().zip(placements.iter()) {
        let w = r.metrics.width;
        let h = r.metrics.height;
        for row in 0..h {
            for col in 0..w {
                let a = r.bitmap[row * w + col];
                let ax = px as usize + col;
                let ay = py as usize + row;
                cov[ay * ATLAS_W as usize + ax] = a;
            }
        }
        packed.push(Packed {
            ident: r.ident,
            atlas_x: px,
            atlas_y: py,
            w: w as u16,
            h: h as u16,
            bearing_x: r.metrics.xmin as i16,
            bearing_y: (r.metrics.ymin + h as i32) as i16,
            advance: r.metrics.advance_width.round() as i16,
        });
    }

    // 4-bit pack: two pixels per byte, row-major, ATLAS_W pixels per row → ATLAS_W/2 bytes per row
    // (ATLAS_W is even). High nibble = even column, low nibble = odd column. Coverage 0..255 → 0..15.
    let row_bytes = ATLAS_W as usize / 2;
    let mut atlas = vec![0u8; row_bytes * atlas_h as usize];
    for y in 0..atlas_h as usize {
        for x in 0..ATLAS_W as usize {
            let a4 = (cov[y * ATLAS_W as usize + x] as u16 * 15 / 255) as u8;
            let bi = y * row_bytes + x / 2;
            if x & 1 == 0 {
                atlas[bi] |= a4 << 4;
            } else {
                atlas[bi] |= a4;
            }
        }
    }

    // ---- emit glyphs.rs ----
    let mut out = String::new();
    out.push_str("//! GENERATED by `rust/glyphgen` — do not edit by hand. Re-run:\n");
    out.push_str("//!     cargo run --manifest-path rust/glyphgen/Cargo.toml\n");
    out.push_str("//!\n");
    out.push_str("//! A 4-bit-alpha atlas of the frozen Bravura SMuFL subset (39 glyphs, the SAME set the web\n");
    out.push_str("//! froze in tools/bravura/subset.py) rasterized at em = 4 x staff_space. `no_std` const data.\n");
    out.push_str("#![allow(dead_code)]\n\n");
    let _ = writeln!(out, "/// Staff space the atlas was rasterized for (px). 1 SMuFL em = 4 staff spaces.");
    let _ = writeln!(out, "pub const STAFF_SPACE: i32 = {};", STAFF_SPACE as i32);
    let _ = writeln!(out, "pub const EM: i32 = {};", EM as i32);
    let _ = writeln!(out, "/// Atlas dimensions in pixels (4-bit alpha, row-packed: 2 px per byte).");
    let _ = writeln!(out, "pub const ATLAS_W: usize = {};", ATLAS_W);
    let _ = writeln!(out, "pub const ATLAS_H: usize = {};", atlas_h);
    out.push('\n');

    // GlyphMeta struct
    out.push_str("/// Per-glyph atlas placement + placement metrics (all in px).\n");
    out.push_str("#[derive(Clone, Copy)]\n");
    out.push_str("pub struct GlyphMeta {\n");
    out.push_str("    pub atlas_x: u16,\n    pub atlas_y: u16,\n    pub w: u16,\n    pub h: u16,\n");
    out.push_str("    /// left edge of the bitmap relative to the glyph origin (pen x)\n    pub bearing_x: i16,\n");
    out.push_str("    /// top edge of the bitmap ABOVE the baseline (positive = up)\n    pub bearing_y: i16,\n");
    out.push_str("    /// horizontal advance\n    pub advance: i16,\n");
    out.push_str("}\n\n");

    // Glyph enum
    out.push_str("/// One frozen SMuFL glyph. `as usize` indexes `GLYPHS`.\n");
    out.push_str("#[derive(Clone, Copy, PartialEq, Eq)]\n");
    out.push_str("pub enum Glyph {\n");
    for p in &packed {
        let _ = writeln!(out, "    {},", p.ident);
    }
    out.push_str("}\n\n");

    // metrics table
    let _ = writeln!(out, "pub const GLYPHS: [GlyphMeta; {}] = [", packed.len());
    for p in &packed {
        let _ = writeln!(
            out,
            "    GlyphMeta {{ atlas_x: {}, atlas_y: {}, w: {}, h: {}, bearing_x: {}, bearing_y: {}, advance: {} }}, // {}",
            p.atlas_x, p.atlas_y, p.w, p.h, p.bearing_x, p.bearing_y, p.advance, p.ident
        );
    }
    out.push_str("];\n\n");

    // atlas bytes
    let _ = writeln!(out, "/// Row-packed 4-bit alpha. byte = (even_col << 4) | odd_col; coverage 0..15.");
    let _ = writeln!(out, "pub const ATLAS: &[u8] = &[");
    for (i, b) in atlas.iter().enumerate() {
        if i % 16 == 0 {
            out.push_str("    ");
        }
        let _ = write!(out, "0x{:02x},", b);
        if i % 16 == 15 {
            out.push('\n');
        } else {
            out.push(' ');
        }
    }
    if atlas.len() % 16 != 0 {
        out.push('\n');
    }
    out.push_str("];\n");

    let dest = here.parent().unwrap().join("pm-ui/src/notation/glyphs.rs");
    std::fs::write(&dest, out).expect("write glyphs.rs");
    println!(
        "wrote {} ({} glyphs, atlas {}x{} = {} bytes 4-bit)",
        dest.display(),
        packed.len(),
        ATLAS_W,
        atlas_h,
        atlas.len()
    );
}
