import type {
  PointerEvent as ReactPointerEvent,
  TransitionEvent as ReactTransitionEvent,
} from "react";
import { useCallback, useEffect, useRef } from "react";

export type SwipeDirection = "left" | "right";

interface UseSwipeCardOptions {
  readonly itemKey: string;
  readonly onSwipe: (direction: SwipeDirection) => void;
}

const SWIPE_THRESHOLD = 82;
const EXIT_DISTANCE = 460;
const EXIT_DURATION_MS = 180;

export const useSwipeCard = ({ itemKey, onSwipe }: UseSwipeCardOptions) => {
  const cardRef = useRef<HTMLElement>(null);
  const checkedLabelRef = useRef<HTMLSpanElement>(null);
  const backLabelRef = useRef<HTMLSpanElement>(null);
  const startXRef = useRef(0);
  const distanceRef = useRef(0);
  const draggingRef = useRef(false);
  const swipingRef = useRef(false);
  const pendingDirectionRef = useRef<SwipeDirection | null>(null);
  const exitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const updateVisuals = useCallback((distance: number, animate = false) => {
    const card = cardRef.current;
    if (!card) {
      return;
    }
    card.style.transition = animate
      ? `transform ${EXIT_DURATION_MS}ms cubic-bezier(0.16, 1, 0.3, 1)`
      : "none";
    card.style.transform = `translate3d(${distance}px, 0, 0) rotate(${distance / 32}deg)`;
    const actionOpacity = Math.min(Math.abs(distance) / SWIPE_THRESHOLD, 1);
    if (checkedLabelRef.current) {
      checkedLabelRef.current.style.opacity =
        distance > 0 ? String(actionOpacity) : "0";
    }
    if (backLabelRef.current) {
      backLabelRef.current.style.opacity =
        distance < 0 ? String(actionOpacity) : "0";
    }
  }, []);

  const reset = useCallback(() => {
    if (exitTimerRef.current) {
      clearTimeout(exitTimerRef.current);
      exitTimerRef.current = null;
    }
    distanceRef.current = 0;
    draggingRef.current = false;
    swipingRef.current = false;
    pendingDirectionRef.current = null;
    updateVisuals(0, true);
  }, [updateVisuals]);

  const finishSwipe = useCallback(() => {
    const direction = pendingDirectionRef.current;
    if (!direction) {
      return;
    }
    pendingDirectionRef.current = null;
    if (exitTimerRef.current) {
      clearTimeout(exitTimerRef.current);
      exitTimerRef.current = null;
    }
    onSwipe(direction);
  }, [onSwipe]);

  const swipe = useCallback(
    (direction: SwipeDirection) => {
      if (swipingRef.current) {
        return;
      }
      swipingRef.current = true;
      pendingDirectionRef.current = direction;
      const distance = direction === "right" ? EXIT_DISTANCE : -EXIT_DISTANCE;
      distanceRef.current = distance;
      updateVisuals(distance, true);
      exitTimerRef.current = setTimeout(finishSwipe, EXIT_DURATION_MS + 80);
    },
    [finishSwipe, updateVisuals]
  );

  useEffect(() => {
    reset();
    return reset;
  }, [itemKey, reset]);

  const onPointerDown = (event: ReactPointerEvent<HTMLElement>) => {
    if (
      swipingRef.current ||
      (event.pointerType === "mouse" && event.button !== 0)
    ) {
      return;
    }
    draggingRef.current = true;
    startXRef.current = event.clientX;
    distanceRef.current = 0;
    event.currentTarget.setPointerCapture(event.pointerId);
  };

  const onPointerMove = (event: ReactPointerEvent<HTMLElement>) => {
    if (!draggingRef.current) {
      return;
    }
    distanceRef.current = event.clientX - startXRef.current;
    updateVisuals(distanceRef.current);
  };

  const onPointerUp = (event: ReactPointerEvent<HTMLElement>) => {
    if (!draggingRef.current) {
      return;
    }
    draggingRef.current = false;
    if (event.currentTarget.hasPointerCapture(event.pointerId)) {
      event.currentTarget.releasePointerCapture(event.pointerId);
    }
    if (Math.abs(distanceRef.current) >= SWIPE_THRESHOLD) {
      swipe(distanceRef.current > 0 ? "right" : "left");
      return;
    }
    reset();
  };

  const onTransitionEnd = (event: ReactTransitionEvent<HTMLElement>) => {
    if (
      event.currentTarget === event.target &&
      event.propertyName === "transform" &&
      swipingRef.current
    ) {
      finishSwipe();
    }
  };

  return {
    backLabelRef,
    cardRef,
    checkedLabelRef,
    dragged: () => Math.abs(distanceRef.current) > 8,
    pointerHandlers: {
      onPointerCancel: reset,
      onPointerDown,
      onPointerMove,
      onPointerUp,
    },
    swipe,
    transitionHandlers: {
      onTransitionEnd,
    },
  } as const;
};
