import { Csound } from '@csound/browser';
import type { SynthEngine, PitchLock } from './SynthEngine';

export interface CsoundParameter {
  channelName: string;
  value: number;
}

export abstract class CsoundEngine<T = any> implements SynthEngine<T> {
  abstract getName(): string;
  abstract getDescription(): string;
  abstract getType(): 'generative' | 'sample' | 'input';

  protected abstract getOrchestra(): string;
  protected abstract getParametersForCsound(params: T): CsoundParameter[];

  abstract randomParams(pitchLock?: PitchLock): T;
  abstract mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T;

  async generate(
    params: T,
    sampleRate: number,
    duration: number,
    pitchLock?: PitchLock
  ): Promise<[Float32Array, Float32Array]> {
    const orchestra = this.getOrchestra();
    const csoundParams = this.getParametersForCsound(params);
    const outputFile = '/output.wav';
    const csd = this.buildCSD(orchestra, duration, sampleRate, csoundParams, outputFile);

    try {
      const csound = await Csound();

      if (!csound) {
        throw new Error('Failed to initialize Csound');
      }

      await csound.compileCSD(csd);
      await csound.start();
      await csound.perform();
      await csound.cleanup();

      const wavData = await csound.fs.readFile(outputFile);
      const audioBuffer = await this.parseWavManually(wavData, sampleRate);

      await csound.terminateInstance();

      let leftChannel = new Float32Array(audioBuffer.leftChannel);
      let rightChannel = new Float32Array(audioBuffer.rightChannel);

      this.removeDCOffset(leftChannel, rightChannel);

      const trimmed = this.trimToZeroCrossing(leftChannel, rightChannel, sampleRate);
      leftChannel = trimmed.left;
      rightChannel = trimmed.right;

      this.applyFadeIn(leftChannel, rightChannel, sampleRate);
      this.applyFadeOut(leftChannel, rightChannel, sampleRate);

      const peak = this.findPeak(leftChannel, rightChannel);
      if (peak > 0.001) {
        const normalizeGain = 0.85 / peak;
        this.applyGain(leftChannel, rightChannel, normalizeGain);
      }

      return [leftChannel, rightChannel];
    } catch (error) {
      console.error('Csound generation failed:', error);
      const numSamples = Math.floor(sampleRate * duration);
      return [new Float32Array(numSamples), new Float32Array(numSamples)];
    }
  }

  private parseWavManually(
    wavData: Uint8Array,
    expectedSampleRate: number
  ): { leftChannel: Float32Array; rightChannel: Float32Array } {
    const view = new DataView(wavData.buffer);

    // Check RIFF header
    const riff = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));
    if (riff !== 'RIFF') {
      throw new Error('Invalid WAV file: no RIFF header');
    }

    // Check WAVE format
    const wave = String.fromCharCode(view.getUint8(8), view.getUint8(9), view.getUint8(10), view.getUint8(11));
    if (wave !== 'WAVE') {
      throw new Error('Invalid WAV file: no WAVE format');
    }

    // Find fmt chunk
    let offset = 12;
    while (offset < wavData.length) {
      const chunkId = String.fromCharCode(
        view.getUint8(offset),
        view.getUint8(offset + 1),
        view.getUint8(offset + 2),
        view.getUint8(offset + 3)
      );
      const chunkSize = view.getUint32(offset + 4, true);

      if (chunkId === 'fmt ') {
        const audioFormat = view.getUint16(offset + 8, true);
        const numChannels = view.getUint16(offset + 10, true);
        const sampleRate = view.getUint32(offset + 12, true);
        const bitsPerSample = view.getUint16(offset + 22, true);

        // Find data chunk
        let dataOffset = offset + 8 + chunkSize;
        while (dataOffset < wavData.length) {
          const dataChunkId = String.fromCharCode(
            view.getUint8(dataOffset),
            view.getUint8(dataOffset + 1),
            view.getUint8(dataOffset + 2),
            view.getUint8(dataOffset + 3)
          );
          const dataChunkSize = view.getUint32(dataOffset + 4, true);

          if (dataChunkId === 'data') {
            const bytesPerSample = bitsPerSample / 8;
            const numSamples = Math.floor(dataChunkSize / bytesPerSample / numChannels);

            const leftChannel = new Float32Array(numSamples);
            const rightChannel = new Float32Array(numSamples);

            let audioDataOffset = dataOffset + 8;

            if (bitsPerSample === 16) {
              // 16-bit PCM
              for (let i = 0; i < numSamples; i++) {
                const leftSample = view.getInt16(audioDataOffset, true);
                leftChannel[i] = leftSample / 32768.0;
                audioDataOffset += 2;

                if (numChannels > 1) {
                  const rightSample = view.getInt16(audioDataOffset, true);
                  rightChannel[i] = rightSample / 32768.0;
                  audioDataOffset += 2;
                } else {
                  rightChannel[i] = leftChannel[i];
                }
              }
            } else if (bitsPerSample === 32 && audioFormat === 3) {
              // 32-bit float
              for (let i = 0; i < numSamples; i++) {
                leftChannel[i] = view.getFloat32(audioDataOffset, true);
                audioDataOffset += 4;

                if (numChannels > 1) {
                  rightChannel[i] = view.getFloat32(audioDataOffset, true);
                  audioDataOffset += 4;
                } else {
                  rightChannel[i] = leftChannel[i];
                }
              }
            } else {
              throw new Error(`Unsupported WAV format: ${bitsPerSample}-bit, format ${audioFormat}`);
            }

            return { leftChannel, rightChannel };
          }

          dataOffset += 8 + dataChunkSize;
        }
        throw new Error('No data chunk found in WAV file');
      }

      offset += 8 + chunkSize;
    }

    throw new Error('No fmt chunk found in WAV file');
  }

  private buildCSD(
    orchestra: string,
    duration: number,
    sampleRate: number,
    parameters: CsoundParameter[],
    outputFile: string
  ): string {
    const paramInit = parameters
      .map(p => `chnset ${p.value}, "${p.channelName}"`)
      .join('\n');

    return `<CsoundSynthesizer>
<CsOptions>
-W -d -m0 -o ${outputFile}
</CsOptions>
<CsInstruments>
sr = ${sampleRate}
ksmps = 64
nchnls = 2
0dbfs = 1.0

${paramInit}

${orchestra}

</CsInstruments>
<CsScore>
i 1 0 ${duration}
e
</CsScore>
</CsoundSynthesizer>`;
  }

  private findPeak(leftChannel: Float32Array, rightChannel: Float32Array): number {
    let peak = 0;
    for (let i = 0; i < leftChannel.length; i++) {
      peak = Math.max(peak, Math.abs(leftChannel[i]), Math.abs(rightChannel[i]));
    }
    return peak;
  }

  private applyGain(
    leftChannel: Float32Array,
    rightChannel: Float32Array,
    gain: number
  ): void {
    for (let i = 0; i < leftChannel.length; i++) {
      leftChannel[i] *= gain;
      rightChannel[i] *= gain;
    }
  }

  private removeDCOffset(leftChannel: Float32Array, rightChannel: Float32Array): void {
    let leftSum = 0;
    let rightSum = 0;
    const length = leftChannel.length;

    for (let i = 0; i < length; i++) {
      leftSum += leftChannel[i];
      rightSum += rightChannel[i];
    }

    const leftDC = leftSum / length;
    const rightDC = rightSum / length;

    for (let i = 0; i < length; i++) {
      leftChannel[i] -= leftDC;
      rightChannel[i] -= rightDC;
    }
  }

  private trimToZeroCrossing(
    leftChannel: Float32Array,
    rightChannel: Float32Array,
    sampleRate: number
  ): { left: Float32Array; right: Float32Array } {
    const maxSearchSamples = Math.min(Math.floor(sampleRate * 0.01), leftChannel.length);
    let trimIndex = 0;

    for (let i = 1; i < maxSearchSamples; i++) {
      const prevL = leftChannel[i - 1];
      const currL = leftChannel[i];
      const prevR = rightChannel[i - 1];
      const currR = rightChannel[i];

      if (
        (prevL <= 0 && currL >= 0) || (prevL >= 0 && currL <= 0) ||
        (prevR <= 0 && currR >= 0) || (prevR >= 0 && currR <= 0)
      ) {
        trimIndex = i;
        break;
      }
    }

    if (trimIndex > 0) {
      const newLeft = new Float32Array(leftChannel.length - trimIndex);
      const newRight = new Float32Array(rightChannel.length - trimIndex);
      newLeft.set(leftChannel.subarray(trimIndex));
      newRight.set(rightChannel.subarray(trimIndex));
      return { left: newLeft, right: newRight };
    }

    return { left: leftChannel, right: rightChannel };
  }

  private applyFadeIn(
    leftChannel: Float32Array,
    rightChannel: Float32Array,
    sampleRate: number
  ): void {
    const fadeInMs = 5;
    const fadeSamples = Math.floor((fadeInMs / 1000) * sampleRate);
    const actualFadeSamples = Math.min(fadeSamples, leftChannel.length);

    for (let i = 0; i < actualFadeSamples; i++) {
      const phase = i / actualFadeSamples;
      const gain = 0.5 - 0.5 * Math.cos(phase * Math.PI);
      leftChannel[i] *= gain;
      rightChannel[i] *= gain;
    }
  }

  private applyFadeOut(
    leftChannel: Float32Array,
    rightChannel: Float32Array,
    sampleRate: number
  ): void {
    const fadeOutMs = 5;
    const fadeSamples = Math.floor((fadeOutMs / 1000) * sampleRate);
    const actualFadeSamples = Math.min(fadeSamples, leftChannel.length);
    const startSample = leftChannel.length - actualFadeSamples;

    for (let i = 0; i < actualFadeSamples; i++) {
      const sampleIndex = startSample + i;
      const phase = i / actualFadeSamples;
      const gain = 0.5 + 0.5 * Math.cos(phase * Math.PI);
      leftChannel[sampleIndex] *= gain;
      rightChannel[sampleIndex] *= gain;
    }
  }

  protected randomRange(min: number, max: number): number {
    return min + Math.random() * (max - min);
  }

  protected randomInt(min: number, max: number): number {
    return Math.floor(this.randomRange(min, max + 1));
  }

  protected randomChoice<U>(choices: readonly U[]): U {
    return choices[Math.floor(Math.random() * choices.length)];
  }

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