/**
 * Synthesised sound. No samples — the whole point of a drawer full of cutlery is
 * the noise it makes, and that noise is velocity-dependent, so it has to be
 * generated per collision rather than picked from a list.
 */
export class Audio {
  private ctx: AudioContext | null = null;
  private master: GainNode | null = null;
  private noise: AudioBuffer | null = null;
  private lastClatter = 0;

  /** Browsers won't let us make noise until the user has done something. */
  resume(): void {
    if (!this.ctx) {
      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.5;
      this.master.connect(this.ctx.destination);
      this.noise = this.makeNoise();
    }
    void this.ctx.resume();
  }

  private makeNoise(): AudioBuffer {
    const ctx = this.ctx!;
    const len = ctx.sampleRate * 0.5;
    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;
  }

  private now(): number {
    return this.ctx?.currentTime ?? 0;
  }

  /**
   * Metal on metal. A bright noise burst through a resonant band, plus a couple
   * of inharmonic partials — that ring is what makes it read as cutlery rather
   * than a click.
   */
  clatter(strength: number, pitch = 1): void {
    const ctx = this.ctx;
    if (!ctx || !this.master || !this.noise) return;
    const t = this.now();
    // Dozens of contacts can resolve in one step; don't stack them all.
    if (t - this.lastClatter < 0.02) return;
    this.lastClatter = t;
    const v = Math.min(1, strength);

    const src = ctx.createBufferSource();
    src.buffer = this.noise;
    src.playbackRate.value = 0.8 + Math.random() * 0.6;
    const bp = ctx.createBiquadFilter();
    bp.type = 'bandpass';
    bp.frequency.value = (2600 + Math.random() * 2600) * pitch;
    bp.Q.value = 2.5;
    const g = ctx.createGain();
    g.gain.setValueAtTime(0.28 * v, t);
    g.gain.exponentialRampToValueAtTime(0.0001, t + 0.06 + v * 0.05);
    src.connect(bp).connect(g).connect(this.master);
    src.start(t);
    src.stop(t + 0.2);

    for (let i = 0; i < 2; i++) {
      const o = ctx.createOscillator();
      o.type = 'triangle';
      o.frequency.value = (1900 + Math.random() * 3200) * pitch;
      const og = ctx.createGain();
      og.gain.setValueAtTime(0.06 * v, t);
      og.gain.exponentialRampToValueAtTime(0.0001, t + 0.12 + v * 0.3);
      o.connect(og).connect(this.master);
      o.start(t);
      o.stop(t + 0.5);
    }
  }

  /** The lever going down, and the pop coming back. */
  clunk(up = false): void {
    const ctx = this.ctx;
    if (!ctx || !this.master) return;
    const t = this.now();
    const o = ctx.createOscillator();
    o.type = 'square';
    o.frequency.setValueAtTime(up ? 180 : 120, t);
    o.frequency.exponentialRampToValueAtTime(up ? 90 : 55, t + 0.09);
    const g = ctx.createGain();
    g.gain.setValueAtTime(0.22, t);
    g.gain.exponentialRampToValueAtTime(0.0001, t + 0.16);
    const lp = ctx.createBiquadFilter();
    lp.type = 'lowpass';
    lp.frequency.value = 900;
    o.connect(lp).connect(g).connect(this.master);
    o.start(t);
    o.stop(t + 0.25);
  }

  /** Knife dragging over toast. Pitch rises with pressure. */
  scrape(pressure: number, speed: number): void {
    const ctx = this.ctx;
    if (!ctx || !this.master || !this.noise) return;
    const t = this.now();
    if (t - this.lastClatter < 0.045) return;
    this.lastClatter = t;
    const src = ctx.createBufferSource();
    src.buffer = this.noise;
    src.playbackRate.value = 0.35 + pressure * 0.5;
    const bp = ctx.createBiquadFilter();
    bp.type = 'bandpass';
    bp.frequency.value = 600 + pressure * 2600;
    bp.Q.value = 1.1;
    const g = ctx.createGain();
    const v = Math.min(0.14, 0.03 + speed * 0.05);
    g.gain.setValueAtTime(v, t);
    g.gain.exponentialRampToValueAtTime(0.0001, t + 0.1);
    src.connect(bp).connect(g).connect(this.master);
    src.start(t);
    src.stop(t + 0.2);
  }

  /** The judge's stamp. */
  stamp(): void {
    const ctx = this.ctx;
    if (!ctx || !this.master) return;
    const t = this.now();
    const o = ctx.createOscillator();
    o.type = 'sine';
    o.frequency.setValueAtTime(150, t);
    o.frequency.exponentialRampToValueAtTime(48, t + 0.11);
    const g = ctx.createGain();
    g.gain.setValueAtTime(0.4, t);
    g.gain.exponentialRampToValueAtTime(0.0001, t + 0.22);
    o.connect(g).connect(this.master);
    o.start(t);
    o.stop(t + 0.3);
  }
}

export const audio = new Audio();
