import { useCallback, useMemo, useState } from 'react';
import { Selection, ProjectState, ProjectCoords, SnapMode } from '../types';
import getBufferListLength from '../audio/audioFunctions/getBufferListLength';
import iterateSamples from '../audio/audioFunctions/iterateSamples';

const audioBufferZeroCache = new Map<AudioBuffer[], number[]>();

const getBufferListZeroes = (audioBuffers: AudioBuffer[]) => {
  const cached = audioBufferZeroCache.get(audioBuffers);
  if (cached) {
    return cached;
  }
  const zeroes: number[] = [];
  const length = getBufferListLength(audioBuffers);
  if (audioBuffers.length > 0){
    zeroes.push(0);
    zeroes.push(length - 1);
    for (let c = 0; c < audioBuffers[0].numberOfChannels; c++) {
      let lastValue = 0;
      iterateSamples(
        (sample, i) => {
          if (sample === 0) {
            zeroes.push(i);
          } else if ((lastValue <= 0) === (sample > 0) || (lastValue !== 0 && sample === 0)) {
            zeroes.push(i);
          }
          lastValue = sample;
        },
        audioBuffers,
        c
      );
    }
  }
  audioBufferZeroCache.set(audioBuffers, zeroes);
  return zeroes;
};

const snapToZeroCrossing = (audioBuffers: AudioBuffer[], inputSamples: number) => {
  const zeroes = getBufferListZeroes(audioBuffers);
  let closestZeroSamples = inputSamples;
  let closestZeroDistance = Infinity;

  zeroes.forEach((sampleIndex) => {
    const distance = Math.abs(inputSamples - sampleIndex);
    if (distance < closestZeroDistance) {
      closestZeroSamples = sampleIndex;
      closestZeroDistance = distance;
    }
  });

  return closestZeroSamples;
};

const snap = (audioBuffers: AudioBuffer[], inputSamples: number, snapMode: SnapMode) => {
  if (snapMode === SnapMode.Off) {
    return inputSamples;
  } else if (snapMode === SnapMode.Zeroes) {
    return snapToZeroCrossing(audioBuffers, inputSamples);
  } else {
    throw new Error(`Unrecognized snap mode ${snapMode}`);
  }
};

export default (state: ProjectState) => {
  const [selection, setSelection] = useState<Selection>([[0,0], []]);
  const [initialCoords, setInitialCoords] = useState<[number, number] | null>(null);
  const [lastTargetTrack, setLastTargetTrack] = useState<number | null>(null);

  const setSnappedSelection = useCallback(
    (inputSelection: Selection, targetTrack: number | null, snapMode: SnapMode) => {
      const [[inputStart, inputEnd], selectedTracks] = inputSelection;
      if (targetTrack === null || !state.tracks[targetTrack]) {
        setSelection(inputSelection);
      } else {
        const initialSelection = initialCoords ? initialCoords[0] : null;
        if (inputEnd === initialSelection) {
          setSelection([
            [
              snap(state.tracks[targetTrack].audioBuffers, inputStart, snapMode),
              inputEnd
            ],
            selectedTracks
          ]);
        } else if (inputStart === initialSelection) {
          setSelection([
            [
              inputStart,
              snap(state.tracks[targetTrack].audioBuffers, inputEnd, snapMode)
            ],
            selectedTracks
          ]);
        } else {
          console.error('Neither selection boundary is initial');
        }
      }
    },
    [setSelection, state, initialCoords]
  );

  const startSelection = useCallback(
    (coords: ProjectCoords | null, snapMode: SnapMode) => {
      if (!coords) return;
      const [xSamples, trackIndex] = coords;
      if (trackIndex === null) {
        setSelection([[0, 0], []]);
        return;
      }
      const snappedXsamples = state.tracks[trackIndex] ? snap(state.tracks[trackIndex].audioBuffers, xSamples, snapMode) : xSamples;
      setInitialCoords([snappedXsamples, trackIndex]);
      setSelection([[snappedXsamples, snappedXsamples], [trackIndex]]);
    },
    [setSelection, setInitialCoords, state]
  );

  const continueSelection = useCallback(
    (coords: ProjectCoords | null, snapMode: SnapMode) => {
      if (!initialCoords || !coords) return;
      const [initialSamples, initialTrack] = initialCoords;
      if (initialTrack === null) return;
      const [xSamples, trackIndex] = coords;
      const newSelectedTime: [number, number] = [Math.min(xSamples, initialSamples), Math.max(xSamples, initialSamples)];
      const selectedTracks = selection[1];
      if (trackIndex === null) {
        setSnappedSelection([newSelectedTime, selectedTracks], lastTargetTrack, snapMode);
      } else {
        setLastTargetTrack(trackIndex);
        const newSelectedTracks = [];
        for (let i = Math.min(trackIndex, initialTrack); i <= Math.max(trackIndex, initialTrack); i ++) {
          newSelectedTracks.push(i);
        }
        setSnappedSelection([newSelectedTime, newSelectedTracks], trackIndex, snapMode);
      }
    },
    [initialCoords, selection, lastTargetTrack, setLastTargetTrack, setSnappedSelection]
  );

  const endSelection = useCallback(
    () => {
      setInitialCoords(null);
      setLastTargetTrack(null);
    },
    [setInitialCoords]
  );

  return useMemo(
    () => ({
      selection,
      interactions: {
        set: setSelection,
        start: startSelection,
        continue: continueSelection,
        end: endSelection
      }
    }),
    [selection, setSelection, startSelection, continueSelection, endSelection]
  );
};
