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 (classic FM)
  Parallel,     // 1+2 (additive)
  Feedback,     // 1→1→2 (self-modulating)
}

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 TwoOpFMParams {
  baseFreq: number;
  algorithm: Algorithm;
  operators: [OperatorParams, OperatorParams];
  lfo: LFOParams;
  feedback: number;
  modIndex: number;
  subOscLevel: number;
  stereoWidth: number;
}

export class TwoOpFM implements SynthEngine<TwoOpFMParams> {
  private lfoSampleHoldValue = 0;
  private lfoSampleHoldPhase = 0;
  private lfoRandomWalkCurrent = 0;
  private lfoRandomWalkTarget = 0;
  private dcBlockerL = 0;
  private dcBlockerR = 0;
  private readonly dcBlockerCutoff = 0.995;

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

  getDescription(): string {
    return 'Classic two-operator FM synthesis with musical ratios and multiple algorithms';
  }

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

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

  generate(params: TwoOpFMParams, 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;

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

    const opPhasesL = [0, Math.PI * params.stereoWidth * 0.1];
    const opPhasesR = [0, Math.PI * params.stereoWidth * 0.15];

    let subPhaseL = 0;
    let subPhaseR = Math.PI * params.stereoWidth * 0.05;

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

    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;

    const gainCompensation = this.getAlgorithmGainCompensation(params.algorithm);

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

      const env1 = this.calculateEnvelope(t, duration, params.operators[0]);
      const env2 = this.calculateEnvelope(t, duration, params.operators[1]);

      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;

      let pitchModL = 0, pitchModR = 0;
      let ampModL = 1, ampModR = 1;
      let modIndexMod = 0;

      if (params.lfo.target === 'pitch') {
        pitchModL = lfoModL * 0.02;
        pitchModR = lfoModR * 0.02;
      } else if (params.lfo.target === 'amplitude') {
        ampModL = 1 + lfoModL * 0.5;
        ampModR = 1 + lfoModR * 0.5;
      } else {
        modIndexMod = lfoModL;
      }

      const [sampleL, sampleR] = this.processAlgorithm(
        params.algorithm,
        params.operators,
        opPhasesL,
        opPhasesR,
        [env1, env2],
        feedbackSampleL,
        feedbackSampleR,
        params.feedback,
        params.modIndex,
        modIndexMod
      );

      const subOscGainCompensation = 1 / (1 + params.subOscLevel * 0.5);

      let outL = sampleL * gainCompensation * ampModL * subOscGainCompensation;
      let outR = sampleR * gainCompensation * ampModR * subOscGainCompensation;

      if (params.subOscLevel > 0) {
        const subL = Math.sin(subPhaseL) * env2 * params.subOscLevel * subOscGainCompensation;
        const subR = Math.sin(subPhaseR) * env2 * params.subOscLevel * subOscGainCompensation;
        outL += subL;
        outR += subR;
      }

      outL = this.softClip(outL);
      outR = this.softClip(outR);

      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;

      feedbackSampleL = outL;
      feedbackSampleR = outR;

      for (let op = 0; op < 2; 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;
        if (opPhasesL[op] > TAU * 1000) opPhasesL[op] -= TAU * 1000;
        if (opPhasesR[op] > TAU * 1000) opPhasesR[op] -= TAU * 1000;
      }

      const subFreqL = leftFreq * 0.5 * (1 + pitchModL);
      const subFreqR = rightFreq * 0.5 * (1 + pitchModR);
      subPhaseL += (TAU * subFreqL) / sampleRate;
      subPhaseR += (TAU * subFreqR) / sampleRate;
      if (subPhaseL > TAU * 1000) subPhaseL -= TAU * 1000;
      if (subPhaseR > TAU * 1000) subPhaseR -= TAU * 1000;

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

    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],
    phasesL: number[],
    phasesR: number[],
    envelopes: number[],
    feedbackL: number,
    feedbackR: number,
    feedbackAmount: number,
    modIndex: number,
    modIndexMod: number
  ): [number, number] {
    const effectiveModIndex = modIndex * (1 + modIndexMod * 2);

    switch (algorithm) {
      case Algorithm.Cascade: {
        // 1→2 - Classic FM synthesis
        const mod1L = Math.sin(phasesL[0]) * envelopes[0] * operators[0].level;
        const mod1R = Math.sin(phasesR[0]) * envelopes[0] * operators[0].level;
        const outL = Math.sin(phasesL[1] + effectiveModIndex * mod1L) * envelopes[1] * operators[1].level;
        const outR = Math.sin(phasesR[1] + effectiveModIndex * mod1R) * envelopes[1] * operators[1].level;
        return [outL, outR];
      }

      case Algorithm.Parallel: {
        // 1+2 - Additive synthesis
        const osc1L = Math.sin(phasesL[0]) * envelopes[0] * operators[0].level;
        const osc1R = Math.sin(phasesR[0]) * envelopes[0] * operators[0].level;
        const osc2L = Math.sin(phasesL[1]) * envelopes[1] * operators[1].level;
        const osc2R = Math.sin(phasesR[1]) * envelopes[1] * operators[1].level;
        return [(osc1L + osc2L) * 0.707, (osc1R + osc2R) * 0.707];
      }

      case Algorithm.Feedback: {
        // 1→1→2 - Self-modulating FM
        const fbAmountScaled = Math.min(feedbackAmount * 0.8, 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 outL = Math.sin(phasesL[1] + effectiveModIndex * mod1L) * envelopes[1] * operators[1].level;
        const outR = Math.sin(phasesR[1] + effectiveModIndex * mod1R) * envelopes[1] * operators[1].level;
        return [outL, outR];
      }

      default:
        return [0, 0];
    }
  }

  private getAlgorithmGainCompensation(algorithm: Algorithm): number {
    switch (algorithm) {
      case Algorithm.Cascade:
      case Algorithm.Feedback:
        return 0.8;
      case Algorithm.Parallel:
        return 0.7;
      default:
        return 0.75;
    }
  }

  private softClip(x: number): number {
    const absX = Math.abs(x);
    if (absX < 0.7) return x;
    if (absX > 3) return Math.sign(x) * 0.98;
    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): TwoOpFMParams {
    const algorithm = this.randomInt(0, 2) as Algorithm;

    let baseFreq: number;
    if (pitchLock?.enabled) {
      baseFreq = pitchLock.frequency;
    } else {
      const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
      baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
    }

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

  private randomOperator(isCarrier: boolean, algorithm: Algorithm): OperatorParams {
    // Musical frequency ratios for 2-OP FM
    // These are carefully chosen for tonal, harmonic sounds
    const carrierRatios = [0.5, 1, 2, 3, 4];
    const modulatorRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8];

    // DX7-inspired ratios for specific timbres
    const electricPianoRatios = [1, 14];      // Classic EP sound
    const bellRatios = [1, 1.4, 3.5, 14];     // Bell-like tones
    const brassRatios = [1, 1, 2, 3];          // Brass-like tones

    let ratio: number;

    if (algorithm === Algorithm.Parallel) {
      // For additive, use harmonic ratios
      ratio = this.randomChoice([1, 2, 3, 4, 5, 6, 8]);
    } else if (isCarrier) {
      // Carrier typically at fundamental or low harmonic
      ratio = this.randomChoice(carrierRatios);
    } else {
      // Modulator can be more varied
      const specialChance = Math.random();
      if (specialChance < 0.2) {
        ratio = this.randomChoice(electricPianoRatios);
      } else if (specialChance < 0.4) {
        ratio = this.randomChoice(bellRatios);
      } else if (specialChance < 0.6) {
        ratio = this.randomChoice(brassRatios);
      } else {
        ratio = this.randomChoice(modulatorRatios);
      }
    }

    // Very subtle detuning for richness without losing tonality
    ratio *= this.randomRange(0.999, 1.001);

    const levelRange = isCarrier ? [0.5, 0.9] : [0.3, 0.8];

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

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

    return {
      baseFreq,
      algorithm: Math.random() < 0.1 ? this.randomInt(0, 2) as Algorithm : params.algorithm,
      operators: params.operators.map((op, i) =>
        this.mutateOperator(op, mutationAmount, i === 1, params.algorithm)
      ) as [OperatorParams, OperatorParams],
      lfo: {
        rate: this.mutateValue(params.lfo.rate, mutationAmount, 0.1, 20),
        depth: this.mutateValue(params.lfo.depth, mutationAmount, 0, 0.6),
        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),
      modIndex: this.mutateValue(params.modIndex, mutationAmount, 0.3, 12),
      subOscLevel: this.mutateValue(params.subOscLevel, mutationAmount, 0, 0.8),
      stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1),
    };
  }

  private mutateOperator(op: OperatorParams, amount: number, isCarrier: boolean, algorithm: Algorithm): OperatorParams {
    const carrierRatios = [0.5, 1, 2, 3, 4];
    const modulatorRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8];
    const electricPianoRatios = [1, 14];
    const bellRatios = [1, 1.4, 3.5, 14];
    const brassRatios = [1, 1, 2, 3];

    let newRatio = op.ratio;
    if (Math.random() < 0.15) {
      if (algorithm === Algorithm.Parallel) {
        newRatio = this.randomChoice([1, 2, 3, 4, 5, 6, 8]);
      } else if (isCarrier) {
        newRatio = this.randomChoice(carrierRatios);
      } else {
        const specialChance = Math.random();
        if (specialChance < 0.2) {
          newRatio = this.randomChoice(electricPianoRatios);
        } else if (specialChance < 0.4) {
          newRatio = this.randomChoice(bellRatios);
        } else if (specialChance < 0.6) {
          newRatio = this.randomChoice(brassRatios);
        } else {
          newRatio = this.randomChoice(modulatorRatios);
        }
      }
      newRatio *= this.randomRange(0.999, 1.001);
    }

    return {
      ratio: newRatio,
      level: this.mutateValue(op.level, amount, 0.1, isCarrier ? 0.95 : 1.0),
      attack: this.mutateValue(op.attack, amount, 0.001, 0.3),
      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));
  }
}
