// Phase Distortion FM AudioWorklet Processor
// 3-operator FM synthesis with phase distortion and DCW envelopes

const PDWaveform = {
  Sawtooth: 0,
  Square: 1,
  Resonant1: 2,
  Resonant2: 3,
  Resonant3: 4,
};

const PDAlgorithm = {
  Stack: 0,        // 1→2→3
  Split: 1,        // 1→(2+3)
  Ring: 2,         // (1×2)→3
  Parallel: 3,     // 1+2+3
  HarmonicStack: 4 // 1→2, 1→3
};

const LFOWaveform = {
  Sine: 0,
  Triangle: 1,
  Square: 2,
  Saw: 3,
  SampleHold: 4,
  RandomWalk: 5,
};

class PhaseDistortionFMProcessor extends AudioWorkletProcessor {
  constructor() {
    super();

    this.opPhasesL = [0, 0, 0];
    this.opPhasesR = [0, 0, 0];
    this.lfoPhaseL = 0;
    this.lfoPhaseR = 0;
    this.lfoSampleHoldValue = 0;
    this.lfoSampleHoldPhase = 0;
    this.lfoRandomWalkCurrent = 0;
    this.lfoRandomWalkTarget = 0;
    this.dcBlockerL = 0;
    this.dcBlockerR = 0;
    this.dcBlockerCutoff = 0.995;
    this.sampleCount = 0;
    this.totalSamples = 0;

    this.port.onmessage = (e) => {
      if (e.data.type === 'init') {
        this.params = e.data.params;
        this.duration = e.data.duration;
        this.totalSamples = Math.floor(sampleRate * this.duration);

        this.lfoSampleHoldValue = Math.random() * 2 - 1;
        this.lfoSampleHoldPhase = 0;
        this.lfoRandomWalkCurrent = Math.random() * 2 - 1;
        this.lfoRandomWalkTarget = Math.random() * 2 - 1;

        const stereoOffset = this.params.stereoWidth * 0.1;
        this.opPhasesL = [0, Math.PI * stereoOffset, 0];
        this.opPhasesR = [0, Math.PI * stereoOffset * 1.5, 0];
        this.lfoPhaseL = 0;
        this.lfoPhaseR = Math.PI * this.params.stereoWidth * 0.3;
      }
    };
  }

  process(inputs, outputs, parameters) {
    if (!this.params || this.sampleCount >= this.totalSamples) {
      return false;
    }

    const output = outputs[0];
    const leftChannel = output[0];
    const rightChannel = output[1];
    const TAU = Math.PI * 2;

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

    for (let i = 0; i < leftChannel.length; i++) {
      if (this.sampleCount >= this.totalSamples) {
        leftChannel[i] = 0;
        rightChannel[i] = 0;
        continue;
      }

      const t = this.sampleCount / sampleRate;

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

      const dcw1 = this.calculateDCWEnvelope(t, this.params.operators[0]);
      const dcw2 = this.calculateDCWEnvelope(t, this.params.operators[1]);
      const dcw3 = this.calculateDCWEnvelope(t, this.params.operators[2]);

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

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

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

      const [sampleL, sampleR] = this.processAlgorithm(
        this.params.algorithm,
        this.opPhasesL,
        this.opPhasesR,
        [env1, env2, env3],
        [dcw1, dcw2, dcw3],
        modIndexMod
      );

      const gainCompensation = this.getAlgorithmGainCompensation(this.params.algorithm);
      let outL = sampleL * gainCompensation * ampModL;
      let outR = sampleR * gainCompensation * ampModR;

      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;

      leftChannel[i] = dcFilteredL * 0.85;
      rightChannel[i] = dcFilteredR * 0.85;

      for (let op = 0; op < 3; op++) {
        const opFreqL = leftFreq * this.params.operators[op].ratio * (1 + pitchModL);
        const opFreqR = rightFreq * this.params.operators[op].ratio * (1 + pitchModR);
        this.opPhasesL[op] += (TAU * opFreqL) / sampleRate;
        this.opPhasesR[op] += (TAU * opFreqR) / sampleRate;
        if (this.opPhasesL[op] > TAU * 1000) this.opPhasesL[op] -= TAU * 1000;
        if (this.opPhasesR[op] > TAU * 1000) this.opPhasesR[op] -= TAU * 1000;
      }

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

      this.sampleCount++;
    }

    return true;
  }

  processAlgorithm(algorithm, phasesL, phasesR, envelopes, dcwEnvs, modIndexMod) {
    const baseModIndex = 3.0;
    const modScale = baseModIndex * (1 + modIndexMod * 2);
    const ops = this.params.operators;

    switch (algorithm) {
      case PDAlgorithm.Stack: {
        // 1→2→3
        const mod1L = this.generateWaveform(phasesL[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
          * envelopes[0] * ops[0].level;
        const mod1R = this.generateWaveform(phasesR[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
          * envelopes[0] * ops[0].level;

        const mod2PhaseL = phasesL[1] + modScale * mod1L;
        const mod2PhaseR = phasesR[1] + modScale * mod1R;
        const mod2L = this.generateWaveform(mod2PhaseL, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
          * envelopes[1] * ops[1].level;
        const mod2R = this.generateWaveform(mod2PhaseR, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
          * envelopes[1] * ops[1].level;

        const outPhaseL = phasesL[2] + modScale * 0.7 * mod2L;
        const outPhaseR = phasesR[2] + modScale * 0.7 * mod2R;
        const outL = this.generateWaveform(outPhaseL, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
          * envelopes[2] * ops[2].level;
        const outR = this.generateWaveform(outPhaseR, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
          * envelopes[2] * ops[2].level;

        return [outL, outR];
      }

      case PDAlgorithm.Split: {
        // 1→(2+3)
        const mod1L = this.generateWaveform(phasesL[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
          * envelopes[0] * ops[0].level;
        const mod1R = this.generateWaveform(phasesR[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
          * envelopes[0] * ops[0].level;

        const car1PhaseL = phasesL[1] + modScale * mod1L;
        const car1PhaseR = phasesR[1] + modScale * mod1R;
        const car1L = this.generateWaveform(car1PhaseL, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
          * envelopes[1] * ops[1].level;
        const car1R = this.generateWaveform(car1PhaseR, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
          * envelopes[1] * ops[1].level;

        const car2PhaseL = phasesL[2] + modScale * mod1L;
        const car2PhaseR = phasesR[2] + modScale * mod1R;
        const car2L = this.generateWaveform(car2PhaseL, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
          * envelopes[2] * ops[2].level;
        const car2R = this.generateWaveform(car2PhaseR, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
          * envelopes[2] * ops[2].level;

        return [(car1L + car2L) * 0.707, (car1R + car2R) * 0.707];
      }

      case PDAlgorithm.Ring: {
        // (1×2)→3
        const osc1L = this.generateWaveform(phasesL[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
          * envelopes[0] * ops[0].level;
        const osc1R = this.generateWaveform(phasesR[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
          * envelopes[0] * ops[0].level;

        const osc2L = this.generateWaveform(phasesL[1], ops[1].waveform, ops[1].distortion, dcwEnvs[1])
          * envelopes[1] * ops[1].level;
        const osc2R = this.generateWaveform(phasesR[1], ops[1].waveform, ops[1].distortion, dcwEnvs[1])
          * envelopes[1] * ops[1].level;

        const ringModL = osc1L * osc2L * this.params.ringModAmount;
        const ringModR = osc1R * osc2R * this.params.ringModAmount;

        const carPhaseL = phasesL[2] + modScale * ringModL;
        const carPhaseR = phasesR[2] + modScale * ringModR;
        const outL = this.generateWaveform(carPhaseL, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
          * envelopes[2] * ops[2].level;
        const outR = this.generateWaveform(carPhaseR, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
          * envelopes[2] * ops[2].level;

        return [outL, outR];
      }

      case PDAlgorithm.Parallel: {
        // 1+2+3
        let sumL = 0, sumR = 0;
        for (let i = 0; i < 3; i++) {
          sumL += this.generateWaveform(phasesL[i], ops[i].waveform, ops[i].distortion, dcwEnvs[i])
            * envelopes[i] * ops[i].level;
          sumR += this.generateWaveform(phasesR[i], ops[i].waveform, ops[i].distortion, dcwEnvs[i])
            * envelopes[i] * ops[i].level;
        }
        return [sumL * 0.577, sumR * 0.577];
      }

      case PDAlgorithm.HarmonicStack: {
        // 1→2, 1→3
        const mod1L = this.generateWaveform(phasesL[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
          * envelopes[0] * ops[0].level;
        const mod1R = this.generateWaveform(phasesR[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
          * envelopes[0] * ops[0].level;

        const car1PhaseL = phasesL[1] + modScale * mod1L;
        const car1PhaseR = phasesR[1] + modScale * mod1R;
        const car1L = this.generateWaveform(car1PhaseL, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
          * envelopes[1] * ops[1].level;
        const car1R = this.generateWaveform(car1PhaseR, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
          * envelopes[1] * ops[1].level;

        const car2PhaseL = phasesL[2] + modScale * 0.8 * mod1L;
        const car2PhaseR = phasesR[2] + modScale * 0.8 * mod1R;
        const car2L = this.generateWaveform(car2PhaseL, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
          * envelopes[2] * ops[2].level;
        const car2R = this.generateWaveform(car2PhaseR, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
          * envelopes[2] * ops[2].level;

        return [(car1L + car2L) * 0.707, (car1R + car2R) * 0.707];
      }

      default:
        return [0, 0];
    }
  }

  generateWaveform(phase, waveform, distortion, dcw) {
    const normalizedPhase = (phase % (Math.PI * 2)) / (Math.PI * 2);
    const warpedPhase = this.applyPhaseDistortion(normalizedPhase, distortion, dcw);

    switch (waveform) {
      case PDWaveform.Sawtooth:
        return warpedPhase * 2 - 1;

      case PDWaveform.Square:
        return warpedPhase < 0.5 ? 1 : -1;

      case PDWaveform.Resonant1:
        return this.resonantSaw(warpedPhase, dcw);

      case PDWaveform.Resonant2:
        return this.resonantPulse(warpedPhase, dcw);

      case PDWaveform.Resonant3:
        return this.doubleResonant(warpedPhase, dcw);

      default:
        return warpedPhase * 2 - 1;
    }
  }

  applyPhaseDistortion(phase, distortion, dcw) {
    const warpAmount = distortion * dcw;

    if (phase < 0.5) {
      return phase * (1 + warpAmount * (1 - 2 * phase));
    } else {
      return 0.5 + (phase - 0.5) * (1 + warpAmount * (2 * phase - 1));
    }
  }

  resonantSaw(phase, brightness) {
    const harmonics = Math.floor(1 + brightness * 8);
    let sum = 0;
    for (let n = 1; n <= harmonics; n++) {
      sum += Math.sin(Math.PI * 2 * phase * n) / n;
    }
    return sum * 0.5;
  }

  resonantPulse(phase, brightness) {
    const pulseWidth = 0.5 - brightness * 0.3;
    const harmonics = Math.floor(1 + brightness * 6);
    let sum = 0;
    for (let n = 1; n <= harmonics; n += 2) {
      sum += Math.sin(Math.PI * 2 * phase * n) * Math.cos(Math.PI * n * pulseWidth) / n;
    }
    return sum * 0.6;
  }

  doubleResonant(phase, brightness) {
    const harm1 = this.resonantSaw(phase, brightness);
    const harm2 = this.resonantSaw((phase + 0.5) % 1, brightness * 0.7);
    return (harm1 + harm2 * 0.5) * 0.6;
  }

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

    if (t < attackTime) {
      return t / attackTime;
    } else if (t < sustainStart) {
      const progress = (t - attackTime) / decayTime;
      return 1 - progress * (1 - op.sustain);
    } else if (t < releaseStart) {
      return op.sustain;
    } else {
      const progress = (t - releaseStart) / releaseTime;
      return op.sustain * (1 - progress);
    }
  }

  calculateDCWEnvelope(t, op) {
    const attackTime = op.dcwAttack * this.duration;
    const decayTime = op.dcwDecay * this.duration;
    const releaseTime = op.dcwRelease * this.duration;
    const sustainStart = attackTime + decayTime;
    const releaseStart = this.duration - releaseTime;

    if (t < attackTime) {
      return t / attackTime;
    } else if (t < sustainStart) {
      const progress = (t - attackTime) / decayTime;
      return 1 - progress * (1 - op.dcwSustain);
    } else if (t < releaseStart) {
      return op.dcwSustain;
    } else {
      const progress = (t - releaseStart) / releaseTime;
      return op.dcwSustain * (1 - progress);
    }
  }

  generateLFO(phase, waveform, rate) {
    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;
    }
  }

  softClip(x) {
    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);
  }

  getAlgorithmGainCompensation(algorithm) {
    switch (algorithm) {
      case PDAlgorithm.Stack:
        return 0.75;
      case PDAlgorithm.Split:
      case PDAlgorithm.HarmonicStack:
        return 0.8;
      case PDAlgorithm.Ring:
        return 0.7;
      case PDAlgorithm.Parallel:
        return 0.65;
      default:
        return 0.75;
    }
  }
}

registerProcessor('phase-distortion-fm-processor', PhaseDistortionFMProcessor);
