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

export class Chorus implements AudioProcessor {
  private readonly sampleRate = 44100;

  getName(): string {
    return 'Chorus';
  }

  getDescription(): string {
    return 'Multiple delayed copies with pitch modulation for thick, ensemble sounds';
  }

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

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

    // Random parameters with better ranges
    const numVoices = Math.floor(Math.random() * 3) + 2; // 2-4 voices
    const baseDelay = Math.random() * 0.015 + 0.015; // 15-30ms (increased for better effect)
    const lfoDepth = Math.random() * 0.003 + 0.001; // 1-4ms modulation depth
    const mix = Math.random() * 0.3 + 0.35; // 35-65% wet (better balance)
    const feedback = Math.random() * 0.15 + 0.05; // 5-20% feedback (reduced for stability)
    const stereoSpread = Math.random() * 0.3 + 0.2; // 20-50% stereo width

    // Convert times to samples
    const baseDelaySamples = Math.floor(baseDelay * this.sampleRate);
    const lfoDepthSamples = lfoDepth * this.sampleRate;
    const maxDelaySamples = Math.ceil(baseDelaySamples + lfoDepthSamples + 1);

    // Create delay buffers with enough space for the entire input plus delay
    const bufferSize = length + maxDelaySamples;
    const leftDelayBuffer = new Float32Array(bufferSize);
    const rightDelayBuffer = new Float32Array(bufferSize);

    // Initialize delay buffers with input signal
    leftDelayBuffer.set(leftIn, maxDelaySamples);
    rightDelayBuffer.set(rightIn, maxDelaySamples);

    // Output buffers
    const leftOut = new Float32Array(length);
    const rightOut = new Float32Array(length);

    // Pre-calculate LFO parameters for each voice
    const voices: Array<{
      lfoPhase: number;
      lfoIncrement: number;
      panLeft: number;
      panRight: number;
    }> = [];

    for (let voice = 0; voice < numVoices; voice++) {
      const phaseOffset = (voice / numVoices) * Math.PI * 2;
      const rateVariation = 0.8 + Math.random() * 0.4; // 0.8-1.2x rate variation
      const lfoRate = (0.5 + Math.random() * 2) * rateVariation; // 0.4-2.5 Hz per voice

      // Stereo panning for each voice
      const pan = (voice / (numVoices - 1) - 0.5) * stereoSpread;

      voices.push({
        lfoPhase: phaseOffset,
        lfoIncrement: (lfoRate * 2 * Math.PI) / this.sampleRate,
        panLeft: Math.cos((pan + 0.5) * Math.PI * 0.5),
        panRight: Math.sin((pan + 0.5) * Math.PI * 0.5)
      });
    }

    // Process each sample
    for (let i = 0; i < length; i++) {
      let leftWet = 0;
      let rightWet = 0;

      // Process each chorus voice
      for (let v = 0; v < numVoices; v++) {
        const voice = voices[v];

        // Calculate LFO value
        const lfo = Math.sin(voice.lfoPhase);
        voice.lfoPhase += voice.lfoIncrement;
        if (voice.lfoPhase > Math.PI * 2) {
          voice.lfoPhase -= Math.PI * 2;
        }

        // Calculate modulated delay time in samples
        const delaySamples = baseDelaySamples + lfo * lfoDepthSamples;

        // Calculate read position with fractional delay
        const readPos = i + maxDelaySamples - delaySamples;
        const readPosInt = Math.floor(readPos);
        const frac = readPos - readPosInt;

        // Ensure we're within bounds
        if (readPosInt >= 0 && readPosInt < bufferSize - 1) {
          // Linear interpolation for fractional delay
          const leftDelayed = this.lerp(
            leftDelayBuffer[readPosInt],
            leftDelayBuffer[readPosInt + 1],
            frac
          );
          const rightDelayed = this.lerp(
            rightDelayBuffer[readPosInt],
            rightDelayBuffer[readPosInt + 1],
            frac
          );

          // Apply panning to each voice for stereo width
          leftWet += leftDelayed * voice.panLeft;
          rightWet += rightDelayed * voice.panRight;
        }
      }

      // Normalize wet signal to maintain level
      const wetGain = 1.0 / Math.sqrt(numVoices);
      leftWet *= wetGain;
      rightWet *= wetGain;

      // Apply feedback (write it to future position to avoid immediate feedback)
      const feedbackPos = i + maxDelaySamples;
      if (feedbackPos < bufferSize) {
        leftDelayBuffer[feedbackPos] += leftWet * feedback;
        rightDelayBuffer[feedbackPos] += rightWet * feedback;
      }

      // Mix dry and wet signals with proper gain compensation
      const dryGain = Math.sqrt(1 - mix);
      const wetGain2 = Math.sqrt(mix) * 1.5; // Slight boost for presence

      leftOut[i] = leftIn[i] * dryGain + leftWet * wetGain2;
      rightOut[i] = rightIn[i] * dryGain + rightWet * wetGain2;

      // Soft clipping to prevent harsh distortion
      leftOut[i] = this.softClip(leftOut[i]);
      rightOut[i] = this.softClip(rightOut[i]);
    }

    return [leftOut, rightOut];
  }

  private lerp(a: number, b: number, t: number): number {
    return a + (b - a) * t;
  }

  private softClip(sample: number): number {
    const threshold = 0.95;
    if (Math.abs(sample) < threshold) {
      return sample;
    }
    const sign = sample < 0 ? -1 : 1;
    const abs = Math.abs(sample);
    return sign * (threshold + (1 - threshold) * Math.tanh((abs - threshold) / (1 - threshold)));
  }
}