// Gargantua v2 audio engine.
// Live: WebAudio sub-bass that pitch-tracks 1/r and a simple disk hiss bed.
// Stubbed: chirp on photon-sphere crossing, flare crackles, reactive narration.
// Real Grok TTS lines are not yet generated -- triggers fire on-screen captions
// instead. Wire the "speak" method into a TTS pipeline to upgrade.

class BlackHoleAudio {
  constructor() {
    this.ctx = null;
    this.subOsc = null;
    this.subGain = null;
    this.hissNode = null;
    this.hissGain = null;
    this.master = null;
    // Camera proximity to the black hole, normalized 0..1. Drives sub-bass +
    // hiss + the composite shader's grain/scanline modulation. Was historically
    // called "intensity" — renamed to make it clear this is geometric, not a
    // real audio-analyser reading.
    this.proximity = 0;
    this.enabled = false;
    this.lastChirp = 0;
    this.lastFlare = 0;
    this.captionListeners = [];
    this.spokenLines = new Set();
  }

  init() {
    if (this.ctx) return;
    try {
      const Ctx = window.AudioContext || window.webkitAudioContext;
      if (!Ctx) return;
      this.ctx = new Ctx();
      this.master = this.ctx.createGain();
      this.master.gain.value = 0.55;
      this.master.connect(this.ctx.destination);

      // sub-bass
      this.subOsc = this.ctx.createOscillator();
      this.subOsc.type = "sine";
      this.subOsc.frequency.value = 36;
      this.subGain = this.ctx.createGain();
      this.subGain.gain.value = 0.0;
      this.subOsc.connect(this.subGain);
      this.subGain.connect(this.master);
      this.subOsc.start();

      // disk hiss: bandpass-filtered white noise
      const bufferSize = 2 * this.ctx.sampleRate;
      const noiseBuf = this.ctx.createBuffer(
        1,
        bufferSize,
        this.ctx.sampleRate,
      );
      const data = noiseBuf.getChannelData(0);
      for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
      this.hissNode = this.ctx.createBufferSource();
      this.hissNode.buffer = noiseBuf;
      this.hissNode.loop = true;
      const filt = this.ctx.createBiquadFilter();
      filt.type = "bandpass";
      filt.frequency.value = 850;
      filt.Q.value = 1.6;
      this.hissGain = this.ctx.createGain();
      this.hissGain.gain.value = 0.0;
      this.hissNode.connect(filt);
      filt.connect(this.hissGain);
      this.hissGain.connect(this.master);
      this.hissNode.start();

      this.enabled = true;
    } catch (e) {
      console.warn("BlackHoleAudio init failed", e);
    }
  }

  resume() {
    if (this.ctx && this.ctx.state === "suspended") {
      this.ctx.resume().catch(() => {});
    }
  }

  // r = camera distance from BH in code units. Closer = louder, deeper.
  setProximity(r) {
    if (!this.enabled) return;
    const norm = Math.max(0, Math.min(1, (16 - r) / 14));
    this.proximity = norm;
    const t = this.ctx.currentTime;
    // sub-bass: 28 Hz at distance, 64 Hz hovering near horizon
    const targetFreq = 28 + norm * 36;
    const targetSubGain = 0.05 + norm * 0.45;
    this.subOsc.frequency.linearRampToValueAtTime(targetFreq, t + 0.2);
    this.subGain.gain.linearRampToValueAtTime(targetSubGain, t + 0.2);
    this.hissGain.gain.linearRampToValueAtTime(0.04 + norm * 0.18, t + 0.3);
  }

  // photon-sphere chirp: low-frequency sweep
  triggerChirp() {
    if (!this.enabled) return;
    const now = this.ctx.currentTime;
    if (now - this.lastChirp < 1.5) return;
    this.lastChirp = now;
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.type = "sine";
    o.frequency.setValueAtTime(40, now);
    o.frequency.exponentialRampToValueAtTime(220, now + 0.6);
    g.gain.setValueAtTime(0.0, now);
    g.gain.linearRampToValueAtTime(0.35, now + 0.05);
    g.gain.exponentialRampToValueAtTime(0.001, now + 0.7);
    o.connect(g);
    g.connect(this.master);
    o.start(now);
    o.stop(now + 0.75);
  }

  // magnetic-reconnection crackle
  triggerFlare() {
    if (!this.enabled) return;
    const now = this.ctx.currentTime;
    if (now - this.lastFlare < 0.4) return;
    this.lastFlare = now;
    // burst of band-passed noise
    const bufferSize = Math.floor(this.ctx.sampleRate * 0.25);
    const buf = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
    const data = buf.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
      const env = Math.exp(-i / (bufferSize * 0.18));
      data[i] = (Math.random() * 2 - 1) * env;
    }
    const src = this.ctx.createBufferSource();
    src.buffer = buf;
    const filt = this.ctx.createBiquadFilter();
    filt.type = "bandpass";
    filt.frequency.value = 1800;
    filt.Q.value = 4.0;
    const g = this.ctx.createGain();
    g.gain.value = 0.4;
    src.connect(filt);
    filt.connect(g);
    g.connect(this.master);
    src.start();
  }

  // Reactive narration: triggers fire onscreen captions for now.
  // Replace caption fan-out with an HTMLAudioElement.play() once TTS files exist.
  speak(key, text, opts) {
    opts = opts || {};
    if (!opts.repeat && this.spokenLines.has(key)) return;
    this.spokenLines.add(key);
    for (const cb of this.captionListeners) {
      try {
        cb({ key: key, text: text, durationMs: opts.durationMs || 4500 });
      } catch (e) {}
    }
  }

  onCaption(cb) {
    this.captionListeners.push(cb);
  }
  clearSpoken() {
    this.spokenLines.clear();
  }

  shutdown() {
    if (!this.ctx) return;
    try {
      this.subOsc && this.subOsc.stop();
    } catch (e) {}
    try {
      this.hissNode && this.hissNode.stop();
    } catch (e) {}
    try {
      this.ctx.close();
    } catch (e) {}
    this.ctx = null;
    this.enabled = false;
    // Drop references so subsequent calls don't touch dead nodes; clear caption
    // subscribers and the dedup set so a fresh init() (e.g. React StrictMode
    // remount) starts clean instead of replaying every spoken-line guard.
    this.subOsc = null;
    this.subGain = null;
    this.hissNode = null;
    this.hissGain = null;
    this.master = null;
    this.captionListeners.length = 0;
    this.spokenLines.clear();
  }
}

window.BlackHoleAudio = BlackHoleAudio;
