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

interface ParticleNoiseParams {
  // Particle characteristics
  density: number;
  impulseLength: number;
  impulseLengthVariation: number;

  // Pitch characteristics (affects filter frequency)
  basePitch: number;
  pitchVariation: number;

  // Texture
  noiseColor: number;
  filterResonance: number;
  clickiness: number;

  // Spatial
  stereoSpread: number;
  panSpeed: number;

  // Dynamics
  globalEnvAttack: number;
  globalEnvDecay: number;
  velocity: number;
}

export class ParticleNoise implements SynthEngine {
  getName(): string {
    return 'Particle';
  }

  getDescription(): string {
    return 'Very short noise impulses and clicks generator';
  }

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

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

  randomParams(pitchLock?: PitchLock): ParticleNoiseParams {
    const densityBias = Math.random();

    let density: number;
    let impulseLength: number;
    let impulseLengthVariation: number;

    if (densityBias < 0.5) {
      // Very sparse particles
      density = 0.01 + Math.random() * 0.08;
      impulseLength = 0.01 + Math.random() * 0.04;
      impulseLengthVariation = 0.5 + Math.random() * 0.4;
    } else if (densityBias < 0.8) {
      // Sparse particles
      density = 0.1 + Math.random() * 0.15;
      impulseLength = 0.008 + Math.random() * 0.03;
      impulseLengthVariation = 0.3 + Math.random() * 0.4;
    } else {
      // Medium density
      density = 0.3 + Math.random() * 0.25;
      impulseLength = 0.005 + Math.random() * 0.02;
      impulseLengthVariation = 0.15 + Math.random() * 0.35;
    }

    let basePitch: number;
    if (pitchLock?.enabled) {
      basePitch = Math.max(0, Math.min(1, (pitchLock.frequency - 100) / 2000));
    } else {
      basePitch = 0.2 + Math.random() * 0.7;
    }

    const pitchVariation = Math.random() * 0.6;

    const noiseColor = Math.random();
    const filterResonance = Math.random() * 0.5;
    const clickiness = Math.random() * 0.8;

    const stereoSpread = Math.random() * 0.8;
    const panSpeed = Math.random() * 0.6;

    const globalEnvAttack = Math.random() * 0.12;
    const globalEnvDecay = 0.25 + Math.random() * 0.5;
    const velocity = 0.6 + Math.random() * 0.4;

    return {
      density,
      impulseLength,
      impulseLengthVariation,
      basePitch,
      pitchVariation,
      noiseColor,
      filterResonance,
      clickiness,
      stereoSpread,
      panSpeed,
      globalEnvAttack,
      globalEnvDecay,
      velocity
    };
  }

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

    return {
      density: mutate(params.density, 0.2),
      impulseLength: mutate(params.impulseLength, 0.2),
      impulseLengthVariation: mutate(params.impulseLengthVariation, 0.2),
      basePitch: pitchLock?.enabled ? params.basePitch : mutate(params.basePitch, 0.25),
      pitchVariation: mutate(params.pitchVariation, 0.2),
      noiseColor: mutate(params.noiseColor, 0.25),
      filterResonance: mutate(params.filterResonance, 0.2),
      clickiness: mutate(params.clickiness, 0.2),
      stereoSpread: mutate(params.stereoSpread, 0.2),
      panSpeed: mutate(params.panSpeed, 0.2),
      globalEnvAttack: mutate(params.globalEnvAttack, 0.15),
      globalEnvDecay: mutate(params.globalEnvDecay, 0.2),
      velocity: mutate(params.velocity, 0.15)
    };
  }

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

    // Calculate number of grains based on density
    const avgGrainsPerSecond = 2 + params.density * 80;
    const totalGrains = Math.floor(avgGrainsPerSecond * duration);

    // Pre-generate impulse timings and parameters
    const impulses: Array<{
      startTime: number;
      duration: number;
      filterFreq: number;
      pan: number;
      amplitude: number;
      isClick: boolean;
    }> = [];

    const baseFilterFreq = pitchLock?.enabled ? pitchLock.frequency : 200 + params.basePitch * 3000;

    for (let i = 0; i < totalGrains; i++) {
      const startTime = Math.random() * duration;

      const baseImpulseDuration = 0.0005 + params.impulseLength * 0.003;
      const impulseDuration = baseImpulseDuration * (0.5 + Math.random() * params.impulseLengthVariation);

      const filterOffset = (Math.random() - 0.5) * params.pitchVariation * baseFilterFreq * 2;
      const filterFreq = Math.max(100, baseFilterFreq + filterOffset);

      const pan = Math.random();
      const amplitude = 0.5 + Math.random() * 0.5;
      const isClick = Math.random() < params.clickiness;

      impulses.push({
        startTime,
        duration: impulseDuration,
        filterFreq,
        pan,
        amplitude,
        isClick
      });
    }

    // Sort impulses by start time for efficient processing
    impulses.sort((a, b) => a.startTime - b.startTime);

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

    let impulseIndex = 0;
    const activeImpulses: Array<{
      impulse: typeof impulses[0];
      startSample: number;
      filterState1: number;
      filterState2: number;
    }> = [];

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

      // Add new impulses that should start at this sample
      while (impulseIndex < impulses.length && impulses[impulseIndex].startTime <= t) {
        activeImpulses.push({
          impulse: impulses[impulseIndex],
          startSample: i,
          filterState1: 0,
          filterState2: 0
        });
        impulseIndex++;
      }

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

      // Pan modulation
      const panLFO = Math.sin(2 * Math.PI * (0.1 + params.panSpeed * 2) * t);

      let sampleL = 0;
      let sampleR = 0;

      // Render all active impulses
      for (let g = activeImpulses.length - 1; g >= 0; g--) {
        const active = activeImpulses[g];
        const impulse = active.impulse;
        const impulseSample = i - active.startSample;
        const impulseTime = impulseSample / sampleRate;

        if (impulseTime >= impulse.duration) {
          activeImpulses.splice(g, 1);
          continue;
        }

        const impulsePhase = impulseTime / impulse.duration;

        // Very fast exponential decay envelope
        const impulseEnv = Math.exp(-impulsePhase * 15);

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

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

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

        // For clicks, use pure white noise burst
        if (impulse.isClick) {
          noiseL = whiteL;
          noiseR = whiteR;
        } else if (params.filterResonance > 0.1) {
          // Apply resonant filter for tonal color
          const resonance = 2 + params.filterResonance * 8;
          const filtered = this.stateVariableFilter(
            noiseL,
            impulse.filterFreq,
            resonance,
            active.filterState1,
            active.filterState2
          );
          active.filterState1 = filtered.state1;
          active.filterState2 = filtered.state2;
          noiseL = filtered.output;
          noiseR = filtered.output;
        }

        // Apply impulse envelope and amplitude
        const impulseOutput = noiseL * impulseEnv * impulse.amplitude * params.velocity;

        // Apply panning with modulation
        const panMod = impulse.pan + panLFO * params.stereoSpread * 0.15;
        const panClamp = Math.max(0, Math.min(1, panMod));
        const panL = Math.cos(panClamp * Math.PI * 0.5);
        const panR = Math.sin(panClamp * Math.PI * 0.5);

        sampleL += impulseOutput * panL;
        sampleR += impulseOutput * panR * (impulse.isClick ? 1 : 1 + (Math.random() - 0.5) * params.stereoSpread * 0.2);
      }

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

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

    // 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 stateVariableFilter(
    input: number,
    cutoff: number,
    resonance: number,
    state1: number,
    state2: number
  ): { output: number; state1: number; state2: number } {
    const normalizedFreq = Math.min(cutoff / 44100, 0.48);
    const f = 2 * Math.sin(Math.PI * normalizedFreq);
    const q = Math.max(1 / Math.min(resonance, 20), 0.01);

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

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

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

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