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

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

enum EnvCurve {
  Linear,
  Exponential,
  Logarithmic,
}

interface RingEnvelope {
  attack: number;
  decay: number;
  sustain: number;
  release: number;
  curve: EnvCurve;
}

interface RingLFO {
  rate: number;
  depth: number;
  waveform: LFOWaveform;
}

export interface RingParams {
  carrierFreq: number;
  modulatorFreq: number;
  secondModulatorFreq: number;
  carrierLevel: number;
  modulatorLevel: number;
  secondModulatorLevel: number;
  dryWet: number;
  dryWetEvolution: number;
  envelope: RingEnvelope;
  lfoAmp: RingLFO;
  lfoPitch: RingLFO;
  lfoMix: RingLFO;
  feedback: number;
  harmonics: number;
  stereoWidth: number;
}

export class Ring implements SynthEngine<RingParams> {
  private lfoAmpSampleHoldValue = 0;
  private lfoAmpSampleHoldPhase = 0;
  private lfoAmpRandomWalkCurrent = 0;
  private lfoAmpRandomWalkTarget = 0;
  private lfoPitchSampleHoldValue = 0;
  private lfoPitchSampleHoldPhase = 0;
  private lfoPitchRandomWalkCurrent = 0;
  private lfoPitchRandomWalkTarget = 0;
  private lfoMixSampleHoldValue = 0;
  private lfoMixSampleHoldPhase = 0;
  private lfoMixRandomWalkCurrent = 0;
  private lfoMixRandomWalkTarget = 0;

  private lfoAmpPrevValue = 0;
  private lfoPitchPrevValue = 0;
  private lfoMixPrevValue = 0;

  private dcBlockerX1L = 0;
  private dcBlockerX1R = 0;
  private dcBlockerY1L = 0;
  private dcBlockerY1R = 0;

  getName(): string {
    return 'Creepy';
  }

  getDescription(): string {
    return 'Complex ring modulator with dual modulators, multiple LFOs, feedback, and evolving timbres';
  }

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

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

  generate(params: RingParams, 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.003);
    const carrierFreqL = params.carrierFreq / detune;
    const carrierFreqR = params.carrierFreq * detune;
    const modulatorFreqL = params.modulatorFreq / detune;
    const modulatorFreqR = params.modulatorFreq * detune;
    const secondModFreqL = params.secondModulatorFreq / (detune * 1.1);
    const secondModFreqR = params.secondModulatorFreq * (detune * 1.1);

    let carrierPhaseL = 0;
    let carrierPhaseR = Math.PI * params.stereoWidth * 0.1;
    let modulatorPhaseL = 0;
    let modulatorPhaseR = Math.PI * params.stereoWidth * 0.15;
    let secondModPhaseL = Math.PI * params.stereoWidth * 0.08;
    let secondModPhaseR = Math.PI * params.stereoWidth * 0.22;

    let lfoAmpPhaseL = 0;
    let lfoAmpPhaseR = Math.PI * params.stereoWidth * 0.2;
    let lfoPitchPhaseL = Math.PI * 0.3;
    let lfoPitchPhaseR = Math.PI * 0.7;
    let lfoMixPhaseL = Math.PI * 0.5;
    let lfoMixPhaseR = Math.PI * 0.9;

    this.lfoAmpSampleHoldValue = Math.random() * 2 - 1;
    this.lfoAmpSampleHoldPhase = 0;
    this.lfoAmpRandomWalkCurrent = Math.random() * 2 - 1;
    this.lfoAmpRandomWalkTarget = Math.random() * 2 - 1;

    this.lfoPitchSampleHoldValue = Math.random() * 2 - 1;
    this.lfoPitchSampleHoldPhase = 0;
    this.lfoPitchRandomWalkCurrent = Math.random() * 2 - 1;
    this.lfoPitchRandomWalkTarget = Math.random() * 2 - 1;

    this.lfoMixSampleHoldValue = Math.random() * 2 - 1;
    this.lfoMixSampleHoldPhase = 0;
    this.lfoMixRandomWalkCurrent = Math.random() * 2 - 1;
    this.lfoMixRandomWalkTarget = Math.random() * 2 - 1;

    this.lfoAmpPrevValue = 0;
    this.lfoPitchPrevValue = 0;
    this.lfoMixPrevValue = 0;

    this.dcBlockerX1L = 0;
    this.dcBlockerX1R = 0;
    this.dcBlockerY1L = 0;
    this.dcBlockerY1R = 0;

    let feedbackL = 0;
    let feedbackR = 0;

    for (let i = 0; i < numSamples; i++) {
      const t = i / sampleRate;
      const progress = t / duration;
      const envelope = this.calculateEnvelope(t, duration, params.envelope);

      const lfoAmpLRaw = this.generateLFO(lfoAmpPhaseL, params.lfoAmp.waveform, params.lfoAmp.rate, sampleRate, 'amp');
      const lfoAmpRRaw = this.generateLFO(lfoAmpPhaseR, params.lfoAmp.waveform, params.lfoAmp.rate, sampleRate, 'amp');
      const lfoPitchLRaw = this.generateLFO(lfoPitchPhaseL, params.lfoPitch.waveform, params.lfoPitch.rate, sampleRate, 'pitch');
      const lfoPitchRRaw = this.generateLFO(lfoPitchPhaseR, params.lfoPitch.waveform, params.lfoPitch.rate, sampleRate, 'pitch');
      const lfoMixLRaw = this.generateLFO(lfoMixPhaseL, params.lfoMix.waveform, params.lfoMix.rate, sampleRate, 'mix');
      const lfoMixRRaw = this.generateLFO(lfoMixPhaseR, params.lfoMix.waveform, params.lfoMix.rate, sampleRate, 'mix');

      const lfoSmoothing = 0.998;
      const lfoAmpAvg = (lfoAmpLRaw + lfoAmpRRaw) * 0.5;
      const lfoPitchAvg = (lfoPitchLRaw + lfoPitchRRaw) * 0.5;
      const lfoMixAvg = (lfoMixLRaw + lfoMixRRaw) * 0.5;

      this.lfoAmpPrevValue = this.lfoAmpPrevValue * lfoSmoothing + lfoAmpAvg * (1 - lfoSmoothing);
      this.lfoPitchPrevValue = this.lfoPitchPrevValue * lfoSmoothing + lfoPitchAvg * (1 - lfoSmoothing);
      this.lfoMixPrevValue = this.lfoMixPrevValue * lfoSmoothing + lfoMixAvg * (1 - lfoSmoothing);

      const lfoAmpL = this.lfoAmpPrevValue;
      const lfoAmpR = this.lfoAmpPrevValue;
      const lfoPitchL = this.lfoPitchPrevValue;
      const lfoPitchR = this.lfoPitchPrevValue;
      const mixMod = this.lfoMixPrevValue * params.lfoMix.depth;

      const ampModL = Math.max(0.2, 1 + lfoAmpL * params.lfoAmp.depth * 0.5);
      const ampModR = Math.max(0.2, 1 + lfoAmpR * params.lfoAmp.depth * 0.5);
      const pitchModL = Math.max(0.5, Math.min(1.5, 1 + lfoPitchL * params.lfoPitch.depth * 0.05));
      const pitchModR = Math.max(0.5, Math.min(1.5, 1 + lfoPitchR * params.lfoPitch.depth * 0.05));

      const currentDryWet = params.dryWet + (progress * params.dryWetEvolution * (1 - params.dryWet));
      const effectiveDryWet = Math.max(0, Math.min(1, currentDryWet + mixMod * 0.3));

      const feedbackAmount = params.feedback * 0.6;
      const feedbackLLimited = this.softClip(feedbackL) * feedbackAmount;
      const feedbackRLimited = this.softClip(feedbackR) * feedbackAmount;

      let carrierL = Math.sin(carrierPhaseL + feedbackLLimited * Math.PI) * params.carrierLevel;
      let carrierR = Math.sin(carrierPhaseR + feedbackRLimited * Math.PI) * params.carrierLevel;

      if (params.harmonics > 0.1) {
        const harmonicGain = params.harmonics * 0.25;
        carrierL += Math.sin(carrierPhaseL * 2) * params.carrierLevel * harmonicGain;
        carrierL += Math.sin(carrierPhaseL * 3) * params.carrierLevel * harmonicGain * 0.5;
        carrierR += Math.sin(carrierPhaseR * 2) * params.carrierLevel * harmonicGain;
        carrierR += Math.sin(carrierPhaseR * 3) * params.carrierLevel * harmonicGain * 0.5;

        const harmonicNorm = 1 / (1 + params.harmonics * 0.375);
        carrierL *= harmonicNorm;
        carrierR *= harmonicNorm;
      }

      const modulatorL = Math.sin(modulatorPhaseL) * params.modulatorLevel;
      const modulatorR = Math.sin(modulatorPhaseR) * params.modulatorLevel;
      const secondModL = Math.sin(secondModPhaseL) * params.secondModulatorLevel;
      const secondModR = Math.sin(secondModPhaseR) * params.secondModulatorLevel;

      const ring1L = carrierL * modulatorL;
      const ring1R = carrierR * modulatorR;
      const ring2L = carrierL * secondModL;
      const ring2R = carrierR * secondModR;
      const doubleRingL = ring1L * secondModL * 0.4;
      const doubleRingR = ring1R * secondModR * 0.4;

      const complexRingL = (ring1L * 0.5 + ring2L * 0.3 + doubleRingL * 0.2) * ampModL;
      const complexRingR = (ring1R * 0.5 + ring2R * 0.3 + doubleRingR * 0.2) * ampModR;

      const dryL = carrierL * (1 - effectiveDryWet);
      const dryR = carrierR * (1 - effectiveDryWet);
      const wetL = complexRingL * effectiveDryWet;
      const wetR = complexRingR * effectiveDryWet;

      let outL = (dryL + wetL) * envelope * 0.5;
      let outR = (dryR + wetR) * envelope * 0.5;

      outL = this.dcBlocker(outL, 'L');
      outR = this.dcBlocker(outR, 'R');

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

      leftBuffer[i] = outL;
      rightBuffer[i] = outR;

      feedbackL = outL;
      feedbackR = outR;

      carrierPhaseL += (TAU * carrierFreqL * pitchModL) / sampleRate;
      carrierPhaseR += (TAU * carrierFreqR * pitchModR) / sampleRate;
      modulatorPhaseL += (TAU * modulatorFreqL) / sampleRate;
      modulatorPhaseR += (TAU * modulatorFreqR) / sampleRate;
      secondModPhaseL += (TAU * secondModFreqL) / sampleRate;
      secondModPhaseR += (TAU * secondModFreqR) / sampleRate;

      lfoAmpPhaseL += (TAU * params.lfoAmp.rate) / sampleRate;
      lfoAmpPhaseR += (TAU * params.lfoAmp.rate) / sampleRate;
      lfoPitchPhaseL += (TAU * params.lfoPitch.rate) / sampleRate;
      lfoPitchPhaseR += (TAU * params.lfoPitch.rate) / sampleRate;
      lfoMixPhaseL += (TAU * params.lfoMix.rate) / sampleRate;
      lfoMixPhaseR += (TAU * params.lfoMix.rate) / sampleRate;

      if (carrierPhaseL > TAU * 1000) carrierPhaseL -= TAU * 1000;
      if (carrierPhaseR > TAU * 1000) carrierPhaseR -= TAU * 1000;
      if (modulatorPhaseL > TAU * 1000) modulatorPhaseL -= TAU * 1000;
      if (modulatorPhaseR > TAU * 1000) modulatorPhaseR -= TAU * 1000;
      if (secondModPhaseL > TAU * 1000) secondModPhaseL -= TAU * 1000;
      if (secondModPhaseR > TAU * 1000) secondModPhaseR -= TAU * 1000;
    }

    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 calculateEnvelope(t: number, duration: number, env: RingEnvelope): number {
    const attackTime = env.attack * duration;
    const decayTime = env.decay * duration;
    const releaseTime = env.release * duration;
    const sustainStart = attackTime + decayTime;
    const releaseStart = duration - releaseTime;

    if (t < attackTime) {
      const progress = t / attackTime;
      return this.applyCurve(progress, env.curve);
    } else if (t < sustainStart) {
      const progress = (t - attackTime) / decayTime;
      const curvedProgress = this.applyCurve(progress, env.curve);
      return 1 - curvedProgress * (1 - env.sustain);
    } else if (t < releaseStart) {
      return env.sustain;
    } else {
      const progress = (t - releaseStart) / releaseTime;
      const curvedProgress = this.applyCurve(progress, env.curve);
      return env.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);
      default:
        return progress;
    }
  }

  private softClip(x: number): number {
    if (x > 1) {
      return 1 - Math.exp(-(x - 1));
    } else if (x < -1) {
      return -1 + Math.exp(x + 1);
    }
    return x;
  }

  private dcBlocker(input: number, channel: 'L' | 'R'): number {
    const R = 0.995;

    if (channel === 'L') {
      const output = input - this.dcBlockerX1L + R * this.dcBlockerY1L;
      this.dcBlockerX1L = input;
      this.dcBlockerY1L = output;
      return output;
    } else {
      const output = input - this.dcBlockerX1R + R * this.dcBlockerY1R;
      this.dcBlockerX1R = input;
      this.dcBlockerY1R = output;
      return output;
    }
  }

  private generateLFO(phase: number, waveform: LFOWaveform, rate: number, sampleRate: number, lfoType: 'amp' | 'pitch' | 'mix'): 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: {
        if (lfoType === 'amp') {
          const cyclesSinceLastHold = phase - this.lfoAmpSampleHoldPhase;
          if (cyclesSinceLastHold >= Math.PI * 2) {
            this.lfoAmpSampleHoldValue = Math.random() * 2 - 1;
            this.lfoAmpSampleHoldPhase = phase;
          }
          return this.lfoAmpSampleHoldValue;
        } else if (lfoType === 'pitch') {
          const cyclesSinceLastHold = phase - this.lfoPitchSampleHoldPhase;
          if (cyclesSinceLastHold >= Math.PI * 2) {
            this.lfoPitchSampleHoldValue = Math.random() * 2 - 1;
            this.lfoPitchSampleHoldPhase = phase;
          }
          return this.lfoPitchSampleHoldValue;
        } else {
          const cyclesSinceLastHold = phase - this.lfoMixSampleHoldPhase;
          if (cyclesSinceLastHold >= Math.PI * 2) {
            this.lfoMixSampleHoldValue = Math.random() * 2 - 1;
            this.lfoMixSampleHoldPhase = phase;
          }
          return this.lfoMixSampleHoldValue;
        }
      }

      case LFOWaveform.RandomWalk: {
        const interpolationSpeed = rate / sampleRate * 20;
        if (lfoType === 'amp') {
          const diff = this.lfoAmpRandomWalkTarget - this.lfoAmpRandomWalkCurrent;
          this.lfoAmpRandomWalkCurrent += diff * interpolationSpeed;
          if (Math.abs(diff) < 0.01) {
            this.lfoAmpRandomWalkTarget = Math.random() * 2 - 1;
          }
          return this.lfoAmpRandomWalkCurrent;
        } else if (lfoType === 'pitch') {
          const diff = this.lfoPitchRandomWalkTarget - this.lfoPitchRandomWalkCurrent;
          this.lfoPitchRandomWalkCurrent += diff * interpolationSpeed;
          if (Math.abs(diff) < 0.01) {
            this.lfoPitchRandomWalkTarget = Math.random() * 2 - 1;
          }
          return this.lfoPitchRandomWalkCurrent;
        } else {
          const diff = this.lfoMixRandomWalkTarget - this.lfoMixRandomWalkCurrent;
          this.lfoMixRandomWalkCurrent += diff * interpolationSpeed;
          if (Math.abs(diff) < 0.01) {
            this.lfoMixRandomWalkTarget = Math.random() * 2 - 1;
          }
          return this.lfoMixRandomWalkCurrent;
        }
      }

      default:
        return 0;
    }
  }

  randomParams(pitchLock?: PitchLock): RingParams {
    const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
    const carrierFreq = pitchLock?.enabled
      ? pitchLock.frequency
      : this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);

    const ratioChoices = [0.5, 0.707, 1, 1.414, 1.732, 2, 2.236, 3, 3.732, 5, 7, 11, 13, 17];
    const modulatorRatio = this.randomChoice(ratioChoices);
    const modulatorFreq = carrierFreq * modulatorRatio * this.randomRange(0.98, 1.02);

    const secondModulatorRatio = this.randomChoice(ratioChoices);
    const secondModulatorFreq = carrierFreq * secondModulatorRatio * this.randomRange(0.97, 1.03);

    return {
      carrierFreq,
      modulatorFreq,
      secondModulatorFreq,
      carrierLevel: this.randomRange(0.5, 0.9),
      modulatorLevel: this.randomRange(0.4, 0.8),
      secondModulatorLevel: this.randomRange(0.3, 0.7),
      dryWet: this.randomRange(0.3, 0.8),
      dryWetEvolution: this.randomRange(-0.3, 0.7),
      envelope: {
        attack: this.randomRange(0.001, 0.15),
        decay: this.randomRange(0.05, 0.35),
        sustain: this.randomRange(0.2, 0.8),
        release: this.randomRange(0.1, 0.6),
        curve: this.randomInt(0, 2) as EnvCurve,
      },
      lfoAmp: {
        rate: this.randomRange(0.3, 12),
        depth: this.randomRange(0.1, 0.9),
        waveform: this.randomInt(0, 5) as LFOWaveform,
      },
      lfoPitch: {
        rate: this.randomRange(0.1, 8),
        depth: this.randomRange(0.2, 1.0),
        waveform: this.randomInt(0, 5) as LFOWaveform,
      },
      lfoMix: {
        rate: this.randomRange(0.05, 5),
        depth: this.randomRange(0.1, 0.8),
        waveform: this.randomInt(0, 5) as LFOWaveform,
      },
      feedback: this.randomRange(0, 0.7),
      harmonics: this.randomRange(0, 0.8),
      stereoWidth: this.randomRange(0.3, 0.95),
    };
  }

  mutateParams(params: RingParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): RingParams {
    const ratioChoices = [0.5, 0.707, 1, 1.414, 1.732, 2, 2.236, 3, 3.732, 5, 7, 11, 13, 17];
    let modulatorFreq = params.modulatorFreq;
    let secondModulatorFreq = params.secondModulatorFreq;

    const carrierFreq = pitchLock?.enabled ? pitchLock.frequency : params.carrierFreq;

    if (Math.random() < 0.1) {
      const newRatio = this.randomChoice(ratioChoices);
      modulatorFreq = carrierFreq * newRatio * this.randomRange(0.98, 1.02);
    } else {
      modulatorFreq = this.mutateValue(params.modulatorFreq, mutationAmount, 20, 2000);
    }

    if (Math.random() < 0.1) {
      const newRatio = this.randomChoice(ratioChoices);
      secondModulatorFreq = carrierFreq * newRatio * this.randomRange(0.97, 1.03);
    } else {
      secondModulatorFreq = this.mutateValue(params.secondModulatorFreq, mutationAmount, 20, 2000);
    }

    return {
      carrierFreq,
      modulatorFreq,
      secondModulatorFreq,
      carrierLevel: this.mutateValue(params.carrierLevel, mutationAmount, 0.3, 1.0),
      modulatorLevel: this.mutateValue(params.modulatorLevel, mutationAmount, 0.2, 1.0),
      secondModulatorLevel: this.mutateValue(params.secondModulatorLevel, mutationAmount, 0.1, 0.9),
      dryWet: this.mutateValue(params.dryWet, mutationAmount, 0, 1),
      dryWetEvolution: this.mutateValue(params.dryWetEvolution, mutationAmount, -0.5, 1.0),
      envelope: {
        attack: this.mutateValue(params.envelope.attack, mutationAmount, 0.001, 0.25),
        decay: this.mutateValue(params.envelope.decay, mutationAmount, 0.02, 0.5),
        sustain: this.mutateValue(params.envelope.sustain, mutationAmount, 0.1, 0.95),
        release: this.mutateValue(params.envelope.release, mutationAmount, 0.05, 0.8),
        curve: Math.random() < 0.05 ? this.randomInt(0, 2) as EnvCurve : params.envelope.curve,
      },
      lfoAmp: {
        rate: this.mutateValue(params.lfoAmp.rate, mutationAmount, 0.1, 20),
        depth: this.mutateValue(params.lfoAmp.depth, mutationAmount, 0, 1),
        waveform: Math.random() < 0.08 ? this.randomInt(0, 5) as LFOWaveform : params.lfoAmp.waveform,
      },
      lfoPitch: {
        rate: this.mutateValue(params.lfoPitch.rate, mutationAmount, 0.05, 15),
        depth: this.mutateValue(params.lfoPitch.depth, mutationAmount, 0, 1),
        waveform: Math.random() < 0.08 ? this.randomInt(0, 5) as LFOWaveform : params.lfoPitch.waveform,
      },
      lfoMix: {
        rate: this.mutateValue(params.lfoMix.rate, mutationAmount, 0.02, 10),
        depth: this.mutateValue(params.lfoMix.depth, mutationAmount, 0, 1),
        waveform: Math.random() < 0.08 ? this.randomInt(0, 5) as LFOWaveform : params.lfoMix.waveform,
      },
      feedback: this.mutateValue(params.feedback, mutationAmount, 0, 1),
      harmonics: this.mutateValue(params.harmonics, mutationAmount, 0, 1),
      stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1),
    };
  }

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