// Tiny WebAudio synth for SFX + streamed soundtrack.
// Two tracks: tracing-the-pattern in the menu, midnight-pursuit in the game
// (falls back to the menu track until the file exists). Music and SFX have
// independent 0..1 volumes, persisted.

let ctx = null;
let master = null;
let boostNodes = null;
let noiseBuf = null;
let unlocked = false;

const MUSIC_BASE = 0.32;   // track loudness at slider 100%
const SFX_BASE = 0.5;

const stored = (key, dflt) => {
  const v = parseFloat(localStorage.getItem(key));
  return Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : dflt;
};
// migrate the old single mute flag
const legacyMuted = localStorage.getItem('tron.muted') === '1';
let musicVol = stored('tron.musicvol', legacyMuted ? 0 : 1);
let sfxVol = stored('tron.sfxvol', legacyMuted ? 0 : 1);

// resolved against this module, so the bundle works from any sub-path (the
// /room/<code> deep link, or a portal iframe that mounts us in a folder)
const TRACKS = {
  menu: new URL('./music/tracing-the-pattern.mp3', import.meta.url).href,
  game: new URL('./music/midnight-pursuit.mp3', import.meta.url).href,
};
const players = {};      // mode -> HTMLAudioElement
let musicMode = 'menu';
// reasons the game is currently silenced (ad break, portal mute switch);
// audio plays only when nothing is holding it down
const silencers = new Set();
const silenced = () => silencers.size > 0;
let gameTrackDead = false;

function trackFor(mode) {
  return mode === 'game' && gameTrackDead ? 'menu' : mode;
}

function player(mode) {
  const key = trackFor(mode);
  let a = players[key];
  if (!a) {
    a = players[key] = new Audio(TRACKS[key]);
    a.loop = true;
    if (key === 'game') {
      a.addEventListener('error', () => {
        gameTrackDead = true;
        delete players.game;
        syncMusic();
      });
    }
  }
  return a;
}

function syncMusic() {
  if (!unlocked) return;
  const active = player(musicMode);
  for (const a of Object.values(players)) {
    if (a !== active && !a.paused) a.pause();
  }
  const vol = silenced() ? 0 : musicVol;
  active.volume = vol * MUSIC_BASE;
  if (vol > 0 && active.paused) active.play().catch(() => {});
  if (vol === 0 && !active.paused) active.pause();
}

function ensure() {
  if (ctx) return true;
  try {
    ctx = new (window.AudioContext || window.webkitAudioContext)();
    master = ctx.createGain();
    master.gain.value = sfxVol * SFX_BASE;
    master.connect(ctx.destination);
  } catch { return false; }
  return true;
}

export function unlock() {
  if (!ensure()) return;
  if (ctx.state === 'suspended') ctx.resume();
  unlocked = true;
  syncMusic();
}

export function setMusicMode(mode) {
  if (mode !== 'menu' && mode !== 'game') return;
  if (musicMode === mode) return;
  musicMode = mode;
  syncMusic();
}

export function getVolumes() { return { music: musicVol, sfx: sfxVol }; }

export function setMusicVolume(v) {
  musicVol = Math.max(0, Math.min(1, v));
  localStorage.setItem('tron.musicvol', String(musicVol));
  syncMusic();
}

export function setSfxVolume(v) {
  sfxVol = Math.max(0, Math.min(1, v));
  localStorage.setItem('tron.sfxvol', String(sfxVol));
  if (master) master.gain.value = (silenced() ? 0 : sfxVol) * SFX_BASE;
}

// Silence everything while `reason` holds — an ad break, or the portal's own
// mute switch, which the guidelines say must win over the in-game sliders.
// Never persisted: a tab that dies mid-ad must not come back muted forever.
export function setAudioOverride(reason, on) {
  if (on) silencers.add(reason); else silencers.delete(reason);
  syncMusic();
  if (master) master.gain.value = (silenced() ? 0 : sfxVol) * SFX_BASE;
}

export const duckAudio = (on) => setAudioOverride('ad', on);

function blip(freq, dur, type = 'square', vol = 0.2, slide = 0) {
  if (!ensure() || ctx.state !== 'running' || sfxVol === 0) return;
  const t = ctx.currentTime;
  const o = ctx.createOscillator();
  const g = ctx.createGain();
  o.type = type;
  o.frequency.setValueAtTime(freq, t);
  if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(30, freq + slide), t + dur);
  g.gain.setValueAtTime(vol, t);
  g.gain.exponentialRampToValueAtTime(0.001, t + dur);
  o.connect(g); g.connect(master);
  o.start(t); o.stop(t + dur + 0.02);
}

export const sfx = {
  turn() { blip(1400, 0.045, 'square', 0.08); },
  orb() { blip(880, 0.09, 'sine', 0.16, 500); },
  levelup() {
    [523, 659, 784, 1046].forEach((f, i) =>
      setTimeout(() => blip(f, 0.14, 'triangle', 0.22), i * 70));
  },
  kill() { blip(200, 0.25, 'sawtooth', 0.25, 300); },
  death() {
    if (!ensure() || ctx.state !== 'running' || sfxVol === 0) return;
    const t = ctx.currentTime;
    const len = 0.5;
    const buf = ctx.createBuffer(1, ctx.sampleRate * len, ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / d.length);
    const src = ctx.createBufferSource();
    src.buffer = buf;
    const g = ctx.createGain();
    g.gain.setValueAtTime(0.4, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + len);
    src.connect(g); g.connect(master);
    src.start(t);
    blip(120, 0.4, 'sawtooth', 0.3, -80);
  },
  spawn() { blip(300, 0.2, 'triangle', 0.18, 600); },
  count() { blip(440, 0.12, 'square', 0.2); },
  go() { blip(880, 0.35, 'square', 0.25); },
  win() {
    [523, 659, 784, 1046, 784, 1046].forEach((f, i) =>
      setTimeout(() => blip(f, 0.16, 'triangle', 0.22), i * 90));
  },
  alarm() { [0, 180, 360].forEach((d) => setTimeout(() => blip(220, 0.15, 'sawtooth', 0.2), d)); },
  phase() { blip(500, 0.35, 'sine', 0.22, 700); },
  emp() {
    blip(85, 0.5, 'sawtooth', 0.32, -45);
    setTimeout(() => blip(340, 0.3, 'sine', 0.18, -260), 40);
  },
  surge() { blip(180, 0.45, 'sawtooth', 0.26, 950); },
  snip() { blip(1100, 0.22, 'triangle', 0.2, -700); },
  shot() { blip(1500, 0.12, 'sawtooth', 0.28, -900); },
  mine() { blip(300, 0.14, 'square', 0.22, -140); },
  pad() { blip(520, 0.22, 'sine', 0.26, 900); },
  lap() {
    [660, 880].forEach((f, i) => setTimeout(() => blip(f, 0.12, 'triangle', 0.22), i * 90));
  },
};

// Boost: deep futuristic synth-whoosh (spec Task 5) — looped noise through a
// swept lowpass over a sub sine, no harsh highs.
export function setBoostSound(on) {
  if (!ensure() || ctx.state !== 'running') return;
  if (on && sfxVol === 0) return;
  if (on && !boostNodes) {
    const t = ctx.currentTime;
    if (!noiseBuf) {
      noiseBuf = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
      const d = noiseBuf.getChannelData(0);
      for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
    }
    const noise = ctx.createBufferSource();
    noise.buffer = noiseBuf;
    noise.loop = true;
    const lp = ctx.createBiquadFilter();
    lp.type = 'lowpass';
    lp.Q.value = 1.4;
    lp.frequency.setValueAtTime(140, t);
    lp.frequency.exponentialRampToValueAtTime(680, t + 0.55);
    const sub = ctx.createOscillator();
    sub.type = 'sine';
    sub.frequency.setValueAtTime(48, t);
    sub.frequency.exponentialRampToValueAtTime(74, t + 0.55);
    const g = ctx.createGain();
    g.gain.setValueAtTime(0, t);
    g.gain.linearRampToValueAtTime(0.32, t + 0.18);
    noise.connect(lp); lp.connect(g);
    const subGain = ctx.createGain();
    subGain.gain.value = 0.4;
    sub.connect(subGain); subGain.connect(g);
    g.connect(master);
    noise.start(t); sub.start(t);
    boostNodes = { noise, sub, g, lp };
  } else if (!on && boostNodes) {
    const { noise, sub, g, lp } = boostNodes;
    boostNodes = null;
    const t = ctx.currentTime;
    g.gain.cancelScheduledValues(t);
    g.gain.setValueAtTime(g.gain.value, t);
    g.gain.linearRampToValueAtTime(0, t + 0.22);
    lp.frequency.cancelScheduledValues(t);
    lp.frequency.setValueAtTime(lp.frequency.value, t);
    lp.frequency.exponentialRampToValueAtTime(120, t + 0.22);
    setTimeout(() => { try { noise.stop(); sub.stop(); } catch {} }, 320);
  }
}
