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

enum EnvCurve {
  Linear,
  Exponential,
  Logarithmic,
  SCurve,
}

enum LFOWaveform {
  Sine,
  Triangle,
  Square,
  Saw,
  SampleHold,
  RandomWalk,
}

enum Algorithm {
  Cascade,      // 1→2→3→4 (deep modulation)
  DualStack,    // (1→2) + (3→4) (two independent stacks)
  Parallel,     // 1+2+3+4 (additive)
  TripleMod,    // 1→2→3 + 4 (complex modulation + pure carrier)
  Bell,         // (1→3, 2→3) + 4 (two modulators converge)
  Feedback,     // 1→1→2→3→4 (self-modulation)
}

interface OperatorParams {
  ratio: number;
  level: number;
  attack: number;
  decay: number;
  sustain: number;
  release: number;
  attackCurve: EnvCurve;
  decayCurve: EnvCurve;
  releaseCurve: EnvCurve;
}

interface LFOParams {
  rate: number;
  depth: number;
  waveform: LFOWaveform;
  target: 'pitch' | 'amplitude' | 'modIndex';
}

export interface FourOpFMParams {
  baseFreq: number;
  algorithm: Algorithm;
  operators: [OperatorParams, OperatorParams, OperatorParams, OperatorParams];
  lfo: LFOParams;
  feedback: number;
  stereoWidth: number;
}

export class FourOpFM implements SynthEngine<FourOpFMParams> {
  private lfoSampleHoldValue = 0;
  private lfoSampleHoldPhase = 0;
  private lfoRandomWalkCurrent = 0;
  private lfoRandomWalkTarget = 0;
  // DC blocking filters for each channel
  private dcBlockerL = 0;
  private dcBlockerR = 0;
  private readonly dcBlockerCutoff = 0.995;

  getName(): string {
    return '4-OP FM';
  }

  getDescription(): string {
    return 'Four-operator FM synthesis with multiple algorithms, envelope curves, and LFO waveforms';
  }

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

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

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

    // More subtle stereo detuning
    const detune = 1 + (params.stereoWidth * 0.001);
    const leftFreq = params.baseFreq / detune;
    const rightFreq = params.baseFreq * detune;

    // Initialize operator phases for stereo with more musical offsets
    const opPhasesL = [0, Math.PI * params.stereoWidth * 0.05, 0, 0];
    const opPhasesR = [0, Math.PI * params.stereoWidth * 0.08, 0, 0];

    let lfoPhaseL = 0;
    let lfoPhaseR = Math.PI * params.stereoWidth * 0.25;

    // Reset non-periodic LFO state
    this.lfoSampleHoldValue = Math.random() * 2 - 1;
    this.lfoSampleHoldPhase = 0;
    this.lfoRandomWalkCurrent = Math.random() * 2 - 1;
    this.lfoRandomWalkTarget = Math.random() * 2 - 1;

    let feedbackSampleL = 0;
    let feedbackSampleR = 0;

    // Get algorithm-specific gain compensation
    const gainCompensation = this.getAlgorithmGainCompensation(params.algorithm);

    for (let i = 0; i < numSamples; i++) {
      const t = i / sampleRate;

      // Calculate envelopes for each operator
      const env1 = this.calculateEnvelope(t, duration, params.operators[0]);
      const env2 = this.calculateEnvelope(t, duration, params.operators[1]);
      const env3 = this.calculateEnvelope(t, duration, params.operators[2]);
      const env4 = this.calculateEnvelope(t, duration, params.operators[3]);

      // Generate LFO modulation
      const lfoL = this.generateLFO(lfoPhaseL, params.lfo.waveform, params.lfo.rate, sampleRate);
      const lfoR = this.generateLFO(lfoPhaseR, params.lfo.waveform, params.lfo.rate, sampleRate);
      const lfoModL = lfoL * params.lfo.depth;
      const lfoModR = lfoR * params.lfo.depth;

      // Apply LFO to target parameter
      let pitchModL = 0, pitchModR = 0;
      let ampModL = 1, ampModR = 1;
      let modIndexMod = 0;

      if (params.lfo.target === 'pitch') {
        // More musical pitch modulation range
        pitchModL = lfoModL * 0.02;
        pitchModR = lfoModR * 0.02;
      } else if (params.lfo.target === 'amplitude') {
        // Tremolo effect
        ampModL = 1 + lfoModL * 0.5;
        ampModR = 1 + lfoModR * 0.5;
      } else {
        // Modulation index modulation
        modIndexMod = lfoModL;
      }

      // Process algorithm - generate left and right samples
      const [sampleL, sampleR] = this.processAlgorithm(
        params.algorithm,
        params.operators,
        opPhasesL,
        opPhasesR,
        [env1, env2, env3, env4],
        feedbackSampleL,
        feedbackSampleR,
        params.feedback,
        modIndexMod
      );

      // Apply gain compensation and amplitude modulation
      let outL = sampleL * gainCompensation * ampModL;
      let outR = sampleR * gainCompensation * ampModR;

      // Soft clipping for musical saturation
      outL = this.softClip(outL);
      outR = this.softClip(outR);

      // DC blocking filter
      const dcFilteredL = outL - this.dcBlockerL;
      this.dcBlockerL += (1 - this.dcBlockerCutoff) * dcFilteredL;

      const dcFilteredR = outR - this.dcBlockerR;
      this.dcBlockerR += (1 - this.dcBlockerCutoff) * dcFilteredR;

      leftBuffer[i] = dcFilteredL;
      rightBuffer[i] = dcFilteredR;

      // Store feedback samples (after soft clipping)
      feedbackSampleL = outL;
      feedbackSampleR = outR;

      // Advance operator phases
      for (let op = 0; op < 4; op++) {
        const opFreqL = leftFreq * params.operators[op].ratio * (1 + pitchModL);
        const opFreqR = rightFreq * params.operators[op].ratio * (1 + pitchModR);
        opPhasesL[op] += (TAU * opFreqL) / sampleRate;
        opPhasesR[op] += (TAU * opFreqR) / sampleRate;
        // Wrap phases to prevent numerical issues
        if (opPhasesL[op] > TAU * 1000) opPhasesL[op] -= TAU * 1000;
        if (opPhasesR[op] > TAU * 1000) opPhasesR[op] -= TAU * 1000;
      }

      // Advance LFO phase
      lfoPhaseL += (TAU * params.lfo.rate) / sampleRate;
      lfoPhaseR += (TAU * params.lfo.rate) / sampleRate;
    }

    // Peak normalization with headroom
    let peakL = 0;
    let peakR = 0;
    for (let i = 0; i < numSamples; i++) {
      peakL = Math.max(peakL, Math.abs(leftBuffer[i]));
      peakR = Math.max(peakR, Math.abs(rightBuffer[i]));
    }
    const peak = Math.max(peakL, peakR);

    if (peak > 0.001) {
      const normalizeGain = 0.85 / peak;
      for (let i = 0; i < numSamples; i++) {
        leftBuffer[i] *= normalizeGain;
        rightBuffer[i] *= normalizeGain;
      }
    }

    return [leftBuffer, rightBuffer];
  }

  private processAlgorithm(
    algorithm: Algorithm,
    operators: [OperatorParams, OperatorParams, OperatorParams, OperatorParams],
    phasesL: number[],
    phasesR: number[],
    envelopes: number[],
    feedbackL: number,
    feedbackR: number,
    feedbackAmount: number,
    modIndexMod: number
  ): [number, number] {
    // More musical modulation scaling
    const baseModIndex = 2.5;
    const modScale = baseModIndex * (1 + modIndexMod * 2);

    switch (algorithm) {
      case Algorithm.Cascade: {
        // 1→2→3→4 - Deep FM chain
        const fbAmountScaled = feedbackAmount * 0.8;
        const mod1L = Math.sin(phasesL[0] + fbAmountScaled * feedbackL) * envelopes[0] * operators[0].level;
        const mod1R = Math.sin(phasesR[0] + fbAmountScaled * feedbackR) * envelopes[0] * operators[0].level;
        const mod2L = Math.sin(phasesL[1] + modScale * mod1L) * envelopes[1] * operators[1].level;
        const mod2R = Math.sin(phasesR[1] + modScale * mod1R) * envelopes[1] * operators[1].level;
        const mod3L = Math.sin(phasesL[2] + modScale * 0.7 * mod2L) * envelopes[2] * operators[2].level;
        const mod3R = Math.sin(phasesR[2] + modScale * 0.7 * mod2R) * envelopes[2] * operators[2].level;
        const outL = Math.sin(phasesL[3] + modScale * 0.5 * mod3L) * envelopes[3] * operators[3].level;
        const outR = Math.sin(phasesR[3] + modScale * 0.5 * mod3R) * envelopes[3] * operators[3].level;
        return [outL, outR];
      }

      case Algorithm.DualStack: {
        // (1→2) + (3→4) - Two parallel FM pairs
        const mod1L = Math.sin(phasesL[0]) * envelopes[0] * operators[0].level;
        const mod1R = Math.sin(phasesR[0]) * envelopes[0] * operators[0].level;
        const car1L = Math.sin(phasesL[1] + modScale * mod1L) * envelopes[1] * operators[1].level;
        const car1R = Math.sin(phasesR[1] + modScale * mod1R) * envelopes[1] * operators[1].level;

        const mod2L = Math.sin(phasesL[2]) * envelopes[2] * operators[2].level;
        const mod2R = Math.sin(phasesR[2]) * envelopes[2] * operators[2].level;
        const car2L = Math.sin(phasesL[3] + modScale * mod2L) * envelopes[3] * operators[3].level;
        const car2R = Math.sin(phasesR[3] + modScale * mod2R) * envelopes[3] * operators[3].level;

        // Mix with proper gain staging
        return [(car1L + car2L) * 0.5, (car1R + car2R) * 0.5];
      }

      case Algorithm.Parallel: {
        // 1+2+3+4 - Additive synthesis
        let sumL = 0, sumR = 0;
        for (let i = 0; i < 4; i++) {
          sumL += Math.sin(phasesL[i]) * envelopes[i] * operators[i].level;
          sumR += Math.sin(phasesR[i]) * envelopes[i] * operators[i].level;
        }
        // Scale by 1/sqrt(4) for constant power mixing
        return [sumL * 0.5, sumR * 0.5];
      }

      case Algorithm.TripleMod: {
        // 1→2→3 + 4 - Complex mod chain plus carrier
        const mod1L = Math.sin(phasesL[0]) * envelopes[0] * operators[0].level;
        const mod1R = Math.sin(phasesR[0]) * envelopes[0] * operators[0].level;
        const mod2L = Math.sin(phasesL[1] + modScale * mod1L) * envelopes[1] * operators[1].level;
        const mod2R = Math.sin(phasesR[1] + modScale * mod1R) * envelopes[1] * operators[1].level;
        const car1L = Math.sin(phasesL[2] + modScale * 0.7 * mod2L) * envelopes[2] * operators[2].level;
        const car1R = Math.sin(phasesR[2] + modScale * 0.7 * mod2R) * envelopes[2] * operators[2].level;
        const car2L = Math.sin(phasesL[3]) * envelopes[3] * operators[3].level;
        const car2R = Math.sin(phasesR[3]) * envelopes[3] * operators[3].level;
        return [(car1L * 0.7 + car2L * 0.3), (car1R * 0.7 + car2R * 0.3)];
      }

      case Algorithm.Bell: {
        // (1→3, 2→3) + 4 - Bell-like tones
        const mod1L = Math.sin(phasesL[0]) * envelopes[0] * operators[0].level;
        const mod1R = Math.sin(phasesR[0]) * envelopes[0] * operators[0].level;
        const mod2L = Math.sin(phasesL[1]) * envelopes[1] * operators[1].level;
        const mod2R = Math.sin(phasesR[1]) * envelopes[1] * operators[1].level;
        const car1L = Math.sin(phasesL[2] + modScale * 0.6 * (mod1L + mod2L)) * envelopes[2] * operators[2].level;
        const car1R = Math.sin(phasesR[2] + modScale * 0.6 * (mod1R + mod2R)) * envelopes[2] * operators[2].level;
        const car2L = Math.sin(phasesL[3]) * envelopes[3] * operators[3].level;
        const car2R = Math.sin(phasesR[3]) * envelopes[3] * operators[3].level;
        return [(car1L + car2L) * 0.5, (car1R + car2R) * 0.5];
      }

      case Algorithm.Feedback: {
        // 1→1→2→3→4 - Self-modulating cascade
        const fbAmountScaled = Math.min(feedbackAmount * 0.7, 1.5);
        const mod1L = Math.sin(phasesL[0] + fbAmountScaled * this.softClip(feedbackL * 2)) * envelopes[0] * operators[0].level;
        const mod1R = Math.sin(phasesR[0] + fbAmountScaled * this.softClip(feedbackR * 2)) * envelopes[0] * operators[0].level;
        const mod2L = Math.sin(phasesL[1] + modScale * mod1L) * envelopes[1] * operators[1].level;
        const mod2R = Math.sin(phasesR[1] + modScale * mod1R) * envelopes[1] * operators[1].level;
        const mod3L = Math.sin(phasesL[2] + modScale * 0.7 * mod2L) * envelopes[2] * operators[2].level;
        const mod3R = Math.sin(phasesR[2] + modScale * 0.7 * mod2R) * envelopes[2] * operators[2].level;
        const outL = Math.sin(phasesL[3] + modScale * 0.5 * mod3L) * envelopes[3] * operators[3].level;
        const outR = Math.sin(phasesR[3] + modScale * 0.5 * mod3R) * envelopes[3] * operators[3].level;
        return [outL, outR];
      }

      default:
        return [0, 0];
    }
  }

  private getAlgorithmGainCompensation(algorithm: Algorithm): number {
    // Compensate for different algorithm output levels
    switch (algorithm) {
      case Algorithm.Cascade:
      case Algorithm.Feedback:
        return 0.7;
      case Algorithm.DualStack:
      case Algorithm.TripleMod:
      case Algorithm.Bell:
        return 0.8;
      case Algorithm.Parallel:
        return 0.6;
      default:
        return 0.75;
    }
  }

  private softClip(x: number): number {
    // Musical soft clipping using tanh approximation
    const absX = Math.abs(x);
    if (absX < 0.7) return x;
    if (absX > 3) return Math.sign(x) * 0.98;
    // Fast tanh approximation for soft saturation
    const x2 = x * x;
    return x * (27 + x2) / (27 + 9 * x2);
  }

  private calculateEnvelope(t: number, duration: number, op: OperatorParams): number {
    const attackTime = op.attack * duration;
    const decayTime = op.decay * duration;
    const releaseTime = op.release * duration;
    const sustainStart = attackTime + decayTime;
    const releaseStart = duration - releaseTime;

    if (t < attackTime) {
      const progress = t / attackTime;
      return this.applyCurve(progress, op.attackCurve);
    } else if (t < sustainStart) {
      const progress = (t - attackTime) / decayTime;
      const curvedProgress = this.applyCurve(progress, op.decayCurve);
      return 1 - curvedProgress * (1 - op.sustain);
    } else if (t < releaseStart) {
      return op.sustain;
    } else {
      const progress = (t - releaseStart) / releaseTime;
      const curvedProgress = this.applyCurve(progress, op.releaseCurve);
      return op.sustain * (1 - curvedProgress);
    }
  }

  private applyCurve(progress: number, curve: EnvCurve): number {
    switch (curve) {
      case EnvCurve.Linear:
        return progress;
      case EnvCurve.Exponential:
        return Math.pow(progress, 3);
      case EnvCurve.Logarithmic:
        return Math.pow(progress, 0.33);
      case EnvCurve.SCurve:
        return (Math.sin((progress - 0.5) * Math.PI) + 1) / 2;
      default:
        return progress;
    }
  }

  private generateLFO(phase: number, waveform: LFOWaveform, rate: number, sampleRate: number): number {
    const normalizedPhase = (phase % (Math.PI * 2)) / (Math.PI * 2);

    switch (waveform) {
      case LFOWaveform.Sine:
        return Math.sin(phase);

      case LFOWaveform.Triangle:
        return normalizedPhase < 0.5
          ? normalizedPhase * 4 - 1
          : 3 - normalizedPhase * 4;

      case LFOWaveform.Square:
        return normalizedPhase < 0.5 ? 1 : -1;

      case LFOWaveform.Saw:
        return normalizedPhase * 2 - 1;

      case LFOWaveform.SampleHold: {
        const cyclesSinceLastHold = phase - this.lfoSampleHoldPhase;
        if (cyclesSinceLastHold >= Math.PI * 2) {
          this.lfoSampleHoldValue = Math.random() * 2 - 1;
          this.lfoSampleHoldPhase = phase;
        }
        return this.lfoSampleHoldValue;
      }

      case LFOWaveform.RandomWalk: {
        const interpolationSpeed = rate / sampleRate * 20;
        const diff = this.lfoRandomWalkTarget - this.lfoRandomWalkCurrent;
        this.lfoRandomWalkCurrent += diff * interpolationSpeed;

        if (Math.abs(diff) < 0.01) {
          this.lfoRandomWalkTarget = Math.random() * 2 - 1;
        }

        return this.lfoRandomWalkCurrent;
      }

      default:
        return 0;
    }
  }

  randomParams(pitchLock?: PitchLock): FourOpFMParams {
    const algorithm = this.randomInt(0, 5) as Algorithm;

    // More musical frequency ratios including inharmonic ones
    const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
    const baseFreq = pitchLock?.enabled
      ? pitchLock.frequency
      : this.randomChoice(baseFreqChoices) * this.randomRange(0.9, 1.1);

    return {
      baseFreq,
      algorithm,
      operators: [
        this.randomOperator(true, algorithm),
        this.randomOperator(false, algorithm),
        this.randomOperator(false, algorithm),
        this.randomOperator(false, algorithm),
      ],
      lfo: {
        rate: this.randomRange(0.1, 12),
        depth: this.randomRange(0, 0.5),
        waveform: this.randomInt(0, 5) as LFOWaveform,
        target: this.randomChoice(['pitch', 'amplitude', 'modIndex'] as const),
      },
      feedback: this.randomRange(0, 1.5),
      stereoWidth: this.randomRange(0.2, 0.8),
    };
  }

  private randomOperator(isCarrier: boolean, algorithm: Algorithm): OperatorParams {
    // More musical ratio choices including inharmonic ones
    const harmonicRatios = [0.5, 1, 2, 3, 4, 5, 6, 7, 8];
    const inharmonicRatios = [1.414, 1.732, 2.236, 3.14, 4.19, 5.13, 6.28];
    const bellRatios = [0.56, 0.92, 1.19, 1.71, 2, 2.74, 3, 3.76, 4.07];

    let ratio: number;
    if (algorithm === Algorithm.Bell && Math.random() < 0.7) {
      ratio = this.randomChoice(bellRatios);
    } else if (Math.random() < 0.3) {
      ratio = this.randomChoice(inharmonicRatios);
    } else {
      ratio = this.randomChoice(harmonicRatios);
    }

    // Add slight detuning for richness
    ratio *= this.randomRange(0.998, 1.002);

    // Carriers typically have lower levels in FM
    const levelRange = isCarrier ? [0.3, 0.7] : [0.2, 0.8];

    return {
      ratio,
      level: this.randomRange(levelRange[0], levelRange[1]),
      attack: this.randomRange(0.001, 0.15),
      decay: this.randomRange(0.02, 0.25),
      sustain: this.randomRange(0.1, 0.8),
      release: this.randomRange(0.05, 0.4),
      attackCurve: this.randomInt(0, 3) as EnvCurve,
      decayCurve: this.randomInt(0, 3) as EnvCurve,
      releaseCurve: this.randomInt(0, 3) as EnvCurve,
    };
  }

  mutateParams(params: FourOpFMParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): FourOpFMParams {
    const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;

    return {
      baseFreq,
      algorithm: Math.random() < 0.08 ? this.randomInt(0, 5) as Algorithm : params.algorithm,
      operators: params.operators.map((op, i) =>
        this.mutateOperator(op, mutationAmount, i === 3, params.algorithm)
      ) as [OperatorParams, OperatorParams, OperatorParams, OperatorParams],
      lfo: {
        rate: this.mutateValue(params.lfo.rate, mutationAmount, 0.1, 20),
        depth: this.mutateValue(params.lfo.depth, mutationAmount, 0, 0.7),
        waveform: Math.random() < 0.08 ? this.randomInt(0, 5) as LFOWaveform : params.lfo.waveform,
        target: Math.random() < 0.08 ? this.randomChoice(['pitch', 'amplitude', 'modIndex'] as const) : params.lfo.target,
      },
      feedback: this.mutateValue(params.feedback, mutationAmount, 0, 2),
      stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1),
    };
  }

  private mutateOperator(op: OperatorParams, amount: number, isCarrier: boolean, algorithm: Algorithm): OperatorParams {
    const harmonicRatios = [0.5, 1, 2, 3, 4, 5, 6, 7, 8];
    const inharmonicRatios = [1.414, 1.732, 2.236, 3.14, 4.19, 5.13, 6.28];
    const bellRatios = [0.56, 0.92, 1.19, 1.71, 2, 2.74, 3, 3.76, 4.07];

    let newRatio = op.ratio;
    if (Math.random() < 0.12) {
      if (algorithm === Algorithm.Bell && Math.random() < 0.7) {
        newRatio = this.randomChoice(bellRatios);
      } else if (Math.random() < 0.3) {
        newRatio = this.randomChoice(inharmonicRatios);
      } else {
        newRatio = this.randomChoice(harmonicRatios);
      }
      newRatio *= this.randomRange(0.998, 1.002);
    }

    return {
      ratio: newRatio,
      level: this.mutateValue(op.level, amount, 0.1, isCarrier ? 0.8 : 1.0),
      attack: this.mutateValue(op.attack, amount, 0.001, 0.25),
      decay: this.mutateValue(op.decay, amount, 0.01, 0.4),
      sustain: this.mutateValue(op.sustain, amount, 0.05, 0.95),
      release: this.mutateValue(op.release, amount, 0.02, 0.6),
      attackCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : op.attackCurve,
      decayCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : op.decayCurve,
      releaseCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : op.releaseCurve,
    };
  }

  private randomRange(min: number, max: number): number {
    return min + Math.random() * (max - min);
  }

  private randomInt(min: number, max: number): number {
    return Math.floor(this.randomRange(min, max + 1));
  }

  private randomChoice<T>(choices: readonly T[]): T {
    return choices[Math.floor(Math.random() * choices.length)];
  }

  private mutateValue(value: number, amount: number, min: number, max: number): number {
    const variation = value * amount * (Math.random() * 2 - 1);
    return Math.max(min, Math.min(max, value + variation));
  }
}