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

interface DustNoiseParams {
  // Dust density and character
  dustDensity: number;
  crackleAmount: number;
  popDensity: number;

  // Dust particle characteristics
  particleDecay: number;
  particlePitchRange: number;
  particleResonance: number;

  // Background texture
  backgroundNoise: number;
  noiseColor: number;
  noiseFilter: number;

  // Pops and clicks
  popIntensity: number;
  popPitchRange: number;
  clickAmount: number;

  // Dynamics and variation
  dynamicRange: number;
  irregularity: number;

  // Stereo field
  stereoWidth: number;

  // Global envelope
  globalAttack: number;
  globalDecay: number;
}

export class DustNoise implements SynthEngine {
  getName(): string {
    return 'Pond';
  }

  getDescription(): string {
    return 'Vinyl dust, crackle, and particle noise generator';
  }

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

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

  randomParams(pitchLock?: PitchLock): DustNoiseParams {
    const characterBias = Math.random();

    let dustDensity: number;
    let crackleAmount: number;
    let popDensity: number;
    let backgroundNoise: number;

    if (characterBias < 0.5) {
      // Very sparse, minimal particles
      dustDensity = 0.01 + Math.random() * 0.08;
      crackleAmount = Math.random() * 0.12;
      popDensity = 0.01 + Math.random() * 0.05;
      backgroundNoise = Math.random() * 0.08;
    } else if (characterBias < 0.8) {
      // Sparse, clean with occasional pops
      dustDensity = 0.1 + Math.random() * 0.15;
      crackleAmount = Math.random() * 0.25;
      popDensity = 0.06 + Math.random() * 0.1;
      backgroundNoise = 0.05 + Math.random() * 0.15;
    } else {
      // Medium vinyl character (was heavy)
      dustDensity = 0.3 + Math.random() * 0.25;
      crackleAmount = 0.25 + Math.random() * 0.3;
      popDensity = 0.18 + Math.random() * 0.15;
      backgroundNoise = 0.15 + Math.random() * 0.25;
    }

    const particleDecay = 0.3 + Math.random() * 0.6;
    const particlePitchRange = 0.2 + Math.random() * 0.7;
    const particleResonance = Math.random() * 0.6;

    const noiseColor = Math.random();
    const noiseFilter = Math.random() * 0.8;

    const popIntensity = 0.3 + Math.random() * 0.6;
    const popPitchRange = 0.2 + Math.random() * 0.7;
    const clickAmount = Math.random() * 0.7;

    const dynamicRange = 0.3 + Math.random() * 0.6;
    const irregularity = Math.random() * 0.7;

    const stereoWidth = Math.random() * 0.8;

    const globalAttack = Math.random() * 0.08;
    const globalDecay = 0.3 + Math.random() * 0.5;

    return {
      dustDensity,
      crackleAmount,
      popDensity,
      particleDecay,
      particlePitchRange,
      particleResonance,
      backgroundNoise,
      noiseColor,
      noiseFilter,
      popIntensity,
      popPitchRange,
      clickAmount,
      dynamicRange,
      irregularity,
      stereoWidth,
      globalAttack,
      globalDecay
    };
  }

  mutateParams(params: DustNoiseParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): DustNoiseParams {
    const mutate = (value: number, amount: number = 0.15): number => {
      return Math.max(0, Math.min(1, value + (Math.random() - 0.5) * amount));
    };

    return {
      dustDensity: mutate(params.dustDensity, 0.2),
      crackleAmount: mutate(params.crackleAmount, 0.25),
      popDensity: mutate(params.popDensity, 0.2),
      particleDecay: mutate(params.particleDecay, 0.2),
      particlePitchRange: pitchLock?.enabled ? params.particlePitchRange : mutate(params.particlePitchRange, 0.25),
      particleResonance: mutate(params.particleResonance, 0.2),
      backgroundNoise: mutate(params.backgroundNoise, 0.2),
      noiseColor: mutate(params.noiseColor, 0.25),
      noiseFilter: mutate(params.noiseFilter, 0.2),
      popIntensity: mutate(params.popIntensity, 0.2),
      popPitchRange: pitchLock?.enabled ? params.popPitchRange : mutate(params.popPitchRange, 0.25),
      clickAmount: mutate(params.clickAmount, 0.2),
      dynamicRange: mutate(params.dynamicRange, 0.2),
      irregularity: mutate(params.irregularity, 0.2),
      stereoWidth: mutate(params.stereoWidth, 0.2),
      globalAttack: mutate(params.globalAttack, 0.15),
      globalDecay: mutate(params.globalDecay, 0.2)
    };
  }

  generate(params: DustNoiseParams, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array] {
    const numSamples = Math.floor(sampleRate * duration);
    const left = new Float32Array(numSamples);
    const right = new Float32Array(numSamples);

    // Generate dust particles
    const avgDustPerSecond = 5 + params.dustDensity * 120;
    const totalDust = Math.floor(avgDustPerSecond * duration);

    // Generate pops
    const avgPopsPerSecond = 0.5 + params.popDensity * 12;
    const totalPops = Math.floor(avgPopsPerSecond * duration);

    // Create dust particles
    const dustParticles: Array<{
      startTime: number;
      decay: number;
      pitch: number;
      amplitude: number;
      resonance: number;
      stereoOffset: number;
    }> = [];

    const baseDustPitch = pitchLock?.enabled ? pitchLock.frequency : 800 + params.particlePitchRange * 2000;
    const basePopPitch = pitchLock?.enabled ? pitchLock.frequency : 200 + params.popPitchRange * 1000;

    for (let i = 0; i < totalDust; i++) {
      const startTime = Math.random() * duration;
      const decay = (0.001 + params.particleDecay * 0.02) * (0.5 + Math.random() * 0.5);
      const pitchVariation = pitchLock?.enabled ? 0.2 : params.particlePitchRange;
      const pitchFreq = baseDustPitch + (Math.random() - 0.5) * pitchVariation * baseDustPitch;
      const amplitude = (0.3 + Math.random() * 0.7) * (0.5 + params.dynamicRange * 0.5);
      const resonance = params.particleResonance * (0.5 + Math.random() * 0.5);
      const stereoOffset = (Math.random() - 0.5) * params.stereoWidth * 0.3;

      dustParticles.push({
        startTime,
        decay,
        pitch: pitchFreq,
        amplitude,
        resonance,
        stereoOffset
      });
    }

    // Create pops
    const pops: Array<{
      startTime: number;
      intensity: number;
      pitch: number;
      isClick: boolean;
      stereoOffset: number;
    }> = [];

    for (let i = 0; i < totalPops; i++) {
      const startTime = Math.random() * duration;
      const intensity = params.popIntensity * (0.5 + Math.random() * 0.5);
      const pitchVariation = pitchLock?.enabled ? 0.2 : params.popPitchRange;
      const pitchFreq = basePopPitch + (Math.random() - 0.5) * pitchVariation * basePopPitch;
      const isClick = Math.random() < params.clickAmount;
      const stereoOffset = (Math.random() - 0.5) * params.stereoWidth * 0.5;

      pops.push({
        startTime,
        intensity,
        pitch: pitchFreq,
        isClick,
        stereoOffset
      });
    }

    // Sort events by time
    dustParticles.sort((a, b) => a.startTime - b.startTime);
    pops.sort((a, b) => a.startTime - b.startTime);

    // Noise state
    const pinkStateL = new Float32Array(7);
    const pinkStateR = new Float32Array(7);
    let brownStateL = 0;
    let brownStateR = 0;

    // Filter state for background noise
    let bgFilterStateL1 = 0;
    let bgFilterStateL2 = 0;
    let bgFilterStateR1 = 0;
    let bgFilterStateR2 = 0;

    // Active particles
    let dustIndex = 0;
    let popIndex = 0;
    const activeDust: Array<{
      particle: typeof dustParticles[0];
      startSample: number;
      phase: number;
    }> = [];
    const activePops: Array<{
      pop: typeof pops[0];
      startSample: number;
      phase: number;
    }> = [];

    // Crackle state (for vinyl crackle texture)
    let cracklePhase = 0;
    const crackleFreq = 20 + params.crackleAmount * 80;

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

      // Add new dust particles
      while (dustIndex < dustParticles.length && dustParticles[dustIndex].startTime <= t) {
        activeDust.push({
          particle: dustParticles[dustIndex],
          startSample: i,
          phase: Math.random() * Math.PI * 2
        });
        dustIndex++;
      }

      // Add new pops
      while (popIndex < pops.length && pops[popIndex].startTime <= t) {
        activePops.push({
          pop: pops[popIndex],
          startSample: i,
          phase: Math.random() * Math.PI * 2
        });
        popIndex++;
      }

      // Global envelope
      const globalEnv = this.globalEnvelope(
        i,
        numSamples,
        params.globalAttack,
        params.globalDecay,
        duration,
        sampleRate
      );

      // Background noise
      const whiteL = Math.random() * 2 - 1;
      const whiteR = Math.random() * 2 - 1;

      brownStateL = this.updateBrownState(brownStateL, whiteL);
      brownStateR = this.updateBrownState(brownStateR, whiteR);

      let bgNoiseL = this.selectNoiseColor(params.noiseColor, whiteL, pinkStateL, brownStateL);
      let bgNoiseR = this.selectNoiseColor(params.noiseColor, whiteR, pinkStateR, brownStateR);

      // Filter background noise
      if (params.noiseFilter > 0.1) {
        const filterFreq = 500 + params.noiseFilter * 3000;
        const filtered = this.stateVariableFilter(
          bgNoiseL,
          filterFreq,
          1,
          sampleRate,
          bgFilterStateL1,
          bgFilterStateL2
        );
        bgFilterStateL1 = filtered.state1;
        bgFilterStateL2 = filtered.state2;
        bgNoiseL = filtered.output;

        const filteredR = this.stateVariableFilter(
          bgNoiseR,
          filterFreq,
          1,
          sampleRate,
          bgFilterStateR1,
          bgFilterStateR2
        );
        bgFilterStateR1 = filteredR.state1;
        bgFilterStateR2 = filteredR.state2;
        bgNoiseR = filteredR.output;
      }

      // Crackle modulation
      cracklePhase += (2 * Math.PI * crackleFreq) / sampleRate;
      const crackleMod = Math.sin(cracklePhase) * 0.5 + 0.5;
      const crackleEnv = Math.pow(crackleMod, 3) * params.crackleAmount;

      bgNoiseL *= params.backgroundNoise * (1 + crackleEnv);
      bgNoiseR *= params.backgroundNoise * (1 + crackleEnv);

      // Render dust particles
      let dustL = 0;
      let dustR = 0;

      for (let d = activeDust.length - 1; d >= 0; d--) {
        const active = activeDust[d];
        const particle = active.particle;
        const elapsed = (i - active.startSample) / sampleRate;

        if (elapsed > particle.decay * 5) {
          activeDust.splice(d, 1);
          continue;
        }

        const env = Math.exp(-elapsed / particle.decay);
        const phaseInc = (2 * Math.PI * particle.pitch) / sampleRate;
        active.phase += phaseInc;

        let signal = Math.sin(active.phase);

        // Add resonance (filter-like character)
        if (particle.resonance > 0.1) {
          signal = signal * (1 - particle.resonance) +
                   Math.sin(active.phase * 2) * particle.resonance * 0.3 +
                   Math.sin(active.phase * 3) * particle.resonance * 0.15;
        }

        const output = signal * env * particle.amplitude;

        const panL = 0.5 - particle.stereoOffset;
        const panR = 0.5 + particle.stereoOffset;

        dustL += output * panL;
        dustR += output * panR;
      }

      // Render pops and clicks
      let popL = 0;
      let popR = 0;

      for (let p = activePops.length - 1; p >= 0; p--) {
        const active = activePops[p];
        const pop = active.pop;
        const elapsed = (i - active.startSample) / sampleRate;

        const maxDuration = pop.isClick ? 0.001 : 0.008;

        if (elapsed > maxDuration) {
          activePops.splice(p, 1);
          continue;
        }

        const env = Math.exp(-elapsed * (pop.isClick ? 2000 : 300));

        let signal: number;
        if (pop.isClick) {
          // Sharp click (very short impulse)
          signal = (Math.random() * 2 - 1) * (elapsed < 0.0003 ? 1 : 0.3);
        } else {
          // Pop with pitch
          const phaseInc = (2 * Math.PI * pop.pitch) / sampleRate;
          active.phase += phaseInc;
          signal = Math.sin(active.phase) * 0.7 + (Math.random() * 2 - 1) * 0.3;
        }

        const output = signal * env * pop.intensity;

        const panL = 0.5 - pop.stereoOffset;
        const panR = 0.5 + pop.stereoOffset;

        popL += output * panL;
        popR += output * panR;
      }

      // Combine all elements
      let sampleL = bgNoiseL + dustL + popL;
      let sampleR = bgNoiseR + dustR + popR;

      // Apply irregularity (random amplitude modulation)
      if (params.irregularity > 0.1) {
        const irregMod = 1 + (Math.random() - 0.5) * params.irregularity * 0.3;
        sampleL *= irregMod;
        sampleR *= irregMod;
      }

      // Apply global envelope
      sampleL *= globalEnv;
      sampleR *= globalEnv;

      // Soft clipping
      left[i] = this.softClip(sampleL * 0.6);
      right[i] = this.softClip(sampleR * 0.6);
    }

    // Normalize
    let peak = 0;
    for (let i = 0; i < numSamples; i++) {
      peak = Math.max(peak, Math.abs(left[i]), Math.abs(right[i]));
    }

    if (peak > 0.001) {
      const normGain = 0.95 / peak;
      for (let i = 0; i < numSamples; i++) {
        left[i] *= normGain;
        right[i] *= normGain;
      }
    }

    return [left, right];
  }

  private globalEnvelope(
    sample: number,
    totalSamples: number,
    attack: number,
    decay: number,
    duration: number,
    sampleRate: number
  ): number {
    const attackSamples = Math.floor(attack * duration * sampleRate);
    const phase = sample / totalSamples;

    if (sample < attackSamples && attackSamples > 0) {
      const attackPhase = sample / attackSamples;
      return attackPhase * attackPhase * (3 - 2 * attackPhase);
    }

    const decayRate = Math.max(decay, 0.1);
    const decayPhase = (sample - attackSamples) / (totalSamples - attackSamples);
    return Math.exp(-decayPhase / decayRate);
  }

  private updateBrownState(brownState: number, whiteNoise: number): number {
    return (brownState + whiteNoise * 0.02) * 0.98;
  }

  private selectNoiseColor(
    colorParam: number,
    whiteNoise: number,
    pinkState: Float32Array,
    brownState: number
  ): number {
    if (colorParam < 0.33) {
      return whiteNoise;
    } else if (colorParam < 0.66) {
      pinkState[0] = 0.99886 * pinkState[0] + whiteNoise * 0.0555179;
      pinkState[1] = 0.99332 * pinkState[1] + whiteNoise * 0.0750759;
      pinkState[2] = 0.96900 * pinkState[2] + whiteNoise * 0.1538520;
      pinkState[3] = 0.86650 * pinkState[3] + whiteNoise * 0.3104856;
      pinkState[4] = 0.55000 * pinkState[4] + whiteNoise * 0.5329522;
      pinkState[5] = -0.7616 * pinkState[5] - whiteNoise * 0.0168980;

      const pink = pinkState[0] + pinkState[1] + pinkState[2] + pinkState[3] +
                   pinkState[4] + pinkState[5] + pinkState[6] + whiteNoise * 0.5362;
      pinkState[6] = whiteNoise * 0.115926;

      return pink * 0.11;
    } else {
      return brownState * 2.5;
    }
  }

  private stateVariableFilter(
    input: number,
    cutoff: number,
    resonance: number,
    sampleRate: number,
    state1: number,
    state2: number
  ): { output: number; state1: number; state2: number } {
    const normalizedFreq = Math.min(cutoff / sampleRate, 0.48);
    const f = 2 * Math.sin(Math.PI * normalizedFreq);
    const q = Math.max(1 / Math.min(resonance, 10), 0.02);

    const lowpass = state2 + f * state1;
    const highpass = input - lowpass - q * state1;
    const bandpass = f * highpass + state1;

    const newState1 = Math.max(-2, Math.min(2, Math.abs(bandpass) > 1e-10 ? bandpass : 0));
    const newState2 = Math.max(-2, Math.min(2, Math.abs(lowpass) > 1e-10 ? lowpass : 0));

    return {
      output: bandpass,
      state1: newState1,
      state2: newState2
    };
  }

  private softClip(x: number): number {
    if (x > 1) {
      return 1;
    } else if (x < -1) {
      return -1;
    } else if (x > 0.66) {
      return (3 - (2 - 3 * x) ** 2) / 3;
    } else if (x < -0.66) {
      return -(3 - (2 - 3 * -x) ** 2) / 3;
    } else {
      return x;
    }
  }
}
