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

export class DopplerEffect implements AudioProcessor {
  getName(): string {
    return 'Doppler Effect';
  }

  getDescription(): string {
    return 'Simulates pitch and amplitude changes from a moving source passing by';
  }

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

    // Random parameters for movement
    const approachSpeed = 30 + Math.random() * 70; // 30-100 m/s (108-360 km/h)
    const closestDistance = 5 + Math.random() * 20; // 5-25 meters
    const sourceHeight = Math.random() * 10; // 0-10 meters above listener
    const leftToRight = Math.random() > 0.5; // Direction of movement

    // Physics constants
    const speedOfSound = 343; // m/s at 20°C
    const startDistance = approachSpeed * duration * 0.5;
    const endDistance = startDistance;

    // Pre-calculate delay buffer size needed
    const maxDelayTime = 0.1; // 100ms maximum delay
    const maxDelaySamples = Math.ceil(maxDelayTime * sampleRate);

    // Process each channel with delay-based pitch shifting
    const outputLeft = this.processDopplerChannel(
      leftToRight ? leftChannel : rightChannel,
      length,
      sampleRate,
      approachSpeed,
      closestDistance,
      sourceHeight,
      startDistance,
      endDistance,
      speedOfSound,
      maxDelaySamples,
      -2 // Left ear offset
    );

    const outputRight = this.processDopplerChannel(
      leftToRight ? rightChannel : leftChannel,
      length,
      sampleRate,
      approachSpeed,
      closestDistance,
      sourceHeight,
      startDistance,
      endDistance,
      speedOfSound,
      maxDelaySamples,
      2 // Right ear offset
    );

    return [outputLeft, outputRight];
  }

  private processDopplerChannel(
    input: Float32Array,
    length: number,
    sampleRate: number,
    speed: number,
    closestDistance: number,
    height: number,
    startDistance: number,
    endDistance: number,
    speedOfSound: number,
    maxDelaySamples: number,
    earOffset: number
  ): Float32Array {
    const output = new Float32Array(length);
    const delayBuffer = new Float32Array(input.length + maxDelaySamples);

    // Copy input to delay buffer with padding
    for (let i = 0; i < input.length; i++) {
      delayBuffer[i + maxDelaySamples] = input[i];
    }

    // Variables for delay line interpolation
    let previousDelay = 0;
    const smoothingFactor = 0.995; // Smooth delay changes to avoid clicks

    for (let i = 0; i < length; i++) {
      const time = i / sampleRate;
      const normalizedTime = time / (length / sampleRate);

      // Calculate source position along path
      const alongPath = -startDistance + (startDistance + endDistance) * normalizedTime;

      // Calculate actual distance including lateral offset and height
      const lateralDistance = Math.sqrt(
        closestDistance * closestDistance +
        height * height +
        earOffset * earOffset
      );
      const distance = Math.sqrt(alongPath * alongPath + lateralDistance * lateralDistance);

      // Calculate radial velocity (component of velocity towards listener)
      const radialVelocity = speed * alongPath / distance;

      // Doppler frequency shift factor
      const dopplerFactor = speedOfSound / (speedOfSound - radialVelocity);

      // Time-varying delay for pitch shift (in samples)
      const targetDelay = (dopplerFactor - 1) * 50; // Scale factor for audible effect
      const currentDelay = previousDelay * smoothingFactor + targetDelay * (1 - smoothingFactor);
      previousDelay = currentDelay;

      // Calculate amplitude based on distance (inverse square law)
      const referenceDistance = 10;
      const amplitude = Math.min(1, (referenceDistance / distance) * (referenceDistance / distance));

      // Read from delay buffer with cubic interpolation
      const readPos = i + maxDelaySamples + currentDelay;
      const readIdx = Math.floor(readPos);
      const frac = readPos - readIdx;

      if (readIdx >= 1 && readIdx < delayBuffer.length - 2) {
        const y0 = delayBuffer[readIdx - 1];
        const y1 = delayBuffer[readIdx];
        const y2 = delayBuffer[readIdx + 1];
        const y3 = delayBuffer[readIdx + 2];

        // Cubic interpolation for smooth pitch transitions
        const interpolated = this.cubicInterpolate(y0, y1, y2, y3, frac);
        output[i] = interpolated * amplitude;
      } else if (readIdx >= 0 && readIdx < delayBuffer.length) {
        output[i] = delayBuffer[readIdx] * amplitude;
      }
    }

    // Apply fade in/out to prevent clicks
    const fadeLength = Math.min(1000, Math.floor(length * 0.01));
    for (let i = 0; i < fadeLength; i++) {
      const fade = i / fadeLength;
      output[i] *= fade;
      output[length - 1 - i] *= fade;
    }

    return output;
  }

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

    const a0 = -0.5 * y0 + 1.5 * y1 - 1.5 * y2 + 0.5 * y3;
    const a1 = y0 - 2.5 * y1 + 2 * y2 - 0.5 * y3;
    const a2 = -0.5 * y0 + 0.5 * y2;
    const a3 = y1;

    return a0 * x3 + a1 * x2 + a2 * x + a3;
  }
}