import {
  createContext,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
  type PropsWithChildren,
} from 'react';
import type { GestureLayerDefinition, PageGestureDefinition, PageGestureState } from '../types';

type GestureTrackingState = {
  gestureId: string;
  startX: number;
  startY: number;
  readyToTrigger: boolean;
};

type GestureContextValue = {
  pageState: PageGestureState;
  setPageState: (nextState: PageGestureState) => void;
  registerLayer: (layer: GestureLayerDefinition) => void;
  unregisterLayer: (layerId: string) => void;
};

const GestureContext = createContext<GestureContextValue | null>(null);

function isInteractiveElement(target: EventTarget | null) {
  if (!(target instanceof Element)) {
    return false;
  }

  return Boolean(
    target.closest(
      'button, a, input, select, textarea, summary, label, [role="button"], [role="link"], [data-no-gesture]',
    ),
  );
}

function isGestureAvailable(pageState: PageGestureState, gesture: PageGestureDefinition) {
  if (gesture.enabled === false) {
    return false;
  }

  if (!gesture.activeStates || gesture.activeStates.length === 0) {
    return true;
  }

  return gesture.activeStates.includes(pageState);
}

export function GestureProvider({ children }: PropsWithChildren) {
  const [pageState, setPageState] = useState<PageGestureState>('anyway');
  const [mobileViewport, setMobileViewport] = useState(false);
  const [layerVersion, setLayerVersion] = useState(0);
  const layersRef = useRef<GestureLayerDefinition[]>([]);
  const trackingRef = useRef<GestureTrackingState | null>(null);

  useEffect(() => {
    const mediaQuery = window.matchMedia('(max-width: 768px)');
    const updateViewport = () => {
      setMobileViewport(mediaQuery.matches);
    };

    updateViewport();
    mediaQuery.addEventListener('change', updateViewport);

    return () => {
      mediaQuery.removeEventListener('change', updateViewport);
    };
  }, []);

  const resolveGesture = (gestureId: string) => {
    for (let layerIndex = layersRef.current.length - 1; layerIndex >= 0; layerIndex -= 1) {
      const layer = layersRef.current[layerIndex];
      const gesture = layer.gestures.find((candidate) => candidate.id === gestureId);

      if (gesture) {
        return gesture;
      }
    }

    return null;
  };

  const topRightGesture = useMemo(() => {
    const resolveGestureByTrigger = (trigger: PageGestureDefinition['trigger']) => {
      for (let layerIndex = layersRef.current.length - 1; layerIndex >= 0; layerIndex -= 1) {
        const layer = layersRef.current[layerIndex];

        if (layer.enabled === false) {
          continue;
        }

        for (let gestureIndex = layer.gestures.length - 1; gestureIndex >= 0; gestureIndex -= 1) {
          const gesture = layer.gestures[gestureIndex];

          if (!isGestureAvailable(pageState, gesture)) {
            continue;
          }

          if (gesture.mobileOnly && !mobileViewport) {
            continue;
          }

          if (gesture.trigger === trigger) {
            return gesture;
          }
        }
      }

      return null;
    };

    return resolveGestureByTrigger('pull-down-top-right');
  }, [layerVersion, mobileViewport, pageState]);

  const middleRightGesture = useMemo(() => {
    for (let layerIndex = layersRef.current.length - 1; layerIndex >= 0; layerIndex -= 1) {
      const layer = layersRef.current[layerIndex];

      if (layer.enabled === false) {
        continue;
      }

      for (let gestureIndex = layer.gestures.length - 1; gestureIndex >= 0; gestureIndex -= 1) {
        const gesture = layer.gestures[gestureIndex];

        if (!isGestureAvailable(pageState, gesture)) {
          continue;
        }

        if (gesture.mobileOnly && !mobileViewport) {
          continue;
        }

        if (gesture.trigger === 'pull-left-middle-right') {
          return gesture;
        }
      }
    }

    return null;
  }, [layerVersion, mobileViewport, pageState]);

  const resetTracking = () => {
    trackingRef.current = null;
  };

  useEffect(() => {
    if ((!topRightGesture && !middleRightGesture) || !mobileViewport) {
      trackingRef.current = null;
      return;
    }

    const handleWindowTouchStart = (event: TouchEvent) => {
      const touch = event.touches[0];

      if (!touch) {
        trackingRef.current = null;
        return;
      }

      const inTopRightZone = Boolean(
        topRightGesture &&
          touch.clientX >= window.innerWidth - (topRightGesture.hotZoneSize ?? 120) &&
          touch.clientY <= (topRightGesture.hotZoneSize ?? 120),
      );
      const middleHotZoneSize = middleRightGesture?.hotZoneSize ?? 120;
      const middleZoneTop = window.innerHeight * 0.25;
      const middleZoneBottom = window.innerHeight * 0.75;
      const inMiddleRightZone = Boolean(
        middleRightGesture &&
          touch.clientX >= window.innerWidth - middleHotZoneSize &&
          touch.clientY >= middleZoneTop &&
          touch.clientY <= middleZoneBottom,
      );

      if ((!inTopRightZone && !inMiddleRightZone) || isInteractiveElement(event.target)) {
        trackingRef.current = null;
        return;
      }

      trackingRef.current = {
        gestureId: inTopRightZone && topRightGesture ? topRightGesture.id : middleRightGesture!.id,
        startX: touch.clientX,
        startY: touch.clientY,
        readyToTrigger: false,
      };
    };

    const handleWindowTouchMove = (event: TouchEvent) => {
      const tracking = trackingRef.current;

      if (!tracking) {
        return;
      }

      const touch = event.touches[0];
      const gesture = resolveGesture(tracking.gestureId);

      if (!touch || !gesture || !isGestureAvailable(pageState, gesture)) {
        return;
      }

      const deltaX = touch.clientX - tracking.startX;
      const deltaY = touch.clientY - tracking.startY;
      const minDistance = Math.max(
        gesture.minDistance ?? 72,
        window.innerWidth * (gesture.minViewportDistanceRatio ?? 0),
      );
      const maxHorizontalDrift = gesture.maxHorizontalDrift ?? 96;
      const isReady =
        gesture.trigger === 'pull-left-middle-right'
          ? deltaX <= minDistance * -1 && Math.abs(deltaY) <= maxHorizontalDrift
          : deltaY >= minDistance && Math.abs(deltaX) <= maxHorizontalDrift;

      if (tracking.readyToTrigger !== isReady) {
        trackingRef.current = {
          ...tracking,
          readyToTrigger: isReady,
        };
      }
    };

    const handleWindowTouchEnd = () => {
      const tracking = trackingRef.current;
      trackingRef.current = null;

      if (!tracking?.readyToTrigger) {
        return;
      }

      const gesture = resolveGesture(tracking.gestureId);

      if (!gesture || !isGestureAvailable(pageState, gesture)) {
        return;
      }

      gesture.onTrigger();
    };

    window.addEventListener('touchstart', handleWindowTouchStart, { passive: true });
    window.addEventListener('touchmove', handleWindowTouchMove, { passive: true });
    window.addEventListener('touchend', handleWindowTouchEnd, { passive: true });
    window.addEventListener('touchcancel', resetTracking, { passive: true });

    return () => {
      window.removeEventListener('touchstart', handleWindowTouchStart);
      window.removeEventListener('touchmove', handleWindowTouchMove);
      window.removeEventListener('touchend', handleWindowTouchEnd);
      window.removeEventListener('touchcancel', resetTracking);
    };
  }, [middleRightGesture, mobileViewport, pageState, topRightGesture]);

  const value = useMemo<GestureContextValue>(
    () => ({
      pageState,
      setPageState,
      registerLayer: (layer) => {
        const nextLayers = layersRef.current.filter((candidate) => candidate.id !== layer.id);
        nextLayers.push(layer);
        layersRef.current = nextLayers;
        setLayerVersion((previous) => previous + 1);
      },
      unregisterLayer: (layerId) => {
        layersRef.current = layersRef.current.filter((candidate) => candidate.id !== layerId);
        setLayerVersion((previous) => previous + 1);
      },
    }),
    [pageState],
  );

  return (
    <GestureContext.Provider value={value}>
      {children}
    </GestureContext.Provider>
  );
}

export function useGestureContext() {
  const context = useContext(GestureContext);

  if (!context) {
    throw new Error('useGestureContext must be used within a GestureProvider.');
  }

  return context;
}
