import { useCallback, useRef } from 'react';

type MouseArgs = {
  event: MouseEvent | TouchEvent;
  deltaXFromLast: number;
  deltaXFromStart: number;
  deltaYFromLast: number;
  deltaYFromStart: number;
  clientX: number;
  clientY: number;
};

type MouseDownCallback = (args: MouseArgs) =>
  | undefined
  | {
      onMouseMove?: (args: MouseArgs) => void;
      onMouseUp?: (args: MouseArgs) => void;
    };

const getEventCoordinates = (event: MouseEvent | TouchEvent) => {
  if ('touches' in event) {
    return {
      clientX: event.touches[0]?.clientX || 0,
      clientY: event.touches[0]?.clientY || 0,
    };
  }

  return {
    clientX: event.clientX,
    clientY: event.clientY,
  };
};

function useClickDrag<ElementType extends HTMLElement = HTMLElement>(
  onMouseDown: MouseDownCallback,
  ignoreButtons?: boolean
) {
  const handleMouseDown = useCallback(
    (event: MouseEvent | TouchEvent) => {
      if (ignoreButtons && (event.target as HTMLElement)?.closest?.('button'))
        return;

      const { clientX, clientY } = getEventCoordinates(event);

      const result = onMouseDown({
        event,
        deltaXFromLast: 0,
        deltaYFromLast: 0,
        deltaXFromStart: 0,
        deltaYFromStart: 0,
        clientX: clientX,
        clientY: clientY,
      });
      if (!result) return;

      event.preventDefault();
      const { onMouseUp, onMouseMove } = result;

      const startX = clientX;
      const startY = clientY;

      let lastX = startX;
      let lastY = startY;

      const handleMouseMove = (event: MouseEvent | TouchEvent) => {
        const { clientX, clientY } = getEventCoordinates(event);

        onMouseMove?.({
          event,
          deltaXFromLast: clientX - lastX,
          deltaYFromLast: clientY - lastY,
          deltaXFromStart: clientX - startX,
          deltaYFromStart: clientY - startY,
          clientX,
          clientY,
        });
        lastX = clientX;
        lastY = clientY;
      };

      const handleMouseUp = (event: MouseEvent | TouchEvent) => {
        onMouseUp?.({
          event,
          deltaXFromLast: 0,
          deltaYFromLast: 0,
          deltaXFromStart: lastX - startX,
          deltaYFromStart: lastY - startY,
          clientX: lastX,
          clientY: lastY,
        });
        window.removeEventListener('mousemove', handleMouseMove);
        window.removeEventListener('mouseup', handleMouseUp);
        window.removeEventListener('touchmove', handleMouseMove);
        window.removeEventListener('touchend', handleMouseUp);
      };

      window.addEventListener('mousemove', handleMouseMove, { passive: false });
      window.addEventListener('touchmove', handleMouseMove, { passive: false });
      window.addEventListener('mouseup', handleMouseUp, { passive: false });
      window.addEventListener('touchend', handleMouseUp, { passive: false });
    },
    [onMouseDown, ignoreButtons]
  );

  const unbindRef = useRef<() => void>(() => {});

  const receiveElementRef = useCallback(
    (elementRef: ElementType | null) => {
      if (!elementRef) {
        unbindRef.current();
        unbindRef.current = () => {};
        return;
      }

      elementRef.addEventListener('mousedown', handleMouseDown, {
        passive: false,
      });
      elementRef.addEventListener('touchstart', handleMouseDown, {
        passive: false,
      });

      unbindRef.current = () => {
        elementRef.removeEventListener('mousedown', handleMouseDown);
        elementRef.removeEventListener('touchstart', handleMouseDown);
      };
    },
    [handleMouseDown]
  );

  return receiveElementRef;
}

export default useClickDrag;
