import {
  HTMLAttributes,
  PointerEvent,
  WheelEvent,
  useEffect,
  useRef,
  useState,
} from "react";
import cn from "classnames";
import styles from "./TouchScrollWrapper.module.css";
import { useCssProperty } from "@mt/utils";

const getNumberPxFormatter = (numberStr: string | null) =>
  Number(numberStr?.replace(/px$/, ""));
const setNumberPxFormatter = (number: number) => `${number}px`;

const { abs, min } = Math;

export const TouchScrollWrapper = ({
  className,
  children,
  ...props
}: HTMLAttributes<HTMLDivElement>) => {
  const contentRef = useRef<HTMLDivElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const scrollbarRef = useRef<HTMLDivElement>(null);

  const scrollbarHeight = useCssProperty<number>(
    "--scrollbar-height",
    scrollbarRef,
    setNumberPxFormatter,
    getNumberPxFormatter
  );
  const scrollbarVisibility = useCssProperty<string>(
    "--scrollbar-visibility",
    scrollbarRef
  );
  const scrollbarOffset = useCssProperty<number>(
    "--scrollbar-offset",
    scrollbarRef,
    setNumberPxFormatter
  );

  const [contentHeight, setContentHeight] = useState<number>(0);
  const [containerHeight, setContainerHeight] = useState<number>(0);
  const [startSwipeY, setStartSwipeY] = useState<number>(0);
  const [startContentOffset, setStartContentOffset] = useState<number>(0);
  const [pointerId, setPointerId] = useState(0);

  useEffect(() => {
    const containerEl = containerRef?.current;
    const contentEl = contentRef?.current;

    if (!(containerEl && contentEl)) return;

    const observer = new ResizeObserver(() => {
      setContainerHeight(containerEl.offsetHeight);
      setContentHeight(containerEl.scrollHeight);
    });

    observer.observe(containerEl);
    observer.observe(contentEl);

    return () => {
      observer.disconnect();
    };
  }, []);

  useEffect(() => {
    if (containerHeight >= contentHeight) {
      scrollbarVisibility.value = "hidden";
    } else {
      scrollbarHeight.value =
        (containerHeight / contentHeight) * containerHeight + 1;
      scrollbarVisibility.value = "visible";
    }
  }, [contentHeight, containerHeight]);

  const handlePointerDown = (e: PointerEvent) => {
    setStartSwipeY(e.clientY);
    setStartContentOffset(containerRef?.current?.scrollTop || 0);
  };

  const handleTouchScroll = (e: PointerEvent) => {
    const swipeDistance = startSwipeY - e.clientY;

    if (
      e.pointerType !== "touch" ||
      containerHeight >= contentHeight ||
      (pointerId && pointerId !== e.pointerId) ||
      abs(swipeDistance) < 2
    ) {
      return;
    }

    setPointerId(e.pointerId);

    containerRef?.current?.scrollTo(0, startContentOffset + swipeDistance);
  };

  const handleScroll = (e: WheelEvent) => {
    if (containerHeight >= contentHeight) {
      return;
    }

    containerRef?.current?.scrollBy(0, e.deltaY);
  };

  const capturePointerUp = (e: PointerEvent) => {
    if (pointerId) {
      e.preventDefault();
      e.stopPropagation();
      setPointerId(0);
    }
  };

  const captureScroll = () => {
    const { scrollTop } = containerRef.current as HTMLDivElement;
    const barHeight = (scrollbarHeight.value as number) + 1; // plus 1 safety px!;
    const barOffset = (scrollTop * barHeight) / containerHeight;
    const maxBarOffset = containerHeight - barHeight;

    scrollbarOffset.value = scrollTop + min(barOffset, maxBarOffset);
  };

  return (
    <div
      className={cn(styles.root, className)}
      ref={containerRef}
      onPointerDown={handlePointerDown}
      onPointerMove={handleTouchScroll}
      onPointerUpCapture={capturePointerUp}
      onWheel={handleScroll}
      onScroll={captureScroll}
      {...props}
    >
      <div className={styles.scrollbar} ref={scrollbarRef} />

      <div ref={contentRef}>{children}</div>
    </div>
  );
};
