import createDefaultState from './createDefaultState';
import { AudioClip, ProcessorProjectState, ProjectState, AudioProcessorState, PlaybackState } from './types';

enum ActionType {
  SetProjectState = 'SetProjectState',
  Play = 'Play',
  Pause = 'Pause',
  Seek = 'Seek',
}

type Action = {
  type: ActionType.SetProjectState;
  data: ProjectState;
} | {
  type: ActionType.Play;
} | {
  type: ActionType.Pause;
} | {
  type: ActionType.Seek;
  data: number;
}

type MessageToProcessor = {[key: string]: any};

type MessageFromProcessor = {
  type: 'status';
  time: number;
  playbackState: PlaybackState;
} | {
  type: 'meters';
  meters: { [key: string]: [number, number] }
}

const formatStateForTransmission = (projectState: ProjectState): ProcessorProjectState => ({
  ...projectState,
  audioBuffers: Object.keys(projectState.audioBuffers)
    .filter((id) =>
      projectState.tracks
        .find(({ clips }) =>
          (clips as AudioClip[])
            .find(({ audioBufferId }) => audioBufferId === id)
        )
    )
    .reduce(
      (output, id) => ({
        ...output,
        [id]: {
          channelDataList: new Array(projectState.audioBuffers[id].numberOfChannels).fill(null).map((_, i) => projectState.audioBuffers[id].getChannelData(i)),
          sampleRate: projectState.audioBuffers[id].sampleRate
        }
      }),
      {}
    )
});

class AudioProcessor {
  private projectState: ProjectState;
  processorState: AudioProcessorState = AudioProcessorState.Initializing;
  processor: AudioWorkletNode | undefined;
  callbacks: { onLoad: (() => void)[] } = { onLoad: [] }
  actionQueue: Action[] = []
  messageQueue: MessageToProcessor[] = [];
  apiKey: string;
  audioContext: AudioContext;
  lastProcessorTime: number = 0;
  lastStatusUpdate: number = Date.now();
  playbackState: PlaybackState = PlaybackState.Paused;

  constructor(apiKey: string, audioContext: AudioContext) {
    this.apiKey = apiKey;
    this.audioContext = audioContext;
    this.projectState = createDefaultState(audioContext);
    this.initializeWorklet().then(this.processorLoaded);
  }

  onLoad = (callback: () => void) => {
    this.callbacks.onLoad.push(callback);
  }

  setProjectState = (projectState: ProjectState) => {
    this.dispatchAction({
      type: ActionType.SetProjectState,
      data: projectState
    });
  }

  getProjectState = () => {
    return this.projectState;
  }

  play = () => {
    this.dispatchAction({ type: ActionType.Play });
  }

  pause = () => {
    this.dispatchAction({ type: ActionType.Pause });
  }

  seek = (samples: number) => {
    this.dispatchAction({ type: ActionType.Seek, data: samples });
  }

  getCurrentTime = () => {
    if (this.playbackState === PlaybackState.Playing) {
      const samplesSinceLastUpdate = ((Date.now() - this.lastStatusUpdate) / 1000) * this.audioContext.sampleRate;
      return this.lastProcessorTime + samplesSinceLastUpdate;
    } else {
      return this.lastProcessorTime;
    }
  }

  private initializeWorklet = async () => {
    if (!this.audioContext.audioWorklet) {
      throw new Error('AudioWorklet is not available. You may need to open this site in a secure context (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts)');
    }
    await this.audioContext.audioWorklet.addModule(`https://wavtool.com/service/processor.js?apiKey=${this.apiKey}`);
    this.processor = new window.AudioWorkletNode(
      this.audioContext,
      'processor',
      { outputChannelCount: [2] }
    );
    this.processor.connect(this.audioContext.destination);
    this.processor.port.onmessage = this.receiveMessage;
  }

  private receiveMessage = (message: { data: MessageFromProcessor }) => {
    if (message.data.type === 'status') {
      this.lastProcessorTime = message.data.time;
      this.lastStatusUpdate = Date.now();
      this.playbackState = message.data.playbackState;
    }
  }

  private sendMessage = (message: MessageToProcessor) => {
    if (this.processor) {
      this.processor.port.postMessage(message);
    } else {
      console.log(message);
      throw new Error('Attempted to send message before processor initialized. Make sure the code path that triggered this can be added to the action queue.');
    }
  }

  private processorLoaded = () => {
    this.setProcessorState(AudioProcessorState.Paused);
    this.callbacks.onLoad.forEach((callback) => {
      callback();
    });
    this.callbacks.onLoad = [];

    this.actionQueue.forEach(this.runAction);
    this.actionQueue = [];
  }

  private setProcessorState = (processorState: AudioProcessorState) => {
    this.processorState = processorState;
  }

  private queueAction = (action: Action) => {
    this.actionQueue.push(action);
  }

  private runAction = (action: Action) => {
    if (action.type === ActionType.SetProjectState) {
      this.sendMessage({ type: 'state', data: formatStateForTransmission(action.data) })
    } else if (action.type === ActionType.Play) {
      if (this.audioContext.state !== 'running') {
        this.audioContext.resume();
      }
      this.sendMessage({ type: 'play' });
    } else if (action.type === ActionType.Pause) {
      this.sendMessage({ type: 'pause' });
    } else if (action.type === ActionType.Seek) {
      this.sendMessage({ type: 'seek', time: action.data });
    }
  }

  private dispatchAction = (action: Action) => {
    if (this.processorState === AudioProcessorState.Initializing) {
      this.queueAction(action);
    } else {
      this.runAction(action);
    }
  }
}

export default AudioProcessor;
