import {
  createContext,
  ReactNode,
  useContext,
  useMemo,
  useState,
  useCallback,
} from "react";
import { SCALE_FACTOR, UP_SCALE } from "./Constants";

const TransformContext = createContext<{
  position: { x: number; y: number };
  scale: number;
  rotation: number;
  screenCenter?: { x: number; y: number };

  setPosition: React.Dispatch<React.SetStateAction<{ x: number; y: number }>>;
  setScale: React.Dispatch<React.SetStateAction<number>>;
  setRotation: React.Dispatch<React.SetStateAction<number>>;
  screenToLocal: (x: number, y: number) => { x: number; y: number };
  localToScreen: (x: number, y: number) => { x: number; y: number };
  rotateToAngle: (to: number, fromPosition?: { x: number; y: number }) => void;
  setTransform: (
    latitude: number,
    longitude: number,
    rotationDegrees?: number,
    scale?: number
  ) => void;
  setScaleOnly: (newScale: number) => void;
  setScaleWithoutMovingCenter: (newScale: number) => void;
  setScreenCenter: React.Dispatch<
    React.SetStateAction<{ x: number; y: number } | undefined>
  >;
  setScaleAtCenter: (newScale: number) => void;
}>({
  position: { x: 0, y: 0 },
  scale: 1,
  rotation: 0,
  screenCenter: undefined,
  setPosition: () => {},
  setScale: () => {},
  setRotation: () => {},
  screenToLocal: () => ({ x: 0, y: 0 }),
  localToScreen: () => ({ x: 0, y: 0 }),
  rotateToAngle: () => {},
  setTransform: () => {},
  setScaleOnly: () => {},
  setScaleWithoutMovingCenter: () => {},
  setScreenCenter: () => {},
  setScaleAtCenter: () => {},
});

export const TransformProvider = ({ children }: { children: ReactNode }) => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [scale, setScale] = useState(1);
  const [rotation, setRotation] = useState(0);
  const [screenCenter, setScreenCenter] = useState<{ x: number; y: number }>();

  const screenToLocal = useCallback(
    (screenX: number, screenY: number) => {
      const translatedX = (screenX - position.x) / scale;
      const translatedY = (screenY - position.y) / scale;

      const cosRotation = Math.cos(-rotation);
      const sinRotation = Math.sin(-rotation);
      const rotatedX = translatedX * cosRotation - translatedY * sinRotation;
      const rotatedY = translatedX * sinRotation + translatedY * cosRotation;

      return {
        x: rotatedX / UP_SCALE,
        y: rotatedY / UP_SCALE,
      };
    },
    [position.x, position.y, scale, rotation]
  );

  const localToScreen = useCallback(
    (localX: number, localY: number) => {
      const upscaledX = localX * UP_SCALE;
      const upscaledY = localY * UP_SCALE;

      const cosRotation = Math.cos(rotation);
      const sinRotation = Math.sin(rotation);
      const rotatedX = upscaledX * cosRotation - upscaledY * sinRotation;
      const rotatedY = upscaledX * sinRotation + upscaledY * cosRotation;

      const translatedX = rotatedX * scale + position.x;
      const translatedY = rotatedY * scale + position.y;

      return {
        x: translatedX,
        y: translatedY,
      };
    },
    [position.x, position.y, scale, rotation]
  );

  const rotateToAngle = useCallback(
    (to: number, fromPosition?: { x: number; y: number }) => {
      const rotationDiff = to - rotation;

      const center = screenCenter ?? { x: 0, y: 0 };
      const cosDelta = Math.cos(rotationDiff);
      const sinDelta = Math.sin(rotationDiff);

      const currentFromPosition = fromPosition ?? position;

      const newPosition = {
        x:
          center.x * (1 - cosDelta) +
          currentFromPosition.x * cosDelta +
          (center.y - currentFromPosition.y) * sinDelta,
        y:
          center.y * (1 - cosDelta) +
          currentFromPosition.y * cosDelta +
          (currentFromPosition.x - center.x) * sinDelta,
      };

      setRotation(to);
      setPosition(newPosition);
    },
    [rotation, position, screenCenter]
  );

  const setTransform = useCallback(
    (
      latitude: number,
      longitude: number,
      rotationDegrees?: number,
      useScale?: number
    ) => {
      const selectedRotation =
        rotationDegrees !== undefined
          ? (rotationDegrees * Math.PI) / 180
          : rotation;
      const selectedScale =
        useScale !== undefined ? useScale / SCALE_FACTOR : scale;
      const center = screenCenter ?? { x: 0, y: 0 };

      const newPosition = {
        x: -latitude * UP_SCALE * selectedScale,
        y: -longitude * UP_SCALE * selectedScale,
      };

      const cosRot = Math.cos(selectedRotation);
      const sinRot = Math.sin(selectedRotation);

      const dx = newPosition.x;
      const dy = newPosition.y;
      newPosition.x = dx * cosRot - dy * sinRot + center.x;
      newPosition.y = dx * sinRot + dy * cosRot + center.y;

      setPosition(newPosition);
      setRotation(selectedRotation);
      setScale(selectedScale);
    },
    [rotation, scale, screenCenter]
  );

  const setScaleAtCenter = useCallback(
    (newScale: number) => {
      if (scale === newScale) return;

      const center = screenCenter ?? { x: 0, y: 0 };

      const actualZoomFactor = newScale / scale;

      const newPosition = {
        x: position.x + (center.x - position.x) * (1 - actualZoomFactor),
        y: position.y + (center.y - position.y) * (1 - actualZoomFactor),
      };

      setPosition(newPosition);
      setScale(newScale);
    },
    [position, scale, screenCenter]
  );

  const setScaleOnly = useCallback((newScale: number) => {
    setScale(newScale);
  }, []);

  const setScaleWithoutMovingCenter = useCallback(
    (newScale: number) => {
      setScale(newScale);
    },
    [setScale]
  );

  const value = useMemo(
    () => ({
      position,
      scale,
      rotation,
      screenCenter,
      setPosition,
      setScale,
      setRotation,
      rotateToAngle,
      screenToLocal,
      localToScreen,
      setTransform,
      setScaleOnly,
      setScaleWithoutMovingCenter,
      setScreenCenter,
      setScaleAtCenter,
    }),
    [
      position,
      scale,
      rotation,
      screenCenter,
      setScale,
      rotateToAngle,
      screenToLocal,
      localToScreen,
      setTransform,
      setScaleOnly,
      setScaleWithoutMovingCenter,
      setScreenCenter,
      setScaleAtCenter,
    ]
  );

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

export const useTransform = () => {
  const context = useContext(TransformContext);
  if (!context) {
    throw new Error("useTransform must be used within a TransformProvider");
  }
  return context;
};
