import get from 'lodash/get';
import { AudioTrack, ProcessorProjectState, PlaybackState } from './types';
import { getClipRelativePoint } from './timeline';
import { applyPanning, getDefaultPanning } from './balance';
import ChannelStrip from './Effects/ChannelStrip';
import { timeEnd, timeStart } from './instrumentation';
import metronome from './metronome';
import NumberSetting from './Settings/NumberSetting';

declare var AudioWorkletProcessor: any;
declare var registerProcessor: any;
declare var sampleRate: any;

// reading sampleRate directly is, weirdly, hella slow
const SAMPLE_RATE = sampleRate;

export interface ComplexArray {
  real: Float32Array;
  imag: Float32Array;
}

export type MapPhasorsFunction = (data: ComplexArray, sampleIndex: number) => ComplexArray;

class Processor extends AudioWorkletProcessor {
  currentTime: number;
  port!: MessagePort;
  projectState: ProcessorProjectState;

  constructor () {
    super();
    this.currentTime = 0;
    this.universalTime = 0;
    this.hasRunMIDIWarmup = false;
    this.projectState = {
      sampleRate: SAMPLE_RATE,
      tracks: [],
      audioBuffers: {},

      bpm: 120,
      beatNumerator: 4,
      beatDenominator: 4,

      metronome: false,

      loopStart: 0,
      loopEnd: SAMPLE_RATE * 30,
      loopEnabled: false,
    };
    this.currentBufferMIDIEvents = [];
    this.playbackState = PlaybackState.Paused;
    this.metronomeEnabled = false;
    this.terminated = false;
    this.meters = {};
    this.trackMixers = {} as {[key: string]: ChannelStrip};
    this.bufferStartTime = 0;
    this.port.onmessage = (message) => {
      if (message.data.type === 'state') {
        const projectState = message.data.data;
        if (!projectState.audioBuffers) {
          projectState.audioBuffers = this.projectState.audioBuffers;
        }
        this.projectState = projectState;

        this.trackMixers = this.projectState.tracks.reduce((output, track) => ({
          ...output,
          [track.id]: new ChannelStrip(
            SAMPLE_RATE,
            2,
            {
              gain: new NumberSetting(track.gain),
              balance: new NumberSetting(track.balance)
            }
          )
        }), {});

        this.resetMeters();
        this.postStatus();
      } else if (message.data.type === 'play') {
        this.playbackState = PlaybackState.Playing;
        this.postStatus();
      } else if (message.data.type === 'pause') {
        this.playbackState = PlaybackState.Paused;
        this.postStatus();
      } else if (message.data.type === 'seek') {
        this.currentTime = message.data.time;
        this.postStatus();
      } else if (message.data.type === 'setting') {
        try {
          get(this, message.data.path).value = message.data.value;
        } catch(e) {
          console.log(this, message.data.path);
        }
      } else if (message.data.type === 'terminate') {
        this.port.onmessage = () => {};
        this.playbackState = PlaybackState.Paused;
        this.terminated = true;
      }
    }
  }

  resetMeters () {
    this.meters = this.projectState.tracks.reduce((output, track) => ({ ...output, [track.id]: [0, 0] }), {});
  }

  postStatus () {
    this.port.postMessage({
      type: 'status',
      time: this.currentTime,
      playbackState: this.playbackState,
    });
  }

  postMeters () {
    this.port.postMessage({
      type: 'meters',
      meters: this.meters
    });
    Object.values(this.meters).forEach((meters) => meters = [0, 0]);
  }

  computeLoopedTime (offsetFromCurrentBufferStart: number) {
    const advancedTime = this.currentTime + offsetFromCurrentBufferStart;
    if (this.projectState.loopEnabled && this.currentTime < this.projectState.loopEnd && advancedTime >= this.projectState.loopEnd) {
      return advancedTime - (this.projectState.loopEnd - this.projectState.loopStart);
    } else {
      return advancedTime;
    }
  }

  applyTrackChain (track: AudioTrack, sample: number, channelIndex: number, channelCount: number, forLeftOutput: boolean) {
    const defaultPan = getDefaultPanning(channelIndex, channelCount);
    const adjustedPan = Math.min(1, Math.max(-1, defaultPan + (2 * track.balance)));
    const gain = (track.mute || (this.projectState.tracks.find((otherTrack) => otherTrack.solo) && !track.solo)) ? 0 : track.gain;
    return gain * applyPanning(sample, adjustedPan, forLeftOutput);
  }

  fade (sample: number, fadeRelativePosition: number, fadeLength: number) {
    return sample * (fadeRelativePosition / fadeLength);
  }

  process (inputs: Float32Array[][], outputs: Float32Array[][], parameters: {[key: string]: AudioParam}) {
    if (this.terminated) return false;
    this.resetMeters();
    this.bufferStartTime = Date.now();
    const output = outputs[0];
    const bufferSize = output[0].length;

    const playing = this.playbackState === PlaybackState.Playing;

    const anyTrackSolo = Boolean(this.projectState.tracks.find(({ solo }) => solo));

    if (playing) {
      this.projectState.tracks.forEach((track) => {
        const trackMixer = this.trackMixers[track.id];
        track.clips.forEach((clip) => {
          let fadingIn = clip.fadeInSamples > 0;
          let fadingOut = clip.timelineEnd - clip.timelineStart <= clip.fadeOutSamples;
          const { channelDataList } = this.projectState.audioBuffers[clip.audioBufferId];

          trackMixer.numberOfChannels = channelDataList.length;
          const inputSamples = new Float32Array(channelDataList.length);
          for (let sampleIndex = 0; sampleIndex < bufferSize; sampleIndex ++) {
            const time = this.computeLoopedTime(sampleIndex);
            if (time < clip.timelineStart || time >= clip.timelineEnd) {
              return;
            }
            const indexInClip = getClipRelativePoint(clip, time);

            for (let channelIndex = 0; channelIndex < channelDataList.length; channelIndex ++) {
              inputSamples[channelIndex] = channelDataList[channelIndex][indexInClip];
            }

            if (fadingIn && time > clip.timelineStart + clip.fadeInSamples) {
              fadingIn = false;
            }

            if (!fadingOut && time >= clip.timelineEnd - clip.fadeOutSamples) {
              fadingOut = true;
            }

            if (fadingIn) {
              inputSamples[0] = this.fade(inputSamples[0], time - clip.timelineStart, clip.fadeInSamples);
              inputSamples[1] = this.fade(inputSamples[1], time - clip.timelineStart, clip.fadeInSamples);
            }

            if (fadingOut) {
              inputSamples[0] = this.fade(inputSamples[0], clip.timelineEnd - time, clip.fadeOutSamples);
              inputSamples[1] = this.fade(inputSamples[1], clip.timelineEnd - time, clip.fadeOutSamples);
            }

            trackMixer.process(inputSamples);

            this.meters[track.id][0] = Math.max(this.meters[track.id][0], Math.abs(trackMixer.outputSamples[0]));
            this.meters[track.id][1] = Math.max(this.meters[track.id][1], Math.abs(trackMixer.outputSamples[1]));

            if (anyTrackSolo === track.solo && !track.mute) {
              output[0][sampleIndex] += trackMixer.outputSamples[0];
              output[1][sampleIndex] += trackMixer.outputSamples[1];
            }
          }
        });
      });
    }

    if (playing && this.projectState.metronome) {
      const samplesPerBeat = SAMPLE_RATE / ((this.projectState.beatDenominator / 4) * (this.projectState.bpm / 60))
      output.forEach((channel) => {
        for (let i = 0; i < bufferSize; i ++) {
          const time = this.computeLoopedTime(i);
          channel[i] += (this.projectState.metronomeGain || 1) * metronome(this.projectState, time, samplesPerBeat, SAMPLE_RATE);
        }
      });
    }

    let requiresStatusUpdate = this.universalTime % Math.pow(2, 16) < bufferSize;
    let requiresMeterUpdate = this.universalTime % Math.pow(2, 10) < bufferSize;

    this.universalTime += bufferSize;

    if (playing) {
      const previousTime = this.currentTime;
      this.currentTime = this.computeLoopedTime(bufferSize);
      if (this.currentTime < previousTime) {
        requiresStatusUpdate = true;
      }
    }

    if (requiresStatusUpdate) {
      this.postStatus();
    }

    if (requiresMeterUpdate) {
      this.postMeters();
    }

    return true
  }
}

registerProcessor('processor', Processor);
