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

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

enum HarmonicSeriesType {
  All,
  OddOnly,
  EvenOnly,
  PrimeOnly,
}

enum DistributionStrategy {
  FundamentalFocused,
  OctaveStack,
  FifthBased,
  LowMidCluster,
  HighShimmer,
  SparseScatter,
  DenseLow,
  SubharmonicEmphasis,
  BellCurve,
  RandomWalk,
}

interface PartialParams {
  ratio: number;
  level: number;
  attack: number;
  decay: number;
  sustain: number;
  release: number;
  attackCurve: EnvCurve;
  decayCurve: EnvCurve;
  releaseCurve: EnvCurve;
  vibratoRate: number;
  vibratoDepth: number;
  tremoloRate: number;
  tremoloDepth: number;
  amplitudeLFORate: number;
  amplitudeLFODepth: number;
  frequencyDrift: number;
}

interface PrecomputedPartial {
  partial: PartialParams;
  evolutionOffsetSamples: number;
  rolloff: number;
  leftGain: number;
  rightGain: number;
  inharmonicRatio: number;
  phaseIncrementL: number;
  phaseIncrementR: number;
  vibratoPhaseIncrement: number;
  tremoloPhaseIncrement: number;
  amplitudeLFOPhaseIncrement: number;
  attackSamples: number;
  decaySamples: number;
  releaseSamples: number;
  sustainStartSamples: number;
  releaseStartSamples: number;
}

export interface AdditiveParams {
  baseFreq: number;
  partials: PartialParams[];
  stereoWidth: number;
  brightness: number;
  inharmonicity: number;
  harmonicSeriesType: HarmonicSeriesType;
  distributionStrategy: DistributionStrategy;
  phaseCoherence: number;
  evolutionAmount: number;
  panSpread: number;
}

export class AdditiveEngine implements SynthEngine<AdditiveParams> {
  getName(): string {
    return 'Glass Prism';
  }

  getDescription(): string {
    return 'Spectral synthesis with up to 50 harmonics, individual envelopes, amplitude LFOs, modulation, inharmonicity, and harmonic filtering';
  }

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

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

  generate(params: AdditiveParams, 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 TAU_DIV_SR = TAU / sampleRate;

    const precomputed: PrecomputedPartial[] = [];
    const partialPhasesL: number[] = [];
    const partialPhasesR: number[] = [];
    const vibratoPhasesL: number[] = [];
    const vibratoPhasesR: number[] = [];
    const tremoloPhasesL: number[] = [];
    const tremoloPhasesR: number[] = [];
    const amplitudeLFOPhases: number[] = [];
    const driftOffsetsL: number[] = [];
    const driftOffsetsR: number[] = [];

    for (let p = 0; p < params.partials.length; p++) {
      const partial = params.partials[p];

      if (!this.shouldIncludePartial(partial.ratio, params.harmonicSeriesType)) {
        continue;
      }

      const partialPosition = p / Math.max(1, params.partials.length - 1);
      const panPosition = (partialPosition * 2 - 1) * params.panSpread;

      const inharmonicRatio = partial.ratio * (1 + params.inharmonicity * partial.ratio * partial.ratio * 0.001);

      const maxFreqL = leftFreq * inharmonicRatio;
      const maxFreqR = rightFreq * inharmonicRatio;
      if (maxFreqL > 16000 || maxFreqR > 16000) {
        continue;
      }

      const randomPhaseL = Math.random() * TAU;
      const coherentPhaseL = (p * TAU * 0.1) % TAU;
      partialPhasesL.push(randomPhaseL * (1 - params.phaseCoherence) + coherentPhaseL * params.phaseCoherence);

      const randomPhaseR = Math.random() * TAU;
      const coherentPhaseR = (p * TAU * 0.1) % TAU;
      partialPhasesR.push(randomPhaseR * (1 - params.phaseCoherence) + coherentPhaseR * params.phaseCoherence);

      vibratoPhasesL.push(0);
      vibratoPhasesR.push(Math.random() * TAU * 0.1);
      tremoloPhasesL.push(0);
      tremoloPhasesR.push(Math.random() * TAU * 0.15);
      amplitudeLFOPhases.push(Math.random() * TAU);
      driftOffsetsL.push(0);
      driftOffsetsR.push(0);

      const position = p / Math.max(1, params.partials.length - 1);
      const rolloff = 1 - (position * (1 - params.brightness));

      precomputed.push({
        partial,
        evolutionOffsetSamples: partialPosition * params.evolutionAmount * duration * 0.3 * sampleRate,
        rolloff,
        leftGain: Math.cos((panPosition + 1) * Math.PI / 4),
        rightGain: Math.sin((panPosition + 1) * Math.PI / 4),
        inharmonicRatio,
        phaseIncrementL: TAU_DIV_SR * leftFreq,
        phaseIncrementR: TAU_DIV_SR * rightFreq,
        vibratoPhaseIncrement: TAU_DIV_SR * partial.vibratoRate,
        tremoloPhaseIncrement: TAU_DIV_SR * partial.tremoloRate,
        amplitudeLFOPhaseIncrement: TAU_DIV_SR * partial.amplitudeLFORate,
        attackSamples: partial.attack * duration * sampleRate,
        decaySamples: partial.decay * duration * sampleRate,
        releaseSamples: partial.release * duration * sampleRate,
        sustainStartSamples: (partial.attack + partial.decay) * duration * sampleRate,
        releaseStartSamples: (duration - partial.release * duration) * sampleRate,
      });
    }

    const invNormFactor = 1.0 / Math.sqrt(precomputed.length);
    let peakL = 0;
    let peakR = 0;

    for (let i = 0; i < numSamples; i++) {
      let sumL = 0;
      let sumR = 0;

      for (let p = 0; p < precomputed.length; p++) {
        const pc = precomputed[p];
        const partial = pc.partial;

        const adjustedSample = Math.max(0, i - pc.evolutionOffsetSamples);
        const envelope = this.calculateEnvelopeFast(
          adjustedSample,
          pc.attackSamples,
          pc.decaySamples,
          pc.sustainStartSamples,
          pc.releaseStartSamples,
          pc.releaseSamples,
          partial.sustain,
          partial.attackCurve,
          partial.decayCurve,
          partial.releaseCurve
        );

        const amplitudeLFO = 1 + Math.sin(amplitudeLFOPhases[p]) * partial.amplitudeLFODepth;

        driftOffsetsL[p] += (Math.random() * 2 - 1) * partial.frequencyDrift * 0.00001;
        driftOffsetsR[p] += (Math.random() * 2 - 1) * partial.frequencyDrift * 0.00001;
        driftOffsetsL[p] = Math.max(-partial.frequencyDrift * 0.005, Math.min(partial.frequencyDrift * 0.005, driftOffsetsL[p]));
        driftOffsetsR[p] = Math.max(-partial.frequencyDrift * 0.005, Math.min(partial.frequencyDrift * 0.005, driftOffsetsR[p]));

        const vibratoL = Math.sin(vibratoPhasesL[p]) * partial.vibratoDepth;
        const vibratoR = Math.sin(vibratoPhasesR[p]) * partial.vibratoDepth;

        const tremoloL = 1 + Math.sin(tremoloPhasesL[p]) * partial.tremoloDepth;
        const tremoloR = 1 + Math.sin(tremoloPhasesR[p]) * partial.tremoloDepth;

        const freqL = pc.inharmonicRatio * (1 + vibratoL + driftOffsetsL[p]);
        const freqR = pc.inharmonicRatio * (1 + vibratoR + driftOffsetsR[p]);

        const levelEnvRolloffL = partial.level * envelope * pc.rolloff * pc.leftGain * amplitudeLFO;
        const levelEnvRolloffR = partial.level * envelope * pc.rolloff * pc.rightGain * amplitudeLFO;

        const sampleL = Math.sin(partialPhasesL[p]) * levelEnvRolloffL * tremoloL;
        const sampleR = Math.sin(partialPhasesR[p]) * levelEnvRolloffR * tremoloR;

        sumL += sampleL;
        sumR += sampleR;

        partialPhasesL[p] += pc.phaseIncrementL * freqL;
        partialPhasesR[p] += pc.phaseIncrementR * freqR;
        vibratoPhasesL[p] += pc.vibratoPhaseIncrement;
        vibratoPhasesR[p] += pc.vibratoPhaseIncrement;
        tremoloPhasesL[p] += pc.tremoloPhaseIncrement;
        tremoloPhasesR[p] += pc.tremoloPhaseIncrement;
        amplitudeLFOPhases[p] += pc.amplitudeLFOPhaseIncrement;

        if (partialPhasesL[p] > TAU) partialPhasesL[p] %= TAU;
        if (partialPhasesR[p] > TAU) partialPhasesR[p] %= TAU;
      }

      const sampL = sumL * invNormFactor;
      const sampR = sumR * invNormFactor;

      leftBuffer[i] = sampL;
      rightBuffer[i] = sampR;

      const absL = Math.abs(sampL);
      const absR = Math.abs(sampR);
      if (absL > peakL) peakL = absL;
      if (absR > peakR) peakR = absR;
    }

    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 calculateRolloff(partialIndex: number, totalPartials: number, brightness: number): number {
    const position = partialIndex / (totalPartials - 1);
    const rolloffAmount = 1 - brightness;
    return 1 - (position * rolloffAmount);
  }

  private shouldIncludePartial(ratio: number, seriesType: HarmonicSeriesType): boolean {
    if (seriesType === HarmonicSeriesType.All) {
      return true;
    }

    if (ratio < 1) {
      return true;
    }

    const roundedRatio = Math.round(ratio);

    switch (seriesType) {
      case HarmonicSeriesType.OddOnly:
        return roundedRatio % 2 === 1 && roundedRatio > 0;
      case HarmonicSeriesType.EvenOnly:
        return roundedRatio % 2 === 0 && roundedRatio > 0;
      case HarmonicSeriesType.PrimeOnly:
        return this.isPrime(roundedRatio);
      default:
        return true;
    }
  }

  private isPrime(n: number): boolean {
    if (n < 2) return false;
    if (n === 2) return true;
    if (n % 2 === 0) return false;
    for (let i = 3; i <= Math.sqrt(n); i += 2) {
      if (n % i === 0) return false;
    }
    return true;
  }

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

    if (t < attackTime) {
      const progress = t / attackTime;
      return this.applyCurve(progress, partial.attackCurve);
    } else if (t < sustainStart) {
      const progress = (t - attackTime) / decayTime;
      const curvedProgress = this.applyCurve(progress, partial.decayCurve);
      return 1 - curvedProgress * (1 - partial.sustain);
    } else if (t < releaseStart) {
      return partial.sustain;
    } else {
      const progress = (t - releaseStart) / releaseTime;
      const curvedProgress = this.applyCurve(progress, partial.releaseCurve);
      return partial.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 applyCurveFast(progress: number, curve: EnvCurve): number {
    switch (curve) {
      case EnvCurve.Linear:
        return progress;
      case EnvCurve.Exponential:
        return progress * progress * progress;
      case EnvCurve.Logarithmic:
        return Math.cbrt(progress);
      case EnvCurve.SCurve:
        const x = (progress - 0.5) * Math.PI;
        return (Math.sin(x) + 1) * 0.5;
      default:
        return progress;
    }
  }

  private calculateEnvelopeFast(
    sample: number,
    attackSamples: number,
    decaySamples: number,
    sustainStartSamples: number,
    releaseStartSamples: number,
    releaseSamples: number,
    sustain: number,
    attackCurve: EnvCurve,
    decayCurve: EnvCurve,
    releaseCurve: EnvCurve
  ): number {
    if (sample < attackSamples) {
      const progress = sample / attackSamples;
      return this.applyCurveFast(progress, attackCurve);
    } else if (sample < sustainStartSamples) {
      const progress = (sample - attackSamples) / decaySamples;
      const curvedProgress = this.applyCurveFast(progress, decayCurve);
      return 1 - curvedProgress * (1 - sustain);
    } else if (sample < releaseStartSamples) {
      return sustain;
    } else {
      const progress = (sample - releaseStartSamples) / releaseSamples;
      const curvedProgress = this.applyCurveFast(progress, releaseCurve);
      return sustain * (1 - curvedProgress);
    }
  }

  randomParams(pitchLock?: PitchLock): AdditiveParams {
    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.95, 1.05);

    const harmonicSeriesType = this.randomInt(0, 3) as HarmonicSeriesType;
    const distributionStrategy = this.randomInt(0, 9) as DistributionStrategy;
    const ratios = this.generateRatiosForStrategy(distributionStrategy);

    const numPartials = Math.min(ratios.length, this.randomInt(Math.min(4, ratios.length), ratios.length));
    const partials: PartialParams[] = [];

    for (let i = 0; i < numPartials; i++) {
      partials.push(this.randomPartial(i, ratios, distributionStrategy));
    }

    return {
      baseFreq,
      partials,
      stereoWidth: this.randomRange(0.2, 0.8),
      brightness: this.randomRange(0.3, 0.9),
      inharmonicity: this.randomRange(0, 0.7),
      harmonicSeriesType,
      distributionStrategy,
      phaseCoherence: this.randomRange(0, 1),
      evolutionAmount: this.randomRange(0, 0.8),
      panSpread: this.randomRange(0, 0.7),
    };
  }

  private generateRatiosForStrategy(strategy: DistributionStrategy): number[] {
    switch (strategy) {
      case DistributionStrategy.FundamentalFocused:
        return [1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 6, 7, 8];

      case DistributionStrategy.OctaveStack:
        return [0.5, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24];

      case DistributionStrategy.FifthBased: {
        const ratios = [1];
        let current = 1;
        for (let i = 0; i < 12; i++) {
          current *= 1.5;
          if (current > 32) current /= 2;
          ratios.push(current);
        }
        return ratios.sort((a, b) => a - b);
      }

      case DistributionStrategy.LowMidCluster:
        return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];

      case DistributionStrategy.HighShimmer:
        return [8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32];

      case DistributionStrategy.SparseScatter:
        return [1, 3, 7, 11, 17, 23, 29];

      case DistributionStrategy.DenseLow:
        return [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6];

      case DistributionStrategy.SubharmonicEmphasis:
        return [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4];

      case DistributionStrategy.BellCurve:
        return [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];

      case DistributionStrategy.RandomWalk: {
        const start = this.randomInt(1, 8);
        const ratios = [];
        let current = start;
        for (let i = 0; i < 15; i++) {
          ratios.push(current);
          current += Math.random() < 0.5 ? 1 : 2;
          if (current > 32) break;
        }
        return ratios;
      }

      default:
        return [1, 2, 3, 4, 5, 6, 7, 8];
    }
  }

  private randomPartial(index: number, ratios: number[], strategy: DistributionStrategy): PartialParams {
    const ratio = this.randomChoice(ratios) * this.randomRange(0.998, 1.002);

    let levelDecay: number;
    switch (strategy) {
      case DistributionStrategy.FundamentalFocused:
        levelDecay = Math.pow(0.65, index * 0.6);
        break;
      case DistributionStrategy.HighShimmer:
        levelDecay = Math.pow(0.88, index * 0.2);
        break;
      case DistributionStrategy.SparseScatter:
        levelDecay = Math.pow(0.85, index * 0.3);
        break;
      case DistributionStrategy.DenseLow:
        levelDecay = Math.pow(0.92, index * 0.4);
        break;
      case DistributionStrategy.SubharmonicEmphasis:
        levelDecay = Math.pow(0.82, Math.abs(index - 3) * 0.5);
        break;
      case DistributionStrategy.BellCurve:
        const centerDist = Math.abs(index - ratios.length / 2);
        levelDecay = Math.pow(0.75, centerDist * 0.5);
        break;
      default:
        levelDecay = Math.pow(0.8, index * 0.4);
    }

    const baseLevel = this.randomRange(0.3, 1.0);
    const level = baseLevel * levelDecay;

    return {
      ratio,
      level,
      attack: this.randomRange(0.001, 0.15),
      decay: this.randomRange(0.02, 0.25),
      sustain: this.randomRange(0.2, 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,
      vibratoRate: this.randomRange(2, 8),
      vibratoDepth: this.randomRange(0, 0.015),
      tremoloRate: this.randomRange(1, 6),
      tremoloDepth: this.randomRange(0, 0.25),
      amplitudeLFORate: this.randomRange(0.1, 2),
      amplitudeLFODepth: this.randomRange(0, 0.6),
      frequencyDrift: this.randomRange(0, 1),
    };
  }

  mutateParams(params: AdditiveParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): AdditiveParams {
    const newHarmonicSeriesType = Math.random() < 0.08 ? this.randomInt(0, 3) as HarmonicSeriesType : params.harmonicSeriesType;
    const newDistributionStrategy = Math.random() < 0.08 ? this.randomInt(0, 9) as DistributionStrategy : params.distributionStrategy;
    const ratios = this.generateRatiosForStrategy(newDistributionStrategy);

    const shouldChangePartialCount = Math.random() < 0.1;
    let newPartials = params.partials.map(p => this.mutatePartial(p, mutationAmount, ratios));

    if (shouldChangePartialCount) {
      if (Math.random() < 0.5 && newPartials.length < 50) {
        newPartials.push(this.randomPartial(newPartials.length, ratios, newDistributionStrategy));
      } else if (newPartials.length > 3) {
        newPartials = newPartials.slice(0, -1);
      }
    }

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

    return {
      baseFreq,
      partials: newPartials,
      stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1),
      brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
      inharmonicity: this.mutateValue(params.inharmonicity, mutationAmount, 0, 1),
      harmonicSeriesType: newHarmonicSeriesType,
      distributionStrategy: newDistributionStrategy,
      phaseCoherence: this.mutateValue(params.phaseCoherence, mutationAmount, 0, 1),
      evolutionAmount: this.mutateValue(params.evolutionAmount, mutationAmount, 0, 1),
      panSpread: this.mutateValue(params.panSpread, mutationAmount, 0, 1),
    };
  }

  private mutatePartial(partial: PartialParams, amount: number, ratios: number[]): PartialParams {
    let newRatio = partial.ratio;

    if (Math.random() < 0.1) {
      newRatio = this.randomChoice(ratios) * this.randomRange(0.998, 1.002);
    }

    return {
      ratio: newRatio,
      level: this.mutateValue(partial.level, amount, 0.05, 1.0),
      attack: this.mutateValue(partial.attack, amount, 0.001, 0.25),
      decay: this.mutateValue(partial.decay, amount, 0.01, 0.4),
      sustain: this.mutateValue(partial.sustain, amount, 0.1, 0.9),
      release: this.mutateValue(partial.release, amount, 0.02, 0.6),
      attackCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : partial.attackCurve,
      decayCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : partial.decayCurve,
      releaseCurve: Math.random() < 0.08 ? this.randomInt(0, 3) as EnvCurve : partial.releaseCurve,
      vibratoRate: this.mutateValue(partial.vibratoRate, amount, 1, 15),
      vibratoDepth: this.mutateValue(partial.vibratoDepth, amount, 0, 0.03),
      tremoloRate: this.mutateValue(partial.tremoloRate, amount, 0.5, 10),
      tremoloDepth: this.mutateValue(partial.tremoloDepth, amount, 0, 0.4),
      amplitudeLFORate: this.mutateValue(partial.amplitudeLFORate, amount, 0.1, 3),
      amplitudeLFODepth: this.mutateValue(partial.amplitudeLFODepth, amount, 0, 0.8),
      frequencyDrift: this.mutateValue(partial.frequencyDrift, amount, 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));
  }
}
