import { ProjectState, Selection, CommandTrack } from '../types';
import fakeAudioBuffer from './fakeAudioBuffer';
import { expandedTrackHeight } from '../styles/dimensions';
import concatBuffers from './concatBuffers';
import { StandardLibrary, MapPhasorsFunction, ComplexArray } from './initStandardLibrary';

type MapSamplesFunction = (sample: number, index: number, selectedSamples: Float32Array) => number;
type MapBuffersFunction = (audioBuffer: AudioBuffer, selectionStart: number, selectionEnd: number) => AudioBuffer;
type MapChannelsFunction = (track: Float32Array, selectionStart: number, selectionEnd: number) => Float32Array;
type MapFrequencySpaceFunction = (frequencyMagnitudeBins: Float32Array, sampleIndex?: number, channelIndex?: number) => Float32Array;

interface ComplexPolarArray {
  magnitudes: Float32Array;
  phases: Float32Array;
}

type MapFourierFunction = (block: ComplexPolarArray, sampleIndex?: number, channelIndex?: number) => ComplexPolarArray;

export default class CommandProject {
  sampleRate: number;
  tracks: CommandTrack[];
  selectionStart: number;
  selectionEnd: number;
  wavtool: StandardLibrary;

  constructor(projectState: ProjectState, selection: Selection, wavtool: StandardLibrary) {
    this.wavtool = wavtool;
    this.sampleRate = projectState.sampleRate;
    [this.selectionStart, this.selectionEnd] = selection[0];
    this.tracks = projectState.tracks.map((track, i) => ({
      title: track.title,
      height: track.height,
      audioBuffer: concatBuffers(track.audioBuffers),
      selected: selection[1].includes(i)
    }));
  }

  getSelectionLength = () => this.selectionEnd - this.selectionStart;
  isSelected = (samplePosition: number) => samplePosition < this.selectionEnd && samplePosition >= this.selectionStart;

  mapSelectedSamplesFourier = (fn: MapFourierFunction, crossfadeSamples = 0, fourierSamples = 2048, fourierShift = 256) => {
    this.tracks
      .filter((t) => t.selected)
      .forEach((track) => {
        for (let i = 0; i < track.audioBuffer.numberOfChannels; i ++) {
          const phasorsFn = (phasors: ComplexArray, sampleIndex: number) => {
            const magnitudes = new Float32Array(phasors.real.length);
            const phases = new Float32Array(phasors.real.length);
            for(let j = 0; j < phasors.real.length; j ++) {
              magnitudes[j] = Math.sqrt(Math.pow(phasors.real[j], 2) + Math.pow(phasors.imag[j], 2));
              phases[j] = Math.atan2(phasors.imag[j], phasors.real[j]);
            }
            const newMagPhases = fn({ magnitudes, phases }, sampleIndex, i);
            for(let j = 0; j < newMagPhases.magnitudes.length; j ++) {
              const mag = newMagPhases.magnitudes[j];
              const phase = newMagPhases.phases[j];
              phasors.real[j] = mag * Math.cos(phase);
              phasors.imag[j] = mag * Math.sin(phase);
            }
            return phasors;
          };
          const channelData = track.audioBuffer.getChannelData(i);
          let marginStart = Math.max(0, this.selectionStart - fourierShift);
          let marginEnd = Math.min(this.selectionEnd + fourierShift, channelData.length);
          if(marginEnd - marginStart <= fourierSamples) {
            let delta = fourierSamples - (marginEnd - marginStart) + 1;
            marginStart = Math.max(0, marginStart - Math.ceil(delta/2));
            delta = Math.max(fourierSamples - (marginEnd - marginStart) + 1, 0);
            marginEnd = Math.min(marginEnd + delta, channelData.length);
            delta = Math.max(fourierSamples - (marginEnd - marginStart) + 1, 0);
            marginStart = Math.max(0, marginStart - delta);
          }
          const newData = this.wavtool.shortTimeFourierTransform(channelData, phasorsFn, marginStart, marginEnd, fourierSamples, fourierShift);
          for(let i = marginStart; i < this.selectionStart; i ++) {
            newData[i - marginStart] = channelData[i];
          }
          for(let i = this.selectionEnd; i < marginEnd; i ++) {
            newData[i - marginStart] = channelData[i];
          }
          const selectedSampleCount = Math.min(this.selectionEnd, channelData.length) - this.selectionStart;
          const fadeLength = Math.min(crossfadeSamples, selectedSampleCount / 2);
          const realSelectionEnd = Math.min(this.selectionEnd, channelData.length);
          for(let i = 0; i < fadeLength; i ++) {
            const alpha = i / fadeLength;
            const newDataStartIdx = this.selectionStart - marginStart + i;
            const newDataEndIdx = newData.length - (marginEnd - realSelectionEnd) - i;
            newData[newDataStartIdx] = alpha * newData[newDataStartIdx] + (1 - alpha) * channelData[this.selectionStart + i];
            newData[newDataEndIdx] = alpha * newData[newDataEndIdx] + (1 - alpha) * channelData[realSelectionEnd - i];
          }
          track.audioBuffer.copyToChannel(newData, i, marginStart);
        }
      });
  };

  mapSelectedSamplesFrequencySpace = (fn: MapFrequencySpaceFunction, crossfadeSamples = 0) => {
    this.mapSelectedSamplesFourier((pm : ComplexPolarArray, si?: number, ci?: number) => {
      const mags = new Float32Array(pm.magnitudes);
      const newMags = fn(mags, si, ci);
      for(let i = 0; i < newMags.length; i ++) {
        const scl = Math.abs(newMags[i] / (mags[i] + 0.0000001));
        pm.magnitudes[i] *= scl;
      }
      return pm;
    }, crossfadeSamples);
  };

  mapSelectedSamples = (fn: MapSamplesFunction) => {
    this.tracks
      .filter((t) => t.selected)
      .forEach((track) => {
        for (let i = 0; i < track.audioBuffer.numberOfChannels; i ++) {
          const channelData = track.audioBuffer.getChannelData(i);
          const samplesToEdit: Float32Array = new Float32Array(this.selectionEnd - this.selectionStart);
          for (let s = this.selectionStart; s < this.selectionEnd; s ++) {
            if (s >= channelData.length) {
              samplesToEdit[s - this.selectionStart] = 0;
            } else {
              samplesToEdit[s - this.selectionStart] = channelData[s];
            };
          }
          for (let j = 0; j < samplesToEdit.length; j ++) {
            samplesToEdit[j] = fn(samplesToEdit[j], j, samplesToEdit);
          }
          track.audioBuffer.copyToChannel(samplesToEdit, i, this.selectionStart);
        }
      });
  };

  mapSelectedAudioBuffers = (fn: MapBuffersFunction) => {
    this.tracks
      .filter((t, i) => t.selected)
      .forEach((track) => {
        const audioBuffer = fakeAudioBuffer({
          numberOfChannels: track.audioBuffer.numberOfChannels,
          length: Math.max(track.audioBuffer.length, this.selectionEnd),
          sampleRate: track.audioBuffer.sampleRate
        });
        for (let i = 0; i < audioBuffer.numberOfChannels; i ++ ){
          audioBuffer.copyToChannel(
            track.audioBuffer.getChannelData(i),
            i
          );
        }
        track.audioBuffer = fn(audioBuffer, this.selectionStart, this.selectionEnd);
      });
  };

  mapSelectedChannels = (fn: MapChannelsFunction) => {
    this.mapSelectedAudioBuffers((audioBuffer, selectionStart, selectionEnd) => {
      const newChannels: Float32Array[] = [];
      for (let i = 0; i < audioBuffer.numberOfChannels; i ++) {
        newChannels.push(fn(audioBuffer.getChannelData(i), selectionStart, selectionEnd));
      }
      const newAudioBuffer = fakeAudioBuffer({
        numberOfChannels: audioBuffer.numberOfChannels,
        length: Math.max(...newChannels.map((c) => c.length)),
        sampleRate: this.sampleRate
      });
      newChannels.forEach((c, i) => {
        newAudioBuffer.copyToChannel(c, i);
      });
      return newAudioBuffer;
    });
  };

  generateTrack = (
    generator: (index: number) => (number[] | number),
    lengthInSamples: number = this.sampleRate, // 1 second by default.
    numberOfChannels: number = 1,
    title?: string,
    index?: number
  ) => {
    const newAudioBuffer = fakeAudioBuffer({
      numberOfChannels,
      length: lengthInSamples,
      sampleRate: this.sampleRate
    });
    const data: (number[] | number)[] = [];
    for (let i = 0; i < lengthInSamples; i ++) {
      data.push(generator(i));
    }
    for (let c = 0; c < numberOfChannels; c ++) {
      const channelData = newAudioBuffer.getChannelData(c);
      for (let i = 0; i < lengthInSamples; i ++) {
        const generated = data[i];
        if (typeof generated === 'number') {
          channelData[i] = generated;
        } else {
          channelData[i] = generated[c];
        }
      }
    }
    const track = this.addTrack(newAudioBuffer, title, index);
    track.selected = true;
    this.selectionStart = 0;
    this.selectionEnd = lengthInSamples;
    return track;
  };

  addTrack = (audioBuffer: AudioBuffer, title: string = `Track ${this.tracks.length + 1}`, index: number = this.tracks.length) => {
    const newTrack = {
      title,
      audioBuffer,
      height: expandedTrackHeight,
      selected: false,
    };
    this.tracks.forEach((track) => track.selected = false);
    this.tracks.splice(Math.min(index, this.tracks.length), 0, newTrack);
    return newTrack;
  };
}
