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

import type { CartesianSpace, Range, Setter } from "@/types";
import { Direction } from "@/types";
import {
  chooseValueByDirection,
  extractEventCoordinates,
  isLeftMouseButton,
  isTouchEvent,
  minmax,
  positionToValue,
  valueToPosition,
} from "@/util";

import { useSmoothAnimation } from "./animation";
import { useScroll } from "./scroll";

if (typeof TouchEvent === "undefined") {
  // @ts-expect-error - intentionally creating global
  window.TouchEvent = window.MouseEvent;
}

function extractEventCoordinateByDirection(
  event: MouseEvent | TouchEvent,
  direction: Direction,
): number {
  const { clientX, clientY } = extractEventCoordinates(event);
  return chooseValueByDirection(direction, clientX, clientY);
}

export function useSlider({
  direction,
  origin,
  dimensions,
  valueRange,
  value,
  setValue,
  invert = false,
}: {
  direction: Direction;
  origin: CartesianSpace;
  dimensions: CartesianSpace;
  valueRange: Range;
  value: number;
  setValue: Setter<number>;
  invert?: boolean;
}) {
  const [isDragging, setIsDragging] = useState(false);
  const sliderRef = useRef<HTMLDivElement>(null);

  // Slider UI refs
  const directionRef = useRef(direction);
  const originRef = useRef(origin);
  const dimensionsRef = useRef(dimensions);

  // Slider value refs
  const setValueRef = useRef(setValue);
  const valueRangeRef = useRef(valueRange);
  const maxPosition = useRef(0);

  // Internal position management
  const position = valueToPosition(
    value,
    chooseValueByDirection(direction, dimensions.x, dimensions.y),
    valueRange,
  );
  const positionRef = useRef(position);

  // Hooks
  const smoothAnimation = useSmoothAnimation();

  useEffect(() => {
    directionRef.current = direction;
    originRef.current = origin;
    dimensionsRef.current = dimensions;
    maxPosition.current = chooseValueByDirection(
      direction,
      dimensions.x,
      dimensions.y,
    );
    positionRef.current = valueToPosition(
      value,
      maxPosition.current,
      valueRangeRef.current,
    );
  }, [direction, origin, dimensions, value]);

  useEffect(() => {
    valueRangeRef.current = valueRange;
  }, [valueRange, valueRangeRef]);

  useEffect(() => {
    setValueRef.current = setValue;
  }, [setValue]);

  useEffect(() => {
    positionRef.current = position;
  }, [position]);

  // Setup drag handlers
  const calculatePosition = useCallback(
    (event: MouseEvent | TouchEvent) => {
      const dir = directionRef.current;
      const orig = originRef.current;
      const dims = dimensionsRef.current;

      const clientCoord = extractEventCoordinateByDirection(event, dir);
      const newPosition = minmax(
        clientCoord - chooseValueByDirection(dir, orig.x, orig.y),
        0,
        chooseValueByDirection(dir, dims.x, dims.y),
      );
      let newValue = positionToValue(
        newPosition,
        maxPosition.current,
        valueRangeRef.current,
      );

      if (invert) {
        newValue = valueRangeRef.current.max - newValue;
      }

      setValueRef.current(newValue);
    },
    [invert],
  );

  const handleMove = useCallback(
    (event: MouseEvent | TouchEvent) => {
      event.preventDefault();
      smoothAnimation(() => calculatePosition(event));
    },
    [calculatePosition, smoothAnimation],
  );

  const handleEnd = useCallback(() => {
    document.removeEventListener("mousemove", handleMove);
    document.removeEventListener("mouseup", handleEnd);
    document.removeEventListener("touchmove", handleMove);
    document.removeEventListener("touchend", handleEnd);
    document.removeEventListener("touchcancel", handleEnd);
    setIsDragging(false);
  }, [handleMove]);

  const handleStart = useCallback(
    (event: MouseEvent | TouchEvent) => {
      if (!isTouchEvent(event) && !isLeftMouseButton(event.buttons)) {
        return;
      }
      event.preventDefault();
      calculatePosition(event);
      setIsDragging(true);

      document.addEventListener("mousemove", handleMove);
      document.addEventListener("mouseup", handleEnd, { passive: true });
      document.addEventListener("touchmove", handleMove);
      document.addEventListener("touchend", handleEnd, { passive: true });
      document.addEventListener("touchcancel", handleEnd, { passive: true });
    },
    [calculatePosition, handleMove, handleEnd],
  );

  // Setup scroll handlers
  const handleScrollUp = useCallback(() => {
    const dir = directionRef.current;
    const dims = dimensionsRef.current;
    const inc = chooseValueByDirection(dir, 1, -1);

    const newPosition = minmax(
      positionRef.current + inc,
      0,
      chooseValueByDirection(dir, dims.x, dims.y),
    );
    const newValue = positionToValue(
      newPosition,
      maxPosition.current,
      valueRangeRef.current,
    );

    setValueRef.current(newValue);
  }, []);

  const handleScrollDown = useCallback(() => {
    const dir = directionRef.current;
    const dims = dimensionsRef.current;
    const inc = chooseValueByDirection(dir, -1, 1);

    const newPosition = minmax(
      positionRef.current + inc,
      0,
      chooseValueByDirection(dir, dims.x, dims.y),
    );
    const newValue = positionToValue(
      newPosition,
      maxPosition.current,
      valueRangeRef.current,
    );

    setValueRef.current(newValue);
  }, []);

  const { addScrollListener, removeScrollListener } = useScroll({
    targetRef: sliderRef,
    onScrollUp: handleScrollUp,
    onScrollDown: handleScrollDown,
  });

  // Set up entry listeners
  useEffect(() => {
    const currentRef = sliderRef.current;
    if (currentRef) {
      addScrollListener();
      currentRef.addEventListener("mousedown", handleStart);
      currentRef.addEventListener("touchstart", handleStart);
    }

    return () => {
      if (currentRef) {
        removeScrollListener();
        currentRef.removeEventListener("mousedown", handleStart);
        currentRef.removeEventListener("touchstart", handleStart);
      }

      document.removeEventListener("mousemove", handleMove);
      document.removeEventListener("mouseup", handleEnd);
      document.removeEventListener("touchmove", handleMove);
      document.removeEventListener("touchend", handleEnd);
      document.removeEventListener("touchcancel", handleEnd);
    };
  }, [
    addScrollListener,
    removeScrollListener,
    handleStart,
    handleMove,
    handleEnd,
  ]);

  return { sliderRef, isDragging };
}
