(() => {
  "use strict";

  const TABLE_SHAPES = new Set(["rectangle", "square", "round"]);
  const OBJECT_TYPES = new Set([
    "rectangle",
    "circle",
    "line",
    "text",
    "arrow",
    "plant",
    "door",
    "stairs",
  ]);
  const GEOMETRY_PROPERTIES = new Set([
    "x",
    "y",
    "width",
    "height",
    "rotation",
  ]);
  const APPEARANCE_PROPERTIES = new Set([
    "fill",
    "stroke",
    "border_width",
    "text",
  ]);
  const SEAT_SIDE_PROPERTIES = [
    "seat_top",
    "seat_right",
    "seat_bottom",
    "seat_left",
  ];

  const clamp = (value, minimum, maximum) =>
    Math.min(maximum, Math.max(minimum, Number(value) || 0));
  const round = (value, precision = 2) => {
    const multiplier = 10 ** precision;

    return Math.round(Number(value) * multiplier) / multiplier;
  };
  const normalizeRotation = (value) => {
    const rotation = Number(value) || 0;

    return round(((rotation + 180) % 360 + 360) % 360 - 180, 1);
  };
  const setCssVariable = (element, property, value) => {
    const inlineStyles = element.style;

    inlineStyles.setProperty(property, String(value));
  };

  const getDefaultLayout = (layout) => {
    const nextLayout = layout && typeof layout === "object" ? layout : {};

    nextLayout.mode = nextLayout.mode === "grid" ? "grid" : "image";
    nextLayout.image_id = Number(nextLayout.image_id) || 0;
    nextLayout.tables =
      nextLayout.tables && typeof nextLayout.tables === "object"
        ? nextLayout.tables
        : {};
    nextLayout.grid_tables =
      nextLayout.grid_tables && typeof nextLayout.grid_tables === "object"
        ? nextLayout.grid_tables
        : {};
    nextLayout.objects =
      nextLayout.objects && typeof nextLayout.objects === "object"
        ? nextLayout.objects
        : {};
    nextLayout.snap_to_grid = nextLayout.snap_to_grid !== false;
    nextLayout.grid_density =
      nextLayout.grid_density === "fine" ? "fine" : "coarse";

    return nextLayout;
  };

  const getVisualSeatCount = (capacity) =>
    Math.min(16, Math.max(1, Number(capacity) || 1));

  const getBalancedSeatSides = (capacity) => {
    const sides = [0, 0, 0, 0];

    for (let index = 0; index < getVisualSeatCount(capacity); index += 1) {
      sides[index % 4] += 1;
    }

    return sides;
  };

  const normalizeSeatSides = (geometry, capacity) => {
    const target = getVisualSeatCount(capacity);
    const rawSides = SEAT_SIDE_PROPERTIES.map((property) =>
      clamp(geometry?.[property] ?? 0, 0, 16),
    );
    const total = rawSides.reduce((sum, value) => sum + value, 0);

    if (total <= 0) {
      return getBalancedSeatSides(target);
    }
    if (total === target) {
      return rawSides;
    }

    const scaled = rawSides.map((value) => (value / total) * target);
    const normalized = scaled.map((value) => Math.floor(value));
    const priority = scaled
      .map((value, index) => ({ index, remainder: value - normalized[index] }))
      .sort((first, second) => second.remainder - first.remainder);
    let remaining = target - normalized.reduce((sum, value) => sum + value, 0);

    while (remaining > 0) {
      normalized[priority[(target - remaining) % priority.length].index] += 1;
      remaining -= 1;
    }

    return normalized;
  };

  const updateSeatSide = (geometry, property, value, capacity) => {
    const target = getVisualSeatCount(capacity);
    const changedIndex = SEAT_SIDE_PROPERTIES.indexOf(property);
    const requested = Math.round(clamp(value, 0, target));
    const otherIndexes = [0, 1, 2, 3].filter((index) => index !== changedIndex);
    const remaining = target - requested;
    const weights = otherIndexes.map((index) =>
      clamp(geometry[SEAT_SIDE_PROPERTIES[index]], 0, target),
    );
    const weightTotal = weights.reduce((sum, weight) => sum + weight, 0);
    const scaled = weights.map((weight) =>
      weightTotal > 0
        ? (weight / weightTotal) * remaining
        : remaining / otherIndexes.length,
    );
    const distributed = scaled.map((amount) => Math.floor(amount));
    const priority = scaled
      .map((amount, index) => ({
        index,
        remainder: amount - distributed[index],
      }))
      .sort((first, second) => second.remainder - first.remainder);
    let outstanding =
      remaining - distributed.reduce((sum, amount) => sum + amount, 0);

    while (outstanding > 0) {
      distributed[
        priority[(remaining - outstanding) % priority.length].index
      ] += 1;
      outstanding -= 1;
    }

    geometry[property] = requested;
    otherIndexes.forEach((sideIndex, index) => {
      geometry[SEAT_SIDE_PROPERTIES[sideIndex]] = distributed[index];
    });
  };

  const normalizeGeometry = (geometry, kind, capacity = 1) => {
    const nextGeometry =
      geometry && typeof geometry === "object" ? geometry : {};
    const normalized = {
      x: round(clamp(nextGeometry.x || 50, 2, 98)),
      y: round(clamp(nextGeometry.y || 50, 2, 98)),
      width: round(clamp(nextGeometry.width || 16, 4, 60)),
      height: round(clamp(nextGeometry.height || 12, 4, 60)),
      rotation: round(clamp(nextGeometry.rotation || 0, -180, 180), 1),
    };

    if (kind === "table") {
      normalized.shape = TABLE_SHAPES.has(nextGeometry.shape)
        ? nextGeometry.shape
        : "rectangle";
      if (normalized.shape === "square" || normalized.shape === "round") {
        normalized.height = normalized.width;
      }
      normalized.seat_mode =
        nextGeometry.seat_mode === "even" ? "even" : "sides";
      const seatSides = normalizeSeatSides(nextGeometry, capacity);
      SEAT_SIDE_PROPERTIES.forEach((property, index) => {
        normalized[property] = seatSides[index];
      });
    }

    return normalized;
  };

  const applyGeometry = (element, geometry) => {
    setCssVariable(element, "--foodord-grid-x", `${geometry.x}%`);
    setCssVariable(element, "--foodord-grid-y", `${geometry.y}%`);
    setCssVariable(element, "--foodord-grid-width", `${geometry.width}%`);
    setCssVariable(element, "--foodord-grid-height", `${geometry.height}%`);
    setCssVariable(
      element,
      "--foodord-grid-rotation",
      `${geometry.rotation}deg`,
    );
  };

  const applyObjectFlip = (element, isFlipped) => {
    const visual = element.querySelector(
      ".foodord-reservations-grid-builder__object-visual",
    );

    if (visual) {
      setCssVariable(
        visual,
        "--foodord-object-flip-x",
        isFlipped ? "-1" : "1",
      );
    }
  };

  const createSeatPositions = (capacity, geometry) => {
    const safeCount = getVisualSeatCount(capacity);
    const positions = [];

    if (geometry.shape === "round" && geometry.seat_mode === "even") {
      for (let index = 0; index < safeCount; index += 1) {
        const angle = -90 + (index / safeCount) * 360;
        const radians = (angle * Math.PI) / 180;
        positions.push({
          left: 50 + Math.cos(radians) * 50,
          top: 50 + Math.sin(radians) * 50,
          rotation: angle + 90,
        });
      }

      return positions;
    }

    const sideCounts = normalizeSeatSides(geometry, safeCount);
    sideCounts.forEach((sideCount, side) => {
      for (let index = 0; index < sideCount; index += 1) {
        const offset = ((index + 1) / (sideCount + 1)) * 100;

        if (side === 0) {
          positions.push({ left: offset, top: 0, rotation: 0 });
        } else if (side === 1) {
          positions.push({ left: 100, top: offset, rotation: 90 });
        } else if (side === 2) {
          positions.push({ left: 100 - offset, top: 100, rotation: 180 });
        } else {
          positions.push({ left: 0, top: 100 - offset, rotation: -90 });
        }
      }
    });

    return positions;
  };

  const addSeats = (element, capacity, geometry) => {
    const seats = document.createElement("span");
    seats.className = "foodord-reservations-grid-builder__seats";

    createSeatPositions(capacity, geometry).forEach((position) => {
      const seat = document.createElement("span");
      seat.className = "foodord-reservations-grid-builder__seat";
      setCssVariable(seat, "--foodord-seat-left", `${position.left}%`);
      setCssVariable(seat, "--foodord-seat-top", `${position.top}%`);
      setCssVariable(
        seat,
        "--foodord-seat-rotation",
        `${position.rotation}deg`,
      );
      seats.appendChild(seat);
    });

    element.insertBefore(
      seats,
      element.querySelector("[data-foodord-reservation-grid-resize]") || null,
    );
  };

  const refreshSeats = (element, table, geometry) => {
    element
      .querySelector(".foodord-reservations-grid-builder__seats")
      ?.remove();
    addSeats(element, table.capacity, geometry);
  };

  const createResizeHandle = () => {
    const handle = document.createElement("span");
    handle.className = "foodord-reservations-grid-builder__resize-handle";
    handle.dataset.foodordReservationGridResize = "";
    handle.setAttribute("aria-hidden", "true");

    return handle;
  };

  const createRotationHandle = () => {
    const handle = document.createElement("span");

    handle.className = "foodord-reservations-grid-builder__rotation-handle";
    handle.dataset.foodordReservationGridRotate = "";
    handle.setAttribute("aria-hidden", "true");

    return handle;
  };

  const createDoorGraphic = () => {
    const namespace = ["http:/", "/www.w3.org/2000/svg"].join("");
    const graphic = document.createElementNS(namespace, "svg");
    const frame = document.createElementNS(namespace, "path");
    const swing = document.createElementNS(namespace, "path");
    const hinge = document.createElementNS(namespace, "circle");

    graphic.classList.add("foodord-reservations-grid-builder__door-graphic");
    graphic.setAttribute("viewBox", "0 0 100 100");
    graphic.setAttribute("aria-hidden", "true");
    graphic.setAttribute("focusable", "false");
    frame.classList.add("foodord-reservations-grid-builder__door-frame");
    frame.setAttribute("d", "M2 92H10M90 92H98M10 92V12");
    swing.classList.add("foodord-reservations-grid-builder__door-swing");
    swing.setAttribute("d", "M10 12A80 80 0 0 1 90 92");
    hinge.classList.add("foodord-reservations-grid-builder__door-hinge");
    hinge.setAttribute("cx", "10");
    hinge.setAttribute("cy", "92");
    hinge.setAttribute("r", "3.5");
    graphic.append(frame, swing, hinge);

    return graphic;
  };

  const createTableElement = (tableId, table, geometry) => {
    const element = document.createElement("button");
    const label = document.createElement("span");

    element.type = "button";
    element.className = `foodord-reservations-grid-builder__item foodord-reservations-grid-builder__table foodord-reservations-grid-builder__table--${geometry.shape}`;
    element.dataset.foodordReservationGridItem = "table";
    element.dataset.foodordReservationGridItemId = tableId;
    element.setAttribute("aria-label", table.name);
    label.className = "foodord-reservations-grid-builder__table-label";
    label.textContent = table.name;
    element.appendChild(label);
    addSeats(element, table.capacity, geometry);
    element.appendChild(createRotationHandle());
    element.appendChild(createResizeHandle());
    applyGeometry(element, geometry);

    return element;
  };

  const createObjectElement = (objectId, object, label) => {
    const element = document.createElement("button");
    const visual = document.createElement("span");

    element.type = "button";
    element.className = `foodord-reservations-grid-builder__item foodord-reservations-grid-builder__object foodord-reservations-grid-builder__object--${object.type}`;
    element.dataset.foodordReservationGridItem = "object";
    element.dataset.foodordReservationGridItemId = objectId;
    element.setAttribute("aria-label", label);
    visual.className = "foodord-reservations-grid-builder__object-visual";
    visual.textContent = object.type === "text" ? object.text || label : "";
    setCssVariable(visual, "--foodord-object-fill", object.fill || "#f3f4f6");
    setCssVariable(
      visual,
      "--foodord-object-stroke",
      object.stroke || "#6b7280",
    );
    setCssVariable(
      visual,
      "--foodord-object-border-width",
      `${clamp(object.border_width ?? 1, 0, 6)}px`,
    );
    if (object.type === "door") {
      visual.appendChild(createDoorGraphic());
    }
    element.appendChild(visual);
    element.appendChild(createResizeHandle());
    applyGeometry(element, object);
    applyObjectFlip(element, object.flip_x);

    return element;
  };

  const getDefaultTableGeometry = (capacity, index) => {
    const safeCapacity = Math.max(1, Number(capacity) || 1);
    const columns = 4;
    const seatSides = getBalancedSeatSides(safeCapacity);

    return {
      x: 14 + (index % columns) * 24,
      y: 18 + Math.floor(index / columns) * 24,
      width: clamp(10 + safeCapacity * 1.7, 12, 30),
      height: safeCapacity <= 2 ? 12 : 14,
      rotation: 0,
      shape: safeCapacity <= 2 ? "square" : "rectangle",
      seat_mode: "sides",
      seat_top: seatSides[0],
      seat_right: seatSides[1],
      seat_bottom: seatSides[2],
      seat_left: seatSides[3],
    };
  };

  const getDefaultObject = (type, index, defaultText) => {
    const dimensions = {
      rectangle: [18, 12],
      circle: [14, 14],
      line: [20, 4],
      text: [22, 8],
      arrow: [20, 6],
      plant: [10, 10],
      door: [12, 14],
      stairs: [14, 18],
    };
    const [width, height] = dimensions[type] || dimensions.rectangle;
    const isPlant = type === "plant";

    return {
      id: "",
      type,
      x: clamp(50 + (index % 5) * 3, 10, 90),
      y: clamp(50 + (index % 4) * 3, 10, 90),
      width,
      height,
      rotation: 0,
      fill: isPlant ? "#3858e9" : "#f3f4f6",
      stroke: isPlant ? "#2741ba" : "#6b7280",
      border_width: 1,
      text: type === "text" ? defaultText : "",
      flip_x: false,
    };
  };

  const initPlanBuilder = ({ plan, layout, syncLayouts }) => {
    const gridBuilder = plan.querySelector(
      "[data-foodord-reservation-grid-builder]",
    );
    const sidebar = plan.querySelector(
      ".foodord-reservations-grid-builder__sidebar",
    );
    const imageEditor = plan.querySelector(
      "[data-foodord-reservation-floor-plan-image-editor]",
    );
    const canvas = plan.querySelector("[data-foodord-reservation-grid-canvas]");
    const objectsLayer = plan.querySelector(
      "[data-foodord-reservation-grid-objects]",
    );
    const empty = plan.querySelector("[data-foodord-reservation-grid-empty]");
    const selectionActions = plan.querySelector(
      "[data-foodord-reservation-grid-selection-actions]",
    );
    const inspector = plan.querySelector(
      "[data-foodord-reservation-grid-inspector]",
    );
    const snapField = plan.querySelector(
      "[data-foodord-reservation-grid-snap]",
    );
    const densityButtons = Array.from(
      plan.querySelectorAll("[data-foodord-reservation-grid-density]"),
    );
    const resetButton = plan.querySelector(
      "[data-foodord-reservation-grid-reset]",
    );
    const tableOptions = Array.from(
      plan.querySelectorAll("[data-foodord-reservation-grid-add-table]"),
    );
    const shapeOptions = Array.from(
      plan.querySelectorAll("[data-foodord-reservation-grid-add-object]"),
    );
    let selected = null;
    let objectCounter = Object.keys(layout.objects).length;

    if (
      !gridBuilder ||
      !sidebar ||
      !imageEditor ||
      !canvas ||
      !objectsLayer ||
      !empty ||
      !selectionActions ||
      !inspector ||
      !snapField
    ) {
      return null;
    }

    const getTable = (tableId) => {
      const option = tableOptions.find(
        (item) =>
          item.dataset.foodordReservationGridAddTable === String(tableId),
      );

      return option
        ? {
            name: option.dataset.tableName || "",
            capacity: Math.max(1, Number(option.dataset.tableCapacity) || 1),
          }
        : null;
    };

    const getObjectLabel = (type) => {
      const option = shapeOptions.find(
        (item) => item.dataset.foodordReservationGridAddObject === String(type),
      );

      return option ? option.dataset.objectLabel || String(type) : String(type);
    };

    const getSelectedData = () => {
      if (!selected) {
        return null;
      }

      return selected.kind === "table"
        ? layout.grid_tables[selected.id] || null
        : layout.objects[selected.id] || null;
    };

    const updateSelectionActions = () => {
      selectionActions.hidden = !selected || !getSelectedData();
    };

    const updateTableOptions = () => {
      tableOptions.forEach((option) => {
        const tableId = option.dataset.foodordReservationGridAddTable || "";
        const isAdded = Boolean(layout.grid_tables[tableId]);
        const icon = option.querySelector(".dashicons");

        option.classList.toggle("is-added", isAdded);
        option.setAttribute("aria-pressed", String(isAdded));
        if (icon) {
          icon.classList.toggle("dashicons-plus-alt2", !isAdded);
          icon.classList.toggle("dashicons-saved", isAdded);
        }
      });
    };

    const updateInspector = () => {
      const data = getSelectedData();
      const title = inspector.querySelector(
        "[data-foodord-reservation-grid-inspector-title]",
      );
      const shapeField = inspector.querySelector(
        "[data-foodord-reservation-grid-field='shape']",
      );
      const textField = inspector.querySelector(
        "[data-foodord-reservation-grid-field='text']",
      );
      const appearance = inspector.querySelector(
        "[data-foodord-reservation-grid-appearance]",
      );
      const seatSettings = inspector.querySelector(
        "[data-foodord-reservation-grid-seat-settings]",
      );
      const evenSeatsField = inspector.querySelector(
        "[data-foodord-reservation-grid-seat-even-field]",
      );
      const seatSides = inspector.querySelector(
        "[data-foodord-reservation-grid-seat-sides]",
      );
      const flipControl = inspector.querySelector(
        "[data-foodord-reservation-grid-object-flip]",
      );
      const flipButton = inspector.querySelector(
        "[data-foodord-reservation-grid-flip-horizontal]",
      );

      inspector.hidden = !data;
      updateSelectionActions();
      if (!data || !selected) {
        return;
      }

      if (title) {
        const table = selected.kind === "table" ? getTable(selected.id) : null;
        title.textContent = table
          ? table.name
          : getObjectLabel(data.type || "");
      }
      if (shapeField) {
        shapeField.hidden = selected.kind !== "table";
      }
      if (textField) {
        textField.hidden = selected.kind !== "object" || data.type !== "text";
      }
      if (appearance) {
        appearance.hidden = selected.kind !== "object";
      }
      if (seatSettings) {
        seatSettings.hidden = selected.kind !== "table";
      }
      if (evenSeatsField) {
        evenSeatsField.hidden =
          selected.kind !== "table" || data.shape !== "round";
      }
      if (seatSides) {
        seatSides.hidden =
          selected.kind !== "table" ||
          (data.shape === "round" && data.seat_mode === "even");
      }
      if (flipControl) {
        flipControl.hidden = selected.kind !== "object";
      }
      if (flipButton) {
        const isFlipped = selected.kind === "object" && data.flip_x === true;

        flipButton.classList.toggle("is-active", isFlipped);
        flipButton.setAttribute("aria-pressed", String(isFlipped));
      }

      inspector
        .querySelectorAll("[data-foodord-reservation-grid-property]")
        .forEach((field) => {
          const property = field.dataset.foodordReservationGridProperty || "";

          if (Object.prototype.hasOwnProperty.call(data, property)) {
            if (property === "seat_mode" && field instanceof HTMLInputElement) {
              field.checked = data[property] === "even";
            } else {
              field.value = String(data[property]);
            }
          }
        });
    };

    const selectItem = (kind, id) => {
      selected = { kind, id };
      objectsLayer
        .querySelectorAll("[data-foodord-reservation-grid-item]")
        .forEach((item) => {
          item.classList.toggle(
            "is-selected",
            item.dataset.foodordReservationGridItem === kind &&
              item.dataset.foodordReservationGridItemId === id,
          );
        });
      updateInspector();
    };

    const clearSelection = () => {
      selected = null;
      objectsLayer
        .querySelectorAll(".is-selected")
        .forEach((item) => item.classList.remove("is-selected"));
      updateInspector();
    };

    const render = () => {
      objectsLayer.textContent = "";

      Object.entries(layout.objects).forEach(([objectId, rawObject]) => {
        if (!rawObject || !OBJECT_TYPES.has(rawObject.type)) {
          return;
        }

        const object = {
          ...normalizeGeometry(rawObject, "object"),
          id: objectId,
          type: rawObject.type,
          fill: rawObject.fill || "#f3f4f6",
          stroke: rawObject.stroke || "#6b7280",
          border_width: clamp(rawObject.border_width ?? 1, 0, 6),
          text: String(rawObject.text || "").slice(0, 80),
          flip_x: rawObject.flip_x === true,
        };
        layout.objects[objectId] = object;
        objectsLayer.appendChild(
          createObjectElement(objectId, object, getObjectLabel(object.type)),
        );
      });

      Object.entries(layout.grid_tables).forEach(([tableId, rawGeometry]) => {
        const table = getTable(tableId);
        if (!table) {
          delete layout.grid_tables[tableId];
          return;
        }

        const geometry = normalizeGeometry(
          rawGeometry,
          "table",
          table.capacity,
        );
        layout.grid_tables[tableId] = geometry;
        objectsLayer.appendChild(createTableElement(tableId, table, geometry));
      });

      empty.hidden = objectsLayer.childElementCount > 0;
      if (resetButton) {
        resetButton.disabled = objectsLayer.childElementCount === 0;
      }
      updateTableOptions();
      if (selected && !getSelectedData()) {
        selected = null;
      }
      if (selected) {
        selectItem(selected.kind, selected.id);
      } else {
        updateInspector();
      }
    };

    const setMode = (mode, shouldSync = true) => {
      layout.mode = mode === "grid" ? "grid" : "image";
      const isGrid = layout.mode === "grid";

      gridBuilder.hidden = !isGrid;
      imageEditor.hidden = isGrid;
      plan
        .querySelectorAll("[data-foodord-reservation-floor-plan-mode]")
        .forEach((button) => {
          const isActive =
            button.dataset.foodordReservationFloorPlanMode === layout.mode;
          button.classList.toggle("is-active", isActive);
          button.setAttribute("aria-pressed", String(isActive));
        });

      if (isGrid) {
        render();
      } else {
        clearSelection();
      }
      if (shouldSync) {
        syncLayouts();
      }
    };

    const getSnapStep = () => (layout.grid_density === "fine" ? 1 : 2);
    const snapValue = (value) => {
      const step = getSnapStep();

      return layout.snap_to_grid
        ? Math.round(value / step) * step
        : round(value);
    };

    const updateSnapState = () => {
      snapField.checked = layout.snap_to_grid;
      canvas.classList.toggle("is-snap-enabled", layout.snap_to_grid);
      canvas.classList.toggle(
        "is-grid-fine",
        layout.snap_to_grid && layout.grid_density === "fine",
      );
      canvas.classList.toggle(
        "is-grid-coarse",
        layout.snap_to_grid && layout.grid_density === "coarse",
      );
      densityButtons.forEach((button) => {
        const isActive =
          button.dataset.foodordReservationGridDensity ===
          layout.grid_density;

        button.disabled = !layout.snap_to_grid;
        button.classList.toggle("is-active", isActive);
        button.setAttribute("aria-pressed", String(isActive));
      });
    };

    const startPointerOperation = (event, item, isResize) => {
      const kind = item.dataset.foodordReservationGridItem || "";
      const id = item.dataset.foodordReservationGridItemId || "";
      const data =
        kind === "table" ? layout.grid_tables[id] : layout.objects[id];
      const rect = canvas.getBoundingClientRect();

      if (!data || !rect.width || !rect.height) {
        return;
      }

      event.preventDefault();
      selectItem(kind, id);
      item.setPointerCapture(event.pointerId);
      item.classList.add(isResize ? "is-resizing" : "is-dragging");

      const start = {
        clientX: event.clientX,
        clientY: event.clientY,
        x: Number(data.x),
        y: Number(data.y),
        width: Number(data.width),
        height: Number(data.height),
      };

      const update = (pointerEvent) => {
        const deltaX =
          ((pointerEvent.clientX - start.clientX) / rect.width) * 100;
        const deltaY =
          ((pointerEvent.clientY - start.clientY) / rect.height) * 100;

        if (isResize) {
          if (kind === "table") {
            const widthChange = deltaX * 2;
            const heightChange = deltaY * 2;
            const resizeFromWidth =
              Math.abs(widthChange / start.width) >=
              Math.abs(heightChange / start.height);

            if (data.shape === "square" || data.shape === "round") {
              const size = snapValue(
                clamp(
                  start.width + (resizeFromWidth ? widthChange : heightChange),
                  4,
                  60,
                ),
              );

              data.width = size;
              data.height = size;
            } else {
              const aspectRatio = start.width / start.height;
              let width;
              let height;

              if (resizeFromWidth) {
                width = snapValue(clamp(start.width + widthChange, 4, 60));
                height = width / aspectRatio;
              } else {
                height = snapValue(clamp(start.height + heightChange, 4, 60));
                width = height * aspectRatio;
              }

              if (width > 60) {
                width = 60;
                height = width / aspectRatio;
              } else if (height > 60) {
                height = 60;
                width = height * aspectRatio;
              } else if (width < 4) {
                width = 4;
                height = width / aspectRatio;
              } else if (height < 4) {
                height = 4;
                width = height * aspectRatio;
              }

              data.width = round(width);
              data.height = round(height);
            }
          } else {
            data.width = snapValue(clamp(start.width + deltaX * 2, 4, 60));
            data.height = snapValue(clamp(start.height + deltaY * 2, 4, 60));
          }
        } else {
          data.x = snapValue(
            clamp(start.x + deltaX, data.width / 2, 100 - data.width / 2),
          );
          data.y = snapValue(
            clamp(start.y + deltaY, data.height / 2, 100 - data.height / 2),
          );
        }

        applyGeometry(item, data);
        updateInspector();
      };

      const stop = () => {
        item.classList.remove("is-dragging", "is-resizing");
        item.removeEventListener("pointermove", update);
        item.removeEventListener("pointerup", stop);
        item.removeEventListener("pointercancel", stop);
        syncLayouts();
      };

      item.addEventListener("pointermove", update);
      item.addEventListener("pointerup", stop);
      item.addEventListener("pointercancel", stop);
    };

    const startRotationOperation = (event, item) => {
      const kind = item.dataset.foodordReservationGridItem || "";
      const id = item.dataset.foodordReservationGridItemId || "";
      const data = kind === "table" ? layout.grid_tables[id] : null;
      const rect = item.getBoundingClientRect();

      if (!data || !rect.width || !rect.height) {
        return;
      }

      event.preventDefault();
      event.stopPropagation();
      selectItem(kind, id);
      item.setPointerCapture(event.pointerId);
      item.classList.add("is-rotating");

      const centerX = rect.left + rect.width / 2;
      const centerY = rect.top + rect.height / 2;
      const startRotation = Number(data.rotation) || 0;
      let previousAngle = Math.atan2(
        event.clientY - centerY,
        event.clientX - centerX,
      );
      let accumulatedAngle = 0;

      const update = (pointerEvent) => {
        const currentAngle = Math.atan2(
          pointerEvent.clientY - centerY,
          pointerEvent.clientX - centerX,
        );
        let angleChange = currentAngle - previousAngle;

        if (angleChange > Math.PI) {
          angleChange -= Math.PI * 2;
        } else if (angleChange < -Math.PI) {
          angleChange += Math.PI * 2;
        }

        accumulatedAngle += angleChange;
        previousAngle = currentAngle;
        data.rotation = normalizeRotation(
          startRotation + (accumulatedAngle * 180) / Math.PI,
        );
        applyGeometry(item, data);
        updateInspector();
      };

      const stop = () => {
        item.classList.remove("is-rotating");
        item.removeEventListener("pointermove", update);
        item.removeEventListener("pointerup", stop);
        item.removeEventListener("pointercancel", stop);
        syncLayouts();
      };

      item.addEventListener("pointermove", update);
      item.addEventListener("pointerup", stop);
      item.addEventListener("pointercancel", stop);
    };

    const removeSelected = () => {
      if (!selected) {
        return;
      }

      if (selected.kind === "table") {
        delete layout.grid_tables[selected.id];
      } else {
        delete layout.objects[selected.id];
      }
      selected = null;
      render();
      syncLayouts();
    };

    const rotateSelectedItem = (degrees) => {
      const data = getSelectedData();

      if (!selected || !data) {
        return;
      }

      data.rotation = normalizeRotation(data.rotation + degrees);
      const element = objectsLayer.querySelector(
        `[data-foodord-reservation-grid-item='${selected.kind}'][data-foodord-reservation-grid-item-id='${selected.id}']`,
      );
      if (element) {
        applyGeometry(element, data);
      }
      updateInspector();
      syncLayouts();
    };

    const flipSelectedObject = () => {
      const data = getSelectedData();

      if (!selected || selected.kind !== "object" || !data) {
        return;
      }

      data.flip_x = data.flip_x !== true;
      const element = objectsLayer.querySelector(
        `[data-foodord-reservation-grid-item='object'][data-foodord-reservation-grid-item-id='${selected.id}']`,
      );
      if (element) {
        applyObjectFlip(element, data.flip_x);
      }
      updateInspector();
      syncLayouts();
    };

    plan
      .querySelectorAll("[data-foodord-reservation-floor-plan-mode]")
      .forEach((button) => {
        button.addEventListener("click", () =>
          setMode(button.dataset.foodordReservationFloorPlanMode || "image"),
        );
      });

    plan
      .querySelectorAll("[data-foodord-reservation-grid-tab]")
      .forEach((tab) => {
        tab.addEventListener("click", () => {
          const tabName = tab.dataset.foodordReservationGridTab || "tables";

          if (tabName === "shapes") {
            clearSelection();
          }

          plan
            .querySelectorAll("[data-foodord-reservation-grid-tab]")
            .forEach((button) => {
              const isActive =
                button.dataset.foodordReservationGridTab === tabName;
              button.classList.toggle("is-active", isActive);
              button.setAttribute("aria-selected", String(isActive));
            });
          plan
            .querySelectorAll("[data-foodord-reservation-grid-panel]")
            .forEach((panel) => {
              panel.hidden =
                panel.dataset.foodordReservationGridPanel !== tabName;
            });
        });
      });

    tableOptions.forEach((option) => {
      option.addEventListener("click", () => {
        const tableId = option.dataset.foodordReservationGridAddTable || "";
        const table = getTable(tableId);

        if (!table) {
          return;
        }
        if (layout.grid_tables[tableId]) {
          return;
        }

        layout.grid_tables[tableId] = getDefaultTableGeometry(
          table.capacity,
          Object.keys(layout.grid_tables).length,
        );
        selected = null;
        render();
        syncLayouts();
      });
    });

    shapeOptions.forEach((button) => {
      button.addEventListener("click", () => {
        const type = button.dataset.foodordReservationGridAddObject || "";
        if (!OBJECT_TYPES.has(type)) {
          return;
        }

        objectCounter += 1;
        const objectId = `object-${Date.now().toString(36)}-${objectCounter}`;
        const object = getDefaultObject(
          type,
          Object.keys(layout.objects).length,
          getObjectLabel("text"),
        );
        object.id = objectId;
        layout.objects[objectId] = object;
        render();
        selectItem("object", objectId);
        syncLayouts();
      });
    });

    objectsLayer.addEventListener("pointerdown", (event) => {
      const target = event.target instanceof Element ? event.target : null;
      const item = target
        ? target.closest("[data-foodord-reservation-grid-item]")
        : null;
      if (!item) {
        return;
      }

      if (target.closest("[data-foodord-reservation-grid-rotate]")) {
        startRotationOperation(event, item);
        return;
      }

      startPointerOperation(
        event,
        item,
        Boolean(target.closest("[data-foodord-reservation-grid-resize]")),
      );
    });

    objectsLayer.addEventListener("click", (event) => {
      const target = event.target instanceof Element ? event.target : null;
      const item = target
        ? target.closest("[data-foodord-reservation-grid-item]")
        : null;
      if (item) {
        selectItem(
          item.dataset.foodordReservationGridItem || "",
          item.dataset.foodordReservationGridItemId || "",
        );
      }
    });

    const handleInspectorChange = (event) => {
      const field =
        event.target instanceof HTMLInputElement ||
        event.target instanceof HTMLSelectElement
          ? event.target
          : null;
      const data = getSelectedData();
      const property = field
        ? field.dataset.foodordReservationGridProperty || ""
        : "";
      const isSeatProperty =
        SEAT_SIDE_PROPERTIES.includes(property) || property === "seat_mode";

      if (
        !field ||
        !data ||
        !selected ||
        (!GEOMETRY_PROPERTIES.has(property) &&
          !APPEARANCE_PROPERTIES.has(property) &&
          property !== "shape" &&
          !isSeatProperty)
      ) {
        return;
      }

      const table = selected.kind === "table" ? getTable(selected.id) : null;
      if (
        property === "seat_mode" &&
        field instanceof HTMLInputElement &&
        table
      ) {
        data.seat_mode = field.checked ? "even" : "sides";
      } else if (SEAT_SIDE_PROPERTIES.includes(property) && table) {
        updateSeatSide(data, property, field.value, table.capacity);
      } else if (
        GEOMETRY_PROPERTIES.has(property) ||
        property === "border_width"
      ) {
        const minimum =
          property === "rotation"
            ? -180
            : property === "border_width"
              ? 0
              : property === "x" || property === "y"
                ? 2
                : 4;
        const maximum =
          property === "rotation"
            ? 180
            : property === "border_width"
              ? 6
              : property === "x" || property === "y"
                ? 98
                : 60;
        data[property] = clamp(field.value, minimum, maximum);
      } else {
        data[property] = String(field.value).slice(0, 80);
      }

      if (property === "shape" && selected.kind === "table") {
        const previousShape = data.shape;
        data.shape = TABLE_SHAPES.has(field.value) ? field.value : "rectangle";
        if (data.shape === "square" || data.shape === "round") {
          data.height = data.width;
        } else if (
          previousShape !== "rectangle" &&
          Math.abs(Number(data.width) - Number(data.height)) < 0.01
        ) {
          data.height = round(clamp(Number(data.width) * 0.7, 4, 60));
        }
        if (data.shape === "round" && previousShape !== "round") {
          data.seat_mode = "even";
        } else if (data.shape !== "round") {
          data.seat_mode = "sides";
        }
      }
      if (
        (property === "width" || property === "height") &&
        selected.kind === "table" &&
        (data.shape === "square" || data.shape === "round")
      ) {
        const size = Number(data[property]);
        data.width = size;
        data.height = size;
      }

      const element = objectsLayer.querySelector(
        `[data-foodord-reservation-grid-item='${selected.kind}'][data-foodord-reservation-grid-item-id='${selected.id}']`,
      );
      if (element) {
        if (selected.kind === "table") {
          element.classList.remove(
            "foodord-reservations-grid-builder__table--rectangle",
            "foodord-reservations-grid-builder__table--square",
            "foodord-reservations-grid-builder__table--round",
          );
          element.classList.add(
            `foodord-reservations-grid-builder__table--${data.shape}`,
          );
          if (table) {
            refreshSeats(element, table, data);
          }
        }
        if (selected.kind === "object") {
          const visual = element.querySelector(
            ".foodord-reservations-grid-builder__object-visual",
          );
          if (visual) {
            if (data.type === "text") {
              visual.textContent = data.text || getObjectLabel("text");
            } else if (data.type !== "door") {
              visual.textContent = "";
            }
            setCssVariable(visual, "--foodord-object-fill", data.fill);
            setCssVariable(visual, "--foodord-object-stroke", data.stroke);
            setCssVariable(
              visual,
              "--foodord-object-border-width",
              `${data.border_width}px`,
            );
          }
          applyObjectFlip(element, data.flip_x);
        }
        applyGeometry(element, data);
      }
      updateInspector();
      syncLayouts();
    };

    inspector.addEventListener("input", handleInspectorChange);
    inspector.addEventListener("change", (event) => {
      if (
        event.target instanceof HTMLSelectElement ||
        (event.target instanceof HTMLInputElement &&
          event.target.type === "checkbox")
      ) {
        handleInspectorChange(event);
      }
    });

    inspector
      .querySelector("[data-foodord-reservation-grid-remove]")
      ?.addEventListener("click", removeSelected);
    inspector
      .querySelector("[data-foodord-reservation-grid-deselect]")
      ?.addEventListener("click", clearSelection);

    selectionActions
      .querySelector("[data-foodord-reservation-grid-rotate-left]")
      ?.addEventListener("click", () => rotateSelectedItem(-15));
    selectionActions
      .querySelector("[data-foodord-reservation-grid-rotate-right]")
      ?.addEventListener("click", () => rotateSelectedItem(15));
    selectionActions
      .querySelector("[data-foodord-reservation-grid-selection-remove]")
      ?.addEventListener("click", removeSelected);
    inspector
      .querySelector("[data-foodord-reservation-grid-flip-horizontal]")
      ?.addEventListener("click", flipSelectedObject);

    snapField.addEventListener("change", () => {
      layout.snap_to_grid = snapField.checked;
      updateSnapState();
      syncLayouts();
    });

    densityButtons.forEach((button) => {
      button.addEventListener("click", () => {
        if (!layout.snap_to_grid) {
          return;
        }

        layout.grid_density =
          button.dataset.foodordReservationGridDensity === "fine"
            ? "fine"
            : "coarse";
        updateSnapState();
        syncLayouts();
      });
    });

    resetButton?.addEventListener("click", () => {
      if (
        resetButton.disabled ||
        !window.confirm(resetButton.dataset.confirmMessage || "")
      ) {
        return;
      }

      layout.grid_tables = {};
      layout.objects = {};
      selected = null;
      render();
      syncLayouts();
    });

    canvas.addEventListener("keydown", (event) => {
      const data = getSelectedData();
      if (!data || !selected) {
        return;
      }

      if (event.key === "Delete" || event.key === "Backspace") {
        event.preventDefault();
        removeSelected();
        return;
      }

      const movements = {
        ArrowLeft: [-1, 0],
        ArrowRight: [1, 0],
        ArrowUp: [0, -1],
        ArrowDown: [0, 1],
      };
      const movement = movements[event.key];
      if (!movement) {
        return;
      }

      event.preventDefault();
      const increment = event.shiftKey
        ? 0.5
        : layout.snap_to_grid
          ? getSnapStep()
          : 2;
      data.x = clamp(
        data.x + movement[0] * increment,
        data.width / 2,
        100 - data.width / 2,
      );
      data.y = clamp(
        data.y + movement[1] * increment,
        data.height / 2,
        100 - data.height / 2,
      );
      const element = objectsLayer.querySelector(
        `[data-foodord-reservation-grid-item='${selected.kind}'][data-foodord-reservation-grid-item-id='${selected.id}']`,
      );
      if (element) {
        applyGeometry(element, data);
      }
      updateInspector();
      syncLayouts();
    });

    canvas.addEventListener("pointerdown", (event) => {
      if (event.target === canvas || event.target === objectsLayer) {
        clearSelection();
      }
    });

    setMode(layout.mode, false);
    updateSnapState();

    return {
      render,
      destroy() {
        clearSelection();
      },
    };
  };

  window.FoodordReservationFloorPlanBuilder = {
    init({ root, getLayout, syncLayouts }) {
      const controllers = new Map();

      root
        .querySelectorAll("[data-foodord-reservation-floor-plan]")
        .forEach((plan) => {
          const floorId = plan.dataset.foodordReservationFloorPlan || "";
          const controller = initPlanBuilder({
            plan,
            layout: getDefaultLayout(getLayout(floorId)),
            syncLayouts,
          });

          if (controller) {
            controllers.set(floorId, controller);
          }
        });

      return {
        refreshPlan(floorId) {
          controllers.get(String(floorId))?.render();
        },
        destroy() {
          controllers.forEach((controller) => controller.destroy());
          controllers.clear();
        },
      };
    },
  };
})();
