// audio.jsx — procedural sound design via Web Audio. No external files.
// Singleton on window.SFX. Lazily inits on first user gesture (autoplay policy).

(function () {
  let ctx = null, master = null, ambGain = null, started = false, muted = false;
  let droneNodes = [], hb = { timer: null, tension: 0 };

  function init() {
    if (ctx) return;
    try {
      ctx = new (window.AudioContext || window.webkitAudioContext)();
      master = ctx.createGain();
      master.gain.value = muted ? 0 : 0.9;
      master.connect(ctx.destination);
    } catch (e) { ctx = null; }
  }
  function resume() { if (ctx && ctx.state === "suspended") ctx.resume(); }

  // ── ambient surveillance drone ──
  function startDrone() {
    if (!ctx || started) return;
    started = true;
    ambGain = ctx.createGain();
    ambGain.gain.value = 0.0;
    ambGain.connect(master);
    // two slightly detuned low oscillators + filtered noise "air"
    const f = ctx.createBiquadFilter();
    f.type = "lowpass"; f.frequency.value = 320; f.Q.value = 0.6;
    f.connect(ambGain);
    [54, 54.4, 81].forEach((freq, i) => {
      const o = ctx.createOscillator();
      o.type = i === 2 ? "sine" : "triangle";
      o.frequency.value = freq;
      const g = ctx.createGain(); g.gain.value = i === 2 ? 0.10 : 0.16;
      o.connect(g); g.connect(f); o.start();
      droneNodes.push(o);
      // slow detune drift
      const lfo = ctx.createOscillator(); lfo.frequency.value = 0.05 + i * 0.03;
      const lg = ctx.createGain(); lg.gain.value = 1.4;
      lfo.connect(lg); lg.connect(o.detune); lfo.start();
      droneNodes.push(lfo);
    });
    // air noise
    const noise = ctx.createBufferSource();
    const buf = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * 0.5;
    noise.buffer = buf; noise.loop = true;
    const nf = ctx.createBiquadFilter(); nf.type = "bandpass"; nf.frequency.value = 600; nf.Q.value = 0.4;
    const ng = ctx.createGain(); ng.gain.value = 0.012;
    noise.connect(nf); nf.connect(ng); ng.connect(ambGain); noise.start();
    ambGain.gain.linearRampToValueAtTime(0.5, ctx.currentTime + 3);
  }
  function setAmbient(level) { // 0..1
    if (ambGain) ambGain.gain.linearRampToValueAtTime(0.25 + level * 0.6, ctx.currentTime + 0.8);
  }

  // ── one-shot helpers ──
  function env(node, gain, dur, peak = 1, atk = 0.005) {
    const g = ctx.createGain();
    g.gain.setValueAtTime(0.0001, ctx.currentTime);
    g.gain.exponentialRampToValueAtTime(peak, ctx.currentTime + atk);
    g.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + dur);
    node.connect(g); g.connect(gain || master);
    return g;
  }
  function osc(type, freq, dur, peak, glideTo) {
    if (!ctx) return;
    const o = ctx.createOscillator(); o.type = type; o.frequency.value = freq;
    if (glideTo) o.frequency.exponentialRampToValueAtTime(glideTo, ctx.currentTime + dur);
    env(o, master, dur, peak); o.start(); o.stop(ctx.currentTime + dur + 0.02);
  }
  function noiseBurst(dur, peak, filterType, freq, Q) {
    if (!ctx) return;
    const n = ctx.createBufferSource();
    const buf = ctx.createBuffer(1, ctx.sampleRate * dur, ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
    n.buffer = buf;
    const f = ctx.createBiquadFilter(); f.type = filterType || "highpass"; f.frequency.value = freq || 2000; f.Q.value = Q || 1;
    n.connect(f); env(f, master, dur, peak); n.start(); n.stop(ctx.currentTime + dur + 0.02);
  }

  const SFX = {
    armed: () => !!ctx,
    arm() { init(); resume(); startDrone(); },
    setMuted(m) { muted = m; if (master) master.gain.linearRampToValueAtTime(m ? 0 : 0.9, ctx ? ctx.currentTime + 0.2 : 0); },
    isMuted: () => muted,
    key() { if (!ctx || muted) return; noiseBurst(0.03, 0.07, "highpass", 3200, 1); osc("square", 2400 + Math.random() * 400, 0.02, 0.015); },
    blip() { osc("sine", 660, 0.07, 0.06, 880); },
    select() { osc("triangle", 420, 0.09, 0.10, 560); noiseBurst(0.04, 0.03, "highpass", 1800); },
    thunk() { osc("sine", 150, 0.18, 0.5, 60); noiseBurst(0.08, 0.12, "lowpass", 500, 1); },
    stamp() { osc("square", 90, 0.10, 0.35, 50); noiseBurst(0.06, 0.18, "bandpass", 900, 0.7); },
    statUp() { osc("sine", 520, 0.16, 0.12, 760); },
    statDown() { osc("sine", 360, 0.2, 0.14, 180); },
    alert() { if (!ctx || muted) return; osc("sawtooth", 440, 0.18, 0.10); setTimeout(() => osc("sawtooth", 392, 0.22, 0.10), 180); },
    glitch() {
      if (!ctx || muted) return;
      for (let i = 0; i < 5; i++) setTimeout(() => { noiseBurst(0.05, 0.14, "bandpass", 800 + Math.random() * 3000, 2); osc("square", 120 + Math.random() * 600, 0.04, 0.06); }, i * 70);
    },
    reveal() { osc("sine", 220, 1.2, 0.10, 440); osc("sine", 330, 1.2, 0.07, 660); },
    sting(good) {
      if (!ctx || muted) return;
      const notes = good ? [196, 261.6, 329.6, 392] : [185, 220, 233, 277];
      notes.forEach((n, i) => setTimeout(() => osc("triangle", n, 1.6, 0.09), i * 90));
    },
    setTension(level) { // 0..1 — drives ambient + heartbeat
      hb.tension = level;
      setAmbient(level);
      if (level > 0.35 && !hb.timer && ctx && !muted) scheduleHeart();
      if (level <= 0.35 && hb.timer) { clearTimeout(hb.timer); hb.timer = null; }
    },
  };

  function scheduleHeart() {
    if (!ctx || muted || hb.tension <= 0.35) { hb.timer = null; return; }
    osc("sine", 60, 0.16, 0.18 + hb.tension * 0.25, 40);
    setTimeout(() => osc("sine", 55, 0.13, 0.12 + hb.tension * 0.2, 38), 150);
    const interval = 1100 - hb.tension * 620;
    hb.timer = setTimeout(scheduleHeart, interval);
  }

  // arm on first gesture anywhere
  ["pointerdown", "keydown"].forEach((ev) =>
    window.addEventListener(ev, function once() { SFX.arm(); window.removeEventListener(ev, once); }, { once: false }));

  window.SFX = SFX;
})();
