import FFTJS from './fftjs.js';

const maxFFTSize = 32768;

class FFTRunner {
  fft: FFTJS;
  complexInput: Float32Array;
  windowedInput: Float32Array;
  lastInverseOutput: Float32Array;
  inverseOutput: Float32Array;
  complexOutput: Float32Array;
  fullBufferIndex: number;
  stepIndex: number;
  blockSizeTimesTwo: number;
  sizeTimesTwo: number;
  size: number;
  oneHalfSize: number;
  blockSize: number;
  isProcessingStep: boolean;
  preComputedBlendFunction: Float32Array;
  spectrumOutput: Float32Array;

  constructor(size: number = 512, blockSize: number = 128, windowType = 'flat') {
    this.size = Math.min(size, maxFFTSize);
    this.fft = new FFTJS(this.size);
    this.sizeTimesTwo = this.size * 2;
    this.blockSize = Math.min(blockSize, this.size);
    this.blockSizeTimesTwo = this.blockSize * 2;
    this.oneHalfSize = this.size / 2;
    this.complexInput = new Float32Array(this.size * 2);
    this.windowedInput = new Float32Array(this.size * 2);
    this.inverseOutput = new Float32Array(this.size * 2);
    this.lastInverseOutput = new Float32Array(this.size * 2);
    this.complexOutput = new Float32Array(this.size * 2);
    this.spectrumOutput = new Float32Array(this.size / 2);

    this.preComputedBlendFunction =
      windowType === 'blackman' ? this.computeBlackmanWindow(this.size) : this.computeFlatTopWindow(this.size);

    this.fullBufferIndex = 0;
    this.stepIndex = 0;
    this.isProcessingStep = false;
  }

  computeBlackmanWindow(length: number) {
    const result = new Float32Array(length);
    const a0 = 0.42;
    const a1 = 0.08;
    for (let i = 0; i < length; i++) {
      result[i] = a0 - 0.5 * Math.cos((2 * Math.PI * i) / length) + a1 * Math.cos((4 * Math.PI * i) / length);
    }
    return result;
  }

  computeFlatTopWindow(length: number) {
    const a0 = 0.21557895;
    const a1 = 0.41663158;
    const a2 = 0.277263158;
    const a3 = 0.083578947;
    const a4 = 0.006947368;
    const result = new Float32Array(length);
    const lengthMinusTwo = length - 2;
    for (let i = 0; i < lengthMinusTwo; i++) {
      // flat-top window:
      result[i + 1] =
        a0 -
        a1 * Math.cos((2 * Math.PI * i) / lengthMinusTwo) +
        a2 * Math.cos((4 * Math.PI * i) / lengthMinusTwo) -
        a3 * Math.cos((6 * Math.PI * i) / lengthMinusTwo) +
        a4 * Math.cos((8 * Math.PI * i) / lengthMinusTwo);
    }
    return result;
  }

  static computeDelay(size: number, blockSize: number) {
    return (size + blockSize) / 2;
  }

  receiveSample(sample: number) {
    if (this.isProcessingStep) {
      this.isProcessingStep = false;
    }
    if (this.fullBufferIndex < this.sizeTimesTwo) {
      this.complexInput[this.fullBufferIndex] = sample;
      this.fullBufferIndex += 2;
      return;
    }

    /*
    we have shifted the old array content back by `blockSize` and are writing a new sample into the last `blockSize` elements of the array.

          V     where we're writing
    [000001  ]
           ^^   unwritten space since last shift
    */
    this.complexInput[this.sizeTimesTwo - this.blockSizeTimesTwo + this.stepIndex] = sample;

    this.stepIndex += 2;

    if (this.stepIndex === this.blockSizeTimesTwo) {
      this.isProcessingStep = true;
    }

    if (this.isProcessingStep) {
      this.stepIndex = 0;
      this.windowedInput.set(this.complexInput);

      for (let i = 0; i < this.windowedInput.length; i += 2) {
        this.windowedInput[i] *= this.preComputedBlendFunction[i / 2];
      }

      this.fft.transform(this.complexOutput, this.windowedInput);

      /*

      before: [12345678]
      after:  [345678  ]

      (blockSizeTimesTwo = 2 in above example)
      (technically the last two elements would be 78 again)

      */
      this.complexInput.set(this.complexInput.subarray(this.blockSizeTimesTwo));
      this.complexInput.fill(0, this.sizeTimesTwo - this.blockSizeTimesTwo);
    }
  }

  updateSpectrum() {
    for (let j = 0; j < this.complexOutput.length / 2; j += 2) {
      this.spectrumOutput[j / 2] = Math.sqrt(
        this.complexOutput[j] * this.complexOutput[j] + this.complexOutput[j + 1] * this.complexOutput[j + 1]
      );
    }
  }

  produceSample() {
    if (this.isProcessingStep) {
      this.fft.completeSpectrum(this.complexOutput);
      this.lastInverseOutput = this.inverseOutput;
      this.inverseOutput = new Float32Array(this.size * 2);
      this.fft.inverseTransform(this.inverseOutput, this.complexOutput);
      return this.lastInverseOutput[this.size + this.blockSize];
    } else {
      const incomingProgress = this.stepIndex / this.blockSizeTimesTwo;
      const outgoingProgress = 1 - incomingProgress;
      return (
        this.inverseOutput[this.size - this.blockSize + this.stepIndex] * incomingProgress +
        this.lastInverseOutput[this.size + this.blockSize + this.stepIndex] * outgoingProgress
      );
    }
  }
}

export default FFTRunner;
