import { useRef, useState } from "react";
import {
  Action,
  Cardinality,
  Constraint,
  darkBgTheme,
  ObjectType,
  tableFieldHeight,
  tableHeaderHeight,
} from "../../data/constants";
import { Toast } from "@douyinfe/semi-ui";
import Table from "./Table";
import Area from "./Area";
import Relationship from "./Relationship";
import Note from "./Note";
import {
  useCanvas,
  useSettings,
  useTransform,
  useDiagram,
  useUndoRedo,
  useSelect,
  useAreas,
  useNotes,
  useLayout,
} from "../../hooks";
import { useTranslation } from "react-i18next";
import { useEventListener } from "usehooks-ts";
import { areFieldsCompatible } from "../../utils/utils";
import { getRectFromEndpoints, isInsideRect } from "../../utils/rect";

export default function Canvas() {
  const { t } = useTranslation();

  const canvasRef = useRef(null);
  const canvasContextValue = useCanvas();
  const {
    canvas: { viewBox },
    pointer,
  } = canvasContextValue;

  const { tables, updateTable, relationships, addRelationship, database } =
    useDiagram();
  const { areas, updateArea } = useAreas();
  const { notes, updateNote } = useNotes();
  const { layout } = useLayout();
  const { settings } = useSettings();
  const { setUndoStack, setRedoStack } = useUndoRedo();
  const { transform, setTransform } = useTransform();
  const {
    selectedElement,
    setSelectedElement,
    bulkSelectedElements,
    setBulkSelectedElements,
  } = useSelect();
  const [dragging, setDragging] = useState({
    element: ObjectType.NONE,
    id: -1,
    prevX: 0,
    prevY: 0,
    initialPositions: [],
  });
  const [linking, setLinking] = useState(false);
  const [linkingLine, setLinkingLine] = useState({
    startTableId: -1,
    startFieldId: -1,
    endTableId: -1,
    endFieldId: -1,
    startX: 0,
    startY: 0,
    endX: 0,
    endY: 0,
  });
  const [grabOffset, setGrabOffset] = useState({ x: 0, y: 0 });
  const [hoveredTable, setHoveredTable] = useState({
    tableId: -1,
    field: -2,
  });
  const [panning, setPanning] = useState({
    isPanning: false,
    panStart: { x: 0, y: 0 },
    cursorStart: { x: 0, y: 0 },
  });
  const [areaResize, setAreaResize] = useState({ id: -1, dir: "none" });
  const [initCoords, setInitCoords] = useState({
    x: 0,
    y: 0,
    width: 0,
    height: 0,
    pointerX: 0,
    pointerY: 0,
  });
  const [bulkSelectRectPts, setBulkSelectRectPts] = useState({
    x1: 0,
    y1: 0,
    x2: 0,
    y2: 0,
    show: false,
  });

  const collectSelectedElements = () => {
    const rect = getRectFromEndpoints(bulkSelectRectPts);

    const elements = [];

    tables.forEach((table) => {
      if (
        isInsideRect(
          {
            x: table.x,
            y: table.y,
            width: settings.tableWidth,
            height:
              table.fields.length * tableFieldHeight + tableHeaderHeight + 7,
          },
          rect,
        )
      ) {
        elements.push({
          id: table.id,
          type: ObjectType.TABLE,
        });
      }
    });

    areas.forEach((area) => {
      if (
        isInsideRect(
          {
            x: area.x,
            y: area.y,
            width: area.width,
            height: area.height,
          },
          rect,
        )
      ) {
        elements.push({
          id: area.id,
          type: ObjectType.AREA,
        });
      }
    });

    notes.forEach((note) => {
      if (
        isInsideRect(
          {
            x: note.x,
            y: note.y,
            width: 180,
            height: note.height,
          },
          rect,
        )
      ) {
        elements.push({
          id: note.id,
          type: ObjectType.NOTE,
        });
      }
    });

    setBulkSelectedElements(elements);
  };

  const getElement = (element) => {
    switch (element.type) {
      case ObjectType.TABLE:
        return tables[element.id];
      case ObjectType.AREA:
        return areas[element.id];
      case ObjectType.NOTE:
        return notes[element.id];
      default:
        return { x: 0, y: 0 };
    }
  };

  /**
   * @param {PointerEvent} e
   * @param {number} id
   * @param {ObjectType[keyof ObjectType]} type
   */
  const handlePointerDownOnElement = (e, id, type) => {
    if (selectedElement.open && !layout.sidebar) return;

    if (!e.isPrimary) return;

    if (type === ObjectType.TABLE) {
      const table = tables.find((t) => t.id === id);
      setGrabOffset({
        x: table.x - pointer.spaces.diagram.x,
        y: table.y - pointer.spaces.diagram.y,
      });
      setDragging((prev) => ({
        ...prev,
        id,
        element: type,
        prevX: table.x,
        prevY: table.y,
      }));
    } else if (type === ObjectType.AREA) {
      const area = areas.find((t) => t.id === id);
      setGrabOffset({
        x: area.x - pointer.spaces.diagram.x,
        y: area.y - pointer.spaces.diagram.y,
      });
      setDragging((prev) => ({
        ...prev,
        id,
        element: type,
        prevX: area.x,
        prevY: area.y,
      }));
    } else if (type === ObjectType.NOTE) {
      const note = notes.find((t) => t.id === id);
      setGrabOffset({
        x: note.x - pointer.spaces.diagram.x,
        y: note.y - pointer.spaces.diagram.y,
      });
      setDragging((prev) => ({
        ...prev,
        id,
        element: type,
        prevX: note.x,
        prevY: note.y,
      }));
    }

    if (bulkSelectedElements.length) {
      setDragging((prev) => ({
        ...prev,
        initialPositions: bulkSelectedElements.map((element) => ({
          ...element,
          undo: {
            x: getElement(element).x,
            y: getElement(element).y,
          },
        })),
      }));
    }
    setSelectedElement((prev) => ({
      ...prev,
      element: type,
      id: id,
      open: false,
    }));
  };

  /**
   * @param {PointerEvent} e
   */
  const handlePointerMove = (e) => {
    if (selectedElement.open && !layout.sidebar) return;

    if (!e.isPrimary) return;

    if (linking) {
      setLinkingLine({
        ...linkingLine,
        endX: pointer.spaces.diagram.x,
        endY: pointer.spaces.diagram.y,
      });
    } else if (
      dragging.element !== ObjectType.NONE &&
      dragging.id >= 0 &&
      bulkSelectedElements.length
    ) {
      const currentX = pointer.spaces.diagram.x + grabOffset.x;
      const currentY = pointer.spaces.diagram.y + grabOffset.y;
      const deltaX = currentX - dragging.prevX;
      const deltaY = currentY - dragging.prevY;

      for (const element of bulkSelectedElements) {
        if (element.type === ObjectType.TABLE) {
          updateTable(element.id, {
            x: tables[element.id].x + deltaX,
            y: tables[element.id].y + deltaY,
          });
        }
        if (element.type === ObjectType.AREA) {
          updateArea(element.id, {
            x: areas[element.id].x + deltaX,
            y: areas[element.id].y + deltaY,
          });
        }
        if (element.type === ObjectType.NOTE) {
          updateNote(element.id, {
            x: notes[element.id].x + deltaX,
            y: notes[element.id].y + deltaY,
          });
        }
      }

      setDragging((prev) => ({
        ...prev,
        prevX: currentX,
        prevY: currentY,
      }));
    } else if (
      panning.isPanning &&
      dragging.element === ObjectType.NONE &&
      areaResize.id === -1
    ) {
      if (!settings.panning) {
        return;
      }
      setTransform((prev) => ({
        ...prev,
        pan: {
          x:
            panning.panStart.x +
            (panning.cursorStart.x - pointer.spaces.screen.x) / transform.zoom,
          y:
            panning.panStart.y +
            (panning.cursorStart.y - pointer.spaces.screen.y) / transform.zoom,
        },
      }));
    } else if (dragging.element === ObjectType.TABLE && dragging.id >= 0) {
      updateTable(dragging.id, {
        x: pointer.spaces.diagram.x + grabOffset.x,
        y: pointer.spaces.diagram.y + grabOffset.y,
      });
    } else if (
      dragging.element === ObjectType.AREA &&
      dragging.id >= 0 &&
      areaResize.id === -1
    ) {
      updateArea(dragging.id, {
        x: pointer.spaces.diagram.x + grabOffset.x,
        y: pointer.spaces.diagram.y + grabOffset.y,
      });
    } else if (dragging.element === ObjectType.NOTE && dragging.id >= 0) {
      updateNote(dragging.id, {
        x: pointer.spaces.diagram.x + grabOffset.x,
        y: pointer.spaces.diagram.y + grabOffset.y,
      });
    } else if (areaResize.id !== -1) {
      if (areaResize.dir === "none") return;
      let newDims = { ...initCoords };
      delete newDims.pointerX;
      delete newDims.pointerY;
      setPanning((old) => ({ ...old, isPanning: false }));

      switch (areaResize.dir) {
        case "br":
          newDims.width = pointer.spaces.diagram.x - initCoords.x;
          newDims.height = pointer.spaces.diagram.y - initCoords.y;
          break;
        case "tl":
          newDims.x = pointer.spaces.diagram.x;
          newDims.y = pointer.spaces.diagram.y;
          newDims.width =
            initCoords.x + initCoords.width - pointer.spaces.diagram.x;
          newDims.height =
            initCoords.y + initCoords.height - pointer.spaces.diagram.y;
          break;
        case "tr":
          newDims.y = pointer.spaces.diagram.y;
          newDims.width = pointer.spaces.diagram.x - initCoords.x;
          newDims.height =
            initCoords.y + initCoords.height - pointer.spaces.diagram.y;
          break;
        case "bl":
          newDims.x = pointer.spaces.diagram.x;
          newDims.width =
            initCoords.x + initCoords.width - pointer.spaces.diagram.x;
          newDims.height = pointer.spaces.diagram.y - initCoords.y;
          break;
      }

      updateArea(areaResize.id, { ...newDims });
    } else if (bulkSelectRectPts.show) {
      setBulkSelectRectPts((prev) => ({
        ...prev,
        x2: pointer.spaces.diagram.x,
        y2: pointer.spaces.diagram.y,
      }));
    }
  };

  /**
   * @param {PointerEvent} e
   */
  const handlePointerDown = (e) => {
    if (selectedElement.open && !layout.sidebar) return;

    if (!e.isPrimary) return;

    // don't pan if the sidesheet for editing a table is open
    if (
      selectedElement.element === ObjectType.TABLE &&
      selectedElement.open &&
      !layout.sidebar
    )
      return;

    if (!settings.panning) {
      setBulkSelectRectPts({
        x1: pointer.spaces.diagram.x,
        y1: pointer.spaces.diagram.y,
        x2: pointer.spaces.diagram.x,
        y2: pointer.spaces.diagram.y,
        show: true,
      });
      pointer.setStyle("crosshair");
    } else {
      setPanning({
        isPanning: true,
        panStart: transform.pan,
        // Diagram space depends on the current panning.
        // Use screen space to avoid circular dependencies and undefined behavior.
        cursorStart: pointer.spaces.screen,
      });
      pointer.setStyle("grabbing");
    }
  };

  const coordsDidUpdate = (element) => {
    const elementData = getElement(element);
    const updated = !(
      dragging.prevX === elementData.x && dragging.prevY === elementData.y
    );

    return (
      updated ||
      dragging.initialPositions.some(
        (el) => !(el.undo.x === elementData.x && el.undo.y === elementData.y),
      )
    );
  };

  const didResize = (id) => {
    return !(
      areas[id].x === initCoords.x &&
      areas[id].y === initCoords.y &&
      areas[id].width === initCoords.width &&
      areas[id].height === initCoords.height
    );
  };

  const didPan = () =>
    !(transform.pan.x === panning.panStart.x && transform.pan.y === panning.panStart.y);

  /**
   * @param {PointerEvent} e
   */
  const handlePointerUp = (e) => {
    if (selectedElement.open && !layout.sidebar) return;

    if (!e.isPrimary) return;

    if (coordsDidUpdate({ id: dragging.id, type: dragging.element })) {
      if (bulkSelectedElements.length) {
        setUndoStack((prev) => [
          ...prev,
          {
            action: Action.MOVE,
            bulk: true,
            message: t("bulk_update"),
            elements: dragging.initialPositions.map((element) => ({
              ...element,
              redo: {
                x: getElement(element).x,
                y: getElement(element).y,
              },
            })),
          },
        ]);
        setSelectedElement((prev) => ({
          ...prev,
          element: ObjectType.NONE,
          id: -1,
          open: false,
        }));
      } else {
        const element = getElement({
          id: dragging.id,
          type: dragging.element,
        });
        setUndoStack((prev) => [
          ...prev,
          {
            action: Action.MOVE,
            element: dragging.element,
            x: dragging.prevX,
            y: dragging.prevY,
            toX: element.x,
            toY: element.y,
            id: dragging.id,
            message: t("move_element", {
              coords: `(${element.x}, ${element.y})`,
              name: getElement({
                id: dragging.id,
                type: dragging.element,
              }).name,
            }),
          },
        ]);
      }
      setRedoStack([]);
    }
    setDragging({
      element: ObjectType.NONE,
      id: -1,
      prevX: 0,
      prevY: 0,
      initialPositions: [],
    });

    if (bulkSelectRectPts.show) {
      setBulkSelectRectPts((prev) => ({
        ...prev,
        x2: pointer.spaces.diagram.x,
        y2: pointer.spaces.diagram.y,
        show: false,
      }));
      collectSelectedElements();
    }

    if (panning.isPanning && didPan()) {
      setUndoStack((prev) => [
        ...prev,
        {
          action: Action.PAN,
          undo: { x: panning.panStart.x, y: panning.panStart.y },
          redo: transform.pan,
          message: t("move_element", {
            coords: `(${transform?.pan.x}, ${transform?.pan.y})`,
            name: "diagram",
          }),
        },
      ]);
      setRedoStack([]);
      setSelectedElement((prev) => ({
        ...prev,
        element: ObjectType.NONE,
        id: -1,
        open: false,
      }));
      setBulkSelectedElements([]);
    }
    setPanning((old) => ({ ...old, isPanning: false }));
    pointer.setStyle("default");

    if (linking) handleLinking();
    setLinking(false);

    if (areaResize.id !== -1 && didResize(areaResize.id)) {
      setUndoStack((prev) => [
        ...prev,
        {
          action: Action.EDIT,
          element: ObjectType.AREA,
          aid: areaResize.id,
          undo: {
            ...areas[areaResize.id],
            x: initCoords.x,
            y: initCoords.y,
            width: initCoords.width,
            height: initCoords.height,
          },
          redo: areas[areaResize.id],
          message: t("edit_area", {
            areaName: areas[areaResize.id].name,
            extra: "[resize]",
          }),
        },
      ]);
      setRedoStack([]);
    }
    setAreaResize({ id: -1, dir: "none" });
    setInitCoords({
      x: 0,
      y: 0,
      width: 0,
      height: 0,
      pointerX: 0,
      pointerY: 0,
    });
  };

  const handleGripField = () => {
    setPanning((old) => ({ ...old, isPanning: false }));
    setDragging({
      element: ObjectType.NONE,
      id: -1,
      prevX: 0,
      prevY: 0,
      initialPositions: [],
    });
    setLinking(true);
  };

  const handleLinking = () => {
    if (hoveredTable.tableId < 0) return;
    if (hoveredTable.field < 0) return;
    if (
      !areFieldsCompatible(
        database,
        tables[linkingLine.startTableId].fields[linkingLine.startFieldId],
        tables[hoveredTable.tableId].fields[hoveredTable.field],
      )
    ) {
      Toast.info(t("cannot_connect"));
      return;
    }
    if (
      linkingLine.startTableId === hoveredTable.tableId &&
      linkingLine.startFieldId === hoveredTable.field
    )
      return;

    const newRelationship = {
      ...linkingLine,
      endTableId: hoveredTable.tableId,
      endFieldId: hoveredTable.field,
      cardinality: Cardinality.ONE_TO_ONE,
      updateConstraint: Constraint.NONE,
      deleteConstraint: Constraint.NONE,
      name: `fk_${tables[linkingLine.startTableId].name}_${
        tables[linkingLine.startTableId].fields[linkingLine.startFieldId].name
      }_${tables[hoveredTable.tableId].name}`,
      id: relationships.length,
    };
    delete newRelationship.startX;
    delete newRelationship.startY;
    delete newRelationship.endX;
    delete newRelationship.endY;
    addRelationship(newRelationship);
  };

  useEventListener(
    "wheel",
    (e) => {
      e.preventDefault();

      if (e.ctrlKey || e.metaKey) {
        // How "eager" the viewport is to
        // center the cursor's coordinates
        const eagernessFactor = 0.05;
        setTransform((prev) => ({
          pan: {
            x:
              prev.pan.x -
              (pointer.spaces.diagram.x - prev.pan.x) *
                eagernessFactor *
                Math.sign(e.deltaY),
            y:
              prev.pan.y -
              (pointer.spaces.diagram.y - prev.pan.y) *
                eagernessFactor *
                Math.sign(e.deltaY),
          },
          zoom: e.deltaY <= 0 ? prev.zoom * 1.05 : prev.zoom / 1.05,
        }));
      } else if (e.shiftKey) {
        setTransform((prev) => ({
          ...prev,
          pan: {
            ...prev.pan,
            x: prev.pan.x + e.deltaY / prev.zoom,
          },
        }));
      } else {
        setTransform((prev) => ({
          ...prev,
          pan: {
            x: prev.pan.x + e.deltaX / prev.zoom,
            y: prev.pan.y + e.deltaY / prev.zoom,
          },
        }));
      }
    },
    canvasRef,
    { passive: false },
  );

  const theme = localStorage.getItem("theme");

  return (
    <div className="grow h-full touch-none" id="canvas">
      <div
        className="w-full h-full"
        style={{
          cursor: pointer.style,
          backgroundColor: theme === "dark" ? darkBgTheme : "white",
        }}
      >
        {settings.showGrid && (
          <svg className="absolute w-full h-full">
            <defs>
              <pattern
                id="pattern-circles"
                x="0"
                y="0"
                width="24"
                height="24"
                patternUnits="userSpaceOnUse"
                patternContentUnits="userSpaceOnUse"
              >
                <circle
                  id="pattern-circle"
                  cx="4"
                  cy="4"
                  r="0.85"
                  fill="rgb(99, 152, 191)"
                />
              </pattern>
            </defs>
            <rect
              x="0"
              y="0"
              width="100%"
              height="100%"
              fill="url(#pattern-circles)"
            />
          </svg>
        )}
        <svg
          id="diagram"
          ref={canvasRef}
          onPointerMove={handlePointerMove}
          onPointerDown={handlePointerDown}
          onPointerUp={handlePointerUp}
          className="absolute w-full h-full touch-none"
          viewBox={`${viewBox.left} ${viewBox.top} ${viewBox.width} ${viewBox.height}`}
        >
          {areas.map((a) => (
            <Area
              key={a.id}
              data={a}
              onPointerDown={(e) =>
                handlePointerDownOnElement(e, a.id, ObjectType.AREA)
              }
              setResize={setAreaResize}
              setInitCoords={setInitCoords}
            />
          ))}
          {relationships.map((e, i) => (
            <Relationship key={i} data={e} />
          ))}
          {tables.map((table) => (
            <Table
              key={table.id}
              tableData={table}
              setHoveredTable={setHoveredTable}
              handleGripField={handleGripField}
              setLinkingLine={setLinkingLine}
              onPointerDown={(e) =>
                handlePointerDownOnElement(e, table.id, ObjectType.TABLE)
              }
            />
          ))}
          {linking && (
            <path
              d={`M ${linkingLine.startX} ${linkingLine.startY} L ${linkingLine.endX} ${linkingLine.endY}`}
              stroke="red"
              strokeDasharray="8,8"
              className="pointer-events-none touch-none"
            />
          )}
          {notes.map((n) => (
            <Note
              key={n.id}
              data={n}
              onPointerDown={(e) =>
                handlePointerDownOnElement(e, n.id, ObjectType.NOTE)
              }
            />
          ))}
          {bulkSelectRectPts.show && (
            <rect
              {...getRectFromEndpoints(bulkSelectRectPts)}
              stroke="grey"
              fill="grey"
              fillOpacity={0.15}
              strokeDasharray={10}
            />
          )}
        </svg>
      </div>
      {settings.showDebugCoordinates && (
        <div className="fixed flex flex-col flex-wrap gap-6 bg-[rgba(var(--semi-grey-1),var(--tw-bg-opacity))]/40 border border-color bottom-4 right-4 p-4 rounded-xl backdrop-blur-xs pointer-events-none select-none">
          <table className="table-auto grow">
            <thead>
              <tr>
                <th className="text-left" colSpan={3}>
                  {t("transform")}
                </th>
              </tr>
              <tr className="italic [&_th]:font-normal [&_th]:text-right">
                <th>pan x</th>
                <th>pan y</th>
                <th>scale</th>
              </tr>
            </thead>
            <tbody className="[&_td]:text-right [&_td]:min-w-[8ch]">
              <tr>
                <td>{transform.pan.x.toFixed(2)}</td>
                <td>{transform.pan.y.toFixed(2)}</td>
                <td>{transform.zoom.toFixed(4)}</td>
              </tr>
            </tbody>
          </table>
          <table className="table-auto grow [&_th]:text-left [&_th:not(:first-of-type)]:text-right [&_td:not(:first-of-type)]:text-right [&_td]:min-w-[8ch]">
            <thead>
              <tr>
                <th colSpan={4}>{t("viewbox")}</th>
              </tr>
              <tr className="italic [&_th]:font-normal">
                <th>left</th>
                <th>top</th>
                <th>width</th>
                <th>height</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>{viewBox.left.toFixed(2)}</td>
                <td>{viewBox.top.toFixed(2)}</td>
                <td>{viewBox.width.toFixed(2)}</td>
                <td>{viewBox.height.toFixed(2)}</td>
              </tr>
            </tbody>
          </table>
          <table className="table-auto grow [&_th]:text-left [&_th:not(:first-of-type)]:text-right [&_td:not(:first-of-type)]:text-right [&_td]:min-w-[8ch]">
            <thead>
              <tr>
                <th colSpan={3}>{t("cursor_coordinates")}</th>
              </tr>
              <tr className="italic [&_th]:font-normal">
                <th>{t("coordinate_space")}</th>
                <th>x</th>
                <th>y</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>{t("coordinate_space_screen")}</td>
                <td>{pointer.spaces.screen.x.toFixed(2)}</td>
                <td>{pointer.spaces.screen.y.toFixed(2)}</td>
              </tr>
              <tr>
                <td>{t("coordinate_space_diagram")}</td>
                <td>{pointer.spaces.diagram.x.toFixed(2)}</td>
                <td>{pointer.spaces.diagram.y.toFixed(2)}</td>
              </tr>
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}
