export let audioBuffer: AudioBuffer | null = null;
export let audioFileName: string | null = null;

// import audio from frontend
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = "audio/*";
fileInput.hidden = true;
document.body.appendChild(fileInput);

const ctx = new AudioContext();
globalThis.addEventListener("beforeunload", () => {
  ctx.close();
});

const importButton = document.querySelector<HTMLButtonElement>("#import")!;
const clearBtn = document.querySelector<HTMLButtonElement>("#clear-audio")!;

importButton.addEventListener("click", () => fileInput.click());

clearBtn.addEventListener("click", () => {
  audioBuffer = null;
  audioFileName = null;
  fileInput.value = "";
  importButton.textContent = "import";
  clearBtn.classList.add("hidden");
});

fileInput.addEventListener("change", () => {
  const file = fileInput.files?.[0];
  if (!file) return;
  decode(file).then((buffer) => {
    audioBuffer = buffer;
    audioFileName = file.name;
    importButton.textContent = file.name;
    clearBtn.classList.remove("hidden");
    console.log("decoded", buffer.duration, "seconds");
  });
});

async function decode(file: File) {
  const arrayBuffer = await file.arrayBuffer();
  return await ctx.decodeAudioData(arrayBuffer);
}

// fft
function fft(real: Float64Array, imag: Float64Array): void {
  const n = real.length;
  // bit-reversal permutation
  for (let i = 1, j = 0; i < n; i++) {
    let bit = n >> 1;
    for (; j & bit; bit >>= 1) j ^= bit;
    j ^= bit;
    if (i < j) {
      let tmp = real[i];
      real[i] = real[j];
      real[j] = tmp;
      tmp = imag[i];
      imag[i] = imag[j];
      imag[j] = tmp;
    }
  }
  // butterfly stages
  for (let len = 2; len <= n; len <<= 1) {
    const half = len >> 1;
    const angle = (-2 * Math.PI) / len;
    for (let i = 0; i < n; i += len) {
      for (let j = 0; j < half; j++) {
        const wRe = Math.cos(angle * j);
        const wIm = Math.sin(angle * j);
        const re = real[i + j + half] * wRe - imag[i + j + half] * wIm;
        const im = real[i + j + half] * wIm + imag[i + j + half] * wRe;
        real[i + j + half] = real[i + j] - re;
        imag[i + j + half] = imag[i + j] - im;
        real[i + j] += re;
        imag[i + j] += im;
      }
    }
  }
}

export function extractFreqFrames(
  buffer: AudioBuffer,
  fps: number,
): Uint8Array[] {
  const fftSize = 2048;
  const sampleRate = buffer.sampleRate;
  const maxFreq = 16000; // cap at 16 kHz — music dies above this
  const freqBinCount = Math.ceil(maxFreq / (sampleRate / fftSize));
  const totalSamples = buffer.length;
  const channels = buffer.numberOfChannels;
  const frameCount = Math.ceil(buffer.duration * fps);
  const stepSize = sampleRate / fps;

  // pre-extract channel data so we don't call getChannelData per frame
  const channelData: Float32Array[] = [];
  for (let ch = 0; ch < channels; ch++) {
    channelData.push(buffer.getChannelData(ch));
  }

  // pre-compute hann window
  const window = new Float64Array(fftSize);
  for (let n = 0; n < fftSize; n++) {
    window[n] = 0.5 * (1 - Math.cos((2 * Math.PI * n) / (fftSize - 1)));
  }

  const real = new Float64Array(fftSize);
  const imag = new Float64Array(fftSize);

  // spectral tilt: boost high frequencies to compensate for music's
  // natural bottom-heavy spectrum. 0 = no tilt, higher = brighter visual.
  const TILT_DB = 18; // cosmetic high-freq boost for visual sparkle
  const GAIN_DB = 1; // global gain: pre-amp before adaptive normalization
  const SMOOTHING = 0.15; // lower = smoother, higher = more responsive

  // pass 1: FFT + tilt + gain + EMA, track per-bin peaks
  const smoothedFrames: Float64Array[] = [];
  const smoothed = new Float64Array(freqBinCount);
  const binPeaks = new Float64Array(freqBinCount).fill(-Infinity);

  for (let i = 0; i < frameCount; i++) {
    const centerSample = Math.floor(i * stepSize);
    const startSample = centerSample - fftSize / 2;

    // extract windowed samples (averaged across channels)
    for (let n = 0; n < fftSize; n++) {
      const idx = startSample + n;
      if (idx < 0 || idx >= totalSamples) {
        real[n] = 0;
      } else {
        let sample = 0;
        for (let ch = 0; ch < channels; ch++) {
          sample += channelData[ch][idx];
        }
        real[n] = (sample / channels) * window[n];
      }
      imag[n] = 0;
    }

    fft(real, imag);

    // compute dB magnitudes + EMA smoothing, store for pass 2
    const frameDb = new Float64Array(freqBinCount);
    for (let j = 0; j < freqBinCount; j++) {
      const mag = Math.sqrt(real[j] * real[j] + imag[j] * imag[j]);
      const db = 20 * Math.log10(Math.max(mag, 1e-10));
      const boosted = db + GAIN_DB + TILT_DB * (j / (freqBinCount - 1));

      smoothed[j] = SMOOTHING * boosted + (1 - SMOOTHING) * smoothed[j];
      frameDb[j] = smoothed[j];
      if (smoothed[j] > binPeaks[j]) binPeaks[j] = smoothed[j];
    }
    smoothedFrames.push(frameDb);
  }

  // pass 2: per-bin normalization — each bin maps to 0-255 independently
  const frames: Uint8Array[] = [];

  for (let i = 0; i < frameCount; i++) {
    const src = smoothedFrames[i];
    const freqData = new Uint8Array(freqBinCount);
    for (let j = 0; j < freqBinCount; j++) {
      const binCeil = binPeaks[j];
      const binFloor = binCeil - 60;
      const t = ((src[j] - binFloor) / (binCeil - binFloor)) * 255;
      freqData[j] = Math.max(0, Math.min(255, Math.round(t)));
    }
    frames.push(freqData);
  }

  return frames;
}
