/**
 * Web Audio procedural BGM + 4 SFX (no external files).
 * BGM: 8-bar 8-bit heal loop (square lead + triangle bass).
 * SFX: dialog beep, task ding, dusk bell, E2 ECG blip.
 */

import type { StageParams } from '../data/corruption';
import { Director } from '../systems/CorruptionDirector';
import { gameEvents } from '../systems/EventBus';
import { T14_FADE_START_MS, T14_STAND_MS } from '../data/constants';

export class ProceduralAudio {
  private ctx: AudioContext | null = null;
  private master: GainNode | null = null;
  private bgmGain: GainNode | null = null;
  private birdGain: GainNode | null = null;
  private sfxGain: GainNode | null = null;
  private lowpass: BiquadFilterNode | null = null;
  private bgmTimer: number | null = null;
  private birdTimer: number | null = null;
  private started = false;
  private mutedAll = false;
  private playbackRate = 1;
  private step = 0;

  // C major pentatonic-ish heal melody (MIDI-ish freqs)
  private readonly melody = [
    523.25, 587.33, 659.25, 783.99, 659.25, 587.33, 523.25, 392.0,
    440.0, 523.25, 587.33, 659.25, 587.33, 523.25, 440.0, 392.0,
    523.25, 659.25, 783.99, 880.0, 783.99, 659.25, 523.25, 587.33,
    659.25, 587.33, 523.25, 440.0, 392.0, 440.0, 523.25, 523.25,
  ];
  private readonly bass = [
    130.81, 130.81, 146.83, 146.83, 164.81, 164.81, 196.0, 196.0,
    174.61, 174.61, 164.81, 164.81, 146.83, 146.83, 130.81, 130.81,
    130.81, 164.81, 196.0, 196.0, 174.61, 174.61, 146.83, 146.83,
    130.81, 130.81, 146.83, 164.81, 196.0, 174.61, 130.81, 130.81,
  ];

  async unlock(): Promise<void> {
    if (!this.ctx) {
      this.ctx = new AudioContext();
      this.master = this.ctx.createGain();
      this.master.gain.value = 0.35;
      this.master.connect(this.ctx.destination);

      this.lowpass = this.ctx.createBiquadFilter();
      this.lowpass.type = 'lowpass';
      this.lowpass.frequency.value = 20000;
      this.lowpass.connect(this.master);

      this.bgmGain = this.ctx.createGain();
      this.bgmGain.gain.value = 0.45;
      this.bgmGain.connect(this.lowpass);

      this.birdGain = this.ctx.createGain();
      this.birdGain.gain.value = 0.2;
      this.birdGain.connect(this.lowpass);

      this.sfxGain = this.ctx.createGain();
      this.sfxGain.gain.value = 0.5;
      this.sfxGain.connect(this.master);
    }
    if (this.ctx.state === 'suspended') {
      await this.ctx.resume();
    }
    if (!this.started) {
      this.started = true;
      this.scheduleBgm();
      this.scheduleBirds();
      this.bindDirector();
    }
  }

  private bindDirector(): void {
    gameEvents.on('stage', (_stage, params) => {
      this.applyStageParams(params as StageParams);
    });
    gameEvents.on('t14Progress', (elapsed) => {
      this.applyT14Fade(elapsed as number);
    });
    gameEvents.on('t14End', () => {
      this.mutedAll = false;
      if (this.master) this.master.gain.value = 0.35;
    });
    gameEvents.on('ending', () => {
      this.stopAll();
    });
  }

  applyStageParams(params: StageParams): void {
    this.playbackRate = params.playbackRate;
    if (this.lowpass) {
      this.lowpass.frequency.value = params.lowpassHz ?? 20000;
    }
    if (this.birdGain) {
      this.birdGain.gain.value = params.birdMute || this.mutedAll ? 0 : 0.2;
    }
  }

  private applyT14Fade(elapsedMs: number): void {
    if (!this.master) return;
    if (elapsedMs < T14_FADE_START_MS) {
      this.master.gain.value = 0.35;
      return;
    }
    const t = Math.min(1, (elapsedMs - T14_FADE_START_MS) / (T14_STAND_MS - T14_FADE_START_MS));
    this.master.gain.value = 0.35 * (1 - t);
    if (t >= 1) this.mutedAll = true;
  }

  private scheduleBgm(): void {
    if (!this.ctx || !this.bgmGain) return;
    const baseInterval = 0.22; // seconds per step at rate 1
    const tick = () => {
      if (!this.ctx || !this.bgmGain || this.mutedAll) {
        this.bgmTimer = window.setTimeout(tick, baseInterval * 1000);
        return;
      }
      const i = this.step % this.melody.length;
      this.playTone(this.melody[i]!, 0.18 / this.playbackRate, 'square', this.bgmGain, 0.08);
      this.playTone(this.bass[i]!, 0.2 / this.playbackRate, 'triangle', this.bgmGain, 0.1);
      this.step += 1;
      const ms = (baseInterval / this.playbackRate) * 1000;
      this.bgmTimer = window.setTimeout(tick, ms);
    };
    tick();
  }

  private scheduleBirds(): void {
    if (!this.ctx || !this.birdGain) return;
    const chirp = () => {
      if (!this.ctx || !this.birdGain) return;
      if (!this.mutedAll && this.birdGain.gain.value > 0) {
        const f = 1200 + Math.random() * 800;
        this.playTone(f, 0.06, 'square', this.birdGain, 0.04);
        window.setTimeout(() => {
          this.playTone(f * 1.2, 0.05, 'square', this.birdGain!, 0.03);
        }, 70);
      }
      this.birdTimer = window.setTimeout(chirp, 1800 + Math.random() * 2200);
    };
    chirp();
  }

  private playTone(
    freq: number,
    dur: number,
    type: OscillatorType,
    dest: AudioNode,
    gainVal: number,
  ): void {
    if (!this.ctx) return;
    const osc = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    osc.type = type;
    osc.frequency.value = freq;
    g.gain.value = gainVal;
    const t0 = this.ctx.currentTime;
    g.gain.setValueAtTime(gainVal, t0);
    g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);
    osc.connect(g);
    g.connect(dest);
    osc.start(t0);
    osc.stop(t0 + dur + 0.02);
  }

  beep(): void {
    if (!this.sfxGain) return;
    this.playTone(880, 0.05, 'square', this.sfxGain, 0.08);
  }

  ding(): void {
    if (!this.sfxGain) return;
    this.playTone(1046.5, 0.12, 'square', this.sfxGain, 0.1);
    this.playTone(1318.5, 0.15, 'square', this.sfxGain, 0.08);
  }

  bell(): void {
    if (!this.sfxGain) return;
    this.playTone(523.25, 0.4, 'sine', this.sfxGain, 0.12);
    this.playTone(659.25, 0.5, 'sine', this.sfxGain, 0.08);
  }

  ecg(): void {
    if (!this.sfxGain) return;
    this.playTone(1000, 0.08, 'square', this.sfxGain, 0.15);
  }

  stopAll(): void {
    if (this.bgmTimer != null) window.clearTimeout(this.bgmTimer);
    if (this.birdTimer != null) window.clearTimeout(this.birdTimer);
    this.bgmTimer = null;
    this.birdTimer = null;
    this.started = false;
    if (this.master) this.master.gain.value = 0;
  }

  /** Sync from current director state */
  syncFromDirector(): void {
    this.applyStageParams(Director.getStageParams());
  }
}

export const audio = new ProceduralAudio();
