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

interface WavetableParams {
  bankIndex: number;
  position: number;
  baseFreq: number;
  filterCutoff: number;
  filterResonance: number;
  attack: number;
  decay: number;
  sustain: number;
  release: number;
}

interface Wavetable {
  name: string;
  samples: Float32Array;
}

export class WavetableEngine implements SynthEngine<WavetableParams> {
  private wavetables: Wavetable[] = [];
  private isLoaded = false;

  constructor() {
    this.generateBasicWavetables();
    this.loadWavetablesAsync();
  }

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

  getDescription(): string {
    return 'Classic wavetable synthesis';
  }

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

  private generateBasicWavetables(): void {
    const size = 2048;
    const tables = [
      { name: 'Sine', gen: (i: number) => Math.sin((i / size) * Math.PI * 2) },
      { name: 'Triangle', gen: (i: number) => {
        const t = i / size;
        return t < 0.25 ? t * 4 : t < 0.75 ? 2 - t * 4 : t * 4 - 4;
      }},
      { name: 'Saw', gen: (i: number) => (i / size) * 2 - 1 },
      { name: 'Square', gen: (i: number) => i < size / 2 ? 1 : -1 },
      { name: 'Pulse25', gen: (i: number) => i < size / 4 ? 1 : -1 },
    ];

    this.wavetables = tables.map(({ name, gen }) => ({
      name,
      samples: Float32Array.from({ length: size }, (_, i) => gen(i)),
    }));
  }

  private async loadWavetablesAsync(): Promise<void> {
    if (this.isLoaded) return;

    try {
      const manifestResponse = await fetch('/wavetables/manifest.txt');
      const manifestText = await manifestResponse.text();
      const filenames = manifestText.trim().split('\n').filter(line => line.endsWith('.wav'));

      const loadPromises = filenames.slice(0, 50).map(async (filename) => {
        try {
          const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
          const response = await fetch(`/wavetables/${filename.trim()}`);
          const arrayBuffer = await response.arrayBuffer();
          const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
          const samples = audioBuffer.getChannelData(0);

          let sum = 0;
          for (let i = 0; i < samples.length; i++) {
            sum += samples[i];
          }
          const dc = sum / samples.length;

          const cleaned = new Float32Array(samples.length);
          let peak = 0;
          for (let i = 0; i < samples.length; i++) {
            cleaned[i] = samples[i] - dc;
            peak = Math.max(peak, Math.abs(cleaned[i]));
          }

          if (peak > 0) {
            for (let i = 0; i < cleaned.length; i++) {
              cleaned[i] /= peak;
            }
          }

          return {
            name: filename.replace('.wav', ''),
            samples: cleaned,
          };
        } catch (error) {
          return null;
        }
      });

      const results = await Promise.all(loadPromises);
      const loaded = results.filter((wt) => wt !== null) as Wavetable[];

      if (loaded.length > 0) {
        this.wavetables = loaded;
        console.log(`Loaded ${this.wavetables.length} wavetables`);
      }

      this.isLoaded = true;
    } catch (error) {
      console.error('Failed to load wavetables:', error);
      this.isLoaded = true;
    }
  }

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

    if (this.wavetables.length < 3) {
      return [left, right];
    }

    const bankSize = 4;
    const numBanks = Math.max(1, this.wavetables.length - bankSize + 1);
    const bankStart = Math.floor(params.bankIndex * numBanks) % numBanks;

    const bank: Wavetable[] = [];
    for (let i = 0; i < bankSize; i++) {
      bank.push(this.wavetables[(bankStart + i) % this.wavetables.length]);
    }

    const tableSize = bank[0].samples.length;
    const phaseIncrement = params.baseFreq / sampleRate;

    let phase = 0;

    const attackSamples = params.attack * duration * sampleRate;
    const decaySamples = params.decay * duration * sampleRate;
    const releaseSamples = params.release * duration * sampleRate;

    let filterLP = 0;
    let filterBP = 0;

    const tablePos = params.position * (bankSize - 1);
    const table1Index = Math.floor(tablePos);
    const table2Index = Math.min(table1Index + 1, bankSize - 1);
    const tableFade = tablePos - table1Index;

    const cutoffNorm = Math.min(params.filterCutoff * 0.45, 0.45);
    const f = 2 * Math.sin(Math.PI * cutoffNorm);
    const q = 1 / Math.max(params.filterResonance * 5 + 0.7, 0.7);

    for (let i = 0; i < numSamples; i++) {
      let env = 1;
      if (i < attackSamples) {
        env = i / attackSamples;
      } else if (i < attackSamples + decaySamples) {
        const t = (i - attackSamples) / decaySamples;
        env = 1 - t * (1 - params.sustain);
      } else if (i < numSamples - releaseSamples) {
        env = params.sustain;
      } else {
        const t = (i - (numSamples - releaseSamples)) / releaseSamples;
        env = params.sustain * (1 - t);
      }

      const index1 = phase * tableSize;
      const i1 = Math.floor(index1);
      const i2 = (i1 + 1) % tableSize;
      const frac = index1 - i1;

      const sample1 = bank[table1Index].samples[i1] * (1 - frac) + bank[table1Index].samples[i2] * frac;
      const sample2 = bank[table2Index].samples[i1] * (1 - frac) + bank[table2Index].samples[i2] * frac;

      let sample = sample1 * (1 - tableFade) + sample2 * tableFade;

      const hp = sample - filterLP - q * filterBP;
      filterBP = filterBP + f * hp;
      filterLP = filterLP + f * filterBP;

      filterBP = Math.max(-2, Math.min(2, filterBP));
      filterLP = Math.max(-2, Math.min(2, filterLP));

      sample = filterLP * env;

      left[i] = sample;
      right[i] = sample;

      phase += phaseIncrement;
      if (phase >= 1) phase -= 1;
    }

    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 gain = 0.9 / peak;
      for (let i = 0; i < numSamples; i++) {
        left[i] *= gain;
        right[i] *= gain;
      }
    }

    return [left, right];
  }

  randomParams(pitchLock?: PitchLock): WavetableParams {
    const freqs = [110, 146.8, 220, 293.7, 440];
    return {
      bankIndex: Math.random(),
      position: 0.2 + Math.random() * 0.6,
      baseFreq: pitchLock?.enabled
        ? pitchLock.frequency
        : freqs[Math.floor(Math.random() * freqs.length)],
      filterCutoff: 0.5 + Math.random() * 0.4,
      filterResonance: Math.random() * 0.5,
      attack: 0.001 + Math.random() * 0.05,
      decay: 0.05 + Math.random() * 0.2,
      sustain: 0.5 + Math.random() * 0.4,
      release: 0.1 + Math.random() * 0.3,
    };
  }

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

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

    return {
      bankIndex: Math.random() < 0.2 ? Math.random() : params.bankIndex,
      position: mutate(params.position, 0.2),
      baseFreq,
      filterCutoff: mutate(params.filterCutoff, 0.2),
      filterResonance: mutate(params.filterResonance, 0.15),
      attack: mutate(params.attack, 0.1),
      decay: mutate(params.decay, 0.15),
      sustain: mutate(params.sustain, 0.15),
      release: mutate(params.release, 0.15),
    };
  }
}
