import { useCallback, useMemo } from 'react';
import deleteAudio from '../audio/audioFunctions/deleteAudio';
import { ProjectState, Selection, Edit, Generator, Track, StoredCommand, CommandSettings } from '../types';
import { getSelectedTracks, getSelectionStart, getSelectionLength } from '../audio/utils/selectionTools';
import useCopyPaste from './useCopyPaste';
import useRecording from './useRecording';
import { expandedTrackHeight } from '../styles/dimensions';
import { updateSelectedBuffers } from '../audio/utils/updateBySelection';
import runCommand from '../audio/commands/runCommand';
import concatBuffers from '../audio/audioFunctions/concatBuffers';
import getBufferListLength from '../audio/audioFunctions/getBufferListLength';

const withValidatedOutput = (input: [ProjectState, Selection]) => {
  const state = input[0];
  for (let i = 0; i < state.tracks.length; i ++) {
    const buffer = concatBuffers(state.tracks[i].audioBuffers);
    for (let j = 0; j < buffer.numberOfChannels; j ++) {
      const channelData = buffer.getChannelData(j);
      for (let k = 0; k < channelData.length; k ++) {
        if (Number.isNaN(channelData[k])) {
          throw new Error('Found NaN in output data');
        }
      }
    }
  }
  return input;
};

export default (
  state: ProjectState,
  selection: Selection,
  pushProjectState: (state: ProjectState, selection: Selection) => void,
  alterProjectState: (state: ProjectState, selection: Selection) => void,
) => {
  const [copy, paste] = useCopyPaste(state, selection);
  const [insertNewRecordingTrack, receiveRecording] = useRecording(state, selection, alterProjectState);

  const emptySelectionSkipper = useCallback(
    (fn: () => void) => () => getSelectionLength(selection) > 0 && fn(),
    [selection]
  );

  const stateUpdater = useCallback(
    (edit: Edit) => async () => {
      const result = await edit();
      if (!result) {
        return;
      } if (Array.isArray(result)) {
        pushProjectState(result[0], result[1]);
      } else {
        pushProjectState(result, selection);
      }
    },
    [pushProjectState, selection]
  );

  const trackAdder = useCallback(
    // todo: proper typing
    (generator: Generator) => (...args: Parameters<Generator>) => {
      const result = generator(...args);
      const newBuffers: AudioBuffer[] = result instanceof Array ? result : [result];
      const newTracks: Track[] = newBuffers.map((audioBuffer, index) => ({
        title: `Track ${index + state.tracks.length + 1}`,
        height: expandedTrackHeight,
        audioBuffers: [audioBuffer]
      }));
      const newIndices = newTracks.map((_, i) => i + state.tracks.length);
      pushProjectState(
        {
          ...state,
          tracks: [...state.tracks, ...newTracks]
        },
        [[0, Math.max(...newTracks.map((t) => getBufferListLength(t.audioBuffers)))], newIndices]
      );
    },
    [pushProjectState, state]
  );

  const loadAudioBufferLists = useCallback(
    (audioBufferLists: AudioBuffer[][]) => {
      const newTracks: Track[] = audioBufferLists.map((buffers, index) => ({
        title: `Track ${index + state.tracks.length + 1}`,
        height: expandedTrackHeight,
        audioBuffers: buffers
      }));
      const newIndices = newTracks.map((_, i) => i + state.tracks.length);
      pushProjectState(
        {
          ...state,
          tracks: [
            ...state.tracks,
            ...newTracks
          ]
        },
        [[0, Math.max(...newTracks.map((track) => getBufferListLength(track.audioBuffers)))], newIndices]
      );
    },
    [pushProjectState, state]
  );

  const cut: Edit = useCallback(
    async () => {
      copy();
      return [
        updateSelectedBuffers(state, selection, (audioBuffer) => deleteAudio(audioBuffer, selection[0])),
        [[getSelectionStart(selection), getSelectionStart(selection)], getSelectedTracks(selection)]
      ];
    },
    [copy, state, selection]
  );

  const executeCommand = useCallback(
    async function<SettingsType>(command: StoredCommand, settings?: SettingsType, receiveDiscoveredSettings?: (discoveredSettings: CommandSettings<any>) => void) {
      try {
        const editFunction: Edit = async () => withValidatedOutput(
          await runCommand(command.code, receiveDiscoveredSettings)(state, selection, settings || {})
        );
        return stateUpdater(editFunction)();
      } catch (e) {
        console.error(e);
      }
    },
    [stateUpdater, state, selection]
  );

  const normalizeTrackContents = useCallback(
    () => {
      alterProjectState(
        {
          ...state,
          tracks: state.tracks.map((t) => {
            if (t.audioBuffers.length === 1) {
              return t;
            } else {
              return {...t, audioBuffers: [concatBuffers(t.audioBuffers)]};
            }
          })
        },
        selection
      );
    },
    [state, selection, alterProjectState]
  );

  return useMemo(
    () => ({
      cut: emptySelectionSkipper(stateUpdater(cut)),
      copy,
      paste: stateUpdater(paste),
      receiveRecording,
      insertNewRecordingTrack: trackAdder(insertNewRecordingTrack),
      loadAudioBufferLists,
      normalizeTrackContents,

      executeCommand
    }),
    [
      emptySelectionSkipper,
      stateUpdater,
      cut,
      paste,
      copy,
      receiveRecording,
      insertNewRecordingTrack,
      loadAudioBufferLists,
      executeCommand,
      normalizeTrackContents,
      trackAdder
    ]
  );
}
