/**
 * Procedural WebAudio: gunshot, growls, door hiss, pickups, hurt, ambient
 * drone with distant hull clanks. No audio files. Context is created lazily
 * on the first user gesture.
 */

export class Sfx {
  private ctx: AudioContext | null = null;
  private master: GainNode | null = null;
  private noiseBuf: AudioBuffer | null = null;
  private clankTimer = 0;

  /** must be called from a user gesture */
  unlock(): void {
    if (this.ctx) {
      if (this.ctx.state === 'suspended') void this.ctx.resume();
      return;
    }
    const Ctor = window.AudioContext ?? (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
    if (!Ctor) return;
    this.ctx = new Ctor();
    this.master = this.ctx.createGain();
    this.master.gain.value = 0.55;
    this.master.connect(this.ctx.destination);
    const len = this.ctx.sampleRate;
    this.noiseBuf = this.ctx.createBuffer(1, len, this.ctx.sampleRate);
    const d = this.noiseBuf.getChannelData(0);
    for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;
    this.startAmbient();
  }

  private noise(dur: number, filterHz: number, q: number, gain: number, decay: number, type: BiquadFilterType = 'lowpass'): void {
    if (!this.ctx || !this.master || !this.noiseBuf) return;
    const t = this.ctx.currentTime;
    const src = this.ctx.createBufferSource();
    src.buffer = this.noiseBuf;
    src.loop = true;
    const f = this.ctx.createBiquadFilter();
    f.type = type;
    f.frequency.value = filterHz;
    f.Q.value = q;
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(gain, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + decay);
    src.connect(f).connect(g).connect(this.master);
    src.start(t, Math.random() * 0.5);
    src.stop(t + dur);
  }

  private tone(freq: number, endFreq: number, dur: number, gain: number, type: OscillatorType): void {
    if (!this.ctx || !this.master) return;
    const t = this.ctx.currentTime;
    const o = this.ctx.createOscillator();
    o.type = type;
    o.frequency.setValueAtTime(freq, t);
    o.frequency.exponentialRampToValueAtTime(Math.max(1, endFreq), t + dur);
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(gain, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + dur);
    o.connect(g).connect(this.master);
    o.start(t);
    o.stop(t + dur);
  }

  shoot(): void {
    this.noise(0.3, 1400, 0.6, 0.9, 0.22);
    this.noise(0.4, 220, 1.2, 0.8, 0.35);
    this.tone(160, 40, 0.18, 0.5, 'square');
  }

  click(): void {
    this.tone(900, 500, 0.05, 0.2, 'square');
  }

  reloadThunk(): void {
    this.tone(120, 70, 0.08, 0.3, 'square');
  }

  growl(kind: 'husk' | 'stalker' | 'brute'): void {
    const base = kind === 'brute' ? 52 : kind === 'husk' ? 78 : 120;
    if (!this.ctx || !this.master) return;
    const t = this.ctx.currentTime;
    const o = this.ctx.createOscillator();
    o.type = 'sawtooth';
    o.frequency.setValueAtTime(base, t);
    o.frequency.linearRampToValueAtTime(base * 0.7, t + 0.5);
    const lfo = this.ctx.createOscillator();
    lfo.frequency.value = kind === 'stalker' ? 16 : 9;
    const lfoGain = this.ctx.createGain();
    lfoGain.gain.value = base * 0.4;
    lfo.connect(lfoGain).connect(o.frequency);
    const f = this.ctx.createBiquadFilter();
    f.type = 'lowpass';
    f.frequency.value = 500;
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(0.0001, t);
    g.gain.exponentialRampToValueAtTime(0.35, t + 0.07);
    g.gain.exponentialRampToValueAtTime(0.001, t + 0.6);
    o.connect(f).connect(g).connect(this.master);
    o.start(t); lfo.start(t);
    o.stop(t + 0.65); lfo.stop(t + 0.65);
  }

  enemyDie(): void {
    this.noise(0.5, 400, 1, 0.5, 0.45);
    this.tone(180, 30, 0.5, 0.3, 'sawtooth');
  }

  hurt(): void {
    this.tone(220, 90, 0.18, 0.4, 'square');
    this.noise(0.2, 800, 1, 0.3, 0.18);
  }

  doorHiss(): void {
    this.noise(0.9, 2600, 0.4, 0.35, 0.8, 'bandpass');
    this.tone(60, 45, 0.7, 0.2, 'triangle');
  }

  locked(): void {
    this.tone(140, 140, 0.09, 0.3, 'square');
    setTimeout(() => this.tone(110, 110, 0.12, 0.3, 'square'), 120);
  }

  pickup(): void {
    this.tone(520, 780, 0.1, 0.25, 'triangle');
  }

  keycard(): void {
    this.tone(440, 660, 0.12, 0.3, 'triangle');
    setTimeout(() => this.tone(660, 990, 0.16, 0.3, 'triangle'), 110);
  }

  win(): void {
    [262, 330, 392, 523].forEach((f, i) =>
      setTimeout(() => this.tone(f, f, 0.25, 0.3, 'triangle'), i * 140));
  }

  projectile(): void {
    this.tone(700, 200, 0.3, 0.18, 'sawtooth');
  }

  private startAmbient(): void {
    if (!this.ctx || !this.master) return;
    const t = this.ctx.currentTime;
    const g = this.ctx.createGain();
    g.gain.value = 0.1;
    g.connect(this.master);
    for (const [freq, detune] of [[48, 0], [48.7, 3], [96.2, -4]] as const) {
      const o = this.ctx.createOscillator();
      o.type = 'sine';
      o.frequency.value = freq;
      o.detune.value = detune;
      o.connect(g);
      o.start(t);
    }
    // slow swell LFO
    const lfo = this.ctx.createOscillator();
    lfo.frequency.value = 0.07;
    const lg = this.ctx.createGain();
    lg.gain.value = 0.05;
    lfo.connect(lg).connect(g.gain);
    lfo.start(t);
  }

  /** call per frame; occasionally a distant hull clank */
  update(dt: number): void {
    if (!this.ctx) return;
    this.clankTimer -= dt;
    if (this.clankTimer <= 0) {
      this.clankTimer = 6 + Math.random() * 12;
      const f = 120 + Math.random() * 240;
      this.noise(0.7, f, 14, 0.16, 0.7, 'bandpass');
    }
  }
}
