import type { SoundEvent } from './types';

type Voice = { source: AudioScheduledSourceNode; nodes: AudioNode[]; music: boolean };
type AudioWindow = Window & { webkitAudioContext?: typeof AudioContext };

/** All instruments are synthesized locally, and the context starts on a gesture. */
export class GameAudio {
  private context: AudioContext | null = null;
  private master: GainNode | null = null;
  private musicBus: GainNode | null = null;
  private noiseBuffer: AudioBuffer | null = null;
  private readonly voices = new Set<Voice>();
  private musicTimer: ReturnType<typeof setInterval> | null = null;
  private muted = false;
  private active = false;
  private disposed = false;
  private nextNote = 0;
  private step = 0;

  async unlock(): Promise<void> {
    if (this.disposed || typeof window === 'undefined') return;
    try {
      if (!this.context) {
        const AudioConstructor = window.AudioContext || (window as AudioWindow).webkitAudioContext;
        if (!AudioConstructor) return;
        const context = new AudioConstructor();
        const master = context.createGain();
        const musicBus = context.createGain();
        master.gain.value = this.muted ? 0 : 0.28;
        musicBus.gain.value = 0.18;
        musicBus.connect(master);
        master.connect(context.destination);
        this.context = context;
        this.master = master;
        this.musicBus = musicBus;
      }
      if (this.context.state === 'suspended') await this.context.resume();
      if (!this.disposed) this.startMusic();
    } catch {
      // Browsers without usable audio still run the complete game silently.
    }
  }

  setMuted(muted: boolean): void {
    this.muted = muted;
    if (this.context && this.master) {
      this.master.gain.cancelScheduledValues(this.context.currentTime);
      this.master.gain.setValueAtTime(muted ? 0 : 0.28, this.context.currentTime);
    }
    if (muted) {
      this.stopMusic();
      for (const voice of [...this.voices]) this.stopVoice(voice);
    } else {
      this.startMusic();
    }
  }

  setActive(active: boolean): void {
    this.active = active;
    if (active) this.startMusic();
    else this.stopMusic();
  }

  play(event: SoundEvent): void {
    if (this.disposed || this.muted || this.context?.state !== 'running') return;
    switch (event) {
      case 'jump':
        this.tone('square', 220, 680, 0.14, 0.15);
        break;
      case 'dash':
        this.noise(0.15, 0.3, 2800);
        this.tone('sawtooth', 230, 60, 0.13, 0.12);
        break;
      case 'shot':
        this.tone('square', 1150, 240, 0.095, 0.17);
        break;
      case 'charge':
        this.tone('triangle', 370, 1480, 0.36, 0.19);
        this.tone('square', 740, 1480, 0.2, 0.07, 0.14);
        break;
      case 'charged-shot':
        this.tone('sawtooth', 740, 90, 0.28, 0.22);
        this.tone('triangle', 1480, 180, 0.28, 0.2);
        this.noise(0.2, 0.2, 3500);
        break;
      case 'hit':
        this.tone('square', 160, 45, 0.16, 0.2);
        this.noise(0.13, 0.23, 1800);
        break;
      case 'explosion':
        this.noise(0.35, 0.4, 1200);
        this.tone('triangle', 100, 28, 0.35, 0.32);
        break;
      case 'pickup':
        [76, 83, 88].forEach((note, index) => this.note(note, 0.14, 0.16, index * 0.075));
        break;
      case 'checkpoint':
        [64, 71, 76, 83].forEach((note, index) => this.note(note, 0.22, 0.18, index * 0.105));
        break;
      case 'death':
        [64, 59, 55, 40].forEach((note, index) => this.tone('triangle', this.frequency(note), this.frequency(note - 3), 0.26, 0.22, index * 0.13));
        this.noise(0.45, 0.2, 850);
        break;
      case 'boss':
        [40, 41, 40].forEach((note, index) => this.tone('sawtooth', this.frequency(note), this.frequency(note), 0.26, 0.16, index * 0.22));
        break;
      case 'win':
        [64, 67, 71, 76, 79, 83, 88].forEach((note, index) => this.note(note, index === 6 ? 0.75 : 0.24, 0.18, index * 0.13));
        break;
    }
  }

  dispose(): void {
    if (this.disposed) return;
    this.disposed = true;
    this.active = false;
    this.stopMusic();
    for (const voice of [...this.voices]) this.stopVoice(voice);
    this.master?.disconnect();
    this.musicBus?.disconnect();
    const context = this.context;
    this.context = null;
    this.master = null;
    this.musicBus = null;
    this.noiseBuffer = null;
    if (context && context.state !== 'closed') void context.close().catch(() => undefined);
  }

  private frequency(note: number): number {
    return 440 * 2 ** ((note - 69) / 12);
  }

  private note(note: number, duration: number, volume: number, delay = 0): void {
    const frequency = this.frequency(note);
    this.tone('square', frequency, frequency, duration, volume, delay);
  }

  private tone(type: OscillatorType, frequency: number, endFrequency: number, duration: number, volume: number, delay = 0, music = false): void {
    const context = this.context;
    const destination = music ? this.musicBus : this.master;
    if (!context || !destination || this.voices.size >= 64) return;
    const start = context.currentTime + Math.max(0, delay);
    const oscillator = context.createOscillator();
    const envelope = context.createGain();
    oscillator.type = type;
    oscillator.frequency.setValueAtTime(frequency, start);
    oscillator.frequency.exponentialRampToValueAtTime(Math.max(20, endFrequency), start + duration);
    envelope.gain.setValueAtTime(0.0001, start);
    envelope.gain.linearRampToValueAtTime(volume, start + 0.004);
    envelope.gain.exponentialRampToValueAtTime(0.0001, start + duration);
    oscillator.connect(envelope);
    envelope.connect(destination);
    this.trackVoice(oscillator, [oscillator, envelope], music);
    oscillator.start(start);
    oscillator.stop(start + duration + 0.01);
  }

  private noise(duration: number, volume: number, cutoff: number, delay = 0, music = false): void {
    const context = this.context;
    const destination = music ? this.musicBus : this.master;
    if (!context || !destination || this.voices.size >= 64) return;
    if (!this.noiseBuffer) {
      this.noiseBuffer = context.createBuffer(1, context.sampleRate, context.sampleRate);
      const data = this.noiseBuffer.getChannelData(0);
      let seed = 1967;
      for (let i = 0; i < data.length; i++) {
        seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
        data[i] = seed / 2147483648 - 1;
      }
    }
    const start = context.currentTime + Math.max(0, delay);
    const source = context.createBufferSource();
    const filter = context.createBiquadFilter();
    const envelope = context.createGain();
    source.buffer = this.noiseBuffer;
    filter.type = 'lowpass';
    filter.frequency.value = cutoff;
    envelope.gain.setValueAtTime(volume, start);
    envelope.gain.exponentialRampToValueAtTime(0.0001, start + duration);
    source.connect(filter);
    filter.connect(envelope);
    envelope.connect(destination);
    this.trackVoice(source, [source, filter, envelope], music);
    source.start(start);
    source.stop(start + duration + 0.01);
  }

  private trackVoice(source: AudioScheduledSourceNode, nodes: AudioNode[], music: boolean): void {
    const voice = { source, nodes, music };
    this.voices.add(voice);
    source.onended = () => {
      for (const node of nodes) node.disconnect();
      this.voices.delete(voice);
    };
  }

  private stopVoice(voice: Voice): void {
    try { voice.source.stop(); } catch { /* Already ended. */ }
    for (const node of voice.nodes) node.disconnect();
    this.voices.delete(voice);
  }

  private startMusic(): void {
    if (this.musicTimer || this.disposed || !this.active || this.muted || this.context?.state !== 'running') return;
    this.step = 0;
    this.nextNote = this.context.currentTime + 0.04;
    this.scheduleMusic();
    this.musicTimer = setInterval(this.scheduleMusic, 90);
  }

  private stopMusic(): void {
    if (this.musicTimer) clearInterval(this.musicTimer);
    this.musicTimer = null;
    for (const voice of [...this.voices]) if (voice.music) this.stopVoice(voice);
  }

  private scheduleMusic = (): void => {
    const context = this.context;
    if (!context || context.state !== 'running' || !this.active || this.muted || this.disposed) return;
    // An original four-bar E-minor pulse, deliberately quieter than action cues.
    const roots = [40, 43, 45, 38];
    const arpeggio = [24, 31, 27, 36, 31, 27, 34, 31];
    if (this.nextNote < context.currentTime) this.nextNote = context.currentTime + 0.02;
    while (this.nextNote < context.currentTime + 0.22) {
      const root = roots[Math.floor(this.step / 16) % roots.length];
      const delay = this.nextNote - context.currentTime;
      if (this.step % 2 === 0) {
        const bass = this.frequency(root + (this.step % 8 === 6 ? 12 : 0));
        this.tone('triangle', bass, bass, 0.235, 0.28, delay, true);
      }
      const lead = this.frequency(root + arpeggio[this.step % arpeggio.length]);
      this.tone('square', lead, lead, 0.08, 0.075, delay, true);
      if (this.step % 4 === 0) this.tone('sine', 130, 38, 0.115, 0.24, delay, true);
      if (this.step % 2 === 1) this.noise(0.028, 0.055, 6500, delay, true);
      this.step++;
      this.nextNote += 0.155;
    }
  };
}
