// music.jsx — procedural music engine for Sendly
// Generates short looping tunes via the Web Audio API.
// Tracks the last-played history in localStorage so the same visitor doesn't
// hear the same track twice within 7 days.

const { useState, useEffect, useRef } = React;

const HISTORY_KEY = "sendly:music-history";
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
const MUTE_KEY = "sendly:music-muted";

/* ----------------------- Engine helpers ----------------------- */
function midi(n) { return 440 * Math.pow(2, (n - 69) / 12); }

class MusicEngine {
  constructor() {
    this.ctx = null;
    this.master = null;
    this.activeNodes = [];
    this.timers = [];
    this.playing = false;
    this.currentTrack = null;
    this.playToken = 0; // bumped on every play()/stop() — in-flight calls check this before scheduling
    this.muted = (typeof localStorage !== "undefined" && localStorage.getItem(MUTE_KEY) === "1");
    this.listeners = new Set();
  }
  _emit() { this.listeners.forEach(fn => fn()); }
  on(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); }

  _init() {
    if (this.ctx) return;
    const AC = window.AudioContext || window.webkitAudioContext;
    if (!AC) return;
    this.ctx = new AC();
    this.master = this.ctx.createGain();
    this.master.gain.value = this.muted ? 0 : 0.12;

    // Subtle high-shelf cut so it doesn't feel piercing in the browser
    const lp = this.ctx.createBiquadFilter();
    lp.type = "lowpass";
    lp.frequency.value = 4800;
    lp.Q.value = 0.4;
    this.master.connect(lp);
    lp.connect(this.ctx.destination);
  }

  // --- low-level note helpers ---
  _note({ freq, t, dur, gain = 0.3, type = "triangle", attack = 0.01, release = 0.1, detune = 0 }) {
    const ctx = this.ctx;
    const osc = ctx.createOscillator();
    const env = ctx.createGain();
    osc.type = type;
    osc.frequency.value = freq;
    osc.detune.value = detune;
    const start = ctx.currentTime + t;
    env.gain.setValueAtTime(0, start);
    env.gain.linearRampToValueAtTime(gain, start + attack);
    env.gain.exponentialRampToValueAtTime(0.0001, start + Math.max(dur, attack + 0.02));
    osc.connect(env); env.connect(this.master);
    osc.start(start);
    osc.stop(start + dur + release);
    this.activeNodes.push(osc);
  }

  _kick(t) {
    const ctx = this.ctx;
    const osc = ctx.createOscillator();
    const env = ctx.createGain();
    const start = ctx.currentTime + t;
    osc.frequency.setValueAtTime(120, start);
    osc.frequency.exponentialRampToValueAtTime(40, start + 0.18);
    env.gain.setValueAtTime(0.55, start);
    env.gain.exponentialRampToValueAtTime(0.001, start + 0.2);
    osc.connect(env); env.connect(this.master);
    osc.start(start); osc.stop(start + 0.25);
    this.activeNodes.push(osc);
  }

  _hat(t, dur = 0.04) {
    const ctx = this.ctx;
    const bufferSize = 0.1 * ctx.sampleRate;
    const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) data[i] = (Math.random() * 2 - 1);
    const src = ctx.createBufferSource();
    src.buffer = buffer;
    const hp = ctx.createBiquadFilter();
    hp.type = "highpass";
    hp.frequency.value = 7000;
    const env = ctx.createGain();
    const start = ctx.currentTime + t;
    env.gain.setValueAtTime(0.13, start);
    env.gain.exponentialRampToValueAtTime(0.001, start + dur);
    src.connect(hp); hp.connect(env); env.connect(this.master);
    src.start(start); src.stop(start + dur + 0.02);
    this.activeNodes.push(src);
  }

  _snap(t) {
    // a soft clap/snap built from noise
    const ctx = this.ctx;
    const bufferSize = 0.12 * ctx.sampleRate;
    const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) data[i] = (Math.random() * 2 - 1);
    const src = ctx.createBufferSource();
    src.buffer = buffer;
    const bp = ctx.createBiquadFilter();
    bp.type = "bandpass"; bp.frequency.value = 1500; bp.Q.value = 1.3;
    const env = ctx.createGain();
    const start = ctx.currentTime + t;
    env.gain.setValueAtTime(0.22, start);
    env.gain.exponentialRampToValueAtTime(0.001, start + 0.12);
    src.connect(bp); bp.connect(env); env.connect(this.master);
    src.start(start); src.stop(start + 0.14);
    this.activeNodes.push(src);
  }

  // FM-style bell — for music-box and glockenspiel timbres
  // The optional `wobble` adds a tiny slow vibrato so it feels like a real wind-up mechanism.
  _bell({ freq, t, dur = 1.6, gain = 0.22, wobble = true }) {
    const ctx = this.ctx;
    const carrier = ctx.createOscillator();
    const modulator = ctx.createOscillator();
    const modGain = ctx.createGain();
    const env = ctx.createGain();
    carrier.type = "sine";
    modulator.type = "sine";
    carrier.frequency.value = freq;
    modulator.frequency.value = freq * 3.0; // bell-like ratio
    modGain.gain.value = freq * 2.0;
    const start = ctx.currentTime + t;
    env.gain.setValueAtTime(0, start);
    env.gain.linearRampToValueAtTime(gain, start + 0.005);
    env.gain.exponentialRampToValueAtTime(0.0001, start + dur);
    modulator.connect(modGain); modGain.connect(carrier.frequency);
    carrier.connect(env); env.connect(this.master);
    // Subtle wind-up wobble — makes the bell feel like a real music-box comb
    let vib, vibGain;
    if (wobble) {
      vib = ctx.createOscillator();
      vibGain = ctx.createGain();
      vib.frequency.value = 4.5;
      vibGain.gain.value = freq * 0.004;
      vib.connect(vibGain); vibGain.connect(carrier.frequency);
      vib.start(start); vib.stop(start + dur + 0.05);
      this.activeNodes.push(vib);
    }
    modulator.start(start); carrier.start(start);
    modulator.stop(start + dur + 0.05); carrier.stop(start + dur + 0.05);
    this.activeNodes.push(carrier, modulator);
  }

  // Music-box comb pluck — brighter, shorter, more mechanical than _bell
  _comb({ freq, t, dur = 0.9, gain = 0.18 }) {
    const ctx = this.ctx;
    // two partials: fundamental + an inharmonic high partial (real combs have one)
    const fund = ctx.createOscillator();
    const part = ctx.createOscillator();
    const env = ctx.createGain();
    const env2 = ctx.createGain();
    fund.type = "sine"; part.type = "sine";
    fund.frequency.value = freq;
    part.frequency.value = freq * 4.2; // metallic shimmer partial
    const start = ctx.currentTime + t;
    env.gain.setValueAtTime(0, start);
    env.gain.linearRampToValueAtTime(gain, start + 0.002);
    env.gain.exponentialRampToValueAtTime(0.0001, start + dur);
    env2.gain.setValueAtTime(0, start);
    env2.gain.linearRampToValueAtTime(gain * 0.35, start + 0.002);
    env2.gain.exponentialRampToValueAtTime(0.0001, start + dur * 0.35);
    fund.connect(env); part.connect(env2);
    env.connect(this.master); env2.connect(this.master);
    fund.start(start); part.start(start);
    fund.stop(start + dur + 0.05); part.stop(start + dur * 0.4 + 0.05);
    this.activeNodes.push(fund, part);
  }

  // Warm sustained pad — for strings/accordion swells
  _pad({ freq, t, dur, gain = 0.08, detune = 0 }) {
    const ctx = this.ctx;
    // Two slightly detuned oscillators for chorus warmth
    const a = ctx.createOscillator();
    const b = ctx.createOscillator();
    const env = ctx.createGain();
    const lp = ctx.createBiquadFilter();
    a.type = "sawtooth"; b.type = "sawtooth";
    a.frequency.value = freq; b.frequency.value = freq;
    a.detune.value = detune - 8; b.detune.value = detune + 8;
    lp.type = "lowpass"; lp.frequency.value = 1600; lp.Q.value = 0.6;
    const start = ctx.currentTime + t;
    env.gain.setValueAtTime(0, start);
    env.gain.linearRampToValueAtTime(gain, start + 0.35);
    env.gain.setValueAtTime(gain, start + dur - 0.4);
    env.gain.exponentialRampToValueAtTime(0.0001, start + dur);
    a.connect(lp); b.connect(lp); lp.connect(env); env.connect(this.master);
    a.start(start); b.start(start);
    a.stop(start + dur + 0.05); b.stop(start + dur + 0.05);
    this.activeNodes.push(a, b);
  }

  // Soft plucked-string (piano/guitar-ish)
  _pluck({ freq, t, dur = 1.2, gain = 0.2 }) {
    const ctx = this.ctx;
    const osc = ctx.createOscillator();
    const env = ctx.createGain();
    const lp = ctx.createBiquadFilter();
    osc.type = "triangle";
    osc.frequency.value = freq;
    lp.type = "lowpass"; lp.frequency.value = 2400; lp.Q.value = 0.5;
    const start = ctx.currentTime + t;
    env.gain.setValueAtTime(0, start);
    env.gain.linearRampToValueAtTime(gain, start + 0.008);
    env.gain.exponentialRampToValueAtTime(0.0001, start + dur);
    osc.connect(lp); lp.connect(env); env.connect(this.master);
    osc.start(start); osc.stop(start + dur + 0.05);
    this.activeNodes.push(osc);
  }

  /* ----------------------- public API ----------------------- */
  async play(track) {
    // Bump the token FIRST so any earlier in-flight play() bails out after its await.
    const myToken = ++this.playToken;
    this.stop({ keepToken: true });
    this._init();
    if (!this.ctx) return false;
    if (this.ctx.state === "suspended") {
      try { await this.ctx.resume(); } catch (e) { /* autoplay blocked */ }
    }
    // If another play() started while we were awaiting, abandon this one.
    if (myToken !== this.playToken) return false;
    if (this.ctx.state !== "running") return false;

    this.currentTrack = track;
    this.playing = true;
    this._emit();
    track.play(this);
    return true;
  }

  stop(opts = {}) {
    if (!opts.keepToken) this.playToken++;
    this.playing = false;
    this.activeNodes.forEach(n => { try { n.stop(); } catch (e) {} });
    this.activeNodes = [];
    this.timers.forEach(t => clearTimeout(t));
    this.timers = [];
    this._emit();
  }

  setMuted(m) {
    this.muted = m;
    if (this.master) this.master.gain.value = m ? 0 : 0.12;
    try { localStorage.setItem(MUTE_KEY, m ? "1" : "0"); } catch (e) {}
    this._emit();
  }

  /* schedule a callback at `t` seconds from now (relative to play start) */
  _at(secs, fn) {
    const id = setTimeout(fn, secs * 1000);
    this.timers.push(id);
  }

  /* loop one measure of the given function every `measureSec` seconds */
  _loop(measureSec, renderMeasure) {
    let offset = 0;
    const run = () => {
      if (!this.playing) return;
      // schedule notes for this measure relative to AudioContext time
      renderMeasure(0); // notes use _note with t relative to "now"
      this._at(measureSec - 0.02, run);
    };
    run();
  }
}

/* ============================ TRACKS ============================
   Each track is a function(engine) that begins looping notes.
   Keep them short and distinct so visitors can tell them apart.
================================================================== */
const TRACKS = [
  {
    id: "little-box",
    name: "The Little Box",
    emoji: "🎁",
    play: (e) => {
      // Classic jewelry-box reveal — delicate, slow, in C major
      const bpm = 68, beat = 60 / bpm, measure = 4 * beat;
      // I – vi – IV – V (the most "opened a music box" progression)
      const chords = [
        { root: 48, tone: [72, 76, 79] }, // C
        { root: 45, tone: [69, 72, 76] }, // Am
        { root: 53, tone: [72, 77, 81] }, // F
        { root: 55, tone: [74, 79, 83] }, // G
      ];
      // an original twinkling melody (not a known tune)
      const phrases = [
        [79, 76, 72, 76, 79, 84, 79, 76],
        [76, 72, 69, 72, 76, 79, 76, 72],
        [77, 81, 84, 81, 77, 72, 77, 81],
        [79, 74, 71, 74, 79, 83, 79, 74],
      ];
      let bar = 0;
      const renderMeasure = () => {
        const ch = chords[bar % chords.length];
        const ph = phrases[bar % phrases.length];
        // sparkling comb melody on top
        ph.forEach((n, i) => {
          e._comb({ freq: midi(n + 12), t: i * (beat / 2), dur: 0.9, gain: 0.13 });
        });
        // slow bell tones underneath, sustained
        ch.tone.forEach((n, i) => {
          e._bell({ freq: midi(n), t: i * 0.04, dur: measure * 0.95, gain: 0.06 });
        });
        // bass tick on 1
        e._bell({ freq: midi(ch.root), t: 0, dur: 2.0, gain: 0.1, wobble: false });
        bar++;
      };
      e._loop(measure, renderMeasure);
    }
  },
  {
    id: "ballerina-turn",
    name: "Ballerina's Turn",
    emoji: "🩰",
    play: (e) => {
      // The pirouetting jewelry-box ballerina — 3/4 waltz on music-box comb
      const bpm = 96, beat = 60 / bpm, measure = 3 * beat;
      // F major waltz progression
      const chords = [
        { root: 41, top: [65, 69, 72] }, // F
        { root: 43, top: [67, 70, 74] }, // Gm
        { root: 41, top: [65, 72, 74] }, // F/A
        { root: 48, top: [67, 72, 76] }, // C
      ];
      // original waltz melody, all in F major
      const mels = [
        [77, 81, 84],
        [82, 79, 77],
        [81, 84, 81],
        [79, 76, 72],
      ];
      let bar = 0;
      const renderMeasure = () => {
        const ch = chords[bar % chords.length];
        // oom-pa-pa on the comb
        e._comb({ freq: midi(ch.root), t: 0, dur: beat * 0.7, gain: 0.18 });
        e._comb({ freq: midi(ch.top[0]), t: beat, dur: beat * 0.6, gain: 0.12 });
        e._comb({ freq: midi(ch.top[1]), t: beat, dur: beat * 0.6, gain: 0.10 });
        e._comb({ freq: midi(ch.top[0]), t: beat * 2, dur: beat * 0.6, gain: 0.12 });
        e._comb({ freq: midi(ch.top[2]), t: beat * 2, dur: beat * 0.6, gain: 0.10 });
        // slow melody (half-bar notes) high above
        const m = mels[bar % mels.length];
        m.forEach((n, i) => {
          e._bell({ freq: midi(n + 12), t: i * beat, dur: beat * 1.4, gain: 0.10 });
        });
        bar++;
      };
      e._loop(measure, renderMeasure);
    }
  },
  {
    id: "wind-up-lullaby",
    name: "Wind-Up Lullaby",
    emoji: "🌙",
    play: (e) => {
      // Slow, sleepy music-box lullaby — like a wind-up toy beside a bed
      const bpm = 60, beat = 60 / bpm, measure = 4 * beat;
      // G major: I – V – vi – IV
      const chords = [
        { root: 43, notes: [67, 71, 74] }, // G
        { root: 50, notes: [62, 66, 69] }, // D
        { root: 40, notes: [64, 67, 71] }, // Em
        { root: 48, notes: [60, 64, 67] }, // C
      ];
      // gentle descending phrases (lullaby contour)
      const phrases = [
        [79, 78, 76, 74, 72, 71, 67, 71],
        [78, 76, 74, 72, 71, 69, 66, 69],
        [76, 74, 72, 71, 67, 64, 67, 71],
        [72, 71, 69, 67, 64, 60, 67, 72],
      ];
      let bar = 0;
      const renderMeasure = () => {
        const ch = chords[bar % chords.length];
        const ph = phrases[bar % phrases.length];
        ph.forEach((n, i) => {
          e._comb({ freq: midi(n + 12), t: i * (beat / 2), dur: 1.4, gain: 0.11 });
        });
        // soft sustained pad underneath — the felt of the lid
        ch.notes.forEach(n => {
          e._pad({ freq: midi(n), t: 0, dur: measure * 0.95, gain: 0.035 });
        });
        // very slow root bell on beat 1
        e._bell({ freq: midi(ch.root), t: 0, dur: 3.0, gain: 0.09 });
        bar++;
      };
      e._loop(measure, renderMeasure);
    }
  },
  {
    id: "snow-globe",
    name: "Snow Globe",
    emoji: "❄️",
    play: (e) => {
      // Tiny chimes drifting through a glass globe — the music inside a souvenir
      const bpm = 72, beat = 60 / bpm, measure = 4 * beat;
      // Em – C – G – D (gentle wonder)
      const chords = [
        { root: 40, tone: [64, 67, 71] },
        { root: 36, tone: [60, 64, 67] },
        { root: 43, tone: [67, 71, 74] },
        { root: 38, tone: [62, 66, 69] },
      ];
      let bar = 0;
      const renderMeasure = () => {
        const ch = chords[bar % chords.length];
        // pad cushion
        ch.tone.forEach((n, i) => {
          e._pad({ freq: midi(n), t: 0, dur: measure * 0.95, gain: 0.04, detune: i * 4 });
        });
        // sparse chime drops — falling snowflakes
        const drops = [
          { n: ch.tone[2] + 12, t: 0 },
          { n: ch.tone[1] + 12, t: beat * 0.75 },
          { n: ch.tone[2] + 19, t: beat * 1.5 },
          { n: ch.tone[0] + 12, t: beat * 2.25 },
          { n: ch.tone[2] + 12, t: beat * 3 },
          { n: ch.tone[1] + 19, t: beat * 3.5 },
        ];
        drops.forEach(d => {
          e._bell({ freq: midi(d.n), t: d.t, dur: 2.8, gain: 0.09 });
        });
        bar++;
      };
      e._loop(measure, renderMeasure);
    }
  },
  {
    id: "carousel-memories",
    name: "Carousel of Memories",
    emoji: "🎠",
    play: (e) => {
      // A music-box carousel theme — turning slowly, lights in the trees
      const bpm = 104, beat = 60 / bpm, measure = 3 * beat;
      // D major waltz — I – IV – V – I
      const chords = [
        { root: 38, top: [62, 66, 69] }, // D
        { root: 43, top: [62, 67, 71] }, // G
        { root: 45, top: [64, 69, 73] }, // A
        { root: 38, top: [62, 66, 69] }, // D
      ];
      // hummable original tune
      const mels = [
        [78, 81, 78, 74, 78],
        [79, 83, 79, 74, 79],
        [80, 78, 76, 73, 76],
        [78, 74, 69, 74, 78],
      ];
      let bar = 0;
      const renderMeasure = () => {
        const ch = chords[bar % chords.length];
        // bass on 1, comb stabs on 2 and 3
        e._comb({ freq: midi(ch.root), t: 0, dur: beat * 0.7, gain: 0.18 });
        e._comb({ freq: midi(ch.top[0]), t: beat, dur: beat * 0.5, gain: 0.10 });
        e._comb({ freq: midi(ch.top[1]), t: beat, dur: beat * 0.5, gain: 0.09 });
        e._comb({ freq: midi(ch.top[2]), t: beat * 2, dur: beat * 0.5, gain: 0.09 });
        e._comb({ freq: midi(ch.top[1]), t: beat * 2, dur: beat * 0.5, gain: 0.09 });
        // melody bells
        const m = mels[bar % mels.length];
        m.forEach((n, i) => {
          e._bell({ freq: midi(n), t: i * (measure / m.length), dur: 1.2, gain: 0.10 });
        });
        bar++;
      };
      e._loop(measure, renderMeasure);
    }
  },
  {
    id: "locket",
    name: "The Locket",
    emoji: "💗",
    play: (e) => {
      // Tiny, intimate, the music inside a kept locket — minor-key tenderness
      const bpm = 76, beat = 60 / bpm, measure = 4 * beat;
      // Am – Em – F – G (wistful, looking back)
      const chords = [
        { root: 45, tone: [69, 72, 76] }, // Am
        { root: 40, tone: [64, 67, 71] }, // Em
        { root: 41, tone: [65, 69, 72] }, // F
        { root: 43, tone: [67, 71, 74] }, // G
      ];
      // descending tender melodies
      const phrases = [
        [81, 80, 76, 72, 76, 80, 76, 72],
        [79, 76, 72, 67, 71, 76, 72, 67],
        [77, 81, 84, 81, 77, 72, 69, 65],
        [79, 83, 79, 74, 71, 67, 71, 74],
      ];
      let bar = 0;
      const renderMeasure = () => {
        const ch = chords[bar % chords.length];
        const ph = phrases[bar % phrases.length];
        ph.forEach((n, i) => {
          e._comb({ freq: midi(n + 12), t: i * (beat / 2), dur: 1.1, gain: 0.11 });
        });
        // sustained pad
        ch.tone.forEach(n => {
          e._pad({ freq: midi(n), t: 0, dur: measure * 0.95, gain: 0.04 });
        });
        // soft heartbeat bass
        e._bell({ freq: midi(ch.root), t: 0, dur: 2.5, gain: 0.09 });
        e._bell({ freq: midi(ch.root), t: beat * 2, dur: 2.0, gain: 0.07 });
        bar++;
      };
      e._loop(measure, renderMeasure);
    }
  },
  {
    id: "wish-upon",
    name: "Wish Upon",
    emoji: "✨",
    play: (e) => {
      // Rising hopeful music-box theme — the wish you made over the candles
      const bpm = 84, beat = 60 / bpm, measure = 4 * beat;
      // F – C – Dm – Bb (a hopeful ascending feel)
      const chords = [
        { root: 41, tone: [65, 69, 72] }, // F
        { root: 48, tone: [67, 72, 76] }, // C
        { root: 38, tone: [65, 69, 74] }, // Dm
        { root: 46, tone: [65, 70, 74] }, // Bb
      ];
      // gently ascending phrases (wishes lifting up)
      const phrases = [
        [72, 74, 77, 81, 84, 81, 77, 74],
        [76, 79, 83, 86, 88, 86, 83, 79],
        [74, 77, 81, 84, 86, 84, 81, 77],
        [70, 74, 77, 82, 86, 82, 77, 74],
      ];
      let bar = 0;
      const renderMeasure = () => {
        const ch = chords[bar % chords.length];
        const ph = phrases[bar % phrases.length];
        ph.forEach((n, i) => {
          e._comb({ freq: midi(n), t: i * (beat / 2), dur: 1.0, gain: 0.11 });
        });
        // pad shimmer
        ch.tone.forEach((n, i) => {
          e._pad({ freq: midi(n), t: 0, dur: measure * 0.95, gain: 0.04, detune: i * 5 });
        });
        // little high sparkle on beat 1 and 3 — like a wish bubble popping
        e._bell({ freq: midi(ch.tone[2] + 24), t: 0, dur: 1.6, gain: 0.07 });
        e._bell({ freq: midi(ch.tone[1] + 24), t: beat * 2, dur: 1.6, gain: 0.06 });
        // bass root
        e._bell({ freq: midi(ch.root), t: 0, dur: measure * 0.95, gain: 0.08, wobble: false });
        bar++;
      };
      e._loop(measure, renderMeasure);
    }
  },
  /* ============================================================
     Public-domain classics — the tunes the whole world remembers.
     All composers d. before 1900; melodies are public domain worldwide.
  ============================================================ */
  {
    id: "twinkle-twinkle",
    name: "Twinkle, Twinkle",
    emoji: "⭐",
    play: (e) => {
      // Mozart's "Ah! vous dirai-je, maman" theme (a.k.a. Twinkle Twinkle / ABC song)
      // The first tune nearly every person on Earth learns. Public domain.
      const bpm = 80, beat = 60 / bpm;
      // Full 12-bar melody, one note per beat
      // C C G G A A G | F F E E D D C | G G F F E E D | G G F F E E D
      // C C G G A A G | F F E E D D C
      const mel = [
        60, 60, 67, 67, 69, 69, 67,  /*rest*/ 65, 65, 64, 64, 62, 62, 60,
        67, 67, 65, 65, 64, 64, 62,  67, 67, 65, 65, 64, 64, 62,
        60, 60, 67, 67, 69, 69, 67,  65, 65, 64, 64, 62, 62, 60,
      ];
      // simple bass — root note per 4-beat group
      const bass = [
        48, 48, 53, 48,
        55, 55, 55, 48,
        48, 48, 53, 48,
      ];
      const tuneLen = mel.length * beat; // total tune in seconds
      const renderMeasure = () => {
        // schedule the whole tune in one shot, then loop
        mel.forEach((n, i) => {
          e._comb({ freq: midi(n + 12), t: i * beat, dur: beat * 1.4, gain: 0.13 });
        });
        bass.forEach((n, i) => {
          e._bell({ freq: midi(n), t: i * (beat * 3.5), dur: beat * 3.2, gain: 0.08, wobble: false });
        });
        // sparkle accents at the start and middle
        e._bell({ freq: midi(84), t: 0, dur: 2.5, gain: 0.07 });
        e._bell({ freq: midi(84), t: tuneLen / 2, dur: 2.5, gain: 0.07 });
      };
      // Use the whole tune as one "measure" so it loops as a complete song
      e._loop(tuneLen + 1.0, renderMeasure);
    }
  },
  {
    id: "brahms-lullaby",
    name: "Brahms' Lullaby",
    emoji: "🌙",
    play: (e) => {
      // Brahms, Wiegenlied Op. 49 No. 4 (1868). Public domain.
      // The music-box lullaby played in cribs around the world.
      // Transposed to C major, 3/4. Approximate phrase shape.
      const bpm = 80, beat = 60 / bpm, measure = 3 * beat;
      // 16-bar simplified arrangement of the melody phrase by phrase
      // Phrase A: G G E' (dotted) | G G E' | G E' C' B A G F E | D C ...
      // We'll lay it out as note+beats arrays per bar (3 beats per bar)
      const phrase = [
        // bar 1-2: "Lul - la - by, and good night"
        [{ n: 67, b: 1 }, { n: 67, b: 1 }, { n: 72, b: 1 }],
        [{ n: 67, b: 1 }, { n: 67, b: 1 }, { n: 72, b: 1 }],
        // bar 3-4: "with pink ro - ses be - dight"
        [{ n: 67, b: 0.5 }, { n: 72, b: 0.5 }, { n: 76, b: 1 }, { n: 74, b: 1 }],
        [{ n: 72, b: 0.5 }, { n: 74, b: 0.5 }, { n: 72, b: 1 }, { n: 71, b: 1 }],
        // bar 5-6: "with li - lies o'er - spread"
        [{ n: 65, b: 1 }, { n: 65, b: 1 }, { n: 71, b: 1 }],
        [{ n: 65, b: 1 }, { n: 65, b: 1 }, { n: 71, b: 1 }],
        // bar 7-8: "is ba - by's wee bed"
        [{ n: 65, b: 0.5 }, { n: 71, b: 0.5 }, { n: 74, b: 1 }, { n: 72, b: 1 }],
        [{ n: 71, b: 0.5 }, { n: 69, b: 0.5 }, { n: 67, b: 2 }],
      ];
      // bass per bar (root of underlying chord — I, V, I, V, IV, IV, V, I)
      const bass = [48, 55, 48, 55, 53, 53, 55, 48];
      let bar = 0;
      const renderMeasure = () => {
        const ph = phrase[bar % phrase.length];
        let t = 0;
        ph.forEach(({ n, b }) => {
          e._comb({ freq: midi(n + 12), t: t * beat, dur: b * beat * 1.1, gain: 0.13 });
          t += b;
        });
        // gentle bass + pad
        const r = bass[bar % bass.length];
        e._bell({ freq: midi(r), t: 0, dur: measure * 0.9, gain: 0.08, wobble: false });
        e._pad({ freq: midi(r + 12), t: 0, dur: measure * 0.9, gain: 0.035 });
        e._pad({ freq: midi(r + 16), t: 0, dur: measure * 0.9, gain: 0.03 });
        bar++;
      };
      e._loop(measure, renderMeasure);
    }
  },
  {
    id: "pachelbel-canon",
    name: "Canon in D",
    emoji: "💍",
    play: (e) => {
      // Pachelbel's Canon in D (c. 1680). Public domain.
      // The chord progression behind a thousand weddings, graduations and goodbyes.
      // Transposed to C major for simplicity. We loop the famous 8-bar ground bass
      // with a flowing eighth-note arpeggio over it (one of the canon's voices).
      const bpm = 72, beat = 60 / bpm, measure = 2 * beat;
      // Pachelbel ground (transposed to C):  C – G – Am – Em – F – C – F – G
      const ground = [
        { root: 48, top: [60, 64, 67, 72] },  // C
        { root: 43, top: [55, 59, 62, 67] },  // G
        { root: 45, top: [57, 60, 64, 69] },  // Am
        { root: 40, top: [52, 55, 59, 64] },  // Em
        { root: 41, top: [53, 57, 60, 65] },  // F
        { root: 48, top: [60, 64, 67, 72] },  // C
        { root: 41, top: [53, 57, 60, 65] },  // F
        { root: 43, top: [55, 59, 62, 67] },  // G
      ];
      // The famous descending eighth-note melodic figure that floats above the ground.
      // Over 2-beat bar, schedule 8 eighth notes outlining the chord.
      let bar = 0;
      const renderMeasure = () => {
        const ch = ground[bar % ground.length];
        // 8 eighth notes — arpeggiate top down to root and back up
        const arp = [
          ch.top[3], ch.top[2], ch.top[1], ch.top[2],
          ch.top[3], ch.top[2], ch.top[1], ch.top[0],
        ];
        arp.forEach((n, i) => {
          e._comb({ freq: midi(n + 12), t: i * (beat / 4), dur: 1.0, gain: 0.11 });
        });
        // bass — root for full bar (one of the canon's two-note ground steps per bar)
        e._bell({ freq: midi(ch.root - 12), t: 0, dur: measure * 0.95, gain: 0.09, wobble: false });
        // inner harmony pad — root + fifth sustained
        e._pad({ freq: midi(ch.top[0]), t: 0, dur: measure * 0.95, gain: 0.04 });
        e._pad({ freq: midi(ch.top[2]), t: 0, dur: measure * 0.95, gain: 0.035 });
        bar++;
      };
      e._loop(measure, renderMeasure);
    }
  },
];

/* ----------------------- History (no-repeat-in-7-days) ----------------------- */
function readHistory() {
  try {
    const raw = localStorage.getItem(HISTORY_KEY);
    return raw ? JSON.parse(raw) : [];
  } catch (e) { return []; }
}
function recordPlay(trackId) {
  try {
    const now = Date.now();
    const hist = readHistory().filter(h => now - h.ts < WEEK_MS); // drop entries older than a week
    hist.push({ id: trackId, ts: now });
    localStorage.setItem(HISTORY_KEY, JSON.stringify(hist.slice(-30)));
  } catch (e) {}
}
function pickFreshTrack(excludeId = null) {
  const now = Date.now();
  const recent = readHistory().filter(h => now - h.ts < WEEK_MS).map(h => h.id);
  let pool = TRACKS.filter(t => !recent.includes(t.id) && t.id !== excludeId);
  if (pool.length === 0) {
    // every track played within the last week — reset and pick any (except current)
    try { localStorage.removeItem(HISTORY_KEY); } catch (e) {}
    pool = TRACKS.filter(t => t.id !== excludeId);
    if (pool.length === 0) pool = TRACKS;
  }
  return pool[Math.floor(Math.random() * pool.length)];
}

/* ----------------------- Singleton + React widget ----------------------- */
const musicEngine = new MusicEngine();

function MusicWidget({ t }) {
  const [, force] = useState(0);
  const [needsTap, setNeedsTap] = useState(true);
  const [trackInfo, setTrackInfo] = useState(null);

  useEffect(() => musicEngine.on(() => force(x => x + 1)), []);

  // Try to auto-start on mount; if blocked, wait for user tap
  useEffect(() => {
    let cancelled = false;
    (async () => {
      const track = pickFreshTrack();
      const ok = await musicEngine.play(track);
      if (cancelled) return;
      if (ok) {
        recordPlay(track.id);
        setTrackInfo(track);
        setNeedsTap(false);
      } else {
        // autoplay blocked — wait for first interaction
        setTrackInfo(track);
        setNeedsTap(true);
      }
    })();
    return () => { cancelled = true; };
  }, []);

  async function start() {
    const track = trackInfo || pickFreshTrack();
    const ok = await musicEngine.play(track);
    if (ok) {
      recordPlay(track.id);
      setNeedsTap(false);
      setTrackInfo(track);
    }
  }
  async function skip() {
    const next = pickFreshTrack(musicEngine.currentTrack && musicEngine.currentTrack.id);
    const ok = await musicEngine.play(next);
    if (ok) {
      recordPlay(next.id);
      setTrackInfo(next);
      setNeedsTap(false);
    }
  }
  function toggleMute() {
    musicEngine.setMuted(!musicEngine.muted);
  }

  // Listen for ANY user gesture and retry start if music is still blocked
  useEffect(() => {
    if (!needsTap) return;
    let triggered = false;
    const handler = () => {
      if (triggered) return;
      triggered = true;
      start();
    };
    window.addEventListener("pointerdown", handler, { once: true });
    return () => window.removeEventListener("pointerdown", handler);
  }, [needsTap, trackInfo]);

  const current = trackInfo || TRACKS[0];

  if (needsTap) {
    return (
      <button className="music-widget music-needs-tap" onClick={start} type="button" title="Tap to play music">
        <span className="music-icon">🎵</span>
        <span className="music-text">
          <span className="music-label">Tap for tunes</span>
          <span className="music-track">{current.emoji} {current.name}</span>
        </span>
      </button>
    );
  }

  return (
    <div className={"music-widget" + (musicEngine.muted ? " muted" : "")}>
      <button className="music-btn music-toggle" onClick={toggleMute} title={musicEngine.muted ? "Unmute" : "Mute"} type="button">
        {musicEngine.muted ? "🔇" : <span className="music-eq"><i /><i /><i /></span>}
      </button>
      <div className="music-text">
        <span className="music-label">Now playing</span>
        <span className="music-track">{current.emoji} {current.name}</span>
      </div>
      <button className="music-btn music-skip" onClick={skip} title="Next track" type="button">⏭</button>
    </div>
  );
}

Object.assign(window, { MusicWidget, musicEngine, TRACKS });
