export type EffectId = 'reverb' | 'echo' | 'flanger' | 'high_pass' | 'low_pass' | 'off';

export type EffectPreset = { id: EffectId; label: string; defaultValue: number };

export const EFFECT_SEQUENCE: EffectPreset[] = [
  { id: 'reverb', label: 'Reverb', defaultValue: 50 },
  { id: 'echo', label: 'Echo', defaultValue: 50 },
  { id: 'flanger', label: 'Flanger', defaultValue: 50 },
  { id: 'high_pass', label: 'High Pass', defaultValue: 50 },
  { id: 'low_pass', label: 'Low Pass', defaultValue: 50 },
  { id: 'off', label: 'Off', defaultValue: 0 },
];

export const EFFECT_IDS = new Set<EffectId>(EFFECT_SEQUENCE.map((effect) => effect.id));

export type EffectRuntime = {
  nodes: AudioNode[];
  flangerLfo: OscillatorNode | null;
  flangerLfoGain: GainNode | null;
};

const reverbImpulseCache = new WeakMap<AudioContext, Map<string, AudioBuffer>>();

export function clampEffectLevel(value: number): number {
  const clamped = Math.max(0, Math.min(100, value));
  return Math.round(clamped * 10) / 10;
}

export function disconnectEffectRuntime(runtime: EffectRuntime | null): void {
  if (!runtime) return;
  for (const node of runtime.nodes) {
    node.disconnect();
  }
  if (runtime.flangerLfo) {
    runtime.flangerLfo.stop();
    runtime.flangerLfo.disconnect();
  }
  runtime.flangerLfoGain?.disconnect();
}

export function connectEffectChain(
  audioCtx: AudioContext,
  input: AudioNode,
  destination: AudioNode,
  effect: EffectId,
  effectValue: number,
): EffectRuntime {
  const runtime: EffectRuntime = {
    nodes: [],
    flangerLfo: null,
    flangerLfoGain: null,
  };
  const effectMix = clampEffectLevel(effectValue) / 100;

  if (effect === 'off') {
    input.connect(destination);
    return runtime;
  }

  if (effect === 'high_pass' || effect === 'low_pass') {
    const filter = audioCtx.createBiquadFilter();
    filter.type = effect === 'high_pass' ? 'highpass' : 'lowpass';
    if (effect === 'high_pass') {
      filter.frequency.value = 120 + effectMix * 7000;
    } else {
      filter.frequency.value = 7800 - effectMix * 7750;
    }
    filter.Q.value = 0.7 + effectMix * 8;
    input.connect(filter);
    filter.connect(destination);
    runtime.nodes.push(filter);
    return runtime;
  }

  if (effect === 'echo') {
    // Retune echo curve so 100 ~= previous 80, then increase decay (more fade) by reducing feedback.
    const tunedMix = effectMix * 0.8;
    const delay = audioCtx.createDelay(1);
    delay.delayTime.value = 0.04 + tunedMix * 0.76;
    const feedback = audioCtx.createGain();
    feedback.gain.value = 0.02 + tunedMix * 0.44;
    const wetGain = audioCtx.createGain();
    wetGain.gain.value = 0.08 + tunedMix * 0.92;
    const dryGain = audioCtx.createGain();
    dryGain.gain.value = 1 - tunedMix * 0.85;

    input.connect(dryGain);
    dryGain.connect(destination);
    input.connect(delay);
    delay.connect(wetGain);
    wetGain.connect(destination);
    delay.connect(feedback);
    feedback.connect(delay);

    runtime.nodes.push(delay, feedback, wetGain, dryGain);
    return runtime;
  }

  if (effect === 'reverb') {
    const convolver = audioCtx.createConvolver();
    convolver.buffer = getCachedImpulseResponse(audioCtx, clampEffectLevel(effectValue));
    const wetGain = audioCtx.createGain();
    wetGain.gain.value = 0.06 + effectMix * 0.94;
    const dryGain = audioCtx.createGain();
    dryGain.gain.value = 1 - effectMix * 0.8;

    input.connect(dryGain);
    dryGain.connect(destination);
    input.connect(convolver);
    convolver.connect(wetGain);
    wetGain.connect(destination);

    runtime.nodes.push(convolver, wetGain, dryGain);
    return runtime;
  }

  const delay = audioCtx.createDelay(0.05);
  delay.delayTime.value = 0.0005 + effectMix * 0.012;
  const feedback = audioCtx.createGain();
  feedback.gain.value = 0.04 + effectMix * 0.9;
  const wetGain = audioCtx.createGain();
  wetGain.gain.value = 0.05 + effectMix * 0.95;
  const dryGain = audioCtx.createGain();
  dryGain.gain.value = 1 - effectMix * 0.82;

  const lfo = audioCtx.createOscillator();
  lfo.type = 'sine';
  lfo.frequency.value = 0.05 + effectMix * 1.8;
  const lfoGain = audioCtx.createGain();
  lfoGain.gain.value = 0.0002 + effectMix * 0.015;

  lfo.connect(lfoGain);
  lfoGain.connect(delay.delayTime);
  lfo.start();

  input.connect(dryGain);
  dryGain.connect(destination);
  input.connect(delay);
  delay.connect(wetGain);
  wetGain.connect(destination);
  delay.connect(feedback);
  feedback.connect(delay);

  runtime.flangerLfo = lfo;
  runtime.flangerLfoGain = lfoGain;
  runtime.nodes.push(delay, feedback, wetGain, lfoGain, dryGain);
  return runtime;
}

/** Returns a cached impulse response for a reverb amount bucket and sample rate. */
function getCachedImpulseResponse(audioCtx: AudioContext, effectValue: number): AudioBuffer {
  const roundedLevel = Math.round(Math.max(0, Math.min(100, effectValue)));
  const cacheKey = `${audioCtx.sampleRate}:${roundedLevel}`;
  let ctxCache = reverbImpulseCache.get(audioCtx);
  if (!ctxCache) {
    ctxCache = new Map<string, AudioBuffer>();
    reverbImpulseCache.set(audioCtx, ctxCache);
  }
  const existing = ctxCache.get(cacheKey);
  if (existing) {
    return existing;
  }
  const mix = roundedLevel / 100;
  const impulse = createImpulseResponse(audioCtx, 0.4 + mix * 4.2, 1 + mix * 3.6);
  ctxCache.set(cacheKey, impulse);
  return impulse;
}

/** Generates a synthetic impulse buffer used by the reverb convolver effect. */
function createImpulseResponse(audioCtx: AudioContext, duration: number, decay: number): AudioBuffer {
  const length = Math.floor(audioCtx.sampleRate * duration);
  const impulse = audioCtx.createBuffer(2, length, audioCtx.sampleRate);
  for (let channel = 0; channel < impulse.numberOfChannels; channel += 1) {
    const data = impulse.getChannelData(channel);
    for (let i = 0; i < length; i += 1) {
      const noise = Math.random() * 2 - 1;
      data[i] = noise * Math.pow(1 - i / length, decay);
    }
  }
  return impulse;
}
