/**
 * Layer 1 — the diegetic minimum. Always on, from the first click, before any discovery.
 *
 * These five sounds are the whole audio world until the player digs up the optical fossil.
 * That is the point: the silence around them is the tease. Everything here is one-shot and
 * self-stopping — no continuous voices, so a pre-discovery factory is genuinely quiet between
 * events rather than humming under a noise floor.
 *
 * Every sound takes (ctx, dest, t0) so it can be rendered offline for spectral verification.
 */
import { envGain, hash01, noiseSource } from './graph';

/**
 * PLACEMENT THUNK — a heavy thing set down on a metal deck.
 * Low sine drops 150→55Hz (the mass) under a filtered noise slap (the contact).
 */
export function thunk(ctx: BaseAudioContext, dest: AudioNode, t0: number, variant = 0) {
  const detune = 1 + (hash01(variant) - 0.5) * 0.18; // no two placements identical

  const osc = ctx.createOscillator();
  osc.type = 'sine';
  osc.frequency.setValueAtTime(150 * detune, t0);
  osc.frequency.exponentialRampToValueAtTime(55 * detune, t0 + 0.11);
  const og = envGain(ctx, t0, 0.5, 0.004, 0.16);
  osc.connect(og).connect(dest);
  osc.start(t0);
  osc.stop(t0 + 0.22);

  const slap = noiseSource(ctx, 'white');
  const bp = ctx.createBiquadFilter();
  bp.type = 'bandpass';
  bp.frequency.value = 1400 * detune;
  bp.Q.value = 0.8;
  const sg = envGain(ctx, t0, 0.22, 0.002, 0.05);
  slap.connect(bp).connect(sg).connect(dest);
  slap.start(t0);
  slap.stop(t0 + 0.09);
}

/**
 * FAX CHATTER — dot-matrix print head. A burst of short square blips at ~55Hz repetition
 * with a paper-feed hiss under it. The commission fax is the game's voice; it should sound
 * like an office machine that has never once been serviced.
 */
export function faxChatter(ctx: BaseAudioContext, dest: AudioNode, t0: number, chars = 14) {
  const bus = ctx.createGain();
  bus.gain.value = 0.5;
  bus.connect(dest);

  for (let i = 0; i < chars; i++) {
    const t = t0 + i * 0.018 + hash01(i * 7.7) * 0.004;
    const o = ctx.createOscillator();
    o.type = 'square';
    o.frequency.value = 320 + hash01(i * 3.3) * 260;
    const g = envGain(ctx, t, 0.16, 0.0015, 0.012);
    o.connect(g).connect(bus);
    o.start(t);
    o.stop(t + 0.02);
  }

  // paper feed
  const paper = noiseSource(ctx, 'white');
  const hp = ctx.createBiquadFilter();
  hp.type = 'highpass';
  hp.frequency.value = 2600;
  const pg = envGain(ctx, t0, 0.09, 0.02, chars * 0.018 + 0.1);
  paper.connect(hp).connect(pg).connect(bus);
  paper.start(t0);
  paper.stop(t0 + chars * 0.018 + 0.2);
}

/**
 * BROWNOUT — the codex's catastrophic underrun, finally audible.
 * A hard pop transient, a burst of crackle, and a power-down whine sliding 420→40Hz as the
 * rails collapse. This is the single most important sound in the game: it is the punishment.
 */
export function brownoutHit(ctx: BaseAudioContext, dest: AudioNode, t0: number) {
  // the pop: a single-sample-ish click, deliberately broadband
  const pop = ctx.createOscillator();
  pop.type = 'square';
  pop.frequency.setValueAtTime(90, t0);
  const pg = envGain(ctx, t0, 0.75, 0.0008, 0.03);
  pop.connect(pg).connect(dest);
  pop.start(t0);
  pop.stop(t0 + 0.04);

  // crackle: brown noise gated hard
  const cr = noiseSource(ctx, 'brown');
  const crf = ctx.createBiquadFilter();
  crf.type = 'bandpass';
  crf.frequency.value = 800;
  crf.Q.value = 0.6;
  const cg = envGain(ctx, t0, 0.5, 0.003, 0.5);
  cr.connect(crf).connect(cg).connect(dest);
  cr.start(t0);
  cr.stop(t0 + 0.7);

  // power-down whine: the flywheel losing it
  const wh = ctx.createOscillator();
  wh.type = 'sawtooth';
  wh.frequency.setValueAtTime(420, t0);
  wh.frequency.exponentialRampToValueAtTime(40, t0 + 0.9);
  const wf = ctx.createBiquadFilter();
  wf.type = 'lowpass';
  wf.frequency.setValueAtTime(2200, t0);
  wf.frequency.exponentialRampToValueAtTime(300, t0 + 0.9);
  const wg = envGain(ctx, t0, 0.3, 0.02, 0.95);
  wh.connect(wf).connect(wg).connect(dest);
  wh.start(t0);
  wh.stop(t0 + 1.0);
}

/** Power coming back: the whine in reverse, shorter and less dramatic. Relief, not triumph. */
export function brownoutRecover(ctx: BaseAudioContext, dest: AudioNode, t0: number) {
  const wh = ctx.createOscillator();
  wh.type = 'sawtooth';
  wh.frequency.setValueAtTime(60, t0);
  wh.frequency.exponentialRampToValueAtTime(360, t0 + 0.35);
  const lp = ctx.createBiquadFilter();
  lp.type = 'lowpass';
  lp.frequency.value = 1400;
  const g = envGain(ctx, t0, 0.2, 0.05, 0.32);
  wh.connect(lp).connect(g).connect(dest);
  wh.start(t0);
  wh.stop(t0 + 0.45);
}

/**
 * SCRAM KLAXON — two-tone industrial alarm, 620/440Hz, square, deliberately unpleasant.
 * An OSHA-compliant noise for an entirely non-compliant machine.
 */
export function klaxon(ctx: BaseAudioContext, dest: AudioNode, t0: number) {
  const bus = ctx.createGain();
  bus.gain.value = 0.34;
  bus.connect(dest);
  const tones = [620, 440, 620, 440];
  for (let i = 0; i < tones.length; i++) {
    const t = t0 + i * 0.22;
    const o = ctx.createOscillator();
    o.type = 'square';
    o.frequency.value = tones[i];
    const g = envGain(ctx, t, 0.5, 0.01, 0.19);
    // a lowpass keeps it a klaxon rather than a buzzer
    const lp = ctx.createBiquadFilter();
    lp.type = 'lowpass';
    lp.frequency.value = 1800;
    o.connect(lp).connect(g).connect(bus);
    o.start(t);
    o.stop(t + 0.21);
  }
}

/**
 * RATIFICATION STING — The Correction resolves; you don't.
 * A clean major triad (F–A–C) on sines with a soft bell attack. It is the ONLY consonant,
 * in-tune, untroubled sound in the entire game. Everything the player makes is filth; this
 * is what the enemy sounds like, and it sounds *nice*, which is the joke.
 */
export function ratifySting(ctx: BaseAudioContext, dest: AudioNode, t0: number) {
  const bus = ctx.createGain();
  bus.gain.value = 0.3;
  bus.connect(dest);
  const triad = [349.23, 440.0, 523.25]; // F4 A4 C5
  triad.forEach((f, i) => {
    const o = ctx.createOscillator();
    o.type = 'sine';
    o.frequency.value = f;
    const g = envGain(ctx, t0 + i * 0.035, 0.4, 0.02, 1.1);
    o.connect(g).connect(bus);
    o.start(t0 + i * 0.035);
    o.stop(t0 + 1.4);
  });
  // a touch of shimmer an octave up so it reads as "official" rather than "warm"
  const shimmer = ctx.createOscillator();
  shimmer.type = 'sine';
  shimmer.frequency.value = 1046.5;
  const sg = envGain(ctx, t0, 0.12, 0.03, 0.9);
  shimmer.connect(sg).connect(bus);
  shimmer.start(t0);
  shimmer.stop(t0 + 1.1);
}

/** TAPE CHUNK — a shipment lands. Layer 3, but it lives here with its siblings. */
export function tapeChunk(ctx: BaseAudioContext, dest: AudioNode, t0: number, variant = 0) {
  const d = 1 + (hash01(variant * 5.5) - 0.5) * 0.2;
  const n = noiseSource(ctx, 'white');
  const bp = ctx.createBiquadFilter();
  bp.type = 'bandpass';
  bp.frequency.value = 520 * d;
  bp.Q.value = 1.4;
  const g = envGain(ctx, t0, 0.3, 0.002, 0.07);
  n.connect(bp).connect(g).connect(dest);
  n.start(t0);
  n.stop(t0 + 0.1);

  const thud = ctx.createOscillator();
  thud.type = 'sine';
  thud.frequency.setValueAtTime(110 * d, t0);
  thud.frequency.exponentialRampToValueAtTime(70 * d, t0 + 0.06);
  const tg = envGain(ctx, t0, 0.22, 0.003, 0.09);
  thud.connect(tg).connect(dest);
  thud.start(t0);
  thud.stop(t0 + 0.12);
}
