import styled from '@emotion/styled';
import { ReactNode, useEffect, useRef } from 'react';

import { MAX_PX_PER_BEAT } from '@/components/studio/constants';

import useModifierKeys from '../useModifierKeys';

const Wrapper = styled.div`
  width: 100%;
  height: 100%;
`;

enum WheelMode {
  ZoomWithCtrl,
  ScrollWithCtrl,
}

// todo: put these in a settings context
const zoomMultiplier = 1;
const scrollMultiplier = 1;
const baseInvertZoom = navigator?.platform?.startsWith('Mac') ? true : false;
const wheelMode = WheelMode.ZoomWithCtrl as WheelMode;

const ScrollCatcher = ({
  children,
  onScroll,
  onZoom,
  zoomOnResize,
  scrollXRef,
  pxPerBeatRef,
  invertZoom: userInvertZoom,
}: {
  children?: ReactNode[] | ReactNode;
  onScroll?: (x: number, y: number, e: WheelEvent) => void;
  onZoom?: (
    xFactor: number,
    yFactor: number,
    xScrollAdjustment: number,
    yScrollAdjustment: number
  ) => void;
  zoomOnResize?: boolean;
  scrollXRef: React.RefObject<number>;
  pxPerBeatRef: React.RefObject<number>;
  invertZoom: boolean;
}) => {
  const wrapperRef = useRef<HTMLDivElement>(null);
  const modifiers = useModifierKeys();
  const lastWidthRef = useRef(0);
  const lastHeightRef = useRef(0);
  const invertZoom = !(userInvertZoom === baseInvertZoom);

  useEffect(() => {
    const wrapper = wrapperRef.current;
    if (!wrapper) return;
    lastWidthRef.current = wrapper.clientWidth;
    lastHeightRef.current = wrapper.clientHeight;
    const handleWheel = (e: WheelEvent) => {
      if ((e.target as HTMLElement)?.closest('[data-block-scroll]')) return;
      const isNativePinchZoom = (e.ctrlKey || e.metaKey) && !modifiers.ctrl;
      const standardZoomFactor = 1 + zoomMultiplier * 0.005;
      const pinchZoomFactor = 1 + zoomMultiplier * 0.015;

      const zoomFactor = isNativePinchZoom
        ? pinchZoomFactor
        : standardZoomFactor;
      const finalInvert = isNativePinchZoom ? false : invertZoom;

      const adjustedDeltaY = e.deltaY / window.devicePixelRatio;

      const isHorizontalZoom =
        !!onZoom &&
        (e.ctrlKey || e.metaKey) === (wheelMode === WheelMode.ZoomWithCtrl) &&
        !e.shiftKey;
      const isVerticalZoom =
        !!onZoom &&
        (e.ctrlKey || e.metaKey) === (wheelMode === WheelMode.ZoomWithCtrl) &&
        e.shiftKey;
      const isHorizontalScroll =
        (e.ctrlKey || e.metaKey) === (wheelMode === WheelMode.ScrollWithCtrl) &&
        e.shiftKey;

      if (isHorizontalZoom) {
        const wrapperRect = wrapper.getBoundingClientRect();
        const targetXFactor = Math.pow(
          zoomFactor,
          adjustedDeltaY * (finalInvert ? 1 : -1)
        );
        const expectedPxPerBeat = pxPerBeatRef.current * targetXFactor;
        const actualPxPerBeat = Math.min(expectedPxPerBeat, MAX_PX_PER_BEAT);
        const xFactor = actualPxPerBeat / pxPerBeatRef.current;

        const targetX = e.clientX - wrapperRect.left;
        const targetScrolledX = targetX + scrollXRef.current;
        const scrollAdjustment = targetScrolledX * xFactor - targetScrolledX;
        onZoom(xFactor, 1, scrollAdjustment, 0);
      } else if (isVerticalZoom) {
        const wrapperRect = wrapper.getBoundingClientRect();
        const yFactor = Math.pow(
          zoomFactor,
          adjustedDeltaY * (finalInvert ? 1 : -1)
        );

        const targetY = e.clientY - wrapperRect.top;
        const targetScrolledY = targetY + scrollXRef.current;
        const scrollAdjustment = targetScrolledY * yFactor - targetScrolledY;
        onZoom(1, yFactor, 0, scrollAdjustment);
      } else if (isHorizontalScroll) {
        onScroll?.((e.deltaY || e.deltaX) * scrollMultiplier, 0, e);
      } else {
        onScroll?.(e.deltaX * scrollMultiplier, e.deltaY * scrollMultiplier, e);
      }

      if (isHorizontalZoom || isVerticalZoom || isHorizontalScroll) {
        e.preventDefault();
      }
    };

    let observer: ResizeObserver | null = null;
    if (zoomOnResize && onZoom) {
      observer = new ResizeObserver((entries) => {
        const widthFactor = entries[0].contentRect.width / lastWidthRef.current;
        const heightFactor =
          entries[0].contentRect.height / lastHeightRef.current;

        const centerX = lastWidthRef.current / 2;

        const targetScrolledX = centerX + scrollXRef.current;

        const xScrollAdjustmentToZoomAroundOriginalCenter =
          targetScrolledX * widthFactor - targetScrolledX;

        const xScrollAdjustmentToMoveOriginalCenterToNewCenter =
          entries[0].contentRect.width / 2 - lastWidthRef.current / 2;

        const xScrollAdjustment =
          xScrollAdjustmentToZoomAroundOriginalCenter -
          xScrollAdjustmentToMoveOriginalCenterToNewCenter;

        lastWidthRef.current = entries[0].contentRect.width;
        lastHeightRef.current = entries[0].contentRect.height;

        onZoom(widthFactor, heightFactor, xScrollAdjustment, 0);
      });
      observer.observe(wrapper as Element);
    }

    wrapper.addEventListener('wheel', handleWheel);
    return () => {
      wrapper.removeEventListener('wheel', handleWheel);
      observer?.unobserve(wrapper as Element);
    };
  }, [modifiers, onScroll, onZoom, scrollXRef, zoomOnResize, invertZoom]);

  return <Wrapper ref={wrapperRef}>{children}</Wrapper>;
};

export default ScrollCatcher;
