//! Drum-notation engine for the device — a faithful port of the web reference `src/notation.js`.
//!
//! `src/notation.js` is the DEBUGGED source of truth for layout (voice->staff map, group-aware
//! beaming, tuplet numerals, ghost parens, accents, flam/drag/roll ornaments, the common-lcm time
//! grid across all lanes incl. polyrhythm, the TUBS box grid, and Konnakol solkattu). This ports the
//! same MATH; the only difference is glyph delivery — the web draws Bravura as a font, the device
//! blits the SAME glyphs from a pre-rasterized atlas (`glyphs.rs` via `atlas::draw_glyph`).
//!
//! Palette choice: LIGHT INK ON DARK, to match the device's other screens (`draw_metronome`). The
//! atlas blit alpha-blends ink over the panel `BG`. (The web uses dark-ink-on-white "paper"; the
//! layout math is identical — only colors differ.)

mod atlas;
pub mod glyphs;

use embedded_graphics::{
    mono_font::{ascii::{FONT_6X10, FONT_9X18_BOLD, FONT_10X20}, MonoTextStyle},
    pixelcolor::Rgb565,
    prelude::*,
    primitives::{Line, PrimitiveStyle, Rectangle},
    text::{Alignment, Text},
};

use crate::{LaneView, Screen};
use atlas::{advance as gadv, draw_glyph, width as gw};
use glyphs::{Glyph, STAFF_SPACE};

/// Which notation view to render.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ViewMode {
    Staff,
    Tubs,
    Konnakol,
}

// ---- palette (light ink on the device's dark panel) ----
const fn rgb(hex: u32) -> Rgb565 {
    Rgb565::new(((hex >> 19) & 0x1F) as u8, ((hex >> 10) & 0x3F) as u8, ((hex >> 3) & 0x1F) as u8)
}
const BG: Rgb565 = rgb(0x06090E); // matches pm-ui BG so the atlas blends cleanly
const INK: Rgb565 = rgb(0xE6ECF3); // staff lines, noteheads, stems, beams
const FAINT: Rgb565 = rgb(0x3A4554); // box outlines / gridlines
const ACCENT: Rgb565 = rgb(0xFF9B2E); // accents
const GHOST: Rgb565 = rgb(0x9A86E0); // ghost notes
const PLAY: Rgb565 = rgb(0x2B7FFF); // playhead

const S: i32 = STAFF_SPACE; // staff space (px); 1 em = 4 S

fn gcd(mut a: u32, mut b: u32) -> u32 {
    while b != 0 {
        let t = b;
        b = a % b;
        a = t;
    }
    if a == 0 {
        1
    } else {
        a
    }
}
fn lcm(a: u32, b: u32) -> u32 {
    a / gcd(a, b) * b
}

/// Voice -> (p, head, stem-up). `p` = half-staff-spaces below the TOP line (top line p=0, each
/// line/space step = 1, bottom line p=8). Ported verbatim from notation.js `voice()`.
struct Voice {
    p: i32,
    head: Glyph,
    up: bool,
}
fn voice(name: &str) -> Voice {
    let v = |p, head, up| Voice { p, head, up };
    if name.starts_with("kick") {
        v(7, Glyph::NoteheadBlack, false)
    } else if name.starts_with("snare") || name.starts_with("clap") || name.starts_with("rim") {
        v(3, Glyph::NoteheadBlack, true)
    } else if name.starts_with("openHat") || name.starts_with("hatOpen") || name.starts_with("hat") {
        v(-1, Glyph::NoteheadX, true)
    } else if name.starts_with("ride") {
        v(0, Glyph::NoteheadX, true)
    } else if name.starts_with("crash") {
        v(-3, Glyph::NoteheadX, true)
    } else if name.starts_with("tomHigh") {
        v(1, Glyph::NoteheadBlack, true)
    } else if name.starts_with("tomMid") || name == "tom808" {
        v(2, Glyph::NoteheadBlack, true)
    } else if name.starts_with("tomLow") || name.starts_with("tom") {
        v(5, Glyph::NoteheadBlack, true)
    } else if name.starts_with("cowbell") {
        v(-1, Glyph::NoteheadCircleX, true)
    } else if name.starts_with("claves") || name.starts_with("woodblock") || name.starts_with("jamblock") {
        v(1, Glyph::NoteheadX, true)
    } else if name.starts_with("tambourine") {
        v(-2, Glyph::NoteheadX, true)
    } else {
        v(3, Glyph::NoteheadBlack, true)
    }
}

/// Digit glyph for a 0..9 value.
fn sig_digit(d: u8) -> Glyph {
    // Sig0..Sig9 are contiguous in the enum.
    match d {
        0 => Glyph::Sig0,
        1 => Glyph::Sig1,
        2 => Glyph::Sig2,
        3 => Glyph::Sig3,
        4 => Glyph::Sig4,
        5 => Glyph::Sig5,
        6 => Glyph::Sig6,
        7 => Glyph::Sig7,
        8 => Glyph::Sig8,
        _ => Glyph::Sig9,
    }
}

fn fmt_u32(mut n: u32, buf: &mut [u8; 12]) -> &str {
    if n == 0 {
        buf[11] = b'0';
        return core::str::from_utf8(&buf[11..]).unwrap();
    }
    let mut i = buf.len();
    while n > 0 {
        i -= 1;
        buf[i] = b'0' + (n % 10) as u8;
        n /= 10;
    }
    core::str::from_utf8(&buf[i..]).unwrap()
}

/// y for a staff position `p` (half-staff-spaces below the top line).
#[inline]
fn y0(staff_top: i32, p: i32) -> i32 {
    staff_top + p * S / 2
}

fn hline<D: DrawTarget<Color = Rgb565>>(d: &mut D, x1: i32, y: i32, x2: i32, c: Rgb565, w: u32) -> Result<(), D::Error> {
    Line::new(Point::new(x1, y), Point::new(x2, y)).into_styled(PrimitiveStyle::with_stroke(c, w)).draw(d)
}
fn vline<D: DrawTarget<Color = Rgb565>>(d: &mut D, x: i32, y1: i32, y2: i32, c: Rgb565, w: u32) -> Result<(), D::Error> {
    Line::new(Point::new(x, y1), Point::new(x, y2)).into_styled(PrimitiveStyle::with_stroke(c, w)).draw(d)
}

/// Draw a glyph centered horizontally on column `cx`, baseline on staff position `p`.
fn cglyph<D: DrawTarget<Color = Rgb565>>(d: &mut D, g: Glyph, cx: i32, staff_top: i32, p: i32, ink: Rgb565) -> Result<(), D::Error> {
    draw_glyph(d, g, Point::new(cx - gw(g) / 2, y0(staff_top, p)), ink, BG)
}

/// Header: track name (left) + big BPM (right). Shared by all views.
fn header<D: DrawTarget<Color = Rgb565>>(d: &mut D, name: &str, bpm: i64, w: i32) -> Result<(), D::Error> {
    Text::new(name, Point::new(14, 22), MonoTextStyle::new(&FONT_10X20, INK)).draw(d)?;
    let mut nb = [0u8; 12];
    Text::with_alignment(
        fmt_u32(bpm.max(0) as u32, &mut nb),
        Point::new(w - 14, 20),
        MonoTextStyle::new(&FONT_9X18_BOLD, ACCENT),
        Alignment::Right,
    )
    .draw(d)?;
    Ok(())
}

/// Top-level notation entry; dispatches on `view`. Replaces the old first-pass `draw_notation`.
pub fn draw<D>(d: &mut D, s: &Screen, view: ViewMode) -> Result<(), D::Error>
where
    D: DrawTarget<Color = Rgb565>,
{
    d.clear(BG)?;
    match view {
        ViewMode::Staff => draw_staff(d, s),
        ViewMode::Tubs => draw_tubs(d, s),
        ViewMode::Konnakol => draw_konnakol(d, s),
    }
}

/// Active (unmuted, non-empty) lanes.
fn active<'a>(s: &'a Screen<'a>) -> impl Iterator<Item = &'a LaneView<'a>> {
    s.lanes.iter().filter(|l| !l.muted && !l.levels.is_empty())
}

/// Common time grid: lcm of ALL active lanes' step counts.
fn grid_res(s: &Screen) -> u32 {
    let mut res = 1u32;
    for l in active(s) {
        res = lcm(res, l.levels.len() as u32);
    }
    res.max(1)
}

/// First non-poly lane is the master (defines the meter); its `beats` field is the summed groups.
fn master_groups<'a>(s: &'a Screen<'a>) -> Option<&'a LaneView<'a>> {
    active(s).find(|l| !l.poly).or_else(|| active(s).next())
}

// =============================== STAFF ====================================
fn draw_staff<D>(d: &mut D, s: &Screen) -> Result<(), D::Error>
where
    D: DrawTarget<Color = Rgb565>,
{
    let bb = d.bounding_box();
    let w = bb.size.width as i32;
    header(d, s.name, s.bpm, w)?;

    let m = 14;
    let clef_w = (glyphs::EM * 6) / 10; // em * 0.6
    let staff_top = 70;
    let x1 = w - m;

    // groups: from the master lane. LaneView exposes `groups`; default [4].
    let master = master_groups(s);
    let groups: &[u32] = master.map(|l| l.groups).filter(|g| !g.is_empty()).unwrap_or(&[4]);
    let beats: u32 = groups.iter().sum::<u32>().max(1);

    // time signature reserved width (additive numerator)
    let ts_digit = gadv(Glyph::Sig4).max((glyphs::EM * 4) / 10);
    let num_parts: &[u32] = if groups.len() > 1 { groups } else { core::slice::from_ref(&beats) };
    let mut num_glyphs = num_parts.len() as i32 - 1; // the '+' separators
    for &n in num_parts {
        num_glyphs += digits(n);
    }
    let ts_x = m + clef_w;
    let ts_w = num_glyphs.max(1) * ts_digit;
    let x0 = ts_x + ts_w + 14;
    let bar_w = (x1 - x0).max(1);

    // staff (5 lines) + barlines
    for i in 0..5 {
        hline(d, m, staff_top + i * S, x1, INK, 1)?;
    }
    vline(d, m, staff_top, staff_top + 4 * S, INK, 2)?;
    vline(d, x1, staff_top, staff_top + 4 * S, INK, 2)?;
    // percussion clef centered on middle line (p=4)
    cglyph(d, Glyph::Clef, m + clef_w / 2, staff_top, 4, INK)?;

    // time signature: additive numerator over a quarter denominator
    draw_sig_row(d, staff_top, ts_x, ts_w, 2, num_parts, ts_digit)?;
    draw_sig_row(d, staff_top, ts_x, ts_w, 6, &[4], ts_digit)?;

    let res = grid_res(s);
    let beamable = res / beats >= 2;
    let head_half = gw(Glyph::NoteheadBlack) / 2;

    let up_tip = staff_top - (S * 26) / 10; // S*2.6
    let dn_tip = staff_top + 4 * S + (S * 26) / 10;

    // beaming carry per stem direction: (stem_x, beat, beam_y)
    let mut up_prev: Option<(i32, u32, i32)> = None;
    let mut dn_prev: Option<(i32, u32, i32)> = None;

    for c in 0..res {
        let cx = x0 + ((2 * c + 1) as i32 * bar_w) / (2 * res as i32);
        let beat = c * beats / res;
        // accumulators per direction: (lo_p, hi_p, any)
        let (mut up_lo, mut up_hi, mut up_any) = (i32::MIN, i32::MAX, false);
        let (mut dn_lo, mut dn_hi, mut dn_any) = (i32::MIN, i32::MAX, false);

        for l in active(s) {
            let steps = l.levels.len() as u32;
            if (c * steps) % res != 0 {
                continue;
            }
            let si = (c * steps / res) as usize;
            let lvl = l.levels[si];
            if lvl == 0 {
                continue;
            }
            let orn = l.orns.get(si).copied().unwrap_or(0);
            let vc = voice(l.name);
            let color = match lvl {
                2 => ACCENT,
                3 => GHOST,
                _ => INK,
            };

            // ghost = parenthesized notehead
            if lvl == 3 {
                cglyph(d, Glyph::ParenL, cx - (S * 85) / 100, staff_top, vc.p, color)?;
            }
            cglyph(d, vc.head, cx, staff_top, vc.p, color)?;
            if lvl == 3 {
                cglyph(d, Glyph::ParenR, cx + (S * 85) / 100, staff_top, vc.p, color)?;
            }

            // accent mark above/below the staff edge
            if lvl == 2 {
                let g = if vc.up { Glyph::AccentAbove } else { Glyph::AccentBelow };
                cglyph(d, g, cx, staff_top, if vc.up { -2 } else { 10 }, color)?;
            }

            // ornaments: flam = one slashed grace; drag = two; roll = tremolo strokes on the stem
            match orn {
                1 => cglyph(d, Glyph::GraceSlash, cx - (S * 14) / 10, staff_top, vc.p, color)?,
                2 => {
                    cglyph(d, Glyph::GraceSlash, cx - (S * 19) / 10, staff_top, vc.p, color)?;
                    cglyph(d, Glyph::GraceSlash, cx - (S * 11) / 10, staff_top, vc.p, color)?;
                }
                3 => {
                    let dx = if vc.up { (S * 55) / 100 } else { -(S * 55) / 100 };
                    let dp = if vc.up { -2 } else { 2 };
                    cglyph(d, Glyph::Trem3, cx + dx, staff_top, vc.p + dp, color)?;
                }
                _ => {}
            }

            // ledger lines above (p<=-2) / below (p>=10) the staff
            let mut lp = -2;
            while lp >= vc.p {
                hline(d, cx - (S * 9) / 10, y0(staff_top, lp), cx + (S * 9) / 10, INK, 1)?;
                lp -= 2;
            }
            let mut lp = 10;
            while lp <= vc.p {
                hline(d, cx - (S * 9) / 10, y0(staff_top, lp), cx + (S * 9) / 10, INK, 1)?;
                lp += 2;
            }

            if vc.up {
                up_any = true;
                up_lo = up_lo.max(vc.p);
                up_hi = up_hi.min(vc.p);
            } else {
                dn_any = true;
                dn_lo = dn_lo.max(vc.p);
                dn_hi = dn_hi.min(vc.p);
            }
        }

        // shared up-stem (hands): right side of head
        if up_any {
            let sx = cx + head_half;
            let top = up_tip.min(y0(staff_top, up_hi) - (S * 12) / 10);
            vline(d, sx, y0(staff_top, up_lo), top, INK, 1)?;
            if beamable {
                if let Some((px, pb, _)) = up_prev {
                    if pb == beat {
                        // beam: thick segment between previous stem-top and this one
                        Rectangle::new(Point::new(px.min(sx), top), Size::new((px - sx).unsigned_abs(), 3))
                            .into_styled(PrimitiveStyle::with_fill(INK))
                            .draw(d)?;
                    }
                }
                up_prev = Some((sx, beat, top));
            } else {
                up_prev = None;
            }
        } else {
            up_prev = None;
        }
        // shared down-stem (feet): left side
        if dn_any {
            let sx = cx - head_half;
            let bot = dn_tip.max(y0(staff_top, dn_lo) + (S * 12) / 10);
            vline(d, sx, y0(staff_top, dn_hi), bot, INK, 1)?;
            if beamable {
                if let Some((px, pb, _)) = dn_prev {
                    if pb == beat {
                        Rectangle::new(Point::new(px.min(sx), bot - 3), Size::new((px - sx).unsigned_abs(), 3))
                            .into_styled(PrimitiveStyle::with_fill(INK))
                            .draw(d)?;
                    }
                }
                dn_prev = Some((sx, beat, bot));
            } else {
                dn_prev = None;
            }
        } else {
            dn_prev = None;
        }
    }

    // tuplet numeral: common subdivision per beat (3,5,6,7…), printed over each beat when not pow2
    let tup_n = ((res + beats / 2) / beats).max(1); // round(res/beats)
    if tup_n >= 3 && (tup_n & (tup_n - 1)) != 0 {
        let ty = up_tip - (S * 7) / 10;
        let mut nb = [0u8; 12];
        let txt = fmt_u32(tup_n, &mut nb);
        for b in 0..beats as i32 {
            let tx = x0 + ((2 * b + 1) * bar_w) / (2 * beats as i32);
            Text::with_alignment(txt, Point::new(tx, ty), MonoTextStyle::new(&FONT_6X10, INK), Alignment::Center).draw(d)?;
        }
    }

    // playhead (shifted half a cell so it lands on the note onset, like the web)
    if s.playing {
        let pf = (s.phase + 0.5 / res as f32).clamp(0.0, 1.0);
        let px = x0 + (pf * bar_w as f32) as i32;
        vline(d, px, staff_top - S, staff_top + 4 * S + S, PLAY, 2)?;
    }
    Ok(())
}

fn digits(mut n: u32) -> i32 {
    if n == 0 {
        return 1;
    }
    let mut c = 0;
    while n > 0 {
        c += 1;
        n /= 10;
    }
    c
}

/// One time-signature row: additive numerator parts joined by `+`, centered in width `tw`.
fn draw_sig_row<D: DrawTarget<Color = Rgb565>>(
    d: &mut D,
    staff_top: i32,
    tx: i32,
    tw: i32,
    p: i32,
    parts: &[u32],
    dw: i32,
) -> Result<(), D::Error> {
    // build the glyph sequence length first (digits + separators)
    let mut seq_len = parts.len() as i32 - 1;
    for &n in parts {
        seq_len += digits(n);
    }
    seq_len = seq_len.max(1);
    let mut xx = tx + (tw - seq_len * dw) / 2 + dw / 2;
    for (i, &n) in parts.iter().enumerate() {
        if i > 0 {
            cglyph(d, Glyph::SigPlus, xx, staff_top, p, INK)?;
            xx += dw;
        }
        // digits of n, most significant first
        let mut div = 1u32;
        while n / div >= 10 {
            div *= 10;
        }
        let mut nn = n;
        loop {
            let dgt = (nn / div) as u8;
            cglyph(d, sig_digit(dgt), xx, staff_top, p, INK)?;
            xx += dw;
            nn %= div;
            if div == 1 {
                break;
            }
            div /= 10;
        }
    }
    Ok(())
}

// =============================== TUBS =====================================
// Rows = voices, columns = time units, filled boxes = hits. All lanes share the common lcm grid so
// columns line up; each lane draws ONE box per real step at its grid column.
fn draw_tubs<D>(d: &mut D, s: &Screen) -> Result<(), D::Error>
where
    D: DrawTarget<Color = Rgb565>,
{
    let bb = d.bounding_box();
    let w = bb.size.width as i32;
    let h = bb.size.height as i32;
    header(d, s.name, s.bpm, w)?;

    let lanes: usize = active(s).count();
    if lanes == 0 {
        return Ok(());
    }
    let m = 14;
    let x0 = m + 92;
    let x1 = w - m;
    let gw_px = (x1 - x0).max(1);
    let top = 42;
    let bot = h - 12;
    let row_h = (((bot - top) / lanes as i32).clamp(20, 40)).max(20);

    let res = grid_res(s);
    let cw = gw_px / res as i32;
    let bs = (row_h - 8).min(cw - 3).clamp(11, 30);

    let groups: &[u32] = master_groups(s).map(|l| l.groups).filter(|g| !g.is_empty()).unwrap_or(&[4]);
    let mbeats: u32 = groups.iter().sum::<u32>().max(1);

    let y_a = top - 4;
    let y_b = top + lanes as i32 * row_h;

    // beat / group dividers — group starts brighter
    let mut starts = [false; 64];
    {
        let mut acc = 0usize;
        for &g in groups {
            if acc < 64 {
                starts[acc] = true;
            }
            acc += g as usize;
        }
    }
    for b in 0..=mbeats {
        let lx = x0 + ((b * res / mbeats) as i32) * cw;
        let bright = (b < mbeats && (b as usize) < 64 && starts[b as usize]) || b == 0 || b == mbeats;
        vline(d, lx, y_a, y_b, if bright { GHOST } else { FAINT }, 1)?;
    }
    // playhead
    if s.playing {
        let px = x0 + ((s.phase + 0.5 / res as f32).clamp(0.0, 1.0) * gw_px as f32) as i32;
        vline(d, px, y_a, y_b, PLAY, 2)?;
    }

    let lbl = MonoTextStyle::new(&FONT_6X10, INK);
    for (r, l) in active(s).enumerate() {
        let cy = top + r as i32 * row_h + row_h / 2;
        Text::new(l.name, Point::new(m, cy + 4), lbl).draw(d)?;
        if l.poly {
            Text::new("~", Point::new(m + 80, cy + 4), MonoTextStyle::new(&FONT_6X10, GHOST)).draw(d)?;
        }
        let steps = l.levels.len() as u32;
        let span = res / steps;
        for i in 0..steps {
            let cx = x0 + ((2 * i * span + span) as i32 * cw) / 2;
            let lvl = l.levels[i as usize];
            // box outline
            Rectangle::new(Point::new(cx - bs / 2, cy - bs / 2), Size::new(bs as u32, bs as u32))
                .into_styled(PrimitiveStyle::with_stroke(FAINT, 1))
                .draw(d)?;
            if lvl > 0 {
                let col = match lvl {
                    2 => ACCENT,
                    3 => GHOST,
                    _ => INK,
                };
                let p = (bs * 2) / 10;
                Rectangle::new(Point::new(cx - bs / 2 + p, cy - bs / 2 + p), Size::new((bs - 2 * p) as u32, (bs - 2 * p) as u32))
                    .into_styled(PrimitiveStyle::with_fill(col))
                    .draw(d)?;
                // ornament marker letter inside the box
                let orn = l.orns.get(i as usize).copied().unwrap_or(0);
                if orn > 0 {
                    let ch = match orn {
                        1 => "f",
                        2 => "d",
                        _ => "z",
                    };
                    Text::with_alignment(ch, Point::new(cx, cy + 4), MonoTextStyle::new(&FONT_6X10, BG), Alignment::Center).draw(d)?;
                }
            }
        }
    }
    Ok(())
}

// ============================= KONNAKOL ===================================
// Spoken-rhythm solkattu syllables for the master lane's subdivision (nadai = lcm/beats).
const SOLKATTU: [&[&str]; 9] = [
    &["ta"],                                              // 0 (unused)
    &["ta"],                                              // 1
    &["ta", "ka"],                                        // 2
    &["ta", "ki", "ta"],                                  // 3
    &["ta", "ka", "di", "mi"],                            // 4
    &["ta", "ka", "ta", "ki", "ta"],                      // 5
    &["ta", "ki", "ta", "ta", "ki", "ta"],               // 6
    &["ta", "ka", "ta", "ki", "ta", "ki", "ta"],         // 7
    &["ta", "ka", "di", "mi", "ta", "ka", "ju", "nu"],   // 8
];

fn draw_konnakol<D>(d: &mut D, s: &Screen) -> Result<(), D::Error>
where
    D: DrawTarget<Color = Rgb565>,
{
    let bb = d.bounding_box();
    let w = bb.size.width as i32;
    let h = bb.size.height as i32;
    header(d, s.name, s.bpm, w)?;

    let master = match master_groups(s) {
        Some(l) => l,
        None => return Ok(()),
    };
    let groups: &[u32] = if master.groups.is_empty() { &[4] } else { master.groups };
    let beats: u32 = groups.iter().sum::<u32>().max(1);
    let res = grid_res(s);
    let sub = ((res + beats / 2) / beats).max(1); // nadai (round)
    let bols = SOLKATTU[(sub as usize).min(8)];

    let m = 14;
    let x0 = m;
    let x1 = w - m;
    let gw_px = x1 - x0;
    let col_w = gw_px / (beats * sub) as i32;
    let cy = h / 2;

    let mut starts = [false; 64];
    {
        let mut acc = 0usize;
        for &g in groups {
            if acc < 64 {
                starts[acc] = true;
            }
            acc += g as usize;
        }
    }

    let big = MonoTextStyle::new(&FONT_10X20, INK);
    let big_acc = MonoTextStyle::new(&FONT_10X20, ACCENT);
    let big_ghost = MonoTextStyle::new(&FONT_10X20, GHOST);
    let small = MonoTextStyle::new(&FONT_6X10, FAINT);

    for b in 0..beats {
        let gs = (b as usize) < 64 && starts[b as usize];
        let sam = b == 0;
        let marker = if sam { "X" } else if gs { "O" } else { "." };
        let mx = x0 + ((2 * b * sub + sub) as i32 * col_w) / 2;
        Text::with_alignment(marker, Point::new(mx, cy - 22), small, Alignment::Center).draw(d)?;
        for sidx in 0..sub {
            let idx = b * sub + sidx;
            let cx = x0 + ((2 * idx + 1) as i32 * col_w) / 2;
            let bol = bols[(sidx as usize) % bols.len()];
            let style = if sam && sidx == 0 {
                big_acc
            } else if sidx == 0 {
                big
            } else {
                big_ghost
            };
            Text::with_alignment(bol, Point::new(cx, cy + 6), style, Alignment::Center).draw(d)?;
        }
    }
    // beat/group dividers
    for b in 0..=beats {
        let lx = x0 + (b * sub) as i32 * col_w;
        let bright = ((b as usize) < 64 && starts[b as usize]) || b == 0 || b == beats;
        vline(d, lx, cy - 30, cy + 22, if bright { GHOST } else { FAINT }, 1)?;
    }
    // footer: tala + nadai
    let foot = MonoTextStyle::new(&FONT_6X10, GHOST);
    Text::new("tala / nadai", Point::new(m, h - 12), foot).draw(d)?;
    let mut nb = [0u8; 12];
    Text::with_alignment(fmt_u32(sub, &mut nb), Point::new(w - 14, h - 12), foot, Alignment::Right).draw(d)?;
    Ok(())
}
