
import { useRef, useCallback, useEffect } from "atomico";

/**
 * copy from https://sourcegraph.com/github.com/xnimorz/use-debounce@a66f0b44bd5880420b1c552e4143c4bb52c00029/-/blob/src/useDebouncedCallback.ts
 * @param callback
 * @param delay
 * @param options
 */
export function useDebouncedCallback<T extends(...args: unknown[]) => unknown>(
  callback: T,
  delay: number,
  options: { maxWait?: number; leading?: boolean; trailing?: boolean } = {}): [T, () => void, () => void] {
  const { maxWait } = options;
  const maxWaitHandler = useRef<ReturnType<typeof setTimeout> | undefined>();
  const maxWaitArguments = useRef<unknown[]>([]);

  const { leading } = options;
  const trailing = options.trailing === undefined ? true : options.trailing;
  const leadingCall = useRef(false);

  const functionTimeoutHandler = useRef<
        ReturnType<typeof setTimeout> | undefined
        >();
  const isComponentUnmounted: { current: boolean } = useRef(false);

  const debouncedFunction = useRef(callback);
  debouncedFunction.current = callback;

  const cancelDebouncedCallback: () => void = useCallback(() => {
    if (functionTimeoutHandler.current != null) {
      clearTimeout(functionTimeoutHandler.current!);
    }
    if (maxWaitHandler.current != null) {
      clearTimeout(maxWaitHandler.current!);
    }
    maxWaitHandler.current = undefined;
    maxWaitArguments.current = [];
    functionTimeoutHandler.current = undefined;
    leadingCall.current = false;
  }, []);

  useEffect(
    () => () => {
      // we use flag, as we allow to call callPending outside the hook
      isComponentUnmounted.current = true;
    },
    [],
  );

  const debouncedCallback = useCallback(
    (...args) => {
      maxWaitArguments.current = args;
      if (functionTimeoutHandler.current != null) {
        clearTimeout(functionTimeoutHandler.current!);
      }
      if (leadingCall.current) {
        leadingCall.current = false;
      }
      if (
        functionTimeoutHandler.current != null
                && leading === true
                && leadingCall.current != null
      ) {
        debouncedFunction.current(...args);
        leadingCall.current = true;
      }

      functionTimeoutHandler.current = setTimeout(() => {
        let shouldCallFunction = true;
        if (leading != null && leadingCall.current) {
          shouldCallFunction = false;
        }
        cancelDebouncedCallback();

        if (!isComponentUnmounted.current && trailing && shouldCallFunction) {
          debouncedFunction.current(...args);
        }
      }, delay);

      if (maxWait != null && maxWaitHandler.current != null && trailing) {
        maxWaitHandler.current = setTimeout(() => {
          const args1 = maxWaitArguments.current;
          cancelDebouncedCallback();

          if (!isComponentUnmounted.current) {
            debouncedFunction.current.apply(null, args1);
          }
        }, maxWait);
      }
    },
    [maxWait, delay, cancelDebouncedCallback, leading, trailing],
  );

  const callPending = (): void => {
    // Call pending callback only if we have anything in our queue
    if (functionTimeoutHandler.current != null) {
      return;
    }

    debouncedFunction.current.apply(null, maxWaitArguments.current);
    cancelDebouncedCallback();
  };

  // At the moment, we use 3 args array so that we save backward compatibility
  return [debouncedCallback as T, cancelDebouncedCallback, callPending];
}
