/* =========================================================================
   PM_E-2 NOTATION ENGINE  —  inlined into pm_e-2.html by build.sh.
   Engraves a groove as a 5-line drum staff onto a 2D <canvas>, using the
   Bravura SMuFL music font (subset inlined via @BUILD:bravura@). The draw API
   is deliberately IMMEDIATE-MODE and mirrors embedded-graphics (drawGlyph /
   line / rect) so the layout math ports near-mechanically to the device
   (rust/pm-ui). Pure view over a normalized model — no engine.js internals.

   model = {
     name, bpm, playing, phase,           // phase 0..1 across the master bar (playhead)
     lanes: [ { sound, groups:[Int], sub, swing, poly, muted,
                levels:[0..3], orns:[0..3] } ]   // levels: rest/normal/accent/ghost
   }
   ========================================================================= */
const NOTATION = (() => {
  // SMuFL codepoints (resolved from glyphnames.json by tools/bravura/subset.py — keep in sync).
  const GLYPH = {
    clef: 0xe069,
    black: 0xe0a4, x: 0xe0a9, circleX: 0xe0b3, half: 0xe0a3, whole: 0xe0a2,
    parenL: 0xe0f5, parenR: 0xe0f6,
    flag8U: 0xe240, flag8D: 0xe241, flag16U: 0xe242, flag16D: 0xe243,
    restW: 0xe4e3, restH: 0xe4e4, restQ: 0xe4e5, rest8: 0xe4e6, rest16: 0xe4e7,
    accentA: 0xe4a0, accentB: 0xe4a1, dot: 0xe1e7,
    sig: [0xe080, 0xe081, 0xe082, 0xe083, 0xe084, 0xe085, 0xe086, 0xe087, 0xe088, 0xe089],
    sigPlus: 0xe08c, sigCommon: 0xe08a, sigCut: 0xe08b,
    graceAcc: 0xe560, graceSlash: 0xe564,
    trem1: 0xe220, trem2: 0xe221, trem3: 0xe222, buzz: 0xe22a,
  };
  const chr = (cp) => String.fromCodePoint(cp);

  // Voice -> staff position. `p` = half-staff-spaces below the TOP line (top line p=0, each line/space
  // step = 1; bottom line p=8). `head` = notehead glyph; `up` = stem direction (hands up / feet down).
  // PAS-style drum key; refined visually in the browser.
  function voice(name) {
    const s = name || "";
    const F = (p, head, up) => ({ p, head, up });
    if (s.startsWith("kick")) return F(7, "black", false);          // bass drum (feet, stem down)
    if (s.startsWith("snare") || s.startsWith("clap") || s.startsWith("rim")) return F(3, "black", true);
    if (s.startsWith("openHat") || s.startsWith("hatOpen") || s.startsWith("hat")) return F(-1, "x", true);
    if (s.startsWith("ride")) return F(0, "x", true);
    if (s.startsWith("crash")) return F(-3, "x", true);
    if (s.startsWith("tomHigh")) return F(1, "black", true);
    if (s.startsWith("tomMid") || s === "tom808") return F(2, "black", true);
    if (s.startsWith("tomLow") || s.startsWith("tom")) return F(5, "black", true);
    if (s.startsWith("cowbell")) return F(-1, "circleX", true);
    if (s.startsWith("claves") || s.startsWith("woodblock") || s.startsWith("jamblock")) return F(1, "x", true);
    if (s.startsWith("tambourine")) return F(-2, "x", true);
    return F(3, "black", true);
  }

  function gcd(a, b) { while (b) { [a, b] = [b, a % b]; } return a || 1; }
  function lcm(a, b) { return a / gcd(a, b) * b; }

  // --- palette ---
  // The notation panel is engraved like paper: dark ink on a WHITE background, theme-independent
  // (matches print sheet music and reads in both page themes).
  function palette() {
    return {
      ink: "#161a1f",      // staff lines, noteheads, stems, beams
      faint: "#aeb6c0",    // box outlines / gridlines / poly-row baseline
      accent: "#c0392b",   // accents (notehead tint + > mark) — reads on white
      ghost: "#7c8794",    // ghost notes
      play: "#2b7fff",     // playhead
      bg: "#ffffff",
    };
  }

  function draw(canvas, model, opts) {
    const view = (model && model.view) || "staff";
    if (view === "tubs") return drawTUBS(canvas, model);
    if (view === "konnakol") return drawKonnakol(canvas, model);
    opts = opts || {};
    const ctx = canvas.getContext("2d");
    const dpr = window.devicePixelRatio || 1;
    const W = canvas.clientWidth, H = canvas.clientHeight;
    if (canvas.width !== W * dpr || canvas.height !== H * dpr) {
      canvas.width = W * dpr; canvas.height = H * dpr;
    }
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    const pal = palette();
    ctx.clearRect(0, 0, W, H);

    const S = opts.staffSpace || 11;     // staff space in px
    const em = 4 * S;                    // SMuFL: 1 em = 4 staff spaces
    const y0 = (p) => staffTop + p * (S / 2);   // staff-position -> y
    ctx.textAlign = "center";
    ctx.textBaseline = "alphabetic";

    const glyph = (name, x, p, color, scale) => {
      const cp = typeof name === "number" ? name : GLYPH[name];
      ctx.fillStyle = color;
      ctx.font = (em * (scale || 1)) + "px Bravura";
      // SMuFL glyphs sit on the baseline = the reference staff line; fillText baseline aligns there.
      ctx.fillText(chr(cp), x, y0(p));
    };
    const line = (x1, yy1, x2, yy2, color, w) => {
      ctx.strokeStyle = color; ctx.lineWidth = w || 1;
      ctx.beginPath(); ctx.moveTo(x1, yy1); ctx.lineTo(x2, yy2); ctx.stroke();
    };

    // geometry + model
    const m = 14;
    const clefW = em * 0.6;
    const x1 = W - m;
    const staffTop = 56;
    const lanes = (model.lanes || []).filter((l) => !l.muted && l.levels && l.levels.length);
    const onStaff = lanes.filter((l) => !l.poly);   // first non-poly lane = master (defines the meter)

    const groups = (onStaff[0] && onStaff[0].groups && onStaff[0].groups.length) ? onStaff[0].groups : [4];
    const beats = groups.reduce((a, b) => a + b, 0) || 4;
    // time signature: additive numerator (2+2+3) for grouped meters; PM's beat is the quarter -> denom 4.
    const tsDigit = em * 0.4;
    const numParts = groups.length > 1 ? groups : [beats];
    const numGlyphs = numParts.reduce((a, n) => a + String(n).length, 0) + (numParts.length - 1);
    const tsX = m + clefW;
    const tsW = Math.max(numGlyphs, 1) * tsDigit;
    // notes start clear of the time signature, or at a shared gutter (so all views' beats line up)
    const x0 = model.gutter != null ? model.gutter : tsX + tsW + 14;
    const barW = Math.max(1, x1 - x0);

    // ---- header: name + BPM ----
    ctx.textAlign = "left";
    ctx.font = "600 15px system-ui, sans-serif";
    ctx.fillStyle = pal.ink;
    ctx.fillText(model.name || "", m, 26);
    ctx.textAlign = "right";
    ctx.font = "700 18px 'Courier New', monospace";
    ctx.fillStyle = pal.accent;
    ctx.fillText((model.bpm | 0) + " BPM", x1, 26);
    ctx.textAlign = "center";

    // ---- staff + barlines + clef + time signature ----
    for (let i = 0; i < 5; i++) line(m, staffTop + i * S, x1, staffTop + i * S, pal.ink, 1);
    line(m, staffTop, m, staffTop + 4 * S, pal.ink, 1.5);
    line(x1, staffTop, x1, staffTop + 4 * S, pal.ink, 1.5);
    glyph("clef", m + clefW * 0.5, 4, pal.ink);          // percussion clef centered on middle line (p=4)

    drawTimeSig(tsX, tsW, numParts, tsDigit);

    // ---- time grid: lcm of ALL lanes' step counts (incl. polyrhythm `~` lanes) → the right common
    // time scale so every voice, including cross-rhythms, sits on ONE staff at aligned columns ----
    let res = 1;
    for (const l of lanes) res = lcm(res, l.levels.length);
    res = Math.max(res, 1);
    const beamable = res / Math.max(beats, 1) >= 2;                 // bar has subdivisions → beam within beats
    ctx.font = em + "px Bravura";
    const headHalf = ctx.measureText(chr(GLYPH.black)).width / 2;   // real notehead half-width → stems touch it

    // beaming state per stem direction (carry previous column's stem x within a beat)
    let upPrev = null, dnPrev = null;
    const upTip = staffTop - S * 2.6, dnTip = staffTop + 4 * S + S * 2.6;

    for (let c = 0; c < res; c++) {
      const cx = x0 + (c + 0.5) * barW / res;
      const beat = Math.floor(c * beats / res);
      let up = null, dn = null;   // {loP, hiP, sub2} accumulators per direction

      for (const l of lanes) {
        const steps = l.levels.length;
        if ((c * steps) % res !== 0) continue;            // no note for this lane at this column
        const si = (c * steps / res) | 0;
        const lvl = l.levels[si] | 0;
        if (!lvl) continue;
        const orn = (l.orns && l.orns[si]) | 0;
        const vc = voice(l.sound);
        const color = lvl === 2 ? pal.accent : lvl === 3 ? pal.ghost : pal.ink;

        // ghost = parenthesized notehead
        if (lvl === 3) glyph("parenL", cx - S * 0.85, vc.p, color);
        const head = vc.head === "x" ? "x" : vc.head === "circleX" ? "circleX" : "black";
        glyph(head, cx, vc.p, color);
        if (lvl === 3) glyph("parenR", cx + S * 0.85, vc.p, color);

        // accent mark above/below the staff edge
        if (lvl === 2) glyph(vc.up ? "accentA" : "accentB", cx, vc.up ? -2 : 10, color, 0.8);

        // ornaments: flam = slashed grace note up-left; roll = tremolo strokes on the stem
        if (orn === 1) glyph("graceSlash", cx - S * 1.4, vc.p - 0.5, color, 0.7);
        else if (orn === 2) { glyph("graceSlash", cx - S * 1.9, vc.p - 0.5, color, 0.7); glyph("graceSlash", cx - S * 1.1, vc.p - 0.5, color, 0.7); }
        else if (orn === 3) glyph("trem3", cx + (vc.up ? S * 0.55 : -S * 0.55), vc.p + (vc.up ? -2 : 2), color, 0.8);

        // ledger lines
        for (let lp = -2; lp >= vc.p; lp -= 2) line(cx - S * 0.9, y0(lp), cx + S * 0.9, y0(lp), pal.ink, 1);
        for (let lp = 10; lp <= vc.p; lp += 2) line(cx - S * 0.9, y0(lp), cx + S * 0.9, y0(lp), pal.ink, 1);

        const sub2 = beamable;
        const acc = vc.up ? (up = up || { loP: -99, hiP: 99, sub2: false }) : (dn = dn || { loP: -99, hiP: 99, sub2: false });
        acc.loP = Math.max(acc.loP, vc.p);   // lowest (largest p)
        acc.hiP = Math.min(acc.hiP, vc.p);   // highest (smallest p)
        acc.sub2 = acc.sub2 || sub2;
      }

      // shared up-stem (hands): right side of head, from lowest head up past the highest
      if (up) {
        const sx = cx + headHalf;
        const top = Math.min(upTip, y0(up.hiP) - S * 1.2);
        line(sx, y0(up.loP), sx, top, pal.ink, 1.4);
        if (up.sub2 && upPrev && upPrev.beat === beat) line(upPrev.x, top, sx, top, pal.ink, 3);
        else if (up.sub2) {} // first of a beam group; flag drawn only if it stays solo (handled below)
        upPrev = up.sub2 ? { x: sx, beat, y: top } : null;
      } else upPrev = null;
      // shared down-stem (feet): left side
      if (dn) {
        const sx = cx - headHalf;
        const bot = Math.max(dnTip, y0(dn.loP) + S * 1.2);
        line(sx, y0(dn.hiP), sx, bot, pal.ink, 1.4);
        if (dn.sub2 && dnPrev && dnPrev.beat === beat) line(dnPrev.x, bot, sx, bot, pal.ink, 3);
        dnPrev = dn.sub2 ? { x: sx, beat, y: bot } : null;
      } else dnPrev = null;
    }

    // ---- tuplet number: the common subdivision per beat (3=triplet, 6=sextuplet, 5, 7…). For beamed
    // groups the modern convention is just the numeral over the beam (no bracket). ----
    const isPow2 = (n) => n > 0 && (n & (n - 1)) === 0;
    const tupN = Math.round(res / Math.max(1, beats));
    if (tupN >= 3 && !isPow2(tupN)) {
      ctx.fillStyle = pal.ink; ctx.textAlign = "center";
      ctx.font = "italic 600 13px Georgia, 'Times New Roman', serif";
      const ty = upTip - S * 0.7;
      for (let b = 0; b < beats; b++) ctx.fillText(String(tupN), x0 + (b + 0.5) * barW / beats, ty);
    }

    // ---- playhead ----
    // `phase` is the master-bar fraction (0..1). Noteheads sit at column CENTERS ((c+0.5)/res), so
    // shift the line by half a cell to land exactly on the note at its onset instead of leading it.
    if (model.playing && model.phase != null) {
      const pf = Math.max(0, Math.min(1, model.phase + 0.5 / res));
      const px = x0 + pf * barW;
      ctx.save(); ctx.globalAlpha = 0.55;
      line(px, staffTop - S, px, staffTop + 4 * S + S, pal.play, 2);
      ctx.restore();
    }

    // ---- hit map for on-staff editing (all in CSS px) ----
    // Each on-staff lane exposes its staff row (p) + column geometry; the page maps a click to the
    // nearest voice row (y) and the column (x) → (laneIndex, step). `idx` indexes model.lanes/meters.
    canvas._hit = {
      kind: "staff", staffTop, S, x0, barW,
      lanes: lanes.map((l) => ({ idx: l.idx, p: voice(l.sound).p, steps: l.levels.length })),
    };

    // time signature: numerator (additive parts joined by timeSigPlus) over a quarter-note denominator,
    // each row centered within the reserved width `tw`. Defined here so it closes over glyph()/em/pal.
    function drawTimeSig(tx, tw, parts, dw) {
      drawSigRow(tx, tw, 2, parts, dw);   // numerator in the upper half of the staff
      drawSigRow(tx, tw, 6, [4], dw);     // denominator (PM beat = quarter) in the lower half
    }
    function drawSigRow(tx, tw, p, parts, dw) {
      const seq = [];
      parts.forEach((n, i) => {
        if (i) seq.push(GLYPH.sigPlus);
        String(n).split("").forEach((d) => seq.push(GLYPH.sig[+d]));
      });
      let xx = tx + (tw - seq.length * dw) / 2 + dw / 2;
      for (const cp of seq) { glyph(cp, xx, p, pal.ink, 0.92); xx += dw; }
    }
  }

  function drawPolyRow(ctx, glyph, line, pal, l, x0, x1, py, S) {
    ctx.textAlign = "left"; ctx.font = "11px system-ui, sans-serif"; ctx.fillStyle = pal.ghost;
    ctx.fillText(l.sound + " ~", x0, py - S * 1.6); ctx.textAlign = "center";
    line(x0, py, x1, py, pal.faint, 1);
    const steps = l.levels.length, barW = x1 - x0;
    const vc = voice(l.sound);
    for (let i = 0; i < steps; i++) {
      const lvl = l.levels[i] | 0; if (!lvl) continue;
      const cx = x0 + (i + 0.5) * barW / steps;
      const color = lvl === 2 ? pal.accent : lvl === 3 ? pal.ghost : pal.ink;
      const head = vc.head === "x" ? "x" : "black";
      ctx.fillStyle = color; ctx.font = (4 * S) + "px Bravura";
      ctx.fillText(chr(vc.head === "x" ? GLYPH.x : GLYPH.black), cx, py);
    }
  }

  // ---- shared setup for the alternate views ----
  function begin(canvas) {
    const ctx = canvas.getContext("2d");
    const dpr = window.devicePixelRatio || 1;
    const W = canvas.clientWidth, H = canvas.clientHeight;
    if (canvas.width !== W * dpr || canvas.height !== H * dpr) { canvas.width = W * dpr; canvas.height = H * dpr; }
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.clearRect(0, 0, W, H);
    return { ctx, W, H, pal: palette() };
  }
  function header(ctx, W, model, pal) {
    ctx.textAlign = "left"; ctx.font = "600 15px system-ui, sans-serif"; ctx.fillStyle = pal.ink;
    ctx.fillText(model.name || "", 14, 26);
    ctx.textAlign = "right"; ctx.font = "700 18px 'Courier New', monospace"; ctx.fillStyle = pal.accent;
    ctx.fillText((model.bpm | 0) + " BPM", W - 14, 26);
    ctx.textAlign = "center";
  }
  // Son/rumba clave fingerprint: split the bar in half, count hits each side → 2-3 or 3-2.
  function claveLabel(l) {
    if (!/^clave/.test(l.sound || "")) return "";
    const n = l.levels.length, h = n >> 1;
    if (!h) return "(clave)";
    const a = l.levels.slice(0, h).filter((v) => v > 0).length;
    const b = l.levels.slice(h).filter((v) => v > 0).length;
    return a === 2 && b === 3 ? "(2-3)" : a === 3 && b === 2 ? "(3-2)" : "(clave)";
  }

  // ---- TUBS (Time Unit Box System): rows = voices, columns = time units, filled boxes = hits ----
  // All bar-sharing lanes are drawn on ONE common time grid (lcm of their step counts) so every
  // column lines up vertically across rows; a coarser lane just fills every Nth cell. Polymeter
  // lanes keep their own spacing across the full width (that IS the cross-rhythm).
  function drawTUBS(canvas, model) {
    const { ctx, W, H, pal } = begin(canvas);
    header(ctx, W, model, pal);
    const lanes = (model.lanes || []).filter((l) => !l.muted && l.levels && l.levels.length);
    if (!lanes.length) { canvas._hit = null; return; }
    const m = 14, x0 = model.gutter != null ? model.gutter : m + 96, x1 = W - m, gw = Math.max(1, x1 - x0);
    const top = 42, bot = H - 12;
    const rowH = Math.min(40, Math.max(20, (bot - top) / lanes.length));

    // common grid = lcm of ALL lanes' step counts so columns line up; each lane draws ONE box per
    // REAL step at its grid column → aligned AND each box is a clickable step.
    let res = 1; for (const l of lanes) res = lcm(res, l.levels.length);
    res = Math.max(res, 1);
    const cw = gw / res, bs = Math.max(11, Math.min(rowH - 8, cw - 3, 30));
    const master = lanes.find((l) => !l.poly) || lanes[0];
    const mg = master.groups && master.groups.length ? master.groups : [4];
    const mbeats = mg.reduce((a, b) => a + b, 0) || 1;
    const starts = new Set(); let acc = 0; for (const g of mg) { starts.add(acc); acc += g; }
    const yA = top - 4, yB = top + lanes.length * rowH;

    // beat / group dividers (group starts brighter)
    for (let b = 0; b <= mbeats; b++) {
      const lx = x0 + (b * res / mbeats) * cw;
      ctx.strokeStyle = (b < mbeats && starts.has(b)) || b === 0 || b === mbeats ? pal.ghost : pal.faint;
      ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(lx, yA); ctx.lineTo(lx, yB); ctx.stroke();
    }
    // playhead column (aligned to box centers like the staff)
    if (model.playing && model.phase != null) {
      const px = x0 + Math.max(0, Math.min(1, model.phase + 0.5 / res)) * gw;
      ctx.save(); ctx.globalAlpha = 0.5; ctx.strokeStyle = pal.play; ctx.lineWidth = 2;
      ctx.beginPath(); ctx.moveTo(px, yA); ctx.lineTo(px, yB); ctx.stroke(); ctx.restore();
    }

    const rows = [];
    lanes.forEach((l, r) => {
      const cy = top + r * rowH + rowH / 2;
      ctx.textAlign = "left"; ctx.font = "12px system-ui, sans-serif"; ctx.fillStyle = pal.ink;
      ctx.fillText(l.sound + (l.poly ? " ~" : "") + (claveLabel(l) ? "  " + claveLabel(l) : ""), m, cy + 4);
      const steps = l.levels.length, span = res / steps;
      rows.push({ idx: l.idx, steps, span });
      for (let i = 0; i < steps; i++) {
        const cx = x0 + (i * span + 0.5) * cw, lvl = l.levels[i] | 0;
        ctx.strokeStyle = pal.faint; ctx.lineWidth = 1; ctx.strokeRect(cx - bs / 2, cy - bs / 2, bs, bs);
        if (lvl > 0) {
          ctx.fillStyle = lvl === 2 ? pal.accent : lvl === 3 ? pal.ghost : pal.ink;
          const p = bs * 0.2; ctx.fillRect(cx - bs / 2 + p, cy - bs / 2 + p, bs - 2 * p, bs - 2 * p);
          const orn = (l.orns && l.orns[i]) | 0;          // flam/drag/roll marker inside the box
          if (orn) { ctx.fillStyle = pal.bg; ctx.font = "700 9px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.fillText(orn === 1 ? "f" : orn === 2 ? "d" : "z", cx, cy + 3.5); ctx.textAlign = "left"; }
        }
      }
    });
    // hit map for editing: click a box to cycle dynamic; Shift-click cycles ornament
    canvas._hit = { kind: "tubs", x0, cw, top, rowH, rows };
  }

  // ---- Konnakol: spoken-rhythm syllables (solkattu) for the master lane's subdivision ----
  const SOLKATTU = {
    1: ["ta"], 2: ["ta", "ka"], 3: ["ta", "ki", "ta"], 4: ["ta", "ka", "di", "mi"],
    5: ["ta", "ka", "ta", "ki", "ta"], 6: ["ta", "ki", "ta", "ta", "ki", "ta"],
    7: ["ta", "ka", "ta", "ki", "ta", "ki", "ta"], 8: ["ta", "ka", "di", "mi", "ta", "ka", "ju", "nu"],
  };
  function drawKonnakol(canvas, model) {
    const { ctx, W, H, pal } = begin(canvas);
    canvas._hit = null;
    header(ctx, W, model, pal);
    const lanes = (model.lanes || []).filter((l) => !l.muted && l.levels && l.levels.length);
    const m0 = lanes.find((l) => !l.poly) || lanes[0];
    if (!m0) return;
    const groups = m0.groups && m0.groups.length ? m0.groups : [4];
    const beats = groups.reduce((a, b) => a + b, 0) || 1;
    let res = 1; for (const l of lanes) res = lcm(res, l.levels.length);   // common (finest) grid
    const sub = Math.max(1, Math.round(res / beats));                       // nadai = subdivisions per beat
    const bols = SOLKATTU[sub] || SOLKATTU[4];
    const m = 14, x0 = model.gutter != null ? model.gutter : m, x1 = W - m, gw = x1 - x0, colW = gw / (beats * sub), cy = H / 2;
    const starts = new Set(); let acc = 0; for (const g of groups) { starts.add(acc); acc += g; }
    ctx.textAlign = "center";
    for (let b = 0; b < beats; b++) {
      const gs = starts.has(b), sam = b === 0;
      ctx.font = "10px system-ui, sans-serif"; ctx.fillStyle = pal.faint;
      ctx.fillText(sam ? "X" : gs ? "O" : "·", x0 + (b * sub + 0.5) * colW, cy - 22);   // sam / anga / beat
      for (let s = 0; s < sub; s++) {
        const idx = b * sub + s, cx = x0 + (idx + 0.5) * colW;
        ctx.font = (s === 0 ? "600 " : "") + "16px system-ui, sans-serif";
        ctx.fillStyle = sam && s === 0 ? pal.accent : s === 0 ? pal.ink : pal.ghost;
        ctx.fillText(bols[s % bols.length], cx, cy + 6);
      }
    }
    for (let b = 0; b <= beats; b++) {                    // beat/group dividers
      const lx = x0 + b * sub * colW;
      ctx.strokeStyle = starts.has(b) || b === 0 || b === beats ? pal.ghost : pal.faint;
      ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(lx, cy - 30); ctx.lineTo(lx, cy + 22); ctx.stroke();
    }
    ctx.textAlign = "left"; ctx.font = "11px system-ui, sans-serif"; ctx.fillStyle = pal.ghost;
    ctx.fillText("tala " + groups.join("+") + "  ·  nadai " + sub + "  ·  X=sam  O=anga", m, H - 12);
  }

  return { draw, GLYPH, voice };
})();
