import {
  ArrowheadArrowIcon,
  CloseIcon,
  TrashIcon,
} from "@excalidraw/excalidraw/components/icons";
import { useExcalidrawAPI } from "@excalidraw/excalidraw/index";
import {
  bootstrapCanvas,
  fillCircle,
  getNormalizedCanvasDimensions,
} from "@excalidraw/excalidraw/renderer/helpers";
import { type AppState } from "@excalidraw/excalidraw/types";
import {
  arrayToMap,
  sceneCoordsToViewportCoords,
  throttleRAF,
  viewportCoordsToSceneCoords,
} from "@excalidraw/common";
import { useCallback, useEffect, useRef, useState } from "react";

import {
  CaptureUpdateAction,
  clearSimpleArrowTangentOverride,
  getGlobalFixedPointForBindableElement,
  getElementAbsoluteCoords,
  getSimpleArrowCurveDebugData,
  isArrowElement,
  isBindableElement,
  setSimpleArrowTangentOverride,
  ShapeCache,
} from "@excalidraw/element";

import {
  isLineSegment,
  pointFrom,
  pointRotateRads,
  type GlobalPoint,
  type LocalPoint,
  type LineSegment,
  type Radians,
} from "@excalidraw/math";
import { isCurve } from "@excalidraw/math/curve";

import React from "react";

import type { Curve } from "@excalidraw/math";
import type {
  DebugElement,
  DebugPolygon,
} from "@excalidraw/element/visualdebug";
import type {
  ElementsMap,
  ExcalidrawArrowElement,
  ExcalidrawBindableElement,
  FixedPointBinding,
  Ordered,
  OrderedExcalidrawElement,
} from "@excalidraw/element/types";
import type { SimpleArrowCurveDebugData } from "@excalidraw/element";

import { STORAGE_KEYS } from "../app_constants";

type DebugCanvasOverlayState = Pick<
  AppState,
  | "width"
  | "height"
  | "zoom"
  | "scrollX"
  | "scrollY"
  | "offsetLeft"
  | "offsetTop"
  | "selectedElementIds"
>;

type SimpleArrowHandleDirection = "incoming" | "outgoing";

type SimpleArrowHandleDescriptor = {
  key: string;
  elementId: string;
  pointIndex: number;
  direction: SimpleArrowHandleDirection;
  point: GlobalPoint;
  handle: GlobalPoint;
  isOverridden: boolean;
  title: string;
};

type SelectedSimpleArrowDebugState = {
  element: ExcalidrawArrowElement;
  debugData: SimpleArrowCurveDebugData<GlobalPoint>;
  handles: SimpleArrowHandleDescriptor[];
};

const areSimpleArrowTangentHandlesEnabled = () =>
  window.EXCALIDRAW_DEBUG_LINEAR_ARROW_TANGENTS !== false;

const pickOverlayState = (appState: AppState): DebugCanvasOverlayState => ({
  width: appState.width,
  height: appState.height,
  zoom: appState.zoom,
  scrollX: appState.scrollX,
  scrollY: appState.scrollY,
  offsetLeft: appState.offsetLeft,
  offsetTop: appState.offsetTop,
  selectedElementIds: appState.selectedElementIds,
});

const getSimpleArrowTransform = (
  element: ExcalidrawArrowElement,
  elementsMap: ElementsMap,
) => {
  const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
  const center = pointFrom<GlobalPoint>(cx, cy);

  return {
    pointToGlobal: (point: LocalPoint): GlobalPoint => {
      const rotated = pointRotateRads(
        pointFrom<GlobalPoint>(element.x + point[0], element.y + point[1]),
        center,
        element.angle,
      );

      return pointFrom<GlobalPoint>(rotated[0], rotated[1]);
    },
    vectorToGlobal: (vector: [number, number]): [number, number] => {
      const rotated = pointRotateRads(
        pointFrom<GlobalPoint>(vector[0], vector[1]),
        pointFrom<GlobalPoint>(0, 0),
        element.angle,
      );

      return [rotated[0], rotated[1]];
    },
    vectorToLocal: (vector: [number, number]): [number, number] => {
      const rotated = pointRotateRads(
        pointFrom<GlobalPoint>(vector[0], vector[1]),
        pointFrom<GlobalPoint>(0, 0),
        -element.angle as Radians,
      );

      return [rotated[0], rotated[1]];
    },
  };
};

const getSimpleArrowDebugStateForElement = (
  elements: readonly OrderedExcalidrawElement[],
  elementId: string,
): SelectedSimpleArrowDebugState | null => {
  const element = elements.find(
    (candidate): candidate is Ordered<ExcalidrawArrowElement> =>
      candidate.id === elementId &&
      !candidate.isDeleted &&
      isArrowElement(candidate) &&
      !!candidate.roundness &&
      !candidate.elbowed,
  );

  if (!element) {
    return null;
  }

  const elementsMap = arrayToMap(elements);
  const transform = getSimpleArrowTransform(element, elementsMap);
  const localDebugData = getSimpleArrowCurveDebugData(element.points, 0.5, {
    elementId: element.id,
  });
  const debugData: SimpleArrowCurveDebugData<GlobalPoint> = {
    ...localDebugData,
    tangents: localDebugData.tangents.map((tangent) => ({
      ...tangent,
      point: transform.pointToGlobal(tangent.point as LocalPoint),
      base: transform.vectorToGlobal(tangent.base),
      autoScaled: transform.vectorToGlobal(tangent.autoScaled),
      scaled: transform.vectorToGlobal(tangent.scaled),
    })),
    segments: localDebugData.segments.map((segment) => ({
      ...segment,
      start: transform.pointToGlobal(segment.start as LocalPoint),
      end: transform.pointToGlobal(segment.end as LocalPoint),
      baseCp1: transform.pointToGlobal(segment.baseCp1 as LocalPoint),
      baseCp2: transform.pointToGlobal(segment.baseCp2 as LocalPoint),
      cp1: transform.pointToGlobal(segment.cp1 as LocalPoint),
      cp2: transform.pointToGlobal(segment.cp2 as LocalPoint),
    })),
  };
  const handles = debugData.tangents.flatMap((tangent, pointIndex) => {
    const descriptors: SimpleArrowHandleDescriptor[] = [];
    const lengthRatio =
      tangent.normalized.finalLengthVsMinNeighbor === null
        ? "n/a"
        : tangent.normalized.finalLengthVsMinNeighbor.toFixed(3);
    const angleDelta = tangent.normalized.angleDelta.toFixed(3);
    const title = `point ${pointIndex} · len/min ${lengthRatio} · dAngle ${angleDelta} · dblclick resets`;

    if (pointIndex < debugData.tangents.length - 1) {
      descriptors.push({
        key: `${element.id}:${pointIndex}:out`,
        elementId: element.id,
        pointIndex,
        direction: "outgoing",
        point: tangent.point,
        handle: pointFrom<GlobalPoint>(
          tangent.point[0] + tangent.scaled[0] / 3,
          tangent.point[1] + tangent.scaled[1] / 3,
        ),
        isOverridden: tangent.isOverridden,
        title,
      });
    }

    if (pointIndex > 0) {
      descriptors.push({
        key: `${element.id}:${pointIndex}:in`,
        elementId: element.id,
        pointIndex,
        direction: "incoming",
        point: tangent.point,
        handle: pointFrom<GlobalPoint>(
          tangent.point[0] - tangent.scaled[0] / 3,
          tangent.point[1] - tangent.scaled[1] / 3,
        ),
        isOverridden: tangent.isOverridden,
        title,
      });
    }

    return descriptors;
  });

  return {
    element,
    debugData,
    handles,
  };
};

const getSelectedSimpleArrowDebugState = (
  elements: readonly OrderedExcalidrawElement[],
  appState: Pick<AppState, "selectedElementIds">,
) => {
  const selectedIds = Object.keys(appState.selectedElementIds);

  if (selectedIds.length !== 1) {
    return null;
  }

  return getSimpleArrowDebugStateForElement(elements, selectedIds[0]);
};

const renderSelectedSimpleArrowTangentOverlay = (
  context: CanvasRenderingContext2D,
  zoom: number,
  debugState: SelectedSimpleArrowDebugState,
) => {
  context.save();
  context.lineWidth = 1;

  context.setLineDash([6, 4]);
  context.strokeStyle = "rgba(134, 142, 150, 0.75)";

  for (const segment of debugState.debugData.segments) {
    context.beginPath();
    context.moveTo(segment.start[0] * zoom, segment.start[1] * zoom);
    context.lineTo(segment.baseCp1[0] * zoom, segment.baseCp1[1] * zoom);
    context.lineTo(segment.baseCp2[0] * zoom, segment.baseCp2[1] * zoom);
    context.lineTo(segment.end[0] * zoom, segment.end[1] * zoom);
    context.stroke();
  }

  context.setLineDash([]);

  for (const segment of debugState.debugData.segments) {
    context.strokeStyle = segment.overshootsBaseline
      ? "rgba(245, 159, 0, 0.9)"
      : "rgba(94, 90, 216, 0.85)";

    context.beginPath();
    context.moveTo(segment.start[0] * zoom, segment.start[1] * zoom);
    context.lineTo(segment.cp1[0] * zoom, segment.cp1[1] * zoom);
    context.lineTo(segment.cp2[0] * zoom, segment.cp2[1] * zoom);
    context.lineTo(segment.end[0] * zoom, segment.end[1] * zoom);
    context.stroke();
  }

  for (const tangent of debugState.debugData.tangents) {
    if (!tangent.isAdjusted) {
      continue;
    }

    context.strokeStyle = tangent.isOverridden
      ? "rgba(230, 73, 128, 0.95)"
      : "rgba(201, 42, 42, 0.85)";
    context.fillStyle = tangent.isOverridden
      ? "rgba(230, 73, 128, 0.95)"
      : "rgba(201, 42, 42, 0.95)";

    fillCircle(
      context,
      tangent.point[0] * zoom,
      tangent.point[1] * zoom,
      3,
      true,
    );
  }

  context.restore();
};

const renderLine = (
  context: CanvasRenderingContext2D,
  zoom: number,
  segment: LineSegment<GlobalPoint>,
  color: string,
) => {
  context.save();
  context.strokeStyle = color;
  context.beginPath();
  context.moveTo(segment[0][0] * zoom, segment[0][1] * zoom);
  context.lineTo(segment[1][0] * zoom, segment[1][1] * zoom);
  context.stroke();
  context.restore();
};

const renderCubicBezier = (
  context: CanvasRenderingContext2D,
  zoom: number,
  [start, control1, control2, end]: Curve<GlobalPoint>,
  color: string,
) => {
  context.save();
  context.strokeStyle = color;
  context.beginPath();
  context.moveTo(start[0] * zoom, start[1] * zoom);
  context.bezierCurveTo(
    control1[0] * zoom,
    control1[1] * zoom,
    control2[0] * zoom,
    control2[1] * zoom,
    end[0] * zoom,
    end[1] * zoom,
  );
  context.stroke();
  context.restore();
};

const renderPolygon = (
  context: CanvasRenderingContext2D,
  zoom: number,
  polygon: DebugPolygon,
  color: string,
) => {
  const { points, fill, close } = polygon;

  if (points.length < 2) {
    return;
  }

  context.save();
  context.beginPath();
  context.moveTo(points[0][0] * zoom, points[0][1] * zoom);
  for (let i = 1; i < points.length; i += 1) {
    context.lineTo(points[i][0] * zoom, points[i][1] * zoom);
  }
  if (close !== false) {
    context.closePath();
  }

  if (fill) {
    context.save();
    context.globalAlpha = 0.15;
    context.fillStyle = color;
    context.fill();
    context.restore();
  }

  context.strokeStyle = color;
  context.stroke();
  context.restore();
};

const isDebugPolygon = (data: DebugElement["data"]): data is DebugPolygon =>
  (data as DebugPolygon).type === "polygon";

const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => {
  context.strokeStyle = "#888";
  context.save();
  context.beginPath();
  context.moveTo(-10 * zoom, -10 * zoom);
  context.lineTo(10 * zoom, 10 * zoom);
  context.moveTo(10 * zoom, -10 * zoom);
  context.lineTo(-10 * zoom, 10 * zoom);
  context.stroke();
  context.save();
};

const _renderBinding = (
  context: CanvasRenderingContext2D,
  binding: FixedPointBinding,
  elementsMap: ElementsMap,
  zoom: number,
  width: number,
  height: number,
  color: string,
) => {
  if (!binding.fixedPoint) {
    console.warn("Binding must have a fixedPoint");
    return;
  }

  const bindable = elementsMap.get(
    binding.elementId,
  ) as ExcalidrawBindableElement;
  const [x, y] = getGlobalFixedPointForBindableElement(
    binding.fixedPoint,
    bindable,
    elementsMap,
  );

  context.save();
  context.strokeStyle = color;
  context.lineWidth = 1;
  context.beginPath();
  context.moveTo(x * zoom, y * zoom);
  context.bezierCurveTo(
    x * zoom - width,
    y * zoom - height,
    x * zoom - width,
    y * zoom + height,
    x * zoom,
    y * zoom,
  );
  context.stroke();
  context.restore();
};

const _renderBindableBinding = (
  binding: FixedPointBinding,
  context: CanvasRenderingContext2D,
  elementsMap: ElementsMap,
  zoom: number,
  width: number,
  height: number,
  color: string,
) => {
  const bindable = elementsMap.get(
    binding.elementId,
  ) as ExcalidrawBindableElement;
  if (!binding.fixedPoint) {
    console.warn("Binding must have a fixedPoint");
    return;
  }

  const [x, y] = getGlobalFixedPointForBindableElement(
    binding.fixedPoint,
    bindable,
    elementsMap,
  );

  context.save();
  context.strokeStyle = color;
  context.lineWidth = 1;
  context.beginPath();
  context.moveTo(x * zoom, y * zoom);
  context.bezierCurveTo(
    x * zoom + width,
    y * zoom + height,
    x * zoom + width,
    y * zoom - height,
    x * zoom,
    y * zoom,
  );
  context.stroke();
  context.restore();
};

const renderBindings = (
  context: CanvasRenderingContext2D,
  elements: readonly OrderedExcalidrawElement[],
  zoom: number,
) => {
  const elementsMap = arrayToMap(elements);
  const dim = 16;
  elements.forEach((element) => {
    if (element.isDeleted) {
      return;
    }

    if (isArrowElement(element)) {
      if (element.startBinding) {
        if (
          !elementsMap
            .get(element.startBinding.elementId)
            ?.boundElements?.find((e) => e.id === element.id)
        ) {
          return;
        }

        _renderBinding(
          context,
          element.startBinding,
          elementsMap,
          zoom,
          dim,
          dim,
          element.startBinding?.mode === "orbit" ? "red" : "black",
        );
      }

      if (element.endBinding) {
        if (
          !elementsMap
            .get(element.endBinding.elementId)
            ?.boundElements?.find((e) => e.id === element.id)
        ) {
          return;
        }
        _renderBinding(
          context,
          element.endBinding,
          elementsMap,
          zoom,
          dim,
          dim,
          element.endBinding?.mode === "orbit" ? "red" : "black",
        );
      }
    }

    if (isBindableElement(element) && element.boundElements?.length) {
      element.boundElements.forEach((boundElement) => {
        if (boundElement.type !== "arrow") {
          return;
        }

        const arrow = elementsMap.get(
          boundElement.id,
        ) as ExcalidrawArrowElement;

        if (arrow && arrow.startBinding?.elementId === element.id) {
          _renderBindableBinding(
            arrow.startBinding,
            context,
            elementsMap,
            zoom,
            dim,
            dim,
            "green",
          );
        }
        if (arrow && arrow.endBinding?.elementId === element.id) {
          _renderBindableBinding(
            arrow.endBinding,
            context,
            elementsMap,
            zoom,
            dim,
            dim,
            "green",
          );
        }
      });
    }
  });
};

const render = (
  frame: DebugElement[],
  context: CanvasRenderingContext2D,
  appState: AppState,
) => {
  frame.forEach((el: DebugElement) => {
    switch (true) {
      case isLineSegment(el.data):
        renderLine(
          context,
          appState.zoom.value,
          el.data as LineSegment<GlobalPoint>,
          el.color,
        );
        break;
      case isCurve(el.data):
        renderCubicBezier(
          context,
          appState.zoom.value,
          el.data as Curve<GlobalPoint>,
          el.color,
        );
        break;
      case isDebugPolygon(el.data):
        renderPolygon(context, appState.zoom.value, el.data, el.color);
        break;
      default:
        throw new Error(`Unknown element type ${JSON.stringify(el)}`);
    }
  });
};

const _debugRenderer = (
  canvas: HTMLCanvasElement,
  appState: AppState,
  elements: readonly OrderedExcalidrawElement[],
  scale: number,
) => {
  const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
    canvas,
    scale,
  );

  const context = bootstrapCanvas({
    canvas,
    scale,
    normalizedWidth,
    normalizedHeight,
    viewBackgroundColor: "transparent",
  });

  // Apply zoom
  context.save();
  context.translate(
    appState.scrollX * appState.zoom.value,
    appState.scrollY * appState.zoom.value,
  );

  renderOrigin(context, appState.zoom.value);
  renderBindings(context, elements, appState.zoom.value);

  const selectedSimpleArrowDebugState = areSimpleArrowTangentHandlesEnabled()
    ? getSelectedSimpleArrowDebugState(elements, appState)
    : null;

  window.EXCALIDRAW_DEBUG_SELECTED_LINEAR_ARROW =
    selectedSimpleArrowDebugState?.debugData;

  if (
    window.visualDebug?.currentFrame &&
    window.visualDebug?.data &&
    window.visualDebug.data.length > 0
  ) {
    // Render only one frame
    const [idx] = debugFrameData();

    render(window.visualDebug.data[idx], context, appState);
  } else {
    // Render all debug frames
    window.visualDebug?.data.forEach((frame) => {
      render(frame, context, appState);
    });
  }

  if (selectedSimpleArrowDebugState) {
    renderSelectedSimpleArrowTangentOverlay(
      context,
      appState.zoom.value,
      selectedSimpleArrowDebugState,
    );
  }

  if (window.visualDebug) {
    window.visualDebug!.data =
      window.visualDebug?.data.map((frame) =>
        frame.filter((el) => el.permanent),
      ) ?? [];
  }
};

const debugFrameData = (): [number, number] => {
  const currentFrame = window.visualDebug?.currentFrame ?? 0;
  const frameCount = window.visualDebug?.data.length ?? 0;

  if (frameCount > 0) {
    return [currentFrame % frameCount, window.visualDebug?.currentFrame ?? 0];
  }

  return [0, 0];
};

export const saveDebugState = (debug: { enabled: boolean }) => {
  try {
    localStorage.setItem(
      STORAGE_KEYS.LOCAL_STORAGE_DEBUG,
      JSON.stringify(debug),
    );
  } catch (error: any) {
    console.error(error);
  }
};

export const debugRenderer = throttleRAF(
  (
    canvas: HTMLCanvasElement,
    appState: AppState,
    elements: readonly OrderedExcalidrawElement[],
    scale: number,
  ) => {
    _debugRenderer(canvas, appState, elements, scale);
  },
);

export const loadSavedDebugState = () => {
  let debug;
  try {
    const savedDebugState = localStorage.getItem(
      STORAGE_KEYS.LOCAL_STORAGE_DEBUG,
    );
    if (savedDebugState) {
      debug = JSON.parse(savedDebugState) as { enabled: boolean };
    }
  } catch (error: any) {
    console.error(error);
  }

  return debug ?? { enabled: false };
};

export const isVisualDebuggerEnabled = () =>
  Array.isArray(window.visualDebug?.data);

export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
  const moveForward = useCallback(() => {
    if (
      !window.visualDebug?.currentFrame ||
      isNaN(window.visualDebug?.currentFrame ?? -1)
    ) {
      window.visualDebug!.currentFrame = 0;
    }
    window.visualDebug!.currentFrame += 1;
    onChange();
  }, [onChange]);
  const moveBackward = useCallback(() => {
    if (
      !window.visualDebug?.currentFrame ||
      isNaN(window.visualDebug?.currentFrame ?? -1) ||
      window.visualDebug?.currentFrame < 1
    ) {
      window.visualDebug!.currentFrame = 1;
    }
    window.visualDebug!.currentFrame -= 1;
    onChange();
  }, [onChange]);
  const reset = useCallback(() => {
    window.visualDebug!.currentFrame = undefined;
    onChange();
  }, [onChange]);
  const trashFrames = useCallback(() => {
    if (window.visualDebug) {
      window.visualDebug.currentFrame = undefined;
      window.visualDebug.data = [];
    }
    onChange();
  }, [onChange]);

  return (
    <>
      <button
        className="ToolIcon_type_button"
        data-testid="debug-forward"
        aria-label="Move forward"
        type="button"
        onClick={trashFrames}
      >
        <div
          className="ToolIcon__icon"
          aria-hidden="true"
          aria-disabled="false"
        >
          {TrashIcon}
        </div>
      </button>
      <button
        className="ToolIcon_type_button"
        data-testid="debug-forward"
        aria-label="Move forward"
        type="button"
        onClick={moveBackward}
      >
        <div
          className="ToolIcon__icon"
          aria-hidden="true"
          aria-disabled="false"
        >
          <ArrowheadArrowIcon flip />
        </div>
      </button>
      <button
        className="ToolIcon_type_button"
        data-testid="debug-forward"
        aria-label="Move forward"
        type="button"
        onClick={reset}
      >
        <div
          className="ToolIcon__icon"
          aria-hidden="true"
          aria-disabled="false"
        >
          {CloseIcon}
        </div>
      </button>
      <button
        className="ToolIcon_type_button"
        data-testid="debug-backward"
        aria-label="Move backward"
        type="button"
        onClick={moveForward}
      >
        <div
          className="ToolIcon__icon"
          aria-hidden="true"
          aria-disabled="false"
        >
          <ArrowheadArrowIcon />
        </div>
      </button>
    </>
  );
};

interface DebugCanvasProps {
  appState: AppState;
  scale: number;
}

const DebugCanvas = React.forwardRef<HTMLCanvasElement, DebugCanvasProps>(
  ({ appState, scale }, ref) => {
    const excalidrawAPI = useExcalidrawAPI();
    const [overlayState, setOverlayState] = useState<DebugCanvasOverlayState>(
      () => pickOverlayState(appState),
    );
    const [selectedSimpleArrowDebugState, setSelectedSimpleArrowDebugState] =
      useState<SelectedSimpleArrowDebugState | null>(null);
    const dragStateRef = useRef<{
      elementId: string;
      pointIndex: number;
      direction: SimpleArrowHandleDirection;
    } | null>(null);

    const syncSelectedSimpleArrowDebugState = useCallback(
      (
        nextAppState: AppState,
        nextElements?: readonly OrderedExcalidrawElement[],
      ) => {
        if (!excalidrawAPI || excalidrawAPI.isDestroyed) {
          return;
        }

        const elements =
          nextElements ??
          (excalidrawAPI.getSceneElements() as OrderedExcalidrawElement[]);

        setOverlayState(pickOverlayState(nextAppState));

        const nextDebugState = areSimpleArrowTangentHandlesEnabled()
          ? getSelectedSimpleArrowDebugState(elements, nextAppState)
          : null;

        window.EXCALIDRAW_DEBUG_SELECTED_LINEAR_ARROW =
          nextDebugState?.debugData;
        setSelectedSimpleArrowDebugState(nextDebugState);
      },
      [excalidrawAPI],
    );

    useEffect(() => {
      if (!excalidrawAPI || excalidrawAPI.isDestroyed) {
        return;
      }

      syncSelectedSimpleArrowDebugState(excalidrawAPI.getAppState());

      const unsubscribeChange = excalidrawAPI.onChange(
        (elements, nextAppState) =>
          syncSelectedSimpleArrowDebugState(
            nextAppState,
            elements as readonly OrderedExcalidrawElement[],
          ),
      );
      const unsubscribeState = excalidrawAPI.onStateChange(
        [
          "selectedElementIds",
          "zoom",
          "scrollX",
          "scrollY",
          "offsetLeft",
          "offsetTop",
          "width",
          "height",
        ],
        (_value, nextAppState) =>
          syncSelectedSimpleArrowDebugState(nextAppState),
      );

      return () => {
        unsubscribeChange();
        unsubscribeState();
      };
    }, [excalidrawAPI, syncSelectedSimpleArrowDebugState]);

    const rerenderSceneForSimpleArrowOverride = useCallback(() => {
      if (!excalidrawAPI || excalidrawAPI.isDestroyed) {
        return;
      }

      excalidrawAPI.updateScene({
        elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
        captureUpdate: CaptureUpdateAction.NEVER,
      });
    }, [excalidrawAPI]);

    const commitOverrideForHandle = useCallback(
      (
        handle: Pick<
          SimpleArrowHandleDescriptor,
          "elementId" | "pointIndex" | "direction"
        >,
        clientX: number,
        clientY: number,
      ) => {
        if (!excalidrawAPI || excalidrawAPI.isDestroyed) {
          return;
        }

        const nextAppState = excalidrawAPI.getAppState();
        const nextElements =
          excalidrawAPI.getSceneElements() as OrderedExcalidrawElement[];
        const debugState = getSimpleArrowDebugStateForElement(
          nextElements,
          handle.elementId,
        );

        if (!debugState) {
          return;
        }

        const scenePoint = viewportCoordsToSceneCoords(
          { clientX, clientY },
          nextAppState,
        );
        const tangent =
          handle.direction === "outgoing"
            ? ([
                (scenePoint.x -
                  debugState.debugData.tangents[handle.pointIndex].point[0]) *
                  3,
                (scenePoint.y -
                  debugState.debugData.tangents[handle.pointIndex].point[1]) *
                  3,
              ] as [number, number])
            : ([
                (debugState.debugData.tangents[handle.pointIndex].point[0] -
                  scenePoint.x) *
                  3,
                (debugState.debugData.tangents[handle.pointIndex].point[1] -
                  scenePoint.y) *
                  3,
              ] as [number, number]);
        const localTangent = getSimpleArrowTransform(
          debugState.element,
          arrayToMap(nextElements),
        ).vectorToLocal(tangent);

        setSimpleArrowTangentOverride(
          debugState.element.id,
          handle.pointIndex,
          localTangent,
        );
        ShapeCache.delete(debugState.element);
        rerenderSceneForSimpleArrowOverride();
        syncSelectedSimpleArrowDebugState(nextAppState, nextElements);
      },
      [
        excalidrawAPI,
        rerenderSceneForSimpleArrowOverride,
        syncSelectedSimpleArrowDebugState,
      ],
    );

    const onPointerMove = useCallback(
      (event: PointerEvent) => {
        if (!dragStateRef.current) {
          return;
        }

        event.preventDefault();
        commitOverrideForHandle(
          dragStateRef.current,
          event.clientX,
          event.clientY,
        );
      },
      [commitOverrideForHandle],
    );

    const onPointerUp = useCallback(
      (event: PointerEvent) => {
        if (!dragStateRef.current) {
          return;
        }

        event.preventDefault();
        dragStateRef.current = null;
        window.removeEventListener("pointermove", onPointerMove);
        window.removeEventListener("pointerup", onPointerUp);
        window.removeEventListener("pointercancel", onPointerUp);
      },
      [onPointerMove],
    );

    useEffect(() => {
      return () => {
        dragStateRef.current = null;
        window.removeEventListener("pointermove", onPointerMove);
        window.removeEventListener("pointerup", onPointerUp);
        window.removeEventListener("pointercancel", onPointerUp);
      };
    }, [onPointerMove, onPointerUp]);

    const handlePointerDown = useCallback(
      (
        handle: SimpleArrowHandleDescriptor,
        event: React.PointerEvent<HTMLDivElement>,
      ) => {
        event.preventDefault();
        event.stopPropagation();

        dragStateRef.current = {
          elementId: handle.elementId,
          pointIndex: handle.pointIndex,
          direction: handle.direction,
        };

        window.addEventListener("pointermove", onPointerMove, {
          passive: false,
        });
        window.addEventListener("pointerup", onPointerUp, {
          passive: false,
        });
        window.addEventListener("pointercancel", onPointerUp, {
          passive: false,
        });

        commitOverrideForHandle(handle, event.clientX, event.clientY);
      },
      [commitOverrideForHandle, onPointerMove, onPointerUp],
    );

    const resetHandleOverride = useCallback(
      (
        handle: SimpleArrowHandleDescriptor,
        event: React.MouseEvent<HTMLDivElement>,
      ) => {
        event.preventDefault();
        event.stopPropagation();

        if (!excalidrawAPI || excalidrawAPI.isDestroyed) {
          return;
        }

        clearSimpleArrowTangentOverride(handle.elementId, handle.pointIndex);

        const nextElements =
          excalidrawAPI.getSceneElements() as OrderedExcalidrawElement[];
        const element = nextElements.find(
          (candidate): candidate is Ordered<ExcalidrawArrowElement> =>
            candidate.id === handle.elementId &&
            !candidate.isDeleted &&
            isArrowElement(candidate),
        );

        if (element) {
          ShapeCache.delete(element);
          rerenderSceneForSimpleArrowOverride();
        }

        syncSelectedSimpleArrowDebugState(
          excalidrawAPI.getAppState(),
          nextElements,
        );
      },
      [
        excalidrawAPI,
        rerenderSceneForSimpleArrowOverride,
        syncSelectedSimpleArrowDebugState,
      ],
    );

    const { width, height } = overlayState;

    return (
      <>
        <canvas
          style={{
            width,
            height,
            position: "absolute",
            inset: 0,
            zIndex: 2,
            pointerEvents: "none",
          }}
          width={width * scale}
          height={height * scale}
          ref={ref}
        >
          Debug Canvas
        </canvas>
        {selectedSimpleArrowDebugState?.handles.map((handle) => {
          const { x, y } = sceneCoordsToViewportCoords(
            {
              sceneX: handle.handle[0],
              sceneY: handle.handle[1],
            },
            overlayState,
          );

          return (
            <div
              key={handle.key}
              title={handle.title}
              onPointerDown={(event) => handlePointerDown(handle, event)}
              onDoubleClick={(event) => resetHandleOverride(handle, event)}
              style={{
                position: "absolute",
                left: x - overlayState.offsetLeft,
                top: y - overlayState.offsetTop,
                width: 12,
                height: 12,
                zIndex: 3,
                borderRadius: handle.direction === "outgoing" ? "999px" : 3,
                transform: "translate(-50%, -50%)",
                background: handle.isOverridden
                  ? "rgba(230, 73, 128, 0.95)"
                  : handle.direction === "outgoing"
                  ? "rgba(94, 90, 216, 0.95)"
                  : "rgba(245, 159, 0, 0.95)",
                border: "2px solid rgba(255,255,255,0.95)",
                boxShadow: "0 0 0 1px rgba(0, 0, 0, 0.18)",
                pointerEvents: "auto",
                cursor: "grab",
              }}
            />
          );
        })}
      </>
    );
  },
);

export default DebugCanvas;
