import { useCallback, useEffect, useRef } from 'react';

type ChoreographyCallback = (
  currentTime: number,
  targetTime: number,
  index: number
) => void;
export type ChoreographySequenceItem = {
  id?: string;
  time: number;
  callback: ChoreographyCallback;
};
export type ChoreographySequence = ChoreographySequenceItem[];

type UseChoreographyOptions = {
  sequence: ChoreographySequence;
  currentTime?: number;
  enabled?: boolean;
};

type UseChoreographyReturn = [
  setTime: (time: number, resetPrevious?: boolean) => void,
];

/**
 * Manages a sequence of callbacks that are invoked in order when driven by an
 * external clock
 */
function useChoreography(
  options: UseChoreographyOptions
): UseChoreographyReturn {
  const { sequence, currentTime, enabled = true } = options;

  const stateRef = useRef({
    currentTime: 0,
    currentIndex: 0,
    sequence: [] as ChoreographySequence,
    enabled,
  });

  useEffect(() => {
    stateRef.current.enabled = enabled;
  }, [enabled]);

  /**
   * Sets the current sequence index based on the given time
   *
   * When multiple callbacks have the same time, this will align to the earliest one
   *
   * Any callbacks starting at `startIndex` will be invoked as long as
   * choreography is not disabled
   */
  const setCurrentTime = useCallback(
    (
      time = stateRef.current.currentTime,
      startIndex = stateRef.current.currentIndex
    ) => {
      stateRef.current.currentTime = time;
      let nextCurrentIndex = -1;
      for (let i = 0; i < stateRef.current.sequence.length; i++) {
        if (i < 0) continue;
        const item = stateRef.current.sequence[i];
        if (item.time > time) break;
        if (stateRef.current.enabled && i >= startIndex) {
          item.callback(time, item.time, i);
        }
        nextCurrentIndex = i;
      }
      stateRef.current.currentIndex = nextCurrentIndex + 1;
    },
    []
  );

  useEffect(() => {
    if (currentTime != null) {
      setCurrentTime(currentTime);
    }
  }, [currentTime, setCurrentTime]);

  useEffect(() => {
    // Ensure sequence items are sorted by time
    stateRef.current.sequence = [...sequence].sort(
      ({ time: timeA }, { time: timeB }) =>
        timeA < timeB ? -1 : timeA > timeB ? 1 : 0
    );
    // Reset current index, but do not invoke any callbacks
    setCurrentTime(stateRef.current.currentTime, Infinity);
  }, [sequence, setCurrentTime]);

  /**
   * Sets the current time and invokes any new callbacks
   *
   * If `reset` is enabled, this will invoke all callbacks from the beginning
   * through the new current time
   */
  const setTime = useCallback(
    (time: number, reset = false) => {
      setCurrentTime(time, reset ? 0 : stateRef.current.currentIndex);
    },
    [setCurrentTime]
  );

  return [setTime];
}

export default useChoreography;
