/**
 * THE SPEAKER's plumbing: master chain, shared noise, scheduling helpers.
 *
 * HOUSE LAW: every sound is synthesized with Web Audio. No asset files, ever. A game about
 * broken media makes its own noise.
 *
 * Two rules that shape everything here:
 *  - **Zero per-frame allocations.** Continuous voices (hum, station beds) are built once at
 *    unlock and then only have their AudioParams driven. Only genuinely one-shot sounds
 *    allocate nodes, and those are short-lived and self-stopping.
 *  - **Context-agnostic.** Nothing below assumes a live AudioContext. Every builder takes a
 *    BaseAudioContext, so the whole synth stack can be rendered in an OfflineAudioContext for
 *    deterministic spectral verification — the same code the player hears.
 */

/** Noise buffers are expensive to fill, so each context gets one of each, cached. */
const noiseCache = new WeakMap<BaseAudioContext, { white: AudioBuffer; brown: AudioBuffer }>();

function buildNoise(ctx: BaseAudioContext) {
  const len = Math.floor(ctx.sampleRate * 2); // 2s loops — long enough to not read as periodic
  const white = ctx.createBuffer(1, len, ctx.sampleRate);
  const brown = ctx.createBuffer(1, len, ctx.sampleRate);
  const w = white.getChannelData(0);
  const b = brown.getChannelData(0);
  // Deterministic PRNG: same noise every run, so spectra are reproducible across verifications.
  let seed = 0x1337c0de;
  const rnd = () => {
    seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5;
    return (seed >>> 0) / 4294967296 * 2 - 1;
  };
  let last = 0;
  for (let i = 0; i < len; i++) {
    const n = rnd();
    w[i] = n;
    // brown/red noise: integrated white, gently leaked to stop DC runaway
    last = (last + 0.02 * n) / 1.02;
    b[i] = last * 3.5;
  }
  return { white, brown };
}

export function noise(ctx: BaseAudioContext) {
  let n = noiseCache.get(ctx);
  if (!n) {
    n = buildNoise(ctx);
    noiseCache.set(ctx, n);
  }
  return n;
}

/** A looping noise source. Caller owns start/stop. */
export function noiseSource(ctx: BaseAudioContext, kind: 'white' | 'brown'): AudioBufferSourceNode {
  const src = ctx.createBufferSource();
  src.buffer = noise(ctx)[kind];
  src.loop = true;
  return src;
}

export interface MasterChain {
  /** everything in the mix connects here */
  input: GainNode;
  /** master volume, pre-limiter */
  volume: GainNode;
}

/**
 * gain → soft limiter → destination.
 *
 * The limiter is a WaveShaper running tanh, not a DynamicsCompressor: a compressor's lookahead
 * and automatic makeup would pump the whole mix every time the klaxon fires. tanh just refuses
 * to go past 1, which is what "don't clip" actually means, and it adds the faint saturation a
 * blown factory PA ought to have.
 */
export function createMasterChain(ctx: BaseAudioContext): MasterChain {
  const input = ctx.createGain();
  const volume = ctx.createGain();
  volume.gain.value = 0.9;

  const limiter = ctx.createWaveShaper();
  const n = 1024;
  const curve = new Float32Array(n);
  for (let i = 0; i < n; i++) {
    const x = (i / (n - 1)) * 2 - 1;
    curve[i] = Math.tanh(x * 1.6);
  }
  limiter.curve = curve;
  limiter.oversample = '2x';

  input.connect(volume);
  volume.connect(limiter);
  limiter.connect(ctx.destination);
  return { input, volume };
}

/** Ramp a param without clicks. Web Audio hates instantaneous jumps on a running graph. */
export function glide(p: AudioParam, to: number, ctx: BaseAudioContext, seconds = 0.05) {
  const t = ctx.currentTime;
  p.cancelScheduledValues(t);
  p.setValueAtTime(p.value, t);
  p.linearRampToValueAtTime(to, t + seconds);
}

/**
 * A one-shot envelope on a fresh gain node: attack to peak, decay to silence, then disconnect.
 * Returns the gain so the caller can hang a source off it.
 */
export function envGain(
  ctx: BaseAudioContext,
  t0: number,
  peak: number,
  attack: number,
  decay: number,
): GainNode {
  const g = ctx.createGain();
  g.gain.setValueAtTime(0.0001, t0);
  g.gain.exponentialRampToValueAtTime(Math.max(0.0002, peak), t0 + attack);
  g.gain.exponentialRampToValueAtTime(0.0001, t0 + attack + decay);
  return g;
}

/** Deterministic 0..1 hash — used anywhere a sound needs stable variation without Math.random. */
export function hash01(n: number): number {
  const x = Math.sin(n * 12.9898 + 78.233) * 43758.5453;
  return x - Math.floor(x);
}
