import type { GameEvent } from "../game/sim";

function clamp(n: number, a: number, b: number): number {
  return Math.max(a, Math.min(b, n));
}

export class AudioBus {
  ctx: AudioContext | null = null;
  master: GainNode | null = null;
  musicGain: GainNode | null = null;
  sfxGain: GainNode | null = null;
  started = false;
  muted = false;
  musicVol = 0.7;
  sfxVol = 0.85;
  private pad: OscillatorNode | null = null;
  private pad2: OscillatorNode | null = null;
  private noise: AudioBufferSourceNode | null = null;
  private hissGain: GainNode | null = null;
  private filter: BiquadFilterNode | null = null;
  private lfo: OscillatorNode | null = null;
  private step = 0;
  private acc = 0;
  private danger = 0;
  private flowing = false;
  private bpm = 86;

  setMusic(v: number): void {
    this.musicVol = clamp(v, 0, 1);
    if (this.musicGain) this.musicGain.gain.value = this.musicVol * 0.45;
  }
  setSfx(v: number): void {
    this.sfxVol = clamp(v, 0, 1);
    if (this.sfxGain) this.sfxGain.gain.value = this.sfxVol;
  }

  async unlock(): Promise<void> {
    try {
      if (this.started) {
        await Promise.race([
          this.ctx?.resume() ?? Promise.resolve(),
          new Promise((r) => setTimeout(r, 250)),
        ]);
        return;
      }
      const ctx = new AudioContext();
      this.ctx = ctx;
      this.master = ctx.createGain();
      this.master.gain.value = 0.9;
      this.master.connect(ctx.destination);

      this.musicGain = ctx.createGain();
      this.musicGain.gain.value = this.musicVol * 0.45;
      this.musicGain.connect(this.master);

      this.sfxGain = ctx.createGain();
      this.sfxGain.gain.value = this.sfxVol;
      this.sfxGain.connect(this.master);

      this.buildMusic();
      this.started = true;
      await Promise.race([
        ctx.resume(),
        new Promise((r) => setTimeout(r, 250)),
      ]);
    } catch {
      /* autoplay policy — first click will retry */
    }
  }

  private noiseBuffer(): AudioBuffer {
    const ctx = this.ctx!;
    const buf = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
    return buf;
  }

  private buildMusic(): void {
    const ctx = this.ctx!;
    const music = this.musicGain!;

    const drone = ctx.createOscillator();
    drone.type = "sine";
    drone.frequency.value = 49;
    const dg = ctx.createGain();
    dg.gain.value = 0.22;
    drone.connect(dg).connect(music);
    drone.start();

    const pad = ctx.createOscillator();
    pad.type = "sawtooth";
    pad.frequency.value = 98;
    const pad2 = ctx.createOscillator();
    pad2.type = "sawtooth";
    pad2.frequency.value = 147;
    pad2.detune.value = 7;
    const f = ctx.createBiquadFilter();
    f.type = "lowpass";
    f.frequency.value = 420;
    f.Q.value = 8;
    const pg = ctx.createGain();
    pg.gain.value = 0.07;
    pad.connect(f);
    pad2.connect(f);
    f.connect(pg).connect(music);
    pad.start();
    pad2.start();
    this.pad = pad;
    this.pad2 = pad2;
    this.filter = f;

    const lfo = ctx.createOscillator();
    lfo.frequency.value = 0.07;
    const lfoG = ctx.createGain();
    lfoG.gain.value = 280;
    lfo.connect(lfoG).connect(f.frequency);
    lfo.start();
    this.lfo = lfo;

    const src = ctx.createBufferSource();
    src.buffer = this.noiseBuffer();
    src.loop = true;
    const nf = ctx.createBiquadFilter();
    nf.type = "bandpass";
    nf.frequency.value = 1800;
    nf.Q.value = 0.7;
    const hg = ctx.createGain();
    hg.gain.value = 0.0;
    src.connect(nf).connect(hg).connect(music);
    src.start();
    this.noise = src;
    this.hissGain = hg;
  }

  tick(dt: number, flowing: boolean, danger: number): void {
    if (!this.started || !this.ctx) return;
    this.flowing = flowing;
    this.danger = danger;
    if (this.hissGain) {
      const t = this.hissGain.gain;
      const want = flowing ? 0.03 + danger * 0.04 : 0.008;
      t.value += (want - t.value) * Math.min(1, dt * 4);
    }
    if (this.filter) {
      const want = 360 + danger * 900 + (flowing ? 180 : 0);
      this.filter.frequency.value +=
        (want - this.filter.frequency.value) * Math.min(1, dt * 2);
    }
    this.acc += dt;
    const stepDur = 60 / this.bpm / 2;
    while (this.acc >= stepDur) {
      this.acc -= stepDur;
      this.step = (this.step + 1) % 16;
      this.seq();
    }
  }

  private seq(): void {
    const s = this.step;
    const scale = [98, 110, 130.81, 146.83, 164.81, 196, 220];
    if (s === 0 || s === 8) this.thump(s === 0 ? 49 : 73);
    if (s % 4 === 2) this.hat();
    const pattern = [0, , 3, , , 4, , 2, 0, , 5, , 3, , 6, 4];
    const idx = pattern[s];
    if (idx !== undefined && this.ctx) {
      const f = scale[idx]! * (this.danger > 0.65 ? 1.12 : 1);
      this.bell(f, 0.045 + (this.flowing ? 0.02 : 0), s % 8 === 0 ? 0.7 : 0.45);
    }
  }

  private env(
    g: GainNode,
    t: number,
    a: number,
    d: number,
    peak: number,
  ): void {
    g.gain.cancelScheduledValues(t);
    g.gain.setValueAtTime(0.0001, t);
    g.gain.exponentialRampToValueAtTime(peak, t + a);
    g.gain.exponentialRampToValueAtTime(0.0001, t + a + d);
  }

  private thump(f: number): void {
    const ctx = this.ctx!;
    const o = ctx.createOscillator();
    o.type = "sine";
    o.frequency.value = f;
    const g = ctx.createGain();
    o.connect(g).connect(this.musicGain!);
    const t = ctx.currentTime;
    this.env(g, t, 0.01, 0.18, 0.16);
    o.frequency.exponentialRampToValueAtTime(f * 0.5, t + 0.16);
    o.start(t);
    o.stop(t + 0.22);
  }

  private hat(): void {
    const ctx = this.ctx!;
    const src = ctx.createBufferSource();
    src.buffer = this.noiseBuffer();
    const f = ctx.createBiquadFilter();
    f.type = "highpass";
    f.frequency.value = 6000;
    const g = ctx.createGain();
    src.connect(f).connect(g).connect(this.musicGain!);
    const t = ctx.currentTime;
    this.env(g, t, 0.005, 0.05, 0.035);
    src.start(t);
    src.stop(t + 0.08);
  }

  private bell(f: number, peak: number, dur: number): void {
    const ctx = this.ctx!;
    const t = ctx.currentTime;
    const o = ctx.createOscillator();
    o.type = "triangle";
    o.frequency.value = f;
    const o2 = ctx.createOscillator();
    o2.type = "sine";
    o2.frequency.value = f * 2.01;
    const g = ctx.createGain();
    o.connect(g);
    o2.connect(g);
    g.connect(this.musicGain!);
    this.env(g, t, 0.01, dur, peak);
    o.start(t);
    o2.start(t);
    o.stop(t + dur + 0.05);
    o2.stop(t + dur + 0.05);
  }

  tone(
    freq: number,
    dur: number,
    type: OscillatorType = "sine",
    peak = 0.2,
    slide?: number,
  ): void {
    if (!this.ctx || !this.sfxGain) return;
    const t = this.ctx.currentTime;
    const o = this.ctx.createOscillator();
    o.type = type;
    o.frequency.value = freq;
    const g = this.ctx.createGain();
    o.connect(g).connect(this.sfxGain);
    this.env(g, t, 0.008, dur, peak);
    if (slide) o.frequency.exponentialRampToValueAtTime(slide, t + dur);
    o.start(t);
    o.stop(t + dur + 0.05);
  }

  noiseBurst(dur: number, peak: number, hp: number): void {
    if (!this.ctx || !this.sfxGain) return;
    const src = this.ctx.createBufferSource();
    src.buffer = this.noiseBuffer();
    const f = this.ctx.createBiquadFilter();
    f.type = "highpass";
    f.frequency.value = hp;
    const g = this.ctx.createGain();
    src.connect(f).connect(g).connect(this.sfxGain);
    const t = this.ctx.currentTime;
    this.env(g, t, 0.01, dur, peak);
    src.start(t);
    src.stop(t + dur + 0.05);
  }

  handle(ev: GameEvent): void {
    switch (ev.type) {
      case "place":
        this.tone(420, 0.06, "square", 0.08);
        this.tone(180, 0.08, "sine", 0.12);
        break;
      case "replace":
        this.tone(140, 0.12, "sawtooth", 0.1, 90);
        this.noiseBurst(0.08, 0.08, 800);
        break;
      case "dump":
        this.noiseBurst(0.12, 0.1, 400);
        this.tone(220, 0.1, "sine", 0.06, 80);
        break;
      case "release":
        this.tone(90, 0.3, "sine", 0.2, 50);
        this.bell(392, 0.12, 0.4);
        break;
      case "enter":
        this.tone(520 + (ev.dir ?? 0) * 40, 0.04, "sine", 0.04);
        break;
      case "loop":
        this.bell(523.25, 0.14, 0.25);
        this.bell(659.25, 0.12, 0.3);
        this.bell(783.99, 0.1, 0.35);
        break;
      case "cross":
        this.bell(440, 0.1, 0.2);
        this.bell(880, 0.08, 0.22);
        break;
      case "reservoir":
        this.tone(180, 0.4, "sine", 0.12, 90);
        break;
      case "spill":
        this.noiseBurst(0.45, 0.28, 200);
        this.tone(200, 0.5, "sine", 0.2, 40);
        break;
      case "win":
        [261, 329, 392, 523, 659].forEach((f, i) => {
          setTimeout(() => this.bell(f, 0.12, 0.45), i * 90);
        });
        break;
      case "lose":
        this.tone(196, 0.5, "sawtooth", 0.1, 80);
        this.tone(110, 0.7, "sine", 0.16, 50);
        break;
      case "slide":
        this.tone(340, 0.05, "square", 0.06);
        break;
      default:
        break;
    }
  }

  ui(): void {
    this.tone(1400, 0.04, "sine", 0.05);
  }

  click(): void {
    this.tone(880, 0.05, "square", 0.04);
    this.tone(220, 0.06, "sine", 0.05);
  }

  warn(): void {
    this.tone(880, 0.08, "square", 0.05, 700);
  }
}
