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

export class PitchShifter implements AudioProcessor {
  getName(): string {
    return 'Pitch Shifter';
  }

  getDescription(): string {
    return 'Transposes audio up or down in semitones without changing duration';
  }

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

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

    // Random pitch shift in semitones (-12 to +12)
    const semitones = (Math.random() - 0.5) * 24;
    const pitchRatio = Math.pow(2, semitones / 12);

    // Phase vocoder parameters
    const frameSize = 2048;
    const hopSize = frameSize / 4;

    const outputLeft = this.pitchShiftPSOLA(leftChannel, pitchRatio, frameSize, hopSize);
    const outputRight = this.pitchShiftPSOLA(rightChannel, pitchRatio, frameSize, hopSize);

    // Ensure output matches input length exactly
    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];
    }

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

    return [finalLeft, finalRight];
  }

  private pitchShiftPSOLA(
    input: Float32Array,
    pitchRatio: number,
    frameSize: number,
    hopSize: number
  ): Float32Array {
    const length = input.length;
    const output = new Float32Array(length + frameSize * 2);

    // Analysis hop size (input reading)
    const analysisHop = hopSize;
    // Synthesis hop size (output writing) - this maintains duration
    const synthesisHop = hopSize;

    let analysisPos = 0;
    let synthesisPos = 0;
    let phase = 0;

    while (analysisPos + frameSize < length && synthesisPos + frameSize < output.length) {
      // Extract frame with windowing
      const frame = new Float32Array(frameSize);
      for (let i = 0; i < frameSize; i++) {
        const window = this.hannWindow(i / frameSize);
        const inputIdx = Math.floor(analysisPos + i);
        if (inputIdx < length) {
          frame[i] = input[inputIdx] * window;
        }
      }

      // Resample frame to achieve pitch shift
      const resampledFrame = this.resampleFrame(frame, pitchRatio);

      // Overlap-add to output
      for (let i = 0; i < resampledFrame.length; i++) {
        const outputIdx = Math.floor(synthesisPos + i);
        if (outputIdx < output.length) {
          output[outputIdx] += resampledFrame[i];
        }
      }

      // Update positions
      // Key insight: read position moves by pitchRatio to get correct pitch
      // Write position moves by fixed hop to maintain duration
      analysisPos += analysisHop * pitchRatio;
      synthesisPos += synthesisHop;

      // Wrap read position if needed (for extreme pitch down)
      if (analysisPos >= length - frameSize && synthesisPos < length / 2) {
        analysisPos = analysisPos % (length - frameSize);
      }
    }

    return output;
  }

  private resampleFrame(frame: Float32Array, pitchRatio: number): Float32Array {
    // Resample the frame to original size but with pitch shift
    const outputSize = frame.length;
    const output = new Float32Array(outputSize);

    for (let i = 0; i < outputSize; i++) {
      // Read from frame at pitch-shifted position
      const srcPos = i * pitchRatio;
      const srcIdx = Math.floor(srcPos);
      const frac = srcPos - srcIdx;

      if (srcIdx < frame.length - 1) {
        // Cubic interpolation for better quality
        const y0 = srcIdx > 0 ? frame[srcIdx - 1] : frame[0];
        const y1 = frame[srcIdx];
        const y2 = frame[srcIdx + 1];
        const y3 = srcIdx < frame.length - 2 ? frame[srcIdx + 2] : frame[frame.length - 1];

        output[i] = this.cubicInterpolate(y0, y1, y2, y3, frac);
      } else if (srcIdx < frame.length) {
        output[i] = frame[srcIdx];
      }
    }

    // Apply window again to smooth the resampled frame
    for (let i = 0; i < outputSize; i++) {
      output[i] *= this.hannWindow(i / outputSize);
    }

    return output;
  }

  private cubicInterpolate(y0: number, y1: number, y2: number, y3: number, x: number): number {
    // Catmull-Rom cubic interpolation
    const x2 = x * x;
    const x3 = x2 * x;

    return y1 + 0.5 * x * (y2 - y0 +
      x * (2 * y0 - 5 * y1 + 4 * y2 - y3 +
        x * (3 * (y1 - y2) + y3 - y0)));
  }

  private hannWindow(position: number): number {
    return 0.5 - 0.5 * Math.cos(2 * Math.PI * position);
  }

  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;
  }
}