import throttle from "lodash.throttle";
import { useEffect, useMemo, useRef } from "react";
import { EVENT } from "../constants";
import { getTransformHandles } from "../element";
import { mutateElement } from "../element/mutateElement";
import { resizeSingleElement } from "../element/resizeElements";
import { ExcalidrawElement } from "../element/types";
import { KEYS } from "../keys";
import { degreeToRadian, radianToDegree, rotatePoint } from "../math";
import Scene from "../scene/Scene";
import { AppState, Point } from "../types";
import { arrayToMap } from "../utils";

const shouldKeepAspectRatio = (element: ExcalidrawElement) => {
  return element.type === "image";
};

type AdjustableProperty = "width" | "height" | "angle" | "x" | "y";

interface DragInputProps {
  label: string | React.ReactNode;
  property: AdjustableProperty;
  element: ExcalidrawElement;
  zoom: AppState["zoom"];
}

const DragInput = ({ label, property, element, zoom }: DragInputProps) => {
  const inputRef = useRef<HTMLInputElement>(null);
  const labelRef = useRef<HTMLDivElement>(null);

  const originalElementsMap = useMemo(
    () => arrayToMap(Scene.getScene(element)?.getNonDeletedElements() ?? []),
    [element],
  );

  const handleChange = useMemo(
    () =>
      (
        initialValue: number,
        delta: number,
        source: "pointerMove" | "keyDown",
        pointerOffset?: number,
      ) => {
        if (inputRef.current) {
          const keepAspectRatio = shouldKeepAspectRatio(element);

          if (
            (property === "width" || property === "height") &&
            source === "pointerMove" &&
            pointerOffset
          ) {
            const handles = getTransformHandles(element, zoom, "mouse");

            let referencePoint: Point | undefined;
            let handleDirection: "e" | "s" | "se" | undefined;

            if (keepAspectRatio && handles.se) {
              referencePoint = [handles.se[0], handles.se[1]];
              handleDirection = "se";
            } else if (property === "width" && handles.e) {
              referencePoint = [handles.e[0], handles.e[1]];
              handleDirection = "e";
            } else if (property === "height" && handles.s) {
              referencePoint = [handles.s[0], handles.s[1]];
              handleDirection = "s";
            }

            if (referencePoint !== undefined && handleDirection !== undefined) {
              const pointerRotated = rotatePoint(
                [
                  referencePoint[0] +
                    (property === "width" ? pointerOffset : 0),
                  referencePoint[1] +
                    (property === "height" ? pointerOffset : 0),
                ],
                referencePoint,
                element.angle,
              );

              resizeSingleElement(
                originalElementsMap,
                keepAspectRatio,
                element,
                handleDirection,
                false,
                pointerRotated[0],
                pointerRotated[1],
              );
            }
          } else if (
            source === "keyDown" ||
            (source === "pointerMove" &&
              property !== "width" &&
              property !== "height")
          ) {
            const incVal = Math.round(
              Math.sign(delta) * Math.pow(Math.abs(delta) / 10, 1.6),
            );
            let newVal = initialValue + incVal;

            newVal =
              property === "angle"
                ? // so the degree converted from radian is an integer
                  degreeToRadian(
                    Math.round(
                      radianToDegree(
                        degreeToRadian(
                          Math.sign(newVal % 360) === -1
                            ? (newVal % 360) + 360
                            : newVal % 360,
                        ),
                      ),
                    ),
                  )
                : Math.round(newVal);

            mutateElement(element, {
              [property]: newVal,
            });
          }
        }
      },
    [element, property, zoom, originalElementsMap],
  );

  const hangleChangeThrottled = useMemo(() => {
    return throttle(handleChange, 16);
  }, [handleChange]);

  useEffect(() => {
    const value =
      Math.round(
        property === "angle"
          ? radianToDegree(element[property]) * 100
          : element[property] * 100,
      ) / 100;

    if (inputRef.current) {
      inputRef.current.value = String(value);
    }
  }, [element, element.version, element.versionNonce, property]);

  useEffect(() => {
    hangleChangeThrottled.cancel();
  });

  return (
    <label className="color-input-container">
      <div
        className="color-picker-hash"
        ref={labelRef}
        style={{
          width: "20px",
        }}
        onPointerDown={(event) => {
          if (inputRef.current) {
            const startPosition = event.clientX;
            let startValue = Number(inputRef.current.value);
            if (isNaN(startValue)) {
              startValue = 0;
            }

            let lastPointerRef: {
              x: number;
              y: number;
            } | null = null;

            document.body.classList.add("dragResize");

            const onPointerMove = (event: PointerEvent) => {
              if (lastPointerRef) {
                hangleChangeThrottled(
                  startValue,
                  Math.ceil(event.clientX - startPosition),
                  "pointerMove",
                  event.clientX - lastPointerRef.x,
                );
              }

              lastPointerRef = {
                x: event.clientX,
                y: event.clientY,
              };
            };

            window.addEventListener(EVENT.POINTER_MOVE, onPointerMove, false);
            window.addEventListener(
              EVENT.POINTER_UP,
              () => {
                window.removeEventListener(
                  EVENT.POINTER_MOVE,
                  onPointerMove,
                  false,
                );

                lastPointerRef = null;

                document.body.classList.remove("dragResize");
              },
              false,
            );
          }
        }}
        onPointerEnter={() => {
          if (labelRef.current) {
            labelRef.current.style.cursor = "ew-resize";
          }
        }}
      >
        {label}
      </div>
      <input
        className="color-picker-input"
        style={{
          width: "66px",
          fontSize: "12px",
        }}
        autoComplete="off"
        spellCheck="false"
        onKeyDown={(event) => {
          const value = Number(event.target.value);
          if (isNaN(value)) {
            return;
          }
          if (event.key === KEYS.ENTER) {
            handleChange(value, 0, "keyDown");
          }
        }}
        ref={inputRef}
      ></input>
    </label>
  );
};

export default DragInput;
