import type { SynthEngine, PitchLock } from './base/SynthEngine';

type HarmonicMode =
  | 'single'        // Just fundamental
  | 'octave'        // 2:1
  | 'fifth'         // 3:2
  | 'fourth'        // 4:3
  | 'majorThird'    // 5:4
  | 'majorSixth'    // 5:3
  | 'octaveFifth'   // 3:1
  | 'doubleOctave'; // 4:1

interface KarplusStrongParams {
  frequency: number;        // Hz (50-2000)
  damping: number;          // 0-1 (higher = longer decay)
  brightness: number;       // 0-1 (higher = brighter tone)
  decayCharacter: number;   // -1 to 1 (negative=brighter over time, positive=darker over time)
  pluckPosition: number;    // 0-1 (where to pluck the string)
  pluckHardness: number;    // 0-1 (0=soft/warm, 1=hard/bright)
  bodyResonance: number;    // 0-1 (amount of body resonance)
  bodyFrequency: number;    // 100-800 Hz (body resonance frequency)
  stereoDetune: number;     // 0-1 (stereo detuning amount)
  outputGain: number;       // 0.5-2.0 (output level boost)
  harmonicMode: HarmonicMode; // Which harmonics to layer
  harmonicDetune: number;   // Amount of detuning from perfect ratio (cents)
  harmonicMix: number;      // 0-1 (how loud the harmonic is vs fundamental)
}

export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
  getName(): string {
    return 'String(s)';
  }

  getDescription(): string {
    return 'Plucked string synthesis using a feedback delay line';
  }

  getType() {
    return 'generative' as const;
  }

  getCategory() {
    return 'Physical' as const;
  }

  generate(params: KarplusStrongParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
    const numSamples = Math.floor(sampleRate * duration);
    const leftBuffer = new Float32Array(numSamples);
    const rightBuffer = new Float32Array(numSamples);

    // Generate fundamental frequency
    const fundamentalLeft = new Float32Array(numSamples);
    const fundamentalRight = new Float32Array(numSamples);

    this.generateChannel(fundamentalLeft, params, sampleRate, 0);

    const rightParams = {
      ...params,
      frequency: params.frequency * (1 + params.stereoDetune * 0.005),
    };
    this.generateChannel(fundamentalRight, rightParams, sampleRate, params.stereoDetune);

    // Copy fundamental to output
    for (let i = 0; i < numSamples; i++) {
      leftBuffer[i] = fundamentalLeft[i];
      rightBuffer[i] = fundamentalRight[i];
    }

    // Add harmonic if not single mode
    if (params.harmonicMode !== 'single') {
      // Map harmonic mode to frequency ratio
      const harmonicRatios: Record<HarmonicMode, number> = {
        single: 1.0,
        octave: 2.0,
        fifth: 1.5,
        fourth: 4 / 3,
        majorThird: 1.25,
        majorSixth: 5 / 3,
        octaveFifth: 3.0,
        doubleOctave: 4.0,
      };

      const harmonicRatio = harmonicRatios[params.harmonicMode];

      // Apply slight detuning (convert cents to ratio)
      // cents = 1200 * log2(ratio), so ratio = 2^(cents/1200)
      const detuneRatio = Math.pow(2, params.harmonicDetune / 1200);
      const detunedRatio = harmonicRatio * detuneRatio;

      const harmonicLeft = new Float32Array(numSamples);
      const harmonicRight = new Float32Array(numSamples);

      const harmonicParams = {
        ...params,
        frequency: params.frequency * detunedRatio,
      };

      const harmonicParamsRight = {
        ...params,
        frequency: params.frequency * detunedRatio * (1 + params.stereoDetune * 0.005),
      };

      this.generateChannel(harmonicLeft, harmonicParams, sampleRate, 0.5);
      this.generateChannel(harmonicRight, harmonicParamsRight, sampleRate, params.stereoDetune + 0.5);

      // Mix harmonic with fundamental
      const harmonicLevel = params.harmonicMix;
      for (let i = 0; i < numSamples; i++) {
        leftBuffer[i] = leftBuffer[i] * (1 - harmonicLevel * 0.5) + harmonicLeft[i] * harmonicLevel;
        rightBuffer[i] = rightBuffer[i] * (1 - harmonicLevel * 0.5) + harmonicRight[i] * harmonicLevel;
      }
    }

    return [leftBuffer, rightBuffer];
  }

  private generateChannel(
    buffer: Float32Array,
    params: KarplusStrongParams,
    sampleRate: number,
    seed: number
  ): void {
    // Calculate delay line length from frequency
    const delayLength = Math.round(sampleRate / params.frequency);
    const delayLine = new Float32Array(delayLength);

    // Initialize delay line with noise burst
    this.fillDelayLine(delayLine, params.pluckPosition, params.pluckHardness, seed);

    // Loop gain controls decay rate
    // Must be < 1 for stability!
    const loopGain = 0.99 + params.damping * 0.0099; // Range: 0.99 to 0.9999

    // Previous sample for filtering
    let prevSample = 0;

    // Delay line read position
    let position = 0;

    // Generate samples
    for (let i = 0; i < buffer.length; i++) {
      // Calculate progress through the sound (0 to 1)
      const progress = i / buffer.length;

      // Modulate brightness over time based on decay character
      // Negative decay character = gets brighter over time
      // Positive decay character = gets darker over time (natural)
      const brightnessModulation = 1 + params.decayCharacter * progress * 0.8;
      const currentBrightness = Math.max(0, Math.min(1, params.brightness * brightnessModulation));

      // Low-pass filter coefficient for brightness
      // 0 = dark/muted, 1 = bright/sustaining
      const filterCoeff = 0.5 + currentBrightness * 0.49; // Range: 0.5 to 0.99

      // Read current sample from delay line
      const current = delayLine[position];

      // Apply simple low-pass filter
      // Classic KS uses: filtered = (current + prev) * 0.5
      // We add brightness control
      const filtered = current * filterCoeff + prevSample * (1 - filterCoeff);

      // Apply loop gain for decay
      const damped = filtered * loopGain;

      // Write back to delay line
      delayLine[position] = damped;

      // Store for next iteration
      prevSample = current;

      // Output
      buffer[i] = current;

      // Advance position
      position = (position + 1) % delayLength;
    }

    // Apply final envelope to smooth start/end
    this.applyEnvelope(buffer, sampleRate);

    // Apply body resonance if enabled
    if (params.bodyResonance > 0.01) {
      this.applyBodyResonance(buffer, params.bodyResonance, params.bodyFrequency, sampleRate);
    }

    // Normalize to ensure consistent output level
    let maxVal = 0;
    for (let i = 0; i < buffer.length; i++) {
      maxVal = Math.max(maxVal, Math.abs(buffer[i]));
    }

    if (maxVal > 0.01) {
      // Normalize to 0.7 and then apply output gain
      const normalizeGain = (0.7 / maxVal) * params.outputGain;
      for (let i = 0; i < buffer.length; i++) {
        buffer[i] *= normalizeGain;
      }
    } else {
      // Sound is too quiet, just apply output gain
      for (let i = 0; i < buffer.length; i++) {
        buffer[i] *= params.outputGain;
      }
    }
  }

  private fillDelayLine(delayLine: Float32Array, pluckPosition: number, pluckHardness: number, seed: number): void {
    // Create a burst of noise to excite the string
    // The pluck position affects the harmonic content

    const burstLength = Math.floor(delayLine.length * 0.1); // 10% of delay line
    const pluckIndex = Math.floor(pluckPosition * delayLine.length);

    // Simple seeded random for stereo variation
    let random = seed * 9301 + 49297;
    const nextRandom = () => {
      random = (random * 9301 + 49297) % 233280;
      return (random / 233280) * 2 - 1;
    };

    // Fill entire delay line with noise burst centered at pluck position
    for (let i = 0; i < delayLine.length; i++) {
      const distanceFromPluck = Math.abs(i - pluckIndex);

      // Noise burst that decays away from pluck position
      if (distanceFromPluck < burstLength) {
        const window = 1 - (distanceFromPluck / burstLength);
        delayLine[i] = (seed > 0 ? nextRandom() : Math.random() * 2 - 1) * window;
      } else {
        delayLine[i] = 0;
      }
    }

    // Apply pluck hardness filter
    // Low hardness = soft/warm (heavy filtering), high hardness = bright/hard (minimal filtering)
    // One-pole lowpass filter: y[n] = y[n-1] + alpha * (x[n] - y[n-1])
    const filterAmount = 1 - pluckHardness; // 0 = no filter, 1 = heavy filter
    const alpha = 0.1 + pluckHardness * 0.9; // 0.1 (soft) to 1.0 (hard)

    if (filterAmount > 0.01) {
      let prevSample = 0;
      for (let i = 0; i < delayLine.length; i++) {
        const input = delayLine[i];
        const output = prevSample + alpha * (input - prevSample);
        delayLine[i] = output;
        prevSample = output;
      }
    }

    // Normalize to prevent instability
    let maxVal = 0;
    for (let i = 0; i < delayLine.length; i++) {
      maxVal = Math.max(maxVal, Math.abs(delayLine[i]));
    }

    if (maxVal > 0) {
      const normalizeGain = 1.0 / maxVal; // Normalize to full amplitude
      for (let i = 0; i < delayLine.length; i++) {
        delayLine[i] *= normalizeGain;
      }
    }
  }

  private applyBodyResonance(
    buffer: Float32Array,
    resonance: number,
    frequency: number,
    sampleRate: number
  ): void {
    // Resonant bandpass filter to simulate acoustic body
    // Higher resonance = more pronounced body effect

    const Q = 2 + resonance * 8; // Quality factor: 2 to 10
    const w0 = (2 * Math.PI * frequency) / sampleRate;
    const alpha = Math.sin(w0) / (2 * Q);

    // Bandpass biquad coefficients
    const b0 = alpha;
    const b1 = 0;
    const b2 = -alpha;
    const a0 = 1 + alpha;
    const a1 = -2 * Math.cos(w0);
    const a2 = 1 - alpha;

    // Normalize coefficients
    const b0n = b0 / a0;
    const b1n = b1 / a0;
    const b2n = b2 / a0;
    const a1n = a1 / a0;
    const a2n = a2 / a0;

    // Filter state
    let x1 = 0, x2 = 0, y1 = 0, y2 = 0;

    // Apply filter with dry/wet mix
    const wet = resonance;
    const dry = 1 - wet * 0.7; // Don't fully remove dry signal

    for (let i = 0; i < buffer.length; i++) {
      const x0 = buffer[i];
      const y0 = b0n * x0 + b1n * x1 + b2n * x2 - a1n * y1 - a2n * y2;

      // Mix dry and wet
      buffer[i] = dry * x0 + wet * y0 * 3; // Boost wet signal

      // Update state
      x2 = x1;
      x1 = x0;
      y2 = y1;
      y1 = y0;
    }
  }

  private applyEnvelope(buffer: Float32Array, sampleRate: number): void {
    // Short fade in to avoid click
    const fadeInSamples = Math.floor(sampleRate * 0.005); // 5ms
    for (let i = 0; i < fadeInSamples && i < buffer.length; i++) {
      buffer[i] *= i / fadeInSamples;
    }

    // Fade out at end
    const fadeOutSamples = Math.floor(sampleRate * 0.05); // 50ms
    const fadeOutStart = buffer.length - fadeOutSamples;
    for (let i = fadeOutStart; i < buffer.length; i++) {
      const fade = (buffer.length - i) / fadeOutSamples;
      buffer[i] *= fade;
    }
  }

  randomParams(pitchLock?: PitchLock): KarplusStrongParams {
    // Musical frequencies (notes from E2 to E5)
    const frequencies = [
      82.41, 87.31, 92.50, 98.00, 103.83, 110.00, 116.54, 123.47, 130.81, 138.59,
      146.83, 155.56, 164.81, 174.61, 185.00, 196.00, 207.65, 220.00, 233.08, 246.94,
      261.63, 277.18, 293.66, 311.13, 329.63, 349.23, 369.99, 392.00, 415.30, 440.00,
      466.16, 493.88, 523.25, 554.37, 587.33, 622.25, 659.25, 698.46, 739.99, 783.99,
      830.61, 880.00, 932.33, 987.77, 1046.50, 1108.73, 1174.66, 1244.51, 1318.51
    ];

    const frequency = pitchLock?.enabled
      ? pitchLock.frequency
      : frequencies[Math.floor(Math.random() * frequencies.length)];

    // Randomly choose harmonic mode
    // Weighted selection: favor more consonant intervals
    const modes: HarmonicMode[] = [
      'single',
      'single',         // More likely to have no harmonic
      'octave',
      'octave',         // Octaves are very consonant
      'fifth',
      'fifth',          // Fifths are very consonant
      'fourth',
      'majorThird',
      'majorSixth',
      'octaveFifth',
      'doubleOctave',
    ];
    const harmonicMode = modes[Math.floor(Math.random() * modes.length)];

    // Small detuning for more natural sound (-5 to +5 cents)
    const harmonicDetune = (Math.random() * 2 - 1) * 5;

    // Body resonance frequencies for different instrument characters
    const bodyFreqs = [
      120, 150, 180, // Deep guitar/bass bodies
      200, 220, 250, 280, // Classical guitar range
      300, 350, 400, // Mandolin/small guitar
      450, 500, 600, 700, // Banjo/ukulele/bright
    ];

    return {
      frequency,
      damping: 0.7 + Math.random() * 0.29, // 0.7 to 0.99
      brightness: Math.random(), // 0 to 1
      decayCharacter: (Math.random() * 2 - 1) * 0.8, // -0.8 to 0.8 (mostly natural darkening)
      pluckPosition: 0.2 + Math.random() * 0.6, // 0.2 to 0.8
      pluckHardness: Math.random(), // 0 to 1 (full range for variety)
      bodyResonance: Math.random() * 0.7, // 0 to 0.7 (not too extreme)
      bodyFrequency: bodyFreqs[Math.floor(Math.random() * bodyFreqs.length)],
      stereoDetune: 0.3 + Math.random() * 0.7, // 0.3 to 1.0
      outputGain: 1.2 + Math.random() * 0.8, // 1.2 to 2.0 for good volume
      harmonicMode,
      harmonicDetune,
      harmonicMix: harmonicMode === 'single' ? 0 : 0.3 + Math.random() * 0.4, // 0.3 to 0.7
    };
  }

  mutateParams(params: KarplusStrongParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): KarplusStrongParams {
    const mutate = (value: number, range: number, min: number, max: number) => {
      const delta = (Math.random() * 2 - 1) * range * mutationAmount;
      return Math.max(min, Math.min(max, value + delta));
    };

    // Occasionally jump to harmonic/subharmonic (unless pitch locked)
    let newFreq: number;
    if (pitchLock?.enabled) {
      newFreq = pitchLock.frequency;
    } else if (Math.random() < 0.15) {
      const multipliers = [0.5, 2, 1.5, 3];
      newFreq = params.frequency * multipliers[Math.floor(Math.random() * multipliers.length)];
      newFreq = Math.max(50, Math.min(2000, newFreq));
    } else {
      newFreq = mutate(params.frequency, 100, 50, 2000);
    }

    // Occasionally change harmonic mode
    let newHarmonicMode = params.harmonicMode;
    if (Math.random() < 0.1) {
      const modes: HarmonicMode[] = [
        'single',
        'single',
        'octave',
        'octave',
        'fifth',
        'fifth',
        'fourth',
        'majorThird',
        'majorSixth',
        'octaveFifth',
        'doubleOctave',
      ];
      newHarmonicMode = modes[Math.floor(Math.random() * modes.length)];
    }

    return {
      frequency: newFreq,
      damping: mutate(params.damping, 0.2, 0.5, 0.99),
      brightness: mutate(params.brightness, 0.3, 0, 1),
      decayCharacter: mutate(params.decayCharacter, 0.5, -1, 1),
      pluckPosition: mutate(params.pluckPosition, 0.4, 0.1, 0.9),
      pluckHardness: mutate(params.pluckHardness, 0.4, 0, 1),
      bodyResonance: mutate(params.bodyResonance, 0.3, 0, 0.9),
      bodyFrequency: mutate(params.bodyFrequency, 100, 100, 800),
      stereoDetune: mutate(params.stereoDetune, 0.5, 0, 1),
      outputGain: mutate(params.outputGain, 0.5, 1.0, 2.0),
      harmonicMode: newHarmonicMode,
      harmonicDetune: mutate(params.harmonicDetune, 3, -5, 5),
      harmonicMix: newHarmonicMode === 'single' ? 0 : mutate(params.harmonicMix, 0.3, 0.2, 0.8),
    };
  }
}
