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

interface UsePolledValueOptions<V> {
  /**
   * Initial state value before polling starts
   */
  initialValue?: V;
  /**
   * Number of milliseconds to wait between updates
   */
  enabled?: boolean;
  /**
   * Number of milliseconds to wait between updates
   */
  pollingInterval?: number;
  /**
   * Use `requestAnimationFrame` instead of `setInterval`
   */
  raf?: boolean;
}

type PolledValueReturn<V> = [
  value: V,
  forceUpdate: () => void,
  resetTimer: () => void,
] & {
  value: V;
  update?: () => void;
  reset?: () => void;
};

/**
 * Uses polling to keep a frequent-changing value up-to-date
 *
 * This is useful for things like media playback to avoid state thraah and
 * tons of potentially unnecessary rerenders. Instead, the UI can pull the
 * current time as often as it wants with an appropriate resolution.
 */
function usePolledValue<V>(
  getValue: (prevValue?: V) => V,
  options?: UsePolledValueOptions<V>
) {
  const {
    initialValue = getValue,
    enabled = true,
    pollingInterval = 200,
    raf: rafEnabled = true,
  } = options || {};

  const [nonce, setNonce] = useState(0);

  const getValueRef = useRef(getValue);
  useEffect(() => {
    getValueRef.current = getValue;
  }, [getValue]);

  const [value, setValue] = useState(initialValue);

  useEffect(() => {
    if (!enabled) return;
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
    nonce; // Dependency to forcibly reset timer

    if (rafEnabled) {
      let rafId: number;
      if (pollingInterval) {
        let lastUpdatedTime = 0;
        const update = () => {
          const now = performance.now();
          if (now - lastUpdatedTime >= pollingInterval) {
            setValue((prevValue) => getValueRef.current(prevValue));
            lastUpdatedTime = now;
          }
          rafId = requestAnimationFrame(update);
        };
        update();
      } else {
        const update = () => {
          setValue((prevValue) => getValueRef.current(prevValue));
          rafId = requestAnimationFrame(update);
        };
        update();
      }

      return () => {
        cancelAnimationFrame(rafId);
      };
    }

    const interval = setInterval(() => {
      setValue((prevValue) => getValueRef.current(prevValue));
    }, pollingInterval);
    return () => {
      setValue((prevValue) => getValueRef.current(prevValue));
      clearInterval(interval);
    };
  }, [enabled, pollingInterval, rafEnabled, nonce]);

  const forceUpdate = useCallback(() => {
    setValue((prevValue) => getValueRef.current(prevValue));
  }, []);
  const resetTimer = useCallback(() => {
    setNonce((prevNonce) => prevNonce + 1);
  }, []);

  const result = [value, forceUpdate, resetTimer] as PolledValueReturn<V>;

  result.value = value;
  result.update = forceUpdate;
  result.reset = resetTimer;

  return result;
}

export default usePolledValue;
