/**
 * Lane F — synthesized one-shots.
 *
 * Every sound is built live from oscillators, the shared noise buffer, filters
 * and envelopes — no samples. Comedy > realism: the squish/boing/splat trio
 * fires constantly, so they get the most layering (noise body + tonal blip +
 * pitch sweep) rather than a bare beep.
 *
 * All are pitch-randomized ±10% per play, all early-out silently while the
 * engine is dormant (`voice()` returns `null`), and all are SHORT (<400 ms)
 * except the fanfare.
 */
import { PALETTE, type PaintColor } from '../contracts'
import type { AudioEngine } from './engine'

// PALETTE key order == the frozen colour order; index → pentatonic step.
const COLOR_ORDER = Object.keys(PALETTE) as PaintColor[]
// Major pentatonic degrees (semitones), one per colour, spanning just past an
// octave so each colour splats at its own recognisable pitch.
const PENTA = [0, 2, 4, 7, 9, 12, 14]

/** 0.9..1.1 — the ±10% per-play pitch wobble. */
const pr = (): number => 0.9 + Math.random() * 0.2
const rand = (a: number, b: number): number => a + Math.random() * (b - a)
const clamp01 = (x: number): number => (x < 0 ? 0 : x > 1 ? 1 : x)

/** Percussive AD envelope on a gain param. Ramps stay >0 for exp legality. */
function perc(
  param: AudioParam,
  t0: number,
  peak: number,
  attack: number,
  hold: number,
  release: number,
): void {
  const floor = 0.0001
  param.cancelScheduledValues(t0)
  param.setValueAtTime(floor, t0)
  param.exponentialRampToValueAtTime(Math.max(peak, floor * 2), t0 + attack)
  const s = t0 + attack + hold
  param.setValueAtTime(Math.max(peak, floor * 2), s)
  param.exponentialRampToValueAtTime(floor, s + release)
}

// ---------------------------------------------------------------------------
// squish — blob:landed. Wet splat whose weight scales with impact velocity.
// Small hops go "blip", big drops go "SPLORÐ". impact is downward speed (>0.8).
// ---------------------------------------------------------------------------
export function squish(engine: AudioEngine, impact = 1): void {
  const ctx = engine.ctx
  if (!ctx) return
  const b = clamp01((impact - 0.8) / 14) // 0 = feather landing, 1 = huge drop
  const t = engine.time
  const dur = 0.09 + b * 0.2
  const out = engine.voice(t + dur + 0.1)
  if (!out) return
  out.gain.value = 1

  // Wet body: lowpass noise with a downward filter sweep = the squelch.
  const noise = engine.noiseSource()
  if (noise) {
    const lp = ctx.createBiquadFilter()
    lp.type = 'lowpass'
    lp.Q.value = 6 + b * 4
    const f0 = (1600 - b * 800) * pr()
    lp.frequency.setValueAtTime(f0, t)
    lp.frequency.exponentialRampToValueAtTime(Math.max(120, f0 * 0.25), t + dur)
    const ng = ctx.createGain()
    perc(ng.gain, t, 0.4 + b * 0.5, 0.004, 0.005, dur)
    noise.connect(lp).connect(ng).connect(out)
    noise.start(t)
    noise.stop(t + dur + 0.05)
  }

  // Tonal "womp" — a sine that drops in pitch, giving the landing its weight.
  const body = ctx.createOscillator()
  body.type = 'sine'
  const bf = (230 - b * 110) * pr()
  body.frequency.setValueAtTime(bf * 1.6, t)
  body.frequency.exponentialRampToValueAtTime(bf * 0.6, t + dur)
  const bg = ctx.createGain()
  perc(bg.gain, t, 0.3 + b * 0.45, 0.004, 0.01, dur * 0.9)
  body.connect(bg).connect(out)
  body.start(t)
  body.stop(t + dur + 0.05)

  // Big landings add a sub thud for real weight.
  if (b > 0.35) {
    const sub = ctx.createOscillator()
    sub.type = 'sine'
    sub.frequency.setValueAtTime(70 * pr(), t)
    sub.frequency.exponentialRampToValueAtTime(45, t + dur)
    const sg = ctx.createGain()
    perc(sg.gain, t, 0.25 + b * 0.35, 0.005, 0.01, dur)
    sub.connect(sg).connect(out)
    sub.start(t)
    sub.stop(t + dur + 0.05)
  }
}

// ---------------------------------------------------------------------------
// boing — blob:jumped (and the gap-launcher). Springy up-then-down sweep with
// a decaying wobble so it reads as a comedy spring, not a synth blip.
// ---------------------------------------------------------------------------
export function boing(engine: AudioEngine): void {
  const ctx = engine.ctx
  if (!ctx) return
  const t = engine.time
  const dur = 0.26
  const out = engine.voice(t + dur + 0.1)
  if (!out) return
  out.gain.value = 1

  const osc = ctx.createOscillator()
  osc.type = 'triangle'
  const f0 = 300 * pr()
  osc.frequency.setValueAtTime(f0, t)
  osc.frequency.exponentialRampToValueAtTime(f0 * 2.2, t + 0.05) // spring loads
  osc.frequency.exponentialRampToValueAtTime(f0 * 0.85, t + dur) // ...and releases

  // Wobble: an LFO on the frequency whose depth decays = the "oi-oi-oing".
  const lfo = ctx.createOscillator()
  lfo.type = 'sine'
  lfo.frequency.value = 26 * pr()
  const lfoGain = ctx.createGain()
  lfoGain.gain.setValueAtTime(90, t + 0.05)
  lfoGain.gain.exponentialRampToValueAtTime(2, t + dur)
  lfo.connect(lfoGain).connect(osc.frequency)

  const g = ctx.createGain()
  perc(g.gain, t, 0.4, 0.004, 0.02, dur)
  osc.connect(g).connect(out)
  osc.start(t)
  lfo.start(t)
  osc.stop(t + dur + 0.05)
  lfo.stop(t + dur + 0.05)
}

// ---------------------------------------------------------------------------
// splat — paint:splatted target:'blob'. Wet noise burst + a pitched "bloop";
// each colour bloops at its own pentatonic pitch (PALETTE order).
// ---------------------------------------------------------------------------
export function splat(engine: AudioEngine, color: PaintColor): void {
  const ctx = engine.ctx
  if (!ctx) return
  const t = engine.time
  const dur = 0.11
  const out = engine.voice(t + dur + 0.1)
  if (!out) return
  out.gain.value = 1

  const idx = Math.max(0, COLOR_ORDER.indexOf(color))
  const semis = PENTA[idx % PENTA.length]
  const tone = 300 * Math.pow(2, semis / 12) * pr()

  // Wet "pfft": bandpassed noise, very short.
  const noise = engine.noiseSource()
  if (noise) {
    const bp = ctx.createBiquadFilter()
    bp.type = 'bandpass'
    bp.frequency.value = (900 + idx * 130) * pr()
    bp.Q.value = 1.4
    const ng = ctx.createGain()
    perc(ng.gain, t, 0.42, 0.002, 0.004, 0.06)
    noise.connect(bp).connect(ng).connect(out)
    noise.start(t)
    noise.stop(t + 0.09)
  }

  // Pitched bloop — the colour's identity, drops a little as it lands.
  const osc = ctx.createOscillator()
  osc.type = 'sine'
  osc.frequency.setValueAtTime(tone, t)
  osc.frequency.exponentialRampToValueAtTime(tone * 0.72, t + dur)
  const g = ctx.createGain()
  perc(g.gain, t, 0.34, 0.003, 0.006, dur)
  osc.connect(g).connect(out)
  osc.start(t)
  osc.stop(t + dur + 0.05)
}

// ---------------------------------------------------------------------------
// sparkle — paint:request-cleanse. Bubbly ascending arpeggio + a fizz of
// highpassed noise: detergent bubbles washing you clean.
// ---------------------------------------------------------------------------
export function sparkle(engine: AudioEngine): void {
  const ctx = engine.ctx
  if (!ctx) return
  const t = engine.time
  const steps = 5
  const gap = 0.05
  const dur = steps * gap + 0.14
  const out = engine.voice(t + dur + 0.1)
  if (!out) return
  out.gain.value = 1

  const root = 660 * pr()
  const arp = [0, 4, 7, 12, 16] // bright, bubbly, ascending
  for (let i = 0; i < steps; i++) {
    const bt = t + i * gap
    const osc = ctx.createOscillator()
    osc.type = 'triangle'
    const f = root * Math.pow(2, arp[i] / 12) * rand(0.99, 1.02)
    osc.frequency.setValueAtTime(f * 0.98, bt)
    osc.frequency.exponentialRampToValueAtTime(f, bt + 0.02) // little bubble "pip"
    const g = ctx.createGain()
    perc(g.gain, bt, 0.16, 0.003, 0.005, 0.07)
    osc.connect(g).connect(out)
    osc.start(bt)
    osc.stop(bt + 0.1)
  }

  // Airy fizz underneath.
  const noise = engine.noiseSource()
  if (noise) {
    const hp = ctx.createBiquadFilter()
    hp.type = 'highpass'
    hp.frequency.value = 3500
    const ng = ctx.createGain()
    perc(ng.gain, t, 0.06, 0.02, 0.02, dur)
    noise.connect(hp).connect(ng).connect(out)
    noise.start(t)
    noise.stop(t + dur)
  }
}

// ---------------------------------------------------------------------------
// clank + windup — machine:signal. A ratchet that accelerates over ~0.5 s to
// match the contraption telegraph, then lands a clunk when it fires.
// ---------------------------------------------------------------------------
export function clank(engine: AudioEngine): void {
  const ctx = engine.ctx
  if (!ctx) return
  const t0 = engine.time
  const windup = 0.5 // matches the 0.5 s machine telegraph
  const out = engine.voice(t0 + windup + 0.22)
  if (!out) return
  out.gain.value = 1

  // Schedule accelerating ticks: interval shrinks, pitch rises = building tension.
  // The interval is floored (so the geometric decay can't converge below `windup`
  // into an infinite loop) and the count is capped as a hard safety rail.
  let tt = t0
  let interval = 0.09
  const pitchBase = 240 * pr()
  let n = 0
  while (tt < t0 + windup && n < 16) {
    const click = ctx.createOscillator()
    click.type = 'square'
    click.frequency.value = pitchBase * (1 + n * 0.12)
    const cg = ctx.createGain()
    perc(cg.gain, tt, 0.14, 0.001, 0.002, 0.02)
    click.connect(cg).connect(out)
    click.start(tt)
    click.stop(tt + 0.03)

    // A tiny noise transient sharpens each ratchet tooth.
    const noise = engine.noiseSource()
    if (noise) {
      const bp = ctx.createBiquadFilter()
      bp.type = 'bandpass'
      bp.frequency.value = 2600
      bp.Q.value = 2
      const ng = ctx.createGain()
      perc(ng.gain, tt, 0.12, 0.001, 0.001, 0.015)
      noise.connect(bp).connect(ng).connect(out)
      noise.start(tt)
      noise.stop(tt + 0.02)
    }

    tt += interval
    interval = Math.max(0.028, interval * 0.82) // accelerate, but floor the tooth gap
    n++
  }

  // Landing clunk right as the machine fires.
  const tf = t0 + windup
  const clunk = ctx.createOscillator()
  clunk.type = 'sine'
  clunk.frequency.setValueAtTime(180 * pr(), tf)
  clunk.frequency.exponentialRampToValueAtTime(70, tf + 0.14)
  const clg = ctx.createGain()
  perc(clg.gain, tf, 0.4, 0.002, 0.01, 0.16)
  clunk.connect(clg).connect(out)
  clunk.start(tf)
  clunk.stop(tf + 0.22)
}

// ---------------------------------------------------------------------------
// tick — race:started. Single crisp starter blip.
// ---------------------------------------------------------------------------
export function tick(engine: AudioEngine): void {
  const ctx = engine.ctx
  if (!ctx) return
  const t = engine.time
  const out = engine.voice(t + 0.14)
  if (!out) return
  out.gain.value = 1

  const osc = ctx.createOscillator()
  osc.type = 'square'
  osc.frequency.value = 1000 * pr()
  const g = ctx.createGain()
  perc(g.gain, t, 0.32, 0.002, 0.03, 0.06)
  osc.connect(g).connect(out)
  osc.start(t)
  osc.stop(t + 0.13)
}

// ---------------------------------------------------------------------------
// fanfare — race:finished. Dumb triumphant major arpeggio (root+fifth "band"),
// with an extra rising flourish + sparkle tail when it's a NEW BEST.
// ---------------------------------------------------------------------------
export function fanfare(engine: AudioEngine, isBest = false): void {
  const ctx = engine.ctx
  if (!ctx) return
  const t0 = engine.time
  const root = 392 * pr() // ~G4
  const notes = [0, 4, 7, 12] // major arpeggio, dumb and happy
  const step = 0.14
  const lastHold = 0.32

  let end = t0 + notes.length * step + lastHold
  const flourish = isBest ? [16, 19, 24] : []
  if (isBest) end += flourish.length * 0.08 + 0.3
  const out = engine.voice(end + 0.15)
  if (!out) return
  out.gain.value = 1

  const blast = (freq: number, at: number, len: number, peak: number): void => {
    // Two detuned voices (root + a fifth on top) for a cheap brass-band stack.
    for (const [mult, det, gainMul] of [
      [1, 0, 1],
      [1.5, 3, 0.5],
    ] as const) {
      const osc = ctx.createOscillator()
      osc.type = 'square'
      osc.frequency.value = freq * mult
      osc.detune.value = det
      const g = ctx.createGain()
      perc(g.gain, at, peak * gainMul, 0.005, len * 0.4, len * 0.6)
      // Gentle lowpass keeps the squares from being harsh.
      const lp = ctx.createBiquadFilter()
      lp.type = 'lowpass'
      lp.frequency.value = 4200
      osc.connect(lp).connect(g).connect(out)
      osc.start(at)
      osc.stop(at + len + 0.05)
    }
  }

  notes.forEach((semi, i) => {
    const at = t0 + i * step
    const len = i === notes.length - 1 ? lastHold : step * 1.05
    blast(root * Math.pow(2, semi / 12), at, len, 0.3)
  })

  if (isBest) {
    // Rising flourish above the arpeggio, then a bright sparkle cap.
    const base = t0 + notes.length * step
    flourish.forEach((semi, i) => {
      blast(root * Math.pow(2, semi / 12), base + i * 0.08, 0.12, 0.26)
    })
    sparkleTail(engine, out, base + flourish.length * 0.08)
  }
}

/** Bright descending twinkle used as the new-best cap (shares the fanfare voice). */
function sparkleTail(engine: AudioEngine, out: GainNode, at: number): void {
  const ctx = engine.ctx
  if (!ctx) return
  const notes = [24, 19, 26, 31]
  const root = 392
  notes.forEach((semi, i) => {
    const bt = at + i * 0.045
    const osc = ctx.createOscillator()
    osc.type = 'triangle'
    osc.frequency.value = root * Math.pow(2, semi / 12)
    const g = ctx.createGain()
    perc(g.gain, bt, 0.14, 0.003, 0.004, 0.09)
    osc.connect(g).connect(out)
    osc.start(bt)
    osc.stop(bt + 0.12)
  })
}

// ---------------------------------------------------------------------------
// Optional ambient — a very quiet lo-fi conveyor hum / room tone. Not started
// automatically (the wire protocol carries no machine-proximity signal, so
// there is nothing to gate it on); the demo exposes it behind a toggle, and
// integration can start/stop it if a proximity cue is ever added.
// ---------------------------------------------------------------------------
export interface Ambient {
  stop(): void
}

export function startAmbientHum(engine: AudioEngine): Ambient | null {
  const ctx = engine.ctx
  if (!ctx) return null
  const t = engine.time
  const out = ctx.createGain()
  out.gain.setValueAtTime(0.0001, t)
  out.gain.exponentialRampToValueAtTime(0.05, t + 0.6) // barely-there
  // Route straight to destination through the engine's own master would be
  // ideal, but master is private; a local gain at -26 dB-ish is safe & quiet.
  const post = ctx.createGain()
  post.gain.value = dbToGainLocal(-12)
  out.connect(post).connect(ctx.destination)

  // Low drone (two detuned saws) + filtered noise = conveyor rumble.
  const oscs: OscillatorNode[] = []
  for (const f of [55, 55.4]) {
    const o = ctx.createOscillator()
    o.type = 'sawtooth'
    o.frequency.value = f
    const lp = ctx.createBiquadFilter()
    lp.type = 'lowpass'
    lp.frequency.value = 220
    const g = ctx.createGain()
    g.gain.value = 0.5
    o.connect(lp).connect(g).connect(out)
    o.start(t)
    oscs.push(o)
  }
  const noise = engine.noiseSource()
  if (noise) {
    const bp = ctx.createBiquadFilter()
    bp.type = 'bandpass'
    bp.frequency.value = 180
    bp.Q.value = 0.7
    const g = ctx.createGain()
    g.gain.value = 0.25
    noise.connect(bp).connect(g).connect(out)
    noise.start(t)
    oscs.push(noise as unknown as OscillatorNode)
  }

  return {
    stop(): void {
      const now = ctx.currentTime
      out.gain.cancelScheduledValues(now)
      out.gain.setValueAtTime(out.gain.value, now)
      out.gain.exponentialRampToValueAtTime(0.0001, now + 0.3)
      for (const o of oscs) {
        try {
          o.stop(now + 0.35)
        } catch {
          /* already stopped */
        }
      }
    },
  }
}

const dbToGainLocal = (db: number): number => Math.pow(10, db / 20)
