/**
 * WebAudio の土台。音源ファイルは一切使わず、全てここで合成する。
 * バスは 音楽 / 効果音 / 環境音 の3系統。
 */

export interface AudioBuses {
  ctx: AudioContext;
  master: GainNode;
  music: GainNode;
  sfx: GainNode;
  ambience: GainNode;
  /** 使い回すホワイトノイズ */
  noise: AudioBuffer;
}

let buses: AudioBuses | null = null;
let muted = false;

const DEFAULT_LEVELS = { master: 0.75, music: 0.42, sfx: 0.95, ambience: 0.42 };

function makeNoiseBuffer(ctx: AudioContext): AudioBuffer {
  const length = Math.floor(ctx.sampleRate * 2);
  const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
  const data = buffer.getChannelData(0);
  for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
  return buffer;
}

/** 初回のユーザー操作で呼ぶ。二度目以降は既存のコンテキストを返す。 */
export function ensureAudio(): AudioBuses | null {
  if (buses) {
    if (buses.ctx.state === 'suspended') void buses.ctx.resume();
    return buses;
  }
  const Ctor = window.AudioContext ?? (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
  if (!Ctor) return null;

  const ctx = new Ctor();
  const master = ctx.createGain();
  master.gain.value = muted ? 0 : DEFAULT_LEVELS.master;
  master.connect(ctx.destination);

  const mk = (level: number): GainNode => {
    const g = ctx.createGain();
    g.gain.value = level;
    g.connect(master);
    return g;
  };

  buses = {
    ctx,
    master,
    music: mk(DEFAULT_LEVELS.music),
    sfx: mk(DEFAULT_LEVELS.sfx),
    ambience: mk(DEFAULT_LEVELS.ambience),
    noise: makeNoiseBuffer(ctx),
  };
  return buses;
}

export function getBuses(): AudioBuses | null {
  return buses;
}

export function setMuted(next: boolean): void {
  muted = next;
  if (!buses) return;
  const { ctx, master } = buses;
  master.gain.cancelScheduledValues(ctx.currentTime);
  master.gain.setTargetAtTime(next ? 0 : DEFAULT_LEVELS.master, ctx.currentTime, 0.05);
}

export function isMuted(): boolean {
  return muted;
}

export function setMasterVolume(v: number): void {
  DEFAULT_LEVELS.master = Math.max(0, Math.min(1, v));
  if (!buses || muted) return;
  buses.master.gain.setTargetAtTime(DEFAULT_LEVELS.master, buses.ctx.currentTime, 0.05);
}

export function masterVolume(): number {
  return DEFAULT_LEVELS.master;
}

// ------------------------------------------------------------------ 合成ヘルパ

export interface ToneOptions {
  type?: OscillatorType;
  /** 開始周波数 */
  freq: number;
  /** 終端周波数 (指定するとポルタメント) */
  toFreq?: number;
  /** 開始時刻 (ctx.currentTime からのオフセット秒) */
  at?: number;
  duration: number;
  gain?: number;
  attack?: number;
  /** ビブラート (Hz, 深さcent) */
  vibrato?: { rate: number; depth: number };
  bus?: GainNode;
  /** ローパスの遮断周波数 */
  lowpass?: number;
}

/** 単純な減衰つき発振音。猫の鳴き声も機械音もこれの組み合わせで作る。 */
export function tone(opts: ToneOptions): void {
  const b = buses;
  if (!b) return;
  const t0 = b.ctx.currentTime + (opts.at ?? 0);
  const osc = b.ctx.createOscillator();
  osc.type = opts.type ?? 'sine';
  osc.frequency.setValueAtTime(opts.freq, t0);
  if (opts.toFreq !== undefined) {
    osc.frequency.exponentialRampToValueAtTime(Math.max(1, opts.toFreq), t0 + opts.duration);
  }

  let node: AudioNode = osc;
  if (opts.lowpass !== undefined) {
    const lp = b.ctx.createBiquadFilter();
    lp.type = 'lowpass';
    lp.frequency.value = opts.lowpass;
    node.connect(lp);
    node = lp;
  }

  const env = b.ctx.createGain();
  const peak = opts.gain ?? 0.2;
  const attack = opts.attack ?? 0.01;
  env.gain.setValueAtTime(0.0001, t0);
  env.gain.exponentialRampToValueAtTime(peak, t0 + attack);
  env.gain.exponentialRampToValueAtTime(0.0001, t0 + opts.duration);
  node.connect(env);
  env.connect(opts.bus ?? b.sfx);

  if (opts.vibrato) {
    const lfo = b.ctx.createOscillator();
    const depth = b.ctx.createGain();
    lfo.frequency.value = opts.vibrato.rate;
    depth.gain.value = opts.vibrato.depth;
    lfo.connect(depth);
    depth.connect(osc.frequency);
    lfo.start(t0);
    lfo.stop(t0 + opts.duration);
  }

  osc.start(t0);
  osc.stop(t0 + opts.duration + 0.02);
}

export interface NoiseOptions {
  at?: number;
  duration: number;
  gain?: number;
  attack?: number;
  filter?: BiquadFilterType;
  freq?: number;
  q?: number;
  /** 終端のフィルタ周波数 (スイープ) */
  toFreq?: number;
  bus?: GainNode;
}

/** フィルタをかけたノイズ。シャーッ、機械のガチャン、サイレンの芯などに使う。 */
export function noise(opts: NoiseOptions): void {
  const b = buses;
  if (!b) return;
  const t0 = b.ctx.currentTime + (opts.at ?? 0);
  const src = b.ctx.createBufferSource();
  src.buffer = b.noise;
  src.loop = true;

  let node: AudioNode = src;
  if (opts.filter) {
    const f = b.ctx.createBiquadFilter();
    f.type = opts.filter;
    f.frequency.setValueAtTime(opts.freq ?? 1000, t0);
    if (opts.toFreq !== undefined) {
      f.frequency.exponentialRampToValueAtTime(Math.max(20, opts.toFreq), t0 + opts.duration);
    }
    f.Q.value = opts.q ?? 1;
    node.connect(f);
    node = f;
  }

  const env = b.ctx.createGain();
  const peak = opts.gain ?? 0.2;
  const attack = opts.attack ?? 0.005;
  env.gain.setValueAtTime(0.0001, t0);
  env.gain.exponentialRampToValueAtTime(peak, t0 + attack);
  env.gain.exponentialRampToValueAtTime(0.0001, t0 + opts.duration);
  node.connect(env);
  env.connect(opts.bus ?? b.sfx);

  src.start(t0);
  src.stop(t0 + opts.duration + 0.02);
}
