/* ============================================================
   AUDIO — synthesized placeholder SFX + per-scene ambient
   Swap these for real files later; the public API stays the same:
     audio.unlock()  · audio.play(name) · audio.setScene(name) · audio.setMuted(bool)
   ============================================================ */
const audio = (function(){
  let ctx = null;
  let masterGain = null;
  let unlocked = false;
  let muted = false;

  // ambient
  let ambientNodes = null;
  let ambientGain = null;
  let currentScene = null;

  function ensure(){
    if(!ctx){
      ctx = new (window.AudioContext || window.webkitAudioContext)();
      masterGain = ctx.createGain();
      masterGain.gain.value = 0.9;
      masterGain.connect(ctx.destination);
    }
    return ctx;
  }

  function unlock(){
    ensure();
    if(ctx.state==='suspended') ctx.resume();
    unlocked = true;
    loadBeat();
  }

  function setMuted(m){
    muted = m;
    if(masterGain) masterGain.gain.value = m ? 0 : 0.9;
  }

  function env(node, gain, t0, attack, decay, peak=1){
    const g = ctx.createGain();
    g.gain.setValueAtTime(0, t0);
    g.gain.linearRampToValueAtTime(peak*gain, t0+attack);
    g.gain.exponentialRampToValueAtTime(0.0001, t0+attack+decay);
    node.connect(g); g.connect(masterGain);
    return g;
  }

  function noiseBuffer(dur){
    const len = Math.floor(ctx.sampleRate*dur);
    const buf = ctx.createBuffer(1, len, ctx.sampleRate);
    const d = buf.getChannelData(0);
    for(let i=0;i<len;i++) d[i] = Math.random()*2-1;
    return buf;
  }

  const sfx = {
    damage(){
      const t=ctx.currentTime;
      // thud
      const o=ctx.createOscillator(); o.type='sawtooth';
      o.frequency.setValueAtTime(180,t); o.frequency.exponentialRampToValueAtTime(45,t+0.25);
      env(o,0.5,t,0.004,0.32); o.start(t); o.stop(t+0.4);
      // impact noise
      const n=ctx.createBufferSource(); n.buffer=noiseBuffer(0.2);
      const f=ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=1400;
      n.connect(f); env(f,0.35,t,0.002,0.18); n.start(t); n.stop(t+0.2);
    },
    heal(){
      const t=ctx.currentTime; const notes=[523,659,784,1046];
      notes.forEach((fr,i)=>{
        const o=ctx.createOscillator(); o.type='sine'; o.frequency.value=fr;
        env(o,0.18,t+i*0.07,0.02,0.5); o.start(t+i*0.07); o.stop(t+i*0.07+0.55);
      });
    },
    death(){
      const t=ctx.currentTime;
      [70,46,35].forEach((fr,i)=>{
        const o=ctx.createOscillator(); o.type='sine'; o.frequency.value=fr;
        env(o,0.35,t,0.05,2.4); o.start(t); o.stop(t+2.5);
      });
      const o2=ctx.createOscillator(); o2.type='sawtooth'; o2.frequency.value=58;
      const f=ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=300;
      o2.connect(f); env(f,0.2,t,0.1,2.2); o2.start(t); o2.stop(t+2.3);
    },
    revive(){
      const t=ctx.currentTime; const notes=[392,523,659,784,1046,1318];
      notes.forEach((fr,i)=>{
        const o=ctx.createOscillator(); o.type='triangle'; o.frequency.value=fr;
        env(o,0.16,t+i*0.08,0.03,0.7); o.start(t+i*0.08); o.stop(t+i*0.08+0.75);
      });
      // shimmer
      const n=ctx.createBufferSource(); n.buffer=noiseBuffer(1.2);
      const f=ctx.createBiquadFilter(); f.type='bandpass'; f.frequency.value=4000; f.Q.value=2;
      n.connect(f); env(f,0.08,t,0.3,1.0); n.start(t); n.stop(t+1.2);
    },
    coin(){
      const t=ctx.currentTime;
      [1568,2093].forEach((fr,i)=>{
        const o=ctx.createOscillator(); o.type='square'; o.frequency.value=fr;
        env(o,0.08,t+i*0.05,0.005,0.12); o.start(t+i*0.05); o.stop(t+i*0.05+0.15);
      });
    },
    card(){
      const t=ctx.currentTime;
      const n=ctx.createBufferSource(); n.buffer=noiseBuffer(0.08);
      const f=ctx.createBiquadFilter(); f.type='highpass'; f.frequency.value=2000;
      n.connect(f); env(f,0.12,t,0.001,0.07); n.start(t); n.stop(t+0.08);
    },
    // hushed, secretive chime — a whisper of intel reaches the player
    intel(){
      const t=ctx.currentTime;
      // soft rising bell triad
      [659.3, 880, 1318.5].forEach((fr,i)=>{
        const o=ctx.createOscillator(); o.type='sine'; o.frequency.value=fr;
        env(o, 0.16, t+i*0.07, 0.02, 0.85); o.start(t+i*0.07); o.stop(t+i*0.07+0.95);
      });
      // airy shimmer sweep
      const n=ctx.createBufferSource(); n.buffer=noiseBuffer(1.0);
      const f=ctx.createBiquadFilter(); f.type='bandpass';
      f.frequency.setValueAtTime(1800,t); f.frequency.exponentialRampToValueAtTime(6500,t+0.8); f.Q.value=1.6;
      n.connect(f); env(f,0.07,t,0.06,0.9); n.start(t); n.stop(t+1.0);
      // low felt thump to anchor it
      const b=ctx.createOscillator(); b.type='sine'; b.frequency.value=130.8;
      env(b,0.18,t,0.01,0.5); b.start(t); b.stop(t+0.55);
    },
    beat(){
      const t=ctx.currentTime;
      const o=ctx.createOscillator(); o.type='sine'; o.frequency.value=330;
      env(o,0.3,t,0.005,0.25); o.start(t); o.stop(t+0.3);
    },
    // clear horn call — a player's turn begins
    turn(){
      const t=ctx.currentTime;
      [392,587].forEach((fr,i)=>{
        const o=ctx.createOscillator(); o.type='triangle'; o.frequency.setValueAtTime(fr,t+i*0.12);
        env(o,0.22,t+i*0.12,0.02,0.5); o.start(t+i*0.12); o.stop(t+i*0.12+0.55);
      });
    },
    // metallic clang — an enemy strikes
    strike(){
      const t=ctx.currentTime;
      const o=ctx.createOscillator(); o.type='square'; o.frequency.setValueAtTime(220,t);
      o.frequency.exponentialRampToValueAtTime(90,t+0.18);
      const f=ctx.createBiquadFilter(); f.type='bandpass'; f.frequency.value=1800; f.Q.value=2;
      o.connect(f); env(f,0.3,t,0.002,0.22); o.start(t); o.stop(t+0.25);
      const n=ctx.createBufferSource(); n.buffer=noiseBuffer(0.12);
      const nf=ctx.createBiquadFilter(); nf.type='highpass'; nf.frequency.value=2600;
      n.connect(nf); env(nf,0.22,t,0.001,0.1); n.start(t); n.stop(t+0.12);
    },
    // bright bell pluck for a player's gate note
    note(freq){
      const t=ctx.currentTime;
      [1,2,3].forEach((mul,i)=>{
        const o=ctx.createOscillator(); o.type= i===0?'triangle':'sine';
        o.frequency.value=freq*mul;
        env(o, i===0?0.34:0.10, t, 0.004, i===0?0.9:0.55); o.start(t); o.stop(t+1.0);
      });
      // soft mallet click
      const n=ctx.createBufferSource(); n.buffer=noiseBuffer(0.05);
      const f=ctx.createBiquadFilter(); f.type='bandpass'; f.frequency.value=freq*2; f.Q.value=4;
      n.connect(f); env(f,0.10,t,0.001,0.05); n.start(t); n.stop(t+0.05);
    },
    // discordant fail when the wrong player sounds out of turn
    wrong(){
      const t=ctx.currentTime;
      [false,true].forEach((b,i)=>{
        const o=ctx.createOscillator(); o.type='sawtooth';
        o.frequency.setValueAtTime(b?150:142,t);
        o.frequency.exponentialRampToValueAtTime(b?60:54, t+0.55);
        const f=ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=900;
        o.connect(f); env(f,0.3,t,0.005,0.6); o.start(t); o.stop(t+0.65);
      });
      const n=ctx.createBufferSource(); n.buffer=noiseBuffer(0.3);
      const nf=ctx.createBiquadFilter(); nf.type='lowpass'; nf.frequency.value=500;
      n.connect(nf); env(nf,0.25,t,0.002,0.28); n.start(t); n.stop(t+0.3);
    },
    // triumphant swell when the gate yields
    gateOpen(){
      const t=ctx.currentTime;
      const chord=[130.8,196,261.6,392,523.3,784];
      chord.forEach((fr,i)=>{
        const o=ctx.createOscillator(); o.type= i<3?'sine':'triangle'; o.frequency.value=fr;
        env(o, 0.16, t+i*0.06, 0.04, 2.6); o.start(t+i*0.06); o.stop(t+i*0.06+2.7);
      });
      // rising shimmer
      const n=ctx.createBufferSource(); n.buffer=noiseBuffer(2.4);
      const f=ctx.createBiquadFilter(); f.type='bandpass'; f.frequency.setValueAtTime(800,t);
      f.frequency.exponentialRampToValueAtTime(7000,t+2.2); f.Q.value=1.4;
      n.connect(f); env(f,0.12,t,0.6,2.0); n.start(t); n.stop(t+2.4);
      // low boom
      const b=ctx.createOscillator(); b.type='sine'; b.frequency.value=49;
      env(b,0.4,t,0.02,2.4); b.start(t); b.stop(t+2.5);
    },
    // CIRCUIT LOCK — a crisp clockwork ratchet as a ring snaps 45°
    gearClick(){
      const t=ctx.currentTime;
      // metallic tick: short square blip with a fast pitch drop
      const o=ctx.createOscillator(); o.type='square';
      o.frequency.setValueAtTime(540,t); o.frequency.exponentialRampToValueAtTime(190,t+0.05);
      const bp=ctx.createBiquadFilter(); bp.type='bandpass'; bp.frequency.value=1500; bp.Q.value=3;
      o.connect(bp); env(bp,0.16,t,0.001,0.06); o.start(t); o.stop(t+0.08);
      // ratchet noise tick
      const n=ctx.createBufferSource(); n.buffer=noiseBuffer(0.04);
      const hp=ctx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=3200;
      n.connect(hp); env(hp,0.12,t,0.0005,0.035); n.start(t); n.stop(t+0.04);
      // soft brass body
      const o2=ctx.createOscillator(); o2.type='triangle'; o2.frequency.value=330;
      env(o2,0.07,t,0.002,0.09); o2.start(t); o2.stop(t+0.1);
    },
    // CIRCUIT LOCK — electric current snaps shut & the core ignites
    circuitComplete(){
      const t=ctx.currentTime;
      // rising electric zap
      const z=ctx.createOscillator(); z.type='sawtooth';
      z.frequency.setValueAtTime(140,t); z.frequency.exponentialRampToValueAtTime(1600,t+0.5);
      const zf=ctx.createBiquadFilter(); zf.type='bandpass'; zf.frequency.value=1200; zf.Q.value=6;
      z.connect(zf); env(zf,0.18,t,0.01,0.6); z.start(t); z.stop(t+0.6);
      // bright resolving chord (root · fifth · octave · maj-tenth)
      const chord=[196,293.7,392,493.9,784];
      chord.forEach((fr,i)=>{
        const o=ctx.createOscillator(); o.type= i<3?'triangle':'sine'; o.frequency.value=fr;
        env(o, 0.15, t+0.32+i*0.05, 0.02, 1.9); o.start(t+0.32+i*0.05); o.stop(t+0.32+i*0.05+2.0);
      });
      // sparkling shimmer
      const n=ctx.createBufferSource(); n.buffer=noiseBuffer(1.6);
      const f=ctx.createBiquadFilter(); f.type='bandpass'; f.frequency.setValueAtTime(2000,t+0.3);
      f.frequency.exponentialRampToValueAtTime(8000,t+1.6); f.Q.value=1.4;
      n.connect(f); env(f,0.10,t+0.3,0.2,1.5); n.start(t+0.3); n.stop(t+1.9);
      // deep clunk of the bolt seating
      const b=ctx.createOscillator(); b.type='sine'; b.frequency.setValueAtTime(120,t+0.3);
      b.frequency.exponentialRampToValueAtTime(55,t+0.7);
      env(b,0.4,t+0.3,0.01,1.4); b.start(t+0.3); b.stop(t+1.8);
    },
  };

  // pentatonic voice per player slot — pressing in order plays a melody
  const NOTE_FREQS = [261.6, 311.1, 392.0, 466.2, 523.3, 587.3];

  function play(name, ...args){
    if(!unlocked || muted) return;
    ensure();
    if(ctx.state==='suspended') ctx.resume();
    try{ (sfx[name]||(()=>{}))(...args); }catch(_){}
  }
  function note(index){ play('note', NOTE_FREQS[index % NOTE_FREQS.length]); }

  /* ---------- ambient pads per scene ---------- */
  const SCENE_TONE = {
    forest:      { base:110, fifth:164, filter:600,  type:'sine'     },
    'deep-forest':{ base:82, fifth:110, filter:420,  type:'sine'     },
    gate:        { base:98,  fifth:147, filter:520,  type:'triangle' },
    warcamp:     { base:73,  fifth:98,  filter:380,  type:'sawtooth' },
    hall:        { base:131, fifth:196, filter:800,  type:'sine'     },
    house:       { base:147, fifth:220, filter:900,  type:'sine'     },
    shop:        { base:165, fifth:247, filter:1100, type:'triangle' },
    battle:      { base:65,  fifth:98,  filter:340,  type:'sawtooth' },
  };

  function stopAmbient(fade=1.5){
    if(!ambientNodes) return;
    const nodes = ambientNodes, g = ambientGain;
    const t = ctx.currentTime;
    g.gain.cancelScheduledValues(t);
    g.gain.setValueAtTime(g.gain.value, t);
    g.gain.linearRampToValueAtTime(0.0001, t+fade);
    setTimeout(()=>{ try{ nodes.forEach(n=>n.stop&&n.stop()); }catch(_){} }, fade*1000+200);
    ambientNodes = null; ambientGain = null;
  }

  function setScene(name){
    if(name===currentScene) return;
    currentScene = name;
    if(!unlocked){ return; }
    ensure();
    stopAmbient(1.5);
    const tone = SCENE_TONE[name] || SCENE_TONE.forest;
    const t = ctx.currentTime;
    const g = ctx.createGain(); g.gain.setValueAtTime(0.0001,t);
    g.gain.linearRampToValueAtTime(0.10, t+1.5);
    g.connect(masterGain);
    const filt = ctx.createBiquadFilter(); filt.type='lowpass'; filt.frequency.value=tone.filter;
    filt.connect(g);
    const nodes=[];
    [tone.base, tone.fifth, tone.base*2].forEach((fr,i)=>{
      const o=ctx.createOscillator(); o.type=tone.type;
      o.frequency.value=fr;
      // slow detune drift
      const lfo=ctx.createOscillator(); lfo.frequency.value=0.07+i*0.03;
      const lg=ctx.createGain(); lg.gain.value=2.5;
      lfo.connect(lg); lg.connect(o.detune); lfo.start(t);
      o.connect(filt); o.start(t); nodes.push(o, lfo);
    });
    // airy noise bed
    const n=ctx.createBufferSource(); n.buffer=noiseBuffer(4); n.loop=true;
    const nf=ctx.createBiquadFilter(); nf.type='bandpass'; nf.frequency.value=tone.filter*2; nf.Q.value=0.6;
    const ng=ctx.createGain(); ng.gain.value=0.12;
    n.connect(nf); nf.connect(ng); ng.connect(g); n.start(t); nodes.push(n);
    ambientNodes = nodes; ambientGain = g;
  }

  /* ---------- beat-puzzle notes: real wav, pitch-shifted to 4 pitches ---------- */
  let beatBuffer = null, beatLoading = false;
  function loadBeat(){
    if(beatBuffer || beatLoading) return;
    ensure();
    beatLoading = true;
    const url = (window.ASSETS && window.ASSETS.beatSfx) || 'assets/sfx/beat.wav';
    fetch(url).then(r=>r.arrayBuffer()).then(b=> ctx.decodeAudioData(b))
      .then(buf=>{ beatBuffer = buf; }).catch(()=>{ beatLoading=false; });
  }
  // four ascending, harmonious pitches (root · maj third · fifth · octave)
  const BEAT_RATES = [1.0, 1.26, 1.5, 2.0];
  const BEAT_FALLBACK = [261.6, 329.6, 392.0, 523.3];
  function beatNote(i){
    if(!unlocked || muted) return;
    ensure();
    i = ((i%4)+4)%4;
    if(beatBuffer){
      const src = ctx.createBufferSource(); src.buffer = beatBuffer; src.playbackRate.value = BEAT_RATES[i];
      const g = ctx.createGain(); g.gain.value = 0.95;
      src.connect(g); g.connect(masterGain); src.start();
    } else {
      loadBeat();
      play('note', BEAT_FALLBACK[i]);   // synth bell until the wav decodes
    }
  }

  /* ---------- music director: per-context mp3 tracks (MAIN SCREEN ONLY) ---------- */
  // context = scene name | 'shop' | 'magicshop' | 'gate' | 'battle'.
  // Plays the matching ASSETS.music file (crossfaded). No file → silence.
  // Player & Moderator devices never call this, so they stay music-free.
  let curMusic = null, curMusicCtx = null;
  function fadeOutMusic(node, fade=1.4){
    if(!node) return;
    try{
      const t=ctx.currentTime;
      node.g.gain.cancelScheduledValues(t);
      node.g.gain.setValueAtTime(node.g.gain.value, t);
      node.g.gain.linearRampToValueAtTime(0.0001, t+fade);
      setTimeout(()=>{ try{ node.el.pause(); node.el.src=''; }catch(_){} }, fade*1000+150);
    }catch(_){}
  }
  function music(context){
    if(!unlocked || context===curMusicCtx) return;
    ensure();
    curMusicCtx = context;
    const url = (window.ASSETS && window.ASSETS.music && window.ASSETS.music[context]) || null;
    fadeOutMusic(curMusic); curMusic = null;
    if(!url) return;                  // silent — no placeholder music
    try{
      const el = new Audio(url); el.loop = true; el.crossOrigin = 'anonymous'; el.preload='auto';
      const g = ctx.createGain(); g.gain.value = 0.0001;
      const node = ctx.createMediaElementSource(el); node.connect(g); g.connect(masterGain);
      el.addEventListener('canplay', ()=>{ const t=ctx.currentTime;
        g.gain.cancelScheduledValues(t); g.gain.setValueAtTime(0.0001,t);
        g.gain.linearRampToValueAtTime(0.6, t+2); }, {once:true});
      el.play().catch(()=>{});
      curMusic = { el, g, node };
    }catch(_){}
  }

  return { unlock, play, note, beatNote, loadBeat, music, setMuted,
           stopMusic: ()=>{ fadeOutMusic(curMusic); curMusic=null; curMusicCtx=null; },
           // resume a suspended context (tab was backgrounded / device slept)
           resume: ()=>{ try{ if(ctx && ctx.state==='suspended') ctx.resume(); }catch(_){} },
           // force the current/given context's music to (re)start after a reconnect
           replayMusic: (context)=>{ if(!unlocked) return; const c = context!=null?context:curMusicCtx; curMusicCtx=null; if(c!=null) music(c); },
           get scene(){ return currentScene; },
           get unlocked(){ return unlocked; }, get muted(){ return muted; } };
})();

window.audio = audio;
// keep audio alive across tab-visibility changes / reconnects: a suspended
// WebAudio context (backgrounded tab, device sleep) is resumed when we return.
try{
  document.addEventListener('visibilitychange', ()=>{ if(!document.hidden) audio.resume(); });
  window.addEventListener('focus', ()=> audio.resume());
}catch(_){}
