import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';

export class SpectralBlur implements AudioProcessor {
  getName(): string {
    return 'Spectral Blur';
  }

  getDescription(): string {
    return 'Smears frequency content across neighboring bins for dreamy, diffused textures';
  }

  getCategory(): ProcessorCategory {
    return 'Spectral';
  }

  process(
    leftChannel: Float32Array,
    rightChannel: Float32Array
  ): [Float32Array, Float32Array] {
    const length = leftChannel.length;

    // Random parameters
    const blurAmount = 0.3 + Math.random() * 0.6; // 0.3 to 0.9
    const frequencyRangeLow = Math.random() * 0.2; // 0 to 0.2 (low freq cutoff)
    const frequencyRangeHigh = 0.5 + Math.random() * 0.5; // 0.5 to 1.0 (high freq cutoff)

    // FFT parameters for quality spectral processing
    const fftSize = 4096;
    const hopSize = Math.floor(fftSize * 0.25); // 75% overlap
    const overlap = fftSize - hopSize;

    const outputLeft = this.processChannel(
      leftChannel,
      fftSize,
      hopSize,
      blurAmount,
      frequencyRangeLow,
      frequencyRangeHigh
    );
    const outputRight = this.processChannel(
      rightChannel,
      fftSize,
      hopSize,
      blurAmount,
      frequencyRangeLow,
      frequencyRangeHigh
    );

    // Ensure output matches input length
    const finalLeft = new Float32Array(length);
    const finalRight = new Float32Array(length);

    const copyLength = Math.min(length, outputLeft.length);
    for (let i = 0; i < copyLength; i++) {
      finalLeft[i] = outputLeft[i];
      finalRight[i] = outputRight[i];
    }

    // Match output RMS to input RMS to maintain perceived loudness
    const inputRMS = this.calculateRMS(leftChannel, rightChannel);
    const outputRMS = this.calculateRMS(finalLeft, finalRight);

    if (outputRMS > 0.0001) {
      const rmsScale = inputRMS / outputRMS;
      for (let i = 0; i < length; i++) {
        finalLeft[i] *= rmsScale;
        finalRight[i] *= rmsScale;
      }
    }

    // Safety limiter to prevent clipping
    const maxAmp = this.findMaxAmplitude(finalLeft, finalRight);
    if (maxAmp > 0.99) {
      const scale = 0.99 / maxAmp;
      for (let i = 0; i < length; i++) {
        finalLeft[i] *= scale;
        finalRight[i] *= scale;
      }
    }

    return [finalLeft, finalRight];
  }

  private processChannel(
    input: Float32Array,
    fftSize: number,
    hopSize: number,
    blurAmount: number,
    freqRangeLow: number,
    freqRangeHigh: number
  ): Float32Array {
    const length = input.length;
    const output = new Float32Array(length + fftSize);
    const window = this.createHannWindow(fftSize);

    // Calculate COLA normalization factor for 75% overlap with Hann window
    // With hopSize = fftSize * 0.25, we have 4x overlap
    const overlap = fftSize / hopSize;
    const colaNorm = this.calculateCOLANorm(window, hopSize);

    let inputPos = 0;
    let outputPos = 0;

    while (inputPos + fftSize <= length) {
      // Extract windowed frame
      const frame = new Float32Array(fftSize);
      for (let i = 0; i < fftSize; i++) {
        frame[i] = input[inputPos + i] * window[i];
      }

      // FFT
      const spectrum = this.fft(frame);

      // Apply spectral blur
      const blurredSpectrum = this.applySpectralBlur(
        spectrum,
        blurAmount,
        freqRangeLow,
        freqRangeHigh
      );

      // IFFT
      const processedFrame = this.ifft(blurredSpectrum);

      // Overlap-add with proper gain compensation
      for (let i = 0; i < fftSize; i++) {
        output[outputPos + i] += processedFrame[i].real * colaNorm;
      }

      inputPos += hopSize;
      outputPos += hopSize;
    }

    // Process remaining samples if any
    if (inputPos < length) {
      const remainingSamples = length - inputPos;
      const frame = new Float32Array(fftSize);

      for (let i = 0; i < remainingSamples; i++) {
        frame[i] = input[inputPos + i] * window[i];
      }

      const spectrum = this.fft(frame);
      const blurredSpectrum = this.applySpectralBlur(
        spectrum,
        blurAmount,
        freqRangeLow,
        freqRangeHigh
      );
      const processedFrame = this.ifft(blurredSpectrum);

      for (let i = 0; i < fftSize; i++) {
        if (outputPos + i < output.length) {
          output[outputPos + i] += processedFrame[i].real * colaNorm;
        }
      }
    }

    return output;
  }

  private applySpectralBlur(
    spectrum: Complex[],
    blurAmount: number,
    freqRangeLow: number,
    freqRangeHigh: number
  ): Complex[] {
    const result = new Array<Complex>(spectrum.length);
    const halfSize = Math.floor(spectrum.length / 2);

    // Create gaussian blur kernel
    const kernelSize = Math.floor(blurAmount * 20) | 1; // Ensure odd
    const kernel = this.createGaussianKernel(kernelSize, blurAmount * 5);
    const kernelCenter = Math.floor(kernelSize / 2);

    // Calculate frequency bin range to process
    const binLow = Math.floor(freqRangeLow * halfSize);
    const binHigh = Math.floor(freqRangeHigh * halfSize);

    // Process positive frequencies
    for (let i = 0; i < halfSize; i++) {
      if (i >= binLow && i <= binHigh) {
        // Apply blur in the specified frequency range
        let realSum = 0;
        let imagSum = 0;
        let weightSum = 0;

        for (let k = 0; k < kernelSize; k++) {
          const binIdx = i + k - kernelCenter;
          if (binIdx >= 0 && binIdx < halfSize) {
            const weight = kernel[k];
            realSum += spectrum[binIdx].real * weight;
            imagSum += spectrum[binIdx].imag * weight;
            weightSum += weight;
          }
        }

        if (weightSum > 0) {
          result[i] = {
            real: realSum / weightSum,
            imag: imagSum / weightSum
          };
        } else {
          result[i] = spectrum[i];
        }
      } else {
        // Keep original spectrum outside the blur range
        result[i] = spectrum[i];
      }
    }

    // Mirror for negative frequencies (maintain Hermitian symmetry)
    for (let i = halfSize; i < spectrum.length; i++) {
      const mirrorIdx = spectrum.length - i;
      if (mirrorIdx > 0 && mirrorIdx < halfSize) {
        result[i] = {
          real: result[mirrorIdx].real,
          imag: -result[mirrorIdx].imag
        };
      } else {
        result[i] = spectrum[i];
      }
    }

    return result;
  }

  private createGaussianKernel(size: number, sigma: number): Float32Array {
    const kernel = new Float32Array(size);
    const center = Math.floor(size / 2);
    let sum = 0;

    for (let i = 0; i < size; i++) {
      const x = i - center;
      kernel[i] = Math.exp(-(x * x) / (2 * sigma * sigma));
      sum += kernel[i];
    }

    // Normalize
    for (let i = 0; i < size; i++) {
      kernel[i] /= sum;
    }

    return kernel;
  }

  private createHannWindow(size: number): Float32Array {
    const window = new Float32Array(size);
    for (let i = 0; i < size; i++) {
      window[i] = 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / (size - 1));
    }
    return window;
  }

  private calculateCOLANorm(window: Float32Array, hopSize: number): number {
    // Calculate the sum of overlapping windows at any point
    const fftSize = window.length;
    let windowSum = 0;

    // Sum all overlapping windows at the center point
    for (let offset = 0; offset < fftSize; offset += hopSize) {
      const idx = Math.floor(fftSize / 2) - offset;
      if (idx >= 0 && idx < fftSize) {
        windowSum += window[idx] * window[idx];
      }
    }

    // Also check overlap from the other direction
    for (let offset = hopSize; offset < fftSize; offset += hopSize) {
      const idx = Math.floor(fftSize / 2) + offset;
      if (idx >= 0 && idx < fftSize) {
        windowSum += window[idx] * window[idx];
      }
    }

    return windowSum > 0 ? 1.0 / windowSum : 1.0;
  }

  // Cooley-Tukey FFT implementation
  private fft(input: Float32Array): Complex[] {
    const n = input.length;
    const output: Complex[] = new Array(n);

    // Initialize with input as real values
    for (let i = 0; i < n; i++) {
      output[i] = { real: input[i], imag: 0 };
    }

    // Bit reversal
    const bits = Math.log2(n);
    for (let i = 0; i < n; i++) {
      const j = this.bitReverse(i, bits);
      if (j > i) {
        const temp = output[i];
        output[i] = output[j];
        output[j] = temp;
      }
    }

    // Cooley-Tukey butterfly operations
    for (let size = 2; size <= n; size *= 2) {
      const halfSize = size / 2;
      const angleStep = -2 * Math.PI / size;

      for (let start = 0; start < n; start += size) {
        for (let i = 0; i < halfSize; i++) {
          const angle = angleStep * i;
          const twiddle = {
            real: Math.cos(angle),
            imag: Math.sin(angle)
          };

          const evenIdx = start + i;
          const oddIdx = start + i + halfSize;

          const even = output[evenIdx];
          const odd = output[oddIdx];

          const twiddledOdd = {
            real: odd.real * twiddle.real - odd.imag * twiddle.imag,
            imag: odd.real * twiddle.imag + odd.imag * twiddle.real
          };

          output[evenIdx] = {
            real: even.real + twiddledOdd.real,
            imag: even.imag + twiddledOdd.imag
          };

          output[oddIdx] = {
            real: even.real - twiddledOdd.real,
            imag: even.imag - twiddledOdd.imag
          };
        }
      }
    }

    return output;
  }

  // Inverse FFT
  private ifft(input: Complex[]): Complex[] {
    const n = input.length;
    const output: Complex[] = new Array(n);

    // Copy input
    for (let i = 0; i < n; i++) {
      output[i] = { real: input[i].real, imag: input[i].imag };
    }

    // Conjugate
    for (let i = 0; i < n; i++) {
      output[i].imag = -output[i].imag;
    }

    // Forward FFT
    const transformed = this.fftComplex(output);

    // Conjugate and scale
    const scale = 1 / n;
    for (let i = 0; i < n; i++) {
      transformed[i].real *= scale;
      transformed[i].imag *= -scale;
    }

    return transformed;
  }

  // FFT for complex input (used by IFFT)
  private fftComplex(input: Complex[]): Complex[] {
    const n = input.length;
    const output: Complex[] = new Array(n);

    // Copy input
    for (let i = 0; i < n; i++) {
      output[i] = { real: input[i].real, imag: input[i].imag };
    }

    // Bit reversal
    const bits = Math.log2(n);
    for (let i = 0; i < n; i++) {
      const j = this.bitReverse(i, bits);
      if (j > i) {
        const temp = output[i];
        output[i] = output[j];
        output[j] = temp;
      }
    }

    // Cooley-Tukey butterfly operations
    for (let size = 2; size <= n; size *= 2) {
      const halfSize = size / 2;
      const angleStep = -2 * Math.PI / size;

      for (let start = 0; start < n; start += size) {
        for (let i = 0; i < halfSize; i++) {
          const angle = angleStep * i;
          const twiddle = {
            real: Math.cos(angle),
            imag: Math.sin(angle)
          };

          const evenIdx = start + i;
          const oddIdx = start + i + halfSize;

          const even = output[evenIdx];
          const odd = output[oddIdx];

          const twiddledOdd = {
            real: odd.real * twiddle.real - odd.imag * twiddle.imag,
            imag: odd.real * twiddle.imag + odd.imag * twiddle.real
          };

          output[evenIdx] = {
            real: even.real + twiddledOdd.real,
            imag: even.imag + twiddledOdd.imag
          };

          output[oddIdx] = {
            real: even.real - twiddledOdd.real,
            imag: even.imag - twiddledOdd.imag
          };
        }
      }
    }

    return output;
  }

  private bitReverse(n: number, bits: number): number {
    let reversed = 0;
    for (let i = 0; i < bits; i++) {
      reversed = (reversed << 1) | ((n >> i) & 1);
    }
    return reversed;
  }

  private findMaxAmplitude(left: Float32Array, right: Float32Array): number {
    let max = 0;
    for (let i = 0; i < left.length; i++) {
      max = Math.max(max, Math.abs(left[i]), Math.abs(right[i]));
    }
    return max;
  }

  private calculateRMS(left: Float32Array, right: Float32Array): number {
    let sumSquares = 0;
    const length = left.length;

    for (let i = 0; i < length; i++) {
      sumSquares += left[i] * left[i] + right[i] * right[i];
    }

    return Math.sqrt(sumSquares / (2 * length));
  }
}

interface Complex {
  real: number;
  imag: number;
}