import type { ComponentChildren, FunctionalComponent } from 'preact';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';

type ScrollAxis = 'vertical' | 'horizontal' | 'both';

type ScrollAreaProps = {
  children: ComponentChildren;
  className?: string;
  viewportClassName?: string;
  contentClassName?: string;
  axis?: ScrollAxis;
  viewportRef?: { current: HTMLDivElement | null } | ((el: HTMLDivElement | null) => void) | null;
  onScroll?: (event: Event) => void;
};

type Metrics = {
  showVertical: boolean;
  showHorizontal: boolean;
  verticalSize: number;
  verticalOffset: number;
  horizontalSize: number;
  horizontalOffset: number;
};

const MIN_THUMB_SIZE = 24;

function clamp(value: number, min: number, max: number) {
  return Math.min(max, Math.max(min, value));
}

function assignViewportRef(
  target: ScrollAreaProps['viewportRef'],
  node: HTMLDivElement | null,
) {
  if (!target) return;
  if (typeof target === 'function') {
    target(node);
    return;
  }
  target.current = node;
}

export const ScrollArea: FunctionalComponent<ScrollAreaProps> = ({
  children,
  className = '',
  viewportClassName = '',
  contentClassName = '',
  axis = 'vertical',
  viewportRef,
  onScroll,
}) => {
  const localViewportRef = useRef<HTMLDivElement | null>(null);
  const hideTimerRef = useRef<number | null>(null);
  const dragRef = useRef<null | {
    axis: 'vertical' | 'horizontal';
    startClient: number;
    startScroll: number;
    trackSize: number;
    thumbSize: number;
    maxScroll: number;
  }>(null);
  const [isHovered, setIsHovered] = useState(false);
  const [isScrolling, setIsScrolling] = useState(false);
  const [isDragging, setIsDragging] = useState(false);
  const [metrics, setMetrics] = useState<Metrics>({
    showVertical: false,
    showHorizontal: false,
    verticalSize: 0,
    verticalOffset: 0,
    horizontalSize: 0,
    horizontalOffset: 0,
  });

  const supportsVertical = axis === 'vertical' || axis === 'both';
  const supportsHorizontal = axis === 'horizontal' || axis === 'both';

  const updateMetrics = useMemo(
    () => () => {
      const el = localViewportRef.current;
      if (!el) return;

      const verticalTrack = Math.max(0, el.clientHeight - 10);
      const horizontalTrack = Math.max(0, el.clientWidth - 10);

      const showVertical = supportsVertical && el.scrollHeight > el.clientHeight + 1;
      const showHorizontal = supportsHorizontal && el.scrollWidth > el.clientWidth + 1;

      const verticalSize = showVertical
        ? clamp((el.clientHeight / Math.max(el.scrollHeight, 1)) * verticalTrack, MIN_THUMB_SIZE, verticalTrack)
        : 0;
      const horizontalSize = showHorizontal
        ? clamp((el.clientWidth / Math.max(el.scrollWidth, 1)) * horizontalTrack, MIN_THUMB_SIZE, horizontalTrack)
        : 0;

      const verticalOffset = showVertical
        ? (Math.max(verticalTrack - verticalSize, 0) * el.scrollTop) /
          Math.max(el.scrollHeight - el.clientHeight, 1)
        : 0;
      const horizontalOffset = showHorizontal
        ? (Math.max(horizontalTrack - horizontalSize, 0) * el.scrollLeft) /
          Math.max(el.scrollWidth - el.clientWidth, 1)
        : 0;

      setMetrics((previous) => {
        if (
          previous.showVertical === showVertical
          && previous.showHorizontal === showHorizontal
          && previous.verticalSize === verticalSize
          && previous.verticalOffset === verticalOffset
          && previous.horizontalSize === horizontalSize
          && previous.horizontalOffset === horizontalOffset
        ) {
          return previous;
        }
        return {
          showVertical,
          showHorizontal,
          verticalSize,
          verticalOffset,
          horizontalSize,
          horizontalOffset,
        };
      });
    },
    [supportsHorizontal, supportsVertical],
  );

  useEffect(() => {
    const el = localViewportRef.current;
    if (!el) return;

    assignViewportRef(viewportRef, el);
    updateMetrics();

    let resizeRaf = 0;
    const handleResize = () => {
      if (resizeRaf) cancelAnimationFrame(resizeRaf);
      resizeRaf = requestAnimationFrame(() => {
        resizeRaf = 0;
        updateMetrics();
      });
    };
    const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(handleResize) : null;
    ro?.observe(el);
    if (el.firstElementChild instanceof HTMLElement) {
      ro?.observe(el.firstElementChild);
    }
    window.addEventListener('resize', handleResize);

    return () => {
      assignViewportRef(viewportRef, null);
      ro?.disconnect();
      if (resizeRaf) cancelAnimationFrame(resizeRaf);
      window.removeEventListener('resize', handleResize);
    };
  }, [updateMetrics, viewportRef, children]);

  useEffect(() => {
    return () => {
      if (hideTimerRef.current) window.clearTimeout(hideTimerRef.current);
    };
  }, []);

  useEffect(() => {
    if (!isDragging) return;

    const handleMove = (event: MouseEvent) => {
      const el = localViewportRef.current;
      const drag = dragRef.current;
      if (!el || !drag) return;

      const currentClient = drag.axis === 'vertical' ? event.clientY : event.clientX;
      const delta = currentClient - drag.startClient;
      const maxThumbTravel = Math.max(drag.trackSize - drag.thumbSize, 1);
      const scrollDelta = (delta / maxThumbTravel) * drag.maxScroll;

      if (drag.axis === 'vertical') {
        el.scrollTop = clamp(drag.startScroll + scrollDelta, 0, drag.maxScroll);
      } else {
        el.scrollLeft = clamp(drag.startScroll + scrollDelta, 0, drag.maxScroll);
      }

      updateMetrics();
      revealScrollbar();
      event.preventDefault();
    };

    const handleUp = () => {
      dragRef.current = null;
      setIsDragging(false);
    };

    window.addEventListener('mousemove', handleMove);
    window.addEventListener('mouseup', handleUp);
    window.addEventListener('mouseleave', handleUp);

    return () => {
      window.removeEventListener('mousemove', handleMove);
      window.removeEventListener('mouseup', handleUp);
      window.removeEventListener('mouseleave', handleUp);
    };
  }, [isDragging, updateMetrics]);

  const revealScrollbar = () => {
    setIsScrolling(true);
    if (hideTimerRef.current) window.clearTimeout(hideTimerRef.current);
    hideTimerRef.current = window.setTimeout(() => {
      setIsScrolling(false);
    }, 700);
  };

  const handleScroll = (event: Event) => {
    updateMetrics();
    revealScrollbar();
    onScroll?.(event);
  };

  const startDrag = (dragAxis: 'vertical' | 'horizontal', event: MouseEvent, jumpToCursor = false) => {
    const el = localViewportRef.current;
    if (!el) return;

    const trackSize = dragAxis === 'vertical' ? Math.max(el.clientHeight - 10, 0) : Math.max(el.clientWidth - 10, 0);
    const thumbSize = dragAxis === 'vertical' ? metrics.verticalSize : metrics.horizontalSize;
    const maxScroll = dragAxis === 'vertical'
      ? Math.max(el.scrollHeight - el.clientHeight, 0)
      : Math.max(el.scrollWidth - el.clientWidth, 0);

    if (maxScroll <= 0) return;

    let startScroll = dragAxis === 'vertical' ? el.scrollTop : el.scrollLeft;
    const startClient = dragAxis === 'vertical' ? event.clientY : event.clientX;

    if (jumpToCursor) {
      const rect = (event.currentTarget as HTMLDivElement).getBoundingClientRect();
      const pointerOffset = dragAxis === 'vertical' ? startClient - rect.top : startClient - rect.left;
      const targetThumbOffset = clamp(pointerOffset - thumbSize / 2, 0, Math.max(trackSize - thumbSize, 0));
      startScroll = (targetThumbOffset / Math.max(trackSize - thumbSize, 1)) * maxScroll;
      if (dragAxis === 'vertical') {
        el.scrollTop = startScroll;
      } else {
        el.scrollLeft = startScroll;
      }
      updateMetrics();
    }

    dragRef.current = {
      axis: dragAxis,
      startClient,
      startScroll,
      trackSize,
      thumbSize,
      maxScroll,
    };
    setIsDragging(true);
    setIsHovered(true);
    revealScrollbar();
    event.preventDefault();
    event.stopPropagation();
  };

  const isActive = isHovered || isScrolling || isDragging;

  return (
    <div
      class={`ask-scroll-area ask-relative ask-min-h-0 ask-min-w-0 ${isActive ? 'is-active' : ''} ${isDragging ? 'is-dragging' : ''} ${className}`}
      onMouseEnter={() => {
        setIsHovered(true);
        updateMetrics();
      }}
      onMouseLeave={() => {
        if (!isDragging) setIsHovered(false);
      }}
    >
      <div
        ref={(node) => {
          localViewportRef.current = node;
          assignViewportRef(viewportRef, node);
        }}
        class={`ask-scroll-area__viewport ask-h-full ask-w-full ask-overflow-auto ask-no-scrollbar ${viewportClassName}`}
        onScroll={handleScroll}
      >
        <div class={`ask-scroll-area__content ${contentClassName}`}>{children}</div>
      </div>

      {metrics.showVertical ? (
        <div
          class="ask-scroll-area__bar ask-scroll-area__bar--vertical"
          aria-hidden="true"
          onMouseDown={(event) => startDrag('vertical', event as unknown as MouseEvent, true)}
        >
          <div
            class="ask-scroll-area__thumb"
            style={{
              height: `${metrics.verticalSize}px`,
              transform: `translateY(${metrics.verticalOffset}px)`,
            }}
            onMouseDown={(event) => startDrag('vertical', event as unknown as MouseEvent)}
          />
        </div>
      ) : null}

      {metrics.showHorizontal ? (
        <div
          class="ask-scroll-area__bar ask-scroll-area__bar--horizontal"
          aria-hidden="true"
          onMouseDown={(event) => startDrag('horizontal', event as unknown as MouseEvent, true)}
        >
          <div
            class="ask-scroll-area__thumb"
            style={{
              width: `${metrics.horizontalSize}px`,
              transform: `translateX(${metrics.horizontalOffset}px)`,
            }}
            onMouseDown={(event) => startDrag('horizontal', event as unknown as MouseEvent)}
          />
        </div>
      ) : null}
    </div>
  );
};
