// Lightweight game audio: one looping "scene music" track plus synthesized one-shot
// SFX. Zero dependencies. Browsers block autoplay until a user gesture, so music/SFX
// are primed on the first pointer interaction.
type Sfx = 'advance' | 'choice' | 'sting'
let baseVolume = 0.5 // authored scene volume (0-1); mute overrides to 0

let ctx: AudioContext | null = null
function audioContext() {
  if (!ctx) { const AC = window.AudioContext || (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; if (AC) ctx = new AC() }
  return ctx
}

let noiseBuffer: AudioBuffer | null = null
function noise(context: AudioContext) {
  if (!noiseBuffer) {
    const length = Math.floor(context.sampleRate * 0.05)
    noiseBuffer = context.createBuffer(1, length, context.sampleRate)
    const data = noiseBuffer.getChannelData(0)
    for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1
  }
  return noiseBuffer
}

let markerNoiseBuffer:AudioBuffer | null=null
function markerNoise(context:AudioContext) {
  if (!markerNoiseBuffer) {
    const length=Math.floor(context.sampleRate * .31)
    markerNoiseBuffer=context.createBuffer(1,length,context.sampleRate)
    const data=markerNoiseBuffer.getChannelData(0)
    let previous=0
    for (let index=0;index < length;index++) {
      const white=Math.random() * 2 - 1
      previous=previous * .66 + white * .34
      data[index]=previous * (.72 + Math.sin(index / 37) * .18)
    }
  }
  return markerNoiseBuffer
}

const music = typeof Audio !== 'undefined' ? new Audio() : null
if (music) music.loop = true
let currentUrl: string | null = null
let muted = false
let pending = false
let fadeTimer: number | undefined

function fade(target: number, done?: () => void) {
  if (!music) return
  window.clearInterval(fadeTimer)
  const step = (target - music.volume) / 12 || (target > music.volume ? 0.1 : -0.1)
  fadeTimer = window.setInterval(() => {
    if (!music) return
    const next = music.volume + step
    if ((step >= 0 && next >= target) || (step <= 0 && next <= target)) { music.volume = target; window.clearInterval(fadeTimer); done?.() }
    else music.volume = Math.max(0, Math.min(1, next))
  }, 40)
}

function startMusic() {
  if (!music || !currentUrl || muted) return
  const promise = music.play()
  if (promise) promise.then(() => { pending = false; fade(baseVolume) }).catch(() => { pending = true })
}

export const audio = {
  // A null url inherits whatever is already playing; a new url crossfades to it.
  // The same url with a new volume just adjusts the level (no restart).
  setMusic(url: string | null, volume?: number) {
    if (!music) return
    if (volume !== undefined) baseVolume = Math.max(0, Math.min(1, volume))
    if (url === currentUrl) { if (volume !== undefined && currentUrl && !muted) fade(baseVolume); return }
    currentUrl = url
    if (!url) { fade(0, () => music.pause()); pending = false; return }
    music.src = url
    music.volume = 0
    startMusic()
  },
  toggleMute() {
    muted = !muted
    if (muted) fade(0)
    else if (currentUrl) { if (music && music.paused) startMusic(); else fade(baseVolume) }
    return muted
  },
  isMuted() { return muted },
  sfx(kind: Sfx) {
    if (muted) return
    const context = audioContext()
    if (!context) return
    // Called from a click, so we can unlock the context right here if it's suspended.
    if (context.state === 'suspended') void context.resume()
    const osc = context.createOscillator(), gain = context.createGain()
    osc.type = 'triangle'
    const now = context.currentTime + 0.01
    const base = kind === 'choice' ? 500 : kind === 'sting' ? 260 : 420
    osc.frequency.setValueAtTime(base, now)
    if (kind === 'choice') osc.frequency.exponentialRampToValueAtTime(base * 1.5, now + 0.09) // a little up-chirp
    gain.gain.setValueAtTime(0.0001, now)
    gain.gain.exponentialRampToValueAtTime(0.16, now + 0.012)
    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.16)
    osc.connect(gain).connect(context.destination)
    osc.start(now); osc.stop(now + 0.18)
  },
  // A dry typewriter key-clack, for the text-reveal clatter. Short filtered noise
  // burst with slight pitch jitter so successive keys differ.
  type() {
    if (muted) return
    const context = audioContext()
    if (!context) return
    if (context.state === 'suspended') void context.resume()
    const src = context.createBufferSource(); src.buffer = noise(context)
    const filter = context.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.value = 1500 + Math.random() * 900; filter.Q.value = 0.9
    const gain = context.createGain()
    const now = context.currentTime
    gain.gain.setValueAtTime(0.06, now)
    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.028)
    src.connect(filter).connect(gain).connect(context.destination)
    src.start(now); src.stop(now + 0.04)
  },
  // A restrained felt-tip-on-paper scratch. The caller supplies the handwriting
  // duration so the sound ends with the incremental letter reveal.
  sharpie(durationMs:number) {
    if (muted) return undefined
    const context=audioContext()
    if (!context) return undefined
    if (context.state === 'suspended') void context.resume()
    const duration=Math.max(.12,Math.min(3,durationMs / 1000))
    const source=context.createBufferSource();source.buffer=markerNoise(context);source.loop=true
    const filter=context.createBiquadFilter();filter.type='bandpass';filter.frequency.value=1180;filter.Q.value=.62
    const gain=context.createGain()
    const now=context.currentTime + .012,end=now + duration
    gain.gain.setValueAtTime(.0001,now)
    gain.gain.linearRampToValueAtTime(.032,now + .035)
    for (let at=now + .055;at < end - .04;at += .045) gain.gain.setValueAtTime(.018 + Math.random() * .026,at)
    gain.gain.exponentialRampToValueAtTime(.0001,end)
    source.connect(filter).connect(gain).connect(context.destination)
    let ended=false
    source.onended=() => { ended=true }
    source.start(now);source.stop(end + .02)
    return () => { if (!ended) { try { source.stop() } catch { /* already stopped */ } } }
  },
  // Resume the context and retry pending music on a user gesture.
  resume() {
    void audioContext()?.resume?.()
    if (pending) startMusic()
  },
}

if (typeof window !== 'undefined') window.addEventListener('pointerdown', () => audio.resume())
