/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck

export function createInteractionController(scope) {
  const documentScope = scope.documentScope || scope;
  const renderScope = scope.renderScope || scope;
  const sessionScope = scope.sessionScope || scope;
  let domEventsBound = false;

  function createCursorUrl(svgMarkup, hotX, hotY, fallback) {
    return `url("data:image/svg+xml,${encodeURIComponent(svgMarkup)}") ${hotX} ${hotY}, ${fallback}`;
  }

  const lineModeCursor = createCursorUrl(
    `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
      <rect x="5" y="8" width="3" height="16" rx="1.5" fill="#08111d"/>
      <rect x="24" y="8" width="3" height="16" rx="1.5" fill="#08111d"/>
      <rect x="6" y="9" width="1" height="14" fill="#cfe8ff"/>
      <rect x="25" y="9" width="1" height="14" fill="#cfe8ff"/>
      <path d="M10 16h8" stroke="#08111d" stroke-width="5" stroke-linecap="round"/>
      <path d="M10 16h8" stroke="#4ff0d1" stroke-width="3" stroke-linecap="round"/>
      <path d="M18 12l6 4-6 4z" fill="#ff6a8a" stroke="#08111d" stroke-width="1.2" stroke-linejoin="round"/>
    </svg>`,
    16,
    16,
    "crosshair",
  );
  const eraserCursor = createCursorUrl(
    `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
      <path d="M8 20l8-8 8 8-6 6H12z" fill="#08111d"/>
      <path d="M9 20l7-7 7 7-5 5h-5z" fill="#ff8d6b" stroke="#08111d" stroke-width="1.2" stroke-linejoin="round"/>
      <path d="M16 13l2-2 7 7-2 2z" fill="#ffd089" stroke="#08111d" stroke-width="1"/>
      <rect x="18" y="24" width="8" height="2" rx="1" fill="#4ff0d1"/>
      <rect x="10" y="24" width="8" height="2" rx="1" fill="#d8e8ff"/>
    </svg>`,
    8,
    24,
    "crosshair",
  );
  const inspectCursor = createCursorUrl(
    `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
      <circle cx="13" cy="13" r="7.5" fill="#dff2ff" stroke="#08111d" stroke-width="2"/>
      <circle cx="13" cy="13" r="4.5" fill="none" stroke="#5fc3ff" stroke-width="1.5"/>
      <path d="M18.5 18.5l7 7" stroke="#08111d" stroke-width="4" stroke-linecap="round"/>
      <path d="M18.5 18.5l7 7" stroke="#ffd36a" stroke-width="2.2" stroke-linecap="round"/>
      <circle cx="11" cy="11" r="1.2" fill="#ffffff" opacity="0.95"/>
    </svg>`,
    10,
    10,
    "crosshair",
  );
  const cursorState = {
    pointerInsideViewport: false,
    overNpc: false,
    leftShiftKey: false,
    rightShiftKey: false,
    leftCtrlKey: false,
    rightCtrlKey: false,
    altKey: false,
    currentCursor: "",
  };
  let zoomRefreshTimer = 0;
  let scrollPreviewTimer = 0;
  let worldSyncFrame = 0;
  let pendingWorldSyncAfterPan = false;
  const dragEdgePan = {
    rafId: 0,
    clientX: 0,
    clientY: 0,
    velocityX: 0,
    velocityY: 0,
  };
  let canvasSelectionFocused = false;

  function setViewportCursor(nextCursor) {
    if (cursorState.currentCursor === nextCursor) {
      return;
    }
    cursorState.currentCursor = nextCursor;
    renderScope.viewport.style.cursor = nextCursor;
  }

  function getNpcDrawWidth(npc) {
    return scope.getScaledSize(npc?.spriteWidth, scope.baseTileSize);
  }

  function getNpcDrawHeight(npc) {
    return scope.getScaledSize(npc?.spriteHeight, scope.baseTileSize);
  }

  function getRowFillChar(layerNumber) {
    return layerNumber === 0 ? "." : " ";
  }

  function getVisibleClearChar(layerNumber) {
    const fillChar = getRowFillChar(layerNumber);
    if (Number(layerNumber) === 0 && fillChar === ".") {
      const hoverTileX = Number(scope.selectedTile?.x);
      const hoverTileY = Number(scope.selectedTile?.y);
      const backgroundTileId = typeof scope.getBackgroundTileIdForLocalTile === "function" && Number.isFinite(hoverTileX) && Number.isFinite(hoverTileY)
        ? String(scope.getBackgroundTileIdForLocalTile(hoverTileX, hoverTileY) || "").trim()
        : String(scope.backgroundTileId || "").trim();
      return String(scope.getTileEntryById(backgroundTileId)?.symbol || scope.getBackgroundTileSymbol() || ".").charAt(0) || ".";
    }
    return fillChar;
  }

  function getEditingLayerNumber() {
    return scope.getEditableLayerNumber();
  }

  function setCanvasSelectionFocus(isFocused) {
    canvasSelectionFocused = isFocused === true;
  }

  function isPointerToolTabActive() {
    return scope.activeSidebarTab === "tiles";
  }

  function isTileSelectModeActive() {
    return isPointerToolTabActive() && scope.canvasToolMode === "select";
  }

  function isElevationToolTabActive() {
    return false;
  }

  function refreshCanvasToolButton() {
    if (!scope.canvasSelectToolBtn) {
      return;
    }
    const isActive = scope.canvasToolMode === "select";
    scope.canvasSelectToolBtn.classList.toggle("active", isActive);
    scope.canvasSelectToolBtn.setAttribute("aria-pressed", isActive ? "true" : "false");
    scope.canvasSelectToolBtn.setAttribute("title", isActive ? "Tile selector: on" : "Tile selector: off");
    scope.canvasSelectToolBtn.setAttribute("aria-label", isActive ? "Disable tile selector" : "Enable tile selector");
  }

  function setCanvasToolMode(nextMode, options) {
    const normalizedMode = nextMode === "select" ? "select" : "paint";
    const force = options && options.force === true;
    const silent = options && options.silent === true;
    const skipDraw = options && options.skipDraw === true;
    if (!force && scope.canvasToolMode === normalizedMode) {
      refreshCanvasToolButton();
      return false;
    }
    scope.canvasToolMode = normalizedMode;
    if (scope.paintingStroke) {
      cancelPaintStroke();
    }
    scope.pointerCandidate = null;
    refreshCanvasToolButton();
    updateViewportCursor();
    if (!skipDraw) {
      scope.draw();
    }
    if (!silent) {
      scope.setStatus(
        normalizedMode === "select"
          ? "Tile selector enabled. Click a cell to select it."
          : "Tile brush enabled. Click or drag to paint.",
        false,
      );
    }
    return true;
  }

  function toggleCanvasToolMode() {
    const nextMode = scope.canvasToolMode === "select" ? "paint" : "select";
    if (nextMode === "select" && scope.activeSidebarTab !== "tiles") {
      scope.setSidebarTab("tiles");
    }
    return setCanvasToolMode(nextMode);
  }

  function updateViewportCursor() {
    if (scope.pan.isPanning || scope.draggingNpc) {
      setViewportCursor("grabbing");
      return;
    }
    if (isTileSelectModeActive()) {
      setViewportCursor(inspectCursor);
      return;
    }
    if (cursorState.overNpc && !scope.paintingStroke) {
      setViewportCursor("grab");
      return;
    }
    if (isPointerToolTabActive()) {
      if (scope.paintingStroke && scope.paintingStroke.mode === "erase") {
        setViewportCursor(eraserCursor);
        return;
      }
      if (!scope.paintingStroke && cursorState.altKey) {
        setViewportCursor(eraserCursor);
        return;
      }
      if (scope.paintingStroke && (scope.paintingStroke.lineLockAxis || cursorState.leftShiftKey)) {
        setViewportCursor(lineModeCursor);
        return;
      }
      if (!scope.paintingStroke && cursorState.leftShiftKey) {
        setViewportCursor(lineModeCursor);
        return;
      }
    }
    setViewportCursor("crosshair");
  }

  function getActiveShapeMode(leftCtrlKey, rightCtrlKey, ctrlKey) {
    if (rightCtrlKey) {
      return "circle";
    }
    if (leftCtrlKey || ctrlKey) {
      return "rectangle";
    }
    return "";
  }

  function updateModifierKeyState(event) {
    const previousLeftShift = cursorState.leftShiftKey;
    const previousRightShift = cursorState.rightShiftKey;
    const previousLeftCtrl = cursorState.leftCtrlKey;
    const previousRightCtrl = cursorState.rightCtrlKey;
    const previousAlt = cursorState.altKey;
    if (event.type === "keydown" || event.type === "keyup") {
      if (event.code === "ShiftLeft") {
        cursorState.leftShiftKey = event.type === "keydown";
      } else if (event.code === "ShiftRight") {
        cursorState.rightShiftKey = event.type === "keydown";
      } else if (event.code === "ControlLeft") {
        cursorState.leftCtrlKey = event.type === "keydown";
      } else if (event.code === "ControlRight") {
        cursorState.rightCtrlKey = event.type === "keydown";
      }
    } else if (event.shiftKey !== true) {
      cursorState.leftShiftKey = false;
      cursorState.rightShiftKey = false;
    }
    if (event.type !== "keydown" && event.type !== "keyup" && event.ctrlKey !== true) {
      cursorState.leftCtrlKey = false;
      cursorState.rightCtrlKey = false;
    }
    cursorState.altKey = event.altKey === true;
    const previousHideGrid = scope.hideTileGrid;
    if (scope.hideTileGrid !== cursorState.rightShiftKey) {
      scope.hideTileGrid = cursorState.rightShiftKey;
    }
    return previousLeftShift !== cursorState.leftShiftKey
      || previousRightShift !== cursorState.rightShiftKey
      || previousLeftCtrl !== cursorState.leftCtrlKey
      || previousRightCtrl !== cursorState.rightCtrlKey
      || previousAlt !== cursorState.altKey
      || previousHideGrid !== scope.hideTileGrid;
  }

  function isClientPointInsideViewport(clientX, clientY) {
    const rect = scope.viewport.getBoundingClientRect();
    return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom;
  }

  function getInteractiveSurfaceRect() {
    const candidates = [
      scope.canvas,
      scope.pixiHost,
      renderScope.viewport,
    ];
    for (const candidate of candidates) {
      if (!candidate || typeof candidate.getBoundingClientRect !== "function") {
        continue;
      }
      const rect = candidate.getBoundingClientRect();
      if (rect && rect.width > 0 && rect.height > 0) {
        return rect;
      }
    }
    return renderScope.viewport.getBoundingClientRect();
  }

  function isNpcPlaced(npc) {
    if (!npc) {
      return false;
    }
    const x = Number(npc.x);
    const y = Number(npc.y);
    return Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0;
  }

  function getSelectedUnplacedNpc() {
    const selectedNpc = documentScope.npcOverlays.find((npc) => npc.id === sessionScope.selectedNpcId) || null;
    if (!selectedNpc || isNpcPlaced(selectedNpc)) {
      return null;
    }
    return selectedNpc;
  }

  function getCanvasPointFromClient(clientX, clientY) {
    const rect = getInteractiveSurfaceRect();
    return {
      x: (clientX - rect.left) + (Number(renderScope.viewport?.scrollLeft) || 0),
      y: (clientY - rect.top) + (Number(renderScope.viewport?.scrollTop) || 0),
    };
  }

  function updateDraggingNpcDrawPositionFromClient(clientX, clientY) {
    if (!sessionScope.draggingNpc) {
      return;
    }
    const { x: canvasX, y: canvasY } = getCanvasPointFromClient(clientX, clientY);
    sessionScope.dragDrawX = canvasX - sessionScope.draggingNpc.offsetX;
    sessionScope.dragDrawY = canvasY - sessionScope.draggingNpc.offsetY;
  }

  function stopDragEdgePan() {
    dragEdgePan.velocityX = 0;
    dragEdgePan.velocityY = 0;
    if (dragEdgePan.rafId) {
      window.cancelAnimationFrame(dragEdgePan.rafId);
      dragEdgePan.rafId = 0;
    }
  }

  function stepDragEdgePan() {
    dragEdgePan.rafId = 0;
    if (!sessionScope.draggingNpc) {
      stopDragEdgePan();
      return;
    }
    const worldPixelWidth = Math.max(1, Number(documentScope.width) * Number(scope.tileSize));
    const worldPixelHeight = Math.max(1, Number(documentScope.height) * Number(scope.tileSize));
    const maxScrollLeft = Math.max(0, worldPixelWidth - renderScope.viewport.clientWidth);
    const maxScrollTop = Math.max(0, worldPixelHeight - renderScope.viewport.clientHeight);
    const nextScrollLeft = Math.max(0, Math.min(maxScrollLeft, renderScope.viewport.scrollLeft + dragEdgePan.velocityX));
    const nextScrollTop = Math.max(0, Math.min(maxScrollTop, renderScope.viewport.scrollTop + dragEdgePan.velocityY));
    const scrollChanged = nextScrollLeft !== renderScope.viewport.scrollLeft || nextScrollTop !== renderScope.viewport.scrollTop;
    if (scrollChanged) {
      renderScope.viewport.scrollLeft = nextScrollLeft;
      renderScope.viewport.scrollTop = nextScrollTop;
    }
    updateDraggingNpcDrawPositionFromClient(dragEdgePan.clientX, dragEdgePan.clientY);
    renderScope.draw();
    if (dragEdgePan.velocityX || dragEdgePan.velocityY) {
      dragEdgePan.rafId = window.requestAnimationFrame(stepDragEdgePan);
    }
  }

  function updateDragEdgePanFromPointer(clientX, clientY) {
    if (!sessionScope.draggingNpc) {
      stopDragEdgePan();
      return;
    }
    const rect = renderScope.viewport.getBoundingClientRect();
    const padX = Math.max(40, Math.min(96, Math.round(rect.width * 0.12)));
    const padY = Math.max(40, Math.min(96, Math.round(rect.height * 0.12)));
    const maxSpeed = Math.max(10, Math.round(scope.tileSize * 0.55));
    dragEdgePan.clientX = clientX;
    dragEdgePan.clientY = clientY;

    function axisVelocity(distanceIntoPad, pad) {
      if (distanceIntoPad <= 0 || pad <= 0) {
        return 0;
      }
      const ratio = Math.max(0, Math.min(1, distanceIntoPad / pad));
      return Math.max(2, Math.round(maxSpeed * ratio));
    }

    let velocityX = 0;
    let velocityY = 0;
    if (clientX < rect.left + padX) {
      velocityX = -axisVelocity((rect.left + padX) - clientX, padX);
    } else if (clientX > rect.right - padX) {
      velocityX = axisVelocity(clientX - (rect.right - padX), padX);
    }
    if (clientY < rect.top + padY) {
      velocityY = -axisVelocity((rect.top + padY) - clientY, padY);
    } else if (clientY > rect.bottom - padY) {
      velocityY = axisVelocity(clientY - (rect.bottom - padY), padY);
    }
    dragEdgePan.velocityX = velocityX;
    dragEdgePan.velocityY = velocityY;

    if ((velocityX || velocityY) && !dragEdgePan.rafId) {
      dragEdgePan.rafId = window.requestAnimationFrame(stepDragEdgePan);
    } else if (!velocityX && !velocityY) {
      stopDragEdgePan();
    }
  }

  function scheduleScrollPreview() {
    renderScope.startScrollPreview(90);
    renderScope.draw();
    if (scrollPreviewTimer) {
      window.clearTimeout(scrollPreviewTimer);
    }
    scrollPreviewTimer = window.setTimeout(() => {
      scrollPreviewTimer = 0;
      sessionScope.scrollPreviewUntil = 0;
      renderScope.drawNow();
    }, 105);
  }

  function cancelScheduledWorldSync() {
    if (!worldSyncFrame) {
      return;
    }
    window.cancelAnimationFrame(worldSyncFrame);
    worldSyncFrame = 0;
  }

  function flushWorldSyncAfterPan() {
    pendingWorldSyncAfterPan = false;
    if (typeof scope.syncWorldNeighborhoodForViewport !== "function") {
      return;
    }
    cancelScheduledWorldSync();
    worldSyncFrame = window.requestAnimationFrame(() => {
      worldSyncFrame = 0;
      scope.syncWorldNeighborhoodForViewport();
    });
  }

  function scheduleWorldSyncFromScroll() {
    if (typeof scope.syncWorldNeighborhoodForViewport !== "function") {
      return;
    }
    if (scope.pan.isPanning) {
      pendingWorldSyncAfterPan = true;
      return;
    }
    if (scope.draggingNpc || scope.paintingStroke) {
      return;
    }
    if (worldSyncFrame) {
      return;
    }
    worldSyncFrame = window.requestAnimationFrame(() => {
      worldSyncFrame = 0;
      scope.syncWorldNeighborhoodForViewport();
    });
  }

  function stopPanInteraction(options) {
    if (!scope.pan.isPanning) {
      return false;
    }
    scope.pan.isPanning = false;
    scope.drawNow();
    if (!(options && options.skipWorldSync === true) && pendingWorldSyncAfterPan) {
      flushWorldSyncAfterPan();
    }
    updateViewportCursor();
    return true;
  }

  function selectTile(tileX, tileY) {
    const targetLayer = getEditingLayerNumber();
    scope.setLayerVisibility(targetLayer, true);
    scope.selectedTile = { x: tileX, y: tileY, layer: targetLayer };
    scope.selectedNpcId = "";
    scope.setSidebarTab("tiles");
    scope.refreshInformationPanel?.();
    scope.draw();
    scope.setStatus("Selected tile (" + tileX + "," + tileY + ") on layer " + targetLayer + ".", false);
  }

  function setTileCharAt(layerNumber, tileX, tileY, ch, tileIdOverride, options) {
    scope.ensureWorldDocumentCurrent?.();
    const layerEntry = scope.roomLayers.find((layer) => Number(layer.layer) === layerNumber);
    if (!layerEntry || tileX < 0 || tileX >= scope.width || tileY < 0 || tileY >= scope.height) {
      return null;
    }
    const fillChar = getRowFillChar(layerNumber);
    const rows = scope.normalizeRows(layerEntry.rows, fillChar);
    const row = rows[tileY] || fillChar.repeat(scope.width);
    const beforeCharRaw = row.charAt(tileX) || fillChar;
    const implicitTileId = Number(layerNumber) === 0
      ? String((typeof scope.getBackgroundTileIdForLocalTile === "function"
        ? scope.getBackgroundTileIdForLocalTile(tileX, tileY)
        : scope.backgroundTileId) || "").trim()
      : "";
    const implicitTileSymbol = implicitTileId
      ? (String(scope.getTileEntryById(implicitTileId)?.symbol || scope.getBackgroundTileSymbol() || ".").charAt(0) || ".")
      : "";
    const beforeChar = beforeCharRaw === fillChar && implicitTileSymbol ? implicitTileSymbol : beforeCharRaw;
    const nextTileId = String(tileIdOverride || "").trim();
    const forceExplicit = options && options.forceExplicit === true;
    const shouldUseImplicitBackground = !forceExplicit && Boolean(implicitTileId) && nextTileId === implicitTileId;
    const nextStoredChar = shouldUseImplicitBackground ? fillChar : ch;
    if (beforeCharRaw === nextStoredChar) {
      return null;
    }

    rows[tileY] = row.slice(0, tileX) + nextStoredChar + row.slice(tileX + 1);
    layerEntry.rows = rows;
    if (typeof scope.syncWorldChunkCellFromLocalTile === "function") {
      scope.syncWorldChunkCellFromLocalTile(layerNumber, tileX, tileY, nextStoredChar);
    }
    const shouldPatchSurface = !!scope.patchTileSurfaceCell && (scope.paintingStroke || (Number(scope.tileMutationBatchDepth) || 0) <= 0);
    if (shouldPatchSurface) {
      scope.patchTileSurfaceCell(tileX, tileY, "tile-paint");
    } else {
      scope.invalidateTileSurface("tile-paint-batch");
    }
    return beforeChar;
  }

  function applyBackgroundCellModeAt(tileX, tileY) {
    const targetLayer = getEditingLayerNumber();
    if (targetLayer !== 0 || tileX < 0 || tileX >= scope.width || tileY < 0 || tileY >= scope.height) {
      return false;
    }
    const mode = String(scope.backgroundCellMode || "inherit");
    const backgroundTileId = String((typeof scope.getBackgroundTileIdForLocalTile === "function"
      ? scope.getBackgroundTileIdForLocalTile(tileX, tileY)
      : scope.backgroundTileId) || "").trim();
    const beforeState = getCellPaintState(targetLayer, tileX, tileY);
    let appliedChar;
    if (mode === "tile") {
      if (!backgroundTileId) {
        scope.setStatus("Fill Background first so there is a background tile to stamp.", true);
        return false;
      }
      const tileEntry = scope.getTileEntryById(backgroundTileId);
      const symbol = String(tileEntry.symbol || ".").charAt(0) || ".";
      appliedChar = setTileCharAt(targetLayer, tileX, tileY, symbol, backgroundTileId, { forceExplicit: true });
    } else if (mode === "hole") {
      appliedChar = setTileCharAt(targetLayer, tileX, tileY, " ", "");
    } else {
      const tileEntry = backgroundTileId ? scope.getTileEntryById(backgroundTileId) : null;
      const symbol = String(tileEntry?.symbol || ".").charAt(0) || ".";
      appliedChar = setTileCharAt(targetLayer, tileX, tileY, symbol, backgroundTileId);
    }
    if (appliedChar == null) {
      return false;
    }
    const afterState = getCellPaintState(targetLayer, tileX, tileY);
    scope.selectedTile = { x: tileX, y: tileY, layer: targetLayer };
    scope.selectedNpcId = "";
    scope.draw();
    const afterLabel = mode === "tile"
      ? "explicit background tile"
      : (mode === "hole" ? "transparent hole" : "inherit");
    scope.registerHistory("Background brush", beforeChar, afterLabel, [
      "Layer: " + targetLayer,
      "Tile: (" + tileX + "," + tileY + ")",
      "Mode: " + afterLabel,
    ], {
      operation: buildTileCellsHistoryOperation([{
        layer: targetLayer,
        x: tileX,
        y: tileY,
        beforeStoredChar: beforeState ? beforeState.storedChar : ".",
        afterStoredChar: afterState ? afterState.storedChar : ".",
      }]),
    });
    scope.setStatus("Applied background brush: " + afterLabel + ".", false);
    return true;
  }

  function getStrokeBrushSymbol(stroke) {
    if (!stroke || stroke.mode === "erase") {
      return getRowFillChar(stroke?.layer ?? getEditingLayerNumber());
    }
    const tileEntry = scope.getTileEntryById(stroke.brush);
    return String(tileEntry?.symbol || ".").charAt(0) || ".";
  }

  function getCellPaintState(layerNumber, tileX, tileY) {
    scope.ensureWorldDocumentCurrent?.();
    const fillChar = getRowFillChar(layerNumber);
    const layerEntry = scope.roomLayers.find((layer) => Number(layer.layer) === Number(layerNumber));
    if (!layerEntry || tileX < 0 || tileX >= scope.width || tileY < 0 || tileY >= scope.height) {
      return null;
    }
    const rows = scope.normalizeRows(layerEntry.rows, fillChar);
    const row = rows[tileY] || fillChar.repeat(scope.width);
    const storedChar = row.charAt(tileX) || fillChar;
    const implicitTileId = Number(layerNumber) === 0
      ? String((typeof scope.getBackgroundTileIdForLocalTile === "function"
        ? scope.getBackgroundTileIdForLocalTile(tileX, tileY)
        : scope.backgroundTileId) || "").trim()
      : "";
    const implicitTileSymbol = implicitTileId
      ? (String(scope.getTileEntryById(implicitTileId)?.symbol || scope.getBackgroundTileSymbol() || ".").charAt(0) || ".")
      : "";
    const visibleChar = storedChar === fillChar && implicitTileSymbol ? implicitTileSymbol : storedChar;
    return {
      storedChar,
      tileId: "",
      visibleChar,
    };
  }

  function buildTileCellsHistoryOperation(cells, options) {
    const config = options && typeof options === "object" ? options : {};
    const worldOffsetX = Math.floor(Number(scope.worldTileOffsetX) || 0);
    const worldOffsetY = Math.floor(Number(scope.worldTileOffsetY) || 0);
    const includeWorldCoords = typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive();
    const operation = {
      type: "tile_cells",
      cells: Array.isArray(cells)
        ? cells
          .map((cell) => {
            const localX = Math.floor(Number(cell.x) || 0);
            const localY = Math.floor(Number(cell.y) || 0);
            const normalizedCell = {
              layer: Number(cell.layer) || 0,
              x: localX,
              y: localY,
              beforeStoredChar: String(cell.beforeStoredChar || ".").charAt(0) || ".",
              afterStoredChar: String(cell.afterStoredChar || ".").charAt(0) || ".",
            };
            if (includeWorldCoords) {
              normalizedCell.worldX = worldOffsetX + localX;
              normalizedCell.worldY = worldOffsetY + localY;
            }
            return normalizedCell;
          })
          .filter((cell) => cell.beforeStoredChar !== cell.afterStoredChar)
        : [],
    };
    if (config.beforeBackgroundTileId !== undefined) {
      operation.beforeBackgroundTileId = String(config.beforeBackgroundTileId || "").trim();
    }
    if (config.afterBackgroundTileId !== undefined) {
      operation.afterBackgroundTileId = String(config.afterBackgroundTileId || "").trim();
    }
    return operation;
  }

  function restoreStrokeCell(stroke, cell) {
    if (!stroke || !cell) {
      return;
    }
    if (stroke.kind === "height") {
      setHeightLayerStoredCharAt(stroke.heightLayerId, cell.x, cell.y, cell.beforeStoredChar);
      return;
    }
    const beforeTileId = String(cell.beforeTileId || "").trim();
    if (beforeTileId) {
      const symbol = String(scope.getTileEntryById(beforeTileId)?.symbol || cell.beforeStoredChar || ".").charAt(0) || ".";
      setTileCharAt(stroke.layer, cell.x, cell.y, symbol, beforeTileId, { forceExplicit: true });
      return;
    }
    setTileCharAt(stroke.layer, cell.x, cell.y, cell.beforeStoredChar, "");
  }

  function appendUniqueCell(cellMap, cells, tileX, tileY) {
    if (tileX < 0 || tileX >= scope.width || tileY < 0 || tileY >= scope.height) {
      return;
    }
    const key = tileX + ":" + tileY;
    if (cellMap.has(key)) {
      return;
    }
    cellMap.add(key);
    cells.push({ x: tileX, y: tileY });
  }

  function getRectangleOutlineCells(fromX, fromY, toX, toY) {
    const minX = Math.min(fromX, toX);
    const maxX = Math.max(fromX, toX);
    const minY = Math.min(fromY, toY);
    const maxY = Math.max(fromY, toY);
    const cellMap = new Set();
    const cells = [];
    for (let x = minX; x <= maxX; x += 1) {
      appendUniqueCell(cellMap, cells, x, minY);
      appendUniqueCell(cellMap, cells, x, maxY);
    }
    for (let y = minY; y <= maxY; y += 1) {
      appendUniqueCell(cellMap, cells, minX, y);
      appendUniqueCell(cellMap, cells, maxX, y);
    }
    return cells;
  }

  function getCircleOutlineCells(centerX, centerY, targetX, targetY) {
    const radius = Math.max(0, Math.round(Math.hypot(targetX - centerX, targetY - centerY)));
    const cellMap = new Set();
    const cells = [];
    if (radius <= 0) {
      appendUniqueCell(cellMap, cells, centerX, centerY);
      return cells;
    }
    let x = radius;
    let y = 0;
    let decision = 1 - radius;
    while (x >= y) {
      appendUniqueCell(cellMap, cells, centerX + x, centerY + y);
      appendUniqueCell(cellMap, cells, centerX + y, centerY + x);
      appendUniqueCell(cellMap, cells, centerX - y, centerY + x);
      appendUniqueCell(cellMap, cells, centerX - x, centerY + y);
      appendUniqueCell(cellMap, cells, centerX - x, centerY - y);
      appendUniqueCell(cellMap, cells, centerX - y, centerY - x);
      appendUniqueCell(cellMap, cells, centerX + y, centerY - x);
      appendUniqueCell(cellMap, cells, centerX + x, centerY - y);
      y += 1;
      if (decision <= 0) {
        decision += (2 * y) + 1;
      } else {
        x -= 1;
        decision += (2 * (y - x)) + 1;
      }
    }
    return cells;
  }

  function getRectangleFillCells(fromX, fromY, toX, toY) {
    const minX = Math.min(fromX, toX);
    const maxX = Math.max(fromX, toX);
    const minY = Math.min(fromY, toY);
    const maxY = Math.max(fromY, toY);
    const cells = [];
    const cellMap = new Set();
    for (let y = minY; y <= maxY; y += 1) {
      for (let x = minX; x <= maxX; x += 1) {
        appendUniqueCell(cellMap, cells, x, y);
      }
    }
    return cells;
  }

  function getCircleFillCells(centerX, centerY, targetX, targetY) {
    const radius = Math.max(0, Math.round(Math.hypot(targetX - centerX, targetY - centerY)));
    const cells = [];
    const cellMap = new Set();
    for (let y = centerY - radius; y <= centerY + radius; y += 1) {
      for (let x = centerX - radius; x <= centerX + radius; x += 1) {
        if (Math.hypot(x - centerX, y - centerY) <= radius + 0.25) {
          appendUniqueCell(cellMap, cells, x, y);
        }
      }
    }
    if (cells.length === 0) {
      appendUniqueCell(cellMap, cells, centerX, centerY);
    }
    return cells;
  }

  function normalizeHeightPatchRows(rows) {
    return Array.isArray(rows)
      ? rows.map((row) => String(row || "").replace(/\./g, " "))
      : [];
  }

  function trimHeightPatchRows(rows, x, y) {
    const normalizedRows = normalizeHeightPatchRows(rows).map((row) => row.replace(/\s+$/g, ""));
    let top = 0;
    let bottom = normalizedRows.length - 1;
    while (top <= bottom && !normalizedRows[top].split("").some((ch) => ch !== " ")) {
      top += 1;
    }
    while (bottom >= top && !normalizedRows[bottom].split("").some((ch) => ch !== " ")) {
      bottom -= 1;
    }
    if (top > bottom) {
      return { x: Math.max(0, Number(x) || 0), y: Math.max(0, Number(y) || 0), rows: [] };
    }
    const croppedRows = normalizedRows.slice(top, bottom + 1);
    let left = Number.POSITIVE_INFINITY;
    let right = -1;
    croppedRows.forEach((row) => {
      row.split("").forEach((ch, index) => {
        if (ch === " ") {
          return;
        }
        left = Math.min(left, index);
        right = Math.max(right, index);
      });
    });
    if (!Number.isFinite(left) || right < left) {
      return { x: Math.max(0, Number(x) || 0), y: Math.max(0, Number(y) || 0), rows: [] };
    }
    return {
      x: Math.max(0, Number(x) || 0) + left,
      y: Math.max(0, Number(y) || 0) + top,
      rows: croppedRows.map((row) => row.slice(left, right + 1).replace(/\s+$/g, "")),
    };
  }

  function getHeightLayerCellState(heightLayerId, tileX, tileY) {
    if (tileX < 0 || tileX >= scope.width || tileY < 0 || tileY >= scope.height) {
      return null;
    }
    const entry = scope.getHeightLayerById(heightLayerId);
    if (!entry) {
      return null;
    }
    const patchX = Math.max(0, Number(entry.x) || 0);
    const patchY = Math.max(0, Number(entry.y) || 0);
    const localX = tileX - patchX;
    const localY = tileY - patchY;
    if (localX < 0 || localY < 0) {
      return { storedChar: " ", visibleChar: " " };
    }
    const row = String((Array.isArray(entry.rows) ? entry.rows[localY] : "") || "");
    const storedChar = String(row.charAt(localX) || " ").charAt(0) || " ";
    return {
      storedChar,
      visibleChar: storedChar === "." ? " " : storedChar,
    };
  }

  function setHeightLayerStoredCharAt(heightLayerId, tileX, tileY, nextStoredChar) {
    if (tileX < 0 || tileX >= scope.width || tileY < 0 || tileY >= scope.height) {
      return null;
    }
    const entry = scope.getHeightLayerById(heightLayerId);
    if (!entry) {
      return null;
    }
    const beforeState = getHeightLayerCellState(heightLayerId, tileX, tileY);
    const safeChar = String(nextStoredChar || " ").charAt(0) || " ";
    if (beforeState && beforeState.storedChar === safeChar) {
      return null;
    }
    const patchX = Math.max(0, Number(entry.x) || 0);
    const patchY = Math.max(0, Number(entry.y) || 0);
    const sourceRows = normalizeHeightPatchRows(entry.rows);
    const sourceHeight = sourceRows.length;
    const sourceWidth = sourceRows.reduce((max, row) => Math.max(max, String(row || "").length), 0);
    const beforeBounds = sourceWidth > 0 && sourceHeight > 0
      ? {
        minX: patchX,
        minY: patchY,
        maxX: patchX + sourceWidth - 1,
        maxY: patchY + sourceHeight - 1,
      }
      : {
        minX: tileX,
        minY: tileY,
        maxX: tileX,
        maxY: tileY,
      };
    const sourceMaxX = sourceWidth > 0 ? patchX + sourceWidth - 1 : patchX;
    const sourceMaxY = sourceHeight > 0 ? patchY + sourceHeight - 1 : patchY;
    const nextMinX = sourceWidth > 0 ? Math.min(patchX, tileX) : tileX;
    const nextMinY = sourceHeight > 0 ? Math.min(patchY, tileY) : tileY;
    const nextMaxX = sourceWidth > 0 ? Math.max(sourceMaxX, tileX) : tileX;
    const nextMaxY = sourceHeight > 0 ? Math.max(sourceMaxY, tileY) : tileY;
    const nextWidth = Math.max(1, nextMaxX - nextMinX + 1);
    const nextHeight = Math.max(1, nextMaxY - nextMinY + 1);
    const nextRows = Array.from({ length: nextHeight }, () => " ".repeat(nextWidth).split(""));
    sourceRows.forEach((row, sourceRowIndex) => {
      const sourceY = patchY + sourceRowIndex;
      row.split("").forEach((ch, sourceColIndex) => {
        const sourceX = patchX + sourceColIndex;
        const destX = sourceX - nextMinX;
        const destY = sourceY - nextMinY;
        if (destX < 0 || destY < 0 || destY >= nextRows.length || destX >= nextRows[destY].length) {
          return;
        }
        nextRows[destY][destX] = ch && ch !== "." ? ch : " ";
      });
    });
    nextRows[tileY - nextMinY][tileX - nextMinX] = safeChar === "." ? " " : safeChar;
    const trimmed = trimHeightPatchRows(nextRows.map((row) => row.join("")), nextMinX, nextMinY);
    entry.x = trimmed.x;
    entry.y = trimmed.y;
    entry.rows = trimmed.rows;
    scope.heightLayers = scope.cloneHeightLayers(scope.heightLayers);
    if (typeof scope.rebuildWorldChunksForLocalBounds === "function" && typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive()) {
      const trimmedWidth = trimmed.rows.reduce((max, row) => Math.max(max, String(row || "").length), 0);
      const trimmedHeight = trimmed.rows.length;
      const afterBounds = trimmedWidth > 0 && trimmedHeight > 0
        ? {
          minX: Math.max(0, Number(trimmed.x) || 0),
          minY: Math.max(0, Number(trimmed.y) || 0),
          maxX: Math.max(0, Number(trimmed.x) || 0) + trimmedWidth - 1,
          maxY: Math.max(0, Number(trimmed.y) || 0) + trimmedHeight - 1,
        }
        : {
          minX: tileX,
          minY: tileY,
          maxX: tileX,
          maxY: tileY,
        };
      scope.rebuildWorldChunksForLocalBounds({
        minX: Math.min(beforeBounds.minX, afterBounds.minX),
        minY: Math.min(beforeBounds.minY, afterBounds.minY),
        maxX: Math.max(beforeBounds.maxX, afterBounds.maxX),
        maxY: Math.max(beforeBounds.maxY, afterBounds.maxY),
      });
    }
    return beforeState ? beforeState.storedChar : " ";
  }

  function repaintShapeStroke(tileX, tileY) {
    const stroke = scope.paintingStroke;
    if (!stroke) {
      return;
    }
    for (let index = stroke.cells.length - 1; index >= 0; index -= 1) {
      restoreStrokeCell(stroke, stroke.cells[index]);
    }
    stroke.cells = [];
    const nextCells = stroke.kind === "height"
      ? (stroke.shapeMode === "circle"
        ? getCircleFillCells(stroke.originX, stroke.originY, tileX, tileY)
        : getRectangleFillCells(stroke.originX, stroke.originY, tileX, tileY))
      : (stroke.shapeMode === "circle"
        ? getCircleOutlineCells(stroke.originX, stroke.originY, tileX, tileY)
        : getRectangleOutlineCells(stroke.originX, stroke.originY, tileX, tileY));
    nextCells.forEach((cell) => {
      applyStrokeCell(cell.x, cell.y, { deferDraw: true });
    });
    stroke.lastTileX = tileX;
    stroke.lastTileY = tileY;
    scope.draw();
  }

  function beginPaintStroke(tileX, tileY, options) {
    const editingHeightLayer = scope.isEditingHeightLayer();
    const targetLayer = getEditingLayerNumber();
    const activeHeightLayer = editingHeightLayer ? scope.getActiveHeightLayer() : null;
    if (!editingHeightLayer) {
      scope.setLayerVisibility(targetLayer, true);
    }
    scope.beginTileInstanceMutationBatch();
    const shapeMode = String(options?.shapeMode || "").trim();
    scope.paintingStroke = {
      kind: editingHeightLayer ? "height" : "tile",
      brush: scope.activeBrushTileId,
      layer: targetLayer,
      heightLayerId: activeHeightLayer ? String(activeHeightLayer.id || "").trim() : "",
      heightLayerName: activeHeightLayer ? scope.getHeightLayerDisplayName(activeHeightLayer) : "",
      z: activeHeightLayer ? Math.max(1, Number(activeHeightLayer.z) || 1) : 0,
      mode: scope.pointerCandidate && scope.pointerCandidate.altKey ? "erase" : "paint",
      cells: [],
      originX: tileX,
      originY: tileY,
      lockOriginX: tileX,
      lockOriginY: tileY,
      lastTileX: tileX,
      lastTileY: tileY,
      lineLockAxis: null,
      shiftPrimed: false,
      shapeMode,
    };
    if (shapeMode) {
      repaintShapeStroke(tileX, tileY);
    } else {
      applyStrokeCell(tileX, tileY);
    }
    updateViewportCursor();
  }

  function applyStrokeCell(tileX, tileY, options) {
    if (!scope.paintingStroke) {
      return;
    }
    const stroke = scope.paintingStroke;
    if (stroke.kind === "height") {
      const beforeState = getHeightLayerCellState(stroke.heightLayerId, tileX, tileY);
      if (!beforeState) {
        return;
      }
      const beforeStoredChar = setHeightLayerStoredCharAt(stroke.heightLayerId, tileX, tileY, stroke.mode === "erase" ? " " : getStrokeBrushSymbol(stroke));
      if (beforeStoredChar === null) {
        return;
      }
      const afterState = getHeightLayerCellState(stroke.heightLayerId, tileX, tileY);
      const key = tileX + ":" + tileY;
      const existing = stroke.cells.find((cell) => cell.key === key);
      if (existing) {
        existing.after = afterState ? afterState.visibleChar : existing.after;
        existing.afterStoredChar = afterState ? afterState.storedChar : existing.afterStoredChar;
      } else {
        stroke.cells.push({
          key,
          x: tileX,
          y: tileY,
          heightLayerId: stroke.heightLayerId,
          before: beforeState.visibleChar,
          beforeStoredChar: beforeStoredChar,
          after: afterState ? afterState.visibleChar : " ",
          afterStoredChar: afterState ? afterState.storedChar : " ",
        });
      }
      if (!options?.deferDraw) {
        scope.draw();
      }
      return;
    }
    const brushSymbol = getStrokeBrushSymbol(stroke);
    const brushTileId = stroke.mode === "erase" ? "" : stroke.brush;
    const beforeState = getCellPaintState(stroke.layer, tileX, tileY);
    if (!beforeState) {
      return;
    }
    const beforeChar = setTileCharAt(stroke.layer, tileX, tileY, brushSymbol, brushTileId);
    if (beforeChar === null) {
      return;
    }
    const afterState = getCellPaintState(stroke.layer, tileX, tileY);
    const key = tileX + ":" + tileY;
    const afterChar = stroke.mode === "erase" ? getVisibleClearChar(stroke.layer) : brushSymbol;
    const existing = stroke.cells.find((cell) => cell.key === key);
    if (existing) {
      existing.after = afterChar;
      existing.afterStoredChar = afterState ? afterState.storedChar : existing.afterStoredChar;
    } else {
      stroke.cells.push({
        key,
        x: tileX,
        y: tileY,
        layer: stroke.layer,
        before: beforeState.visibleChar,
        beforeStoredChar: beforeState.storedChar,
        beforeTileId: beforeState.tileId,
        after: afterChar,
        afterStoredChar: afterState ? afterState.storedChar : beforeState.storedChar,
      });
    }
    if (!options?.deferDraw) {
      scope.draw();
    }
  }

  function getStrokeTargetCell(tileX, tileY, shiftKey) {
    const stroke = scope.paintingStroke;
    if (!stroke) {
      return { x: tileX, y: tileY };
    }
    if (shiftKey && !stroke.shiftPrimed && !stroke.lineLockAxis) {
      stroke.shiftPrimed = true;
      stroke.lockOriginX = stroke.lastTileX;
      stroke.lockOriginY = stroke.lastTileY;
    } else if (!shiftKey && !stroke.lineLockAxis) {
      stroke.shiftPrimed = false;
      stroke.lockOriginX = stroke.lastTileX;
      stroke.lockOriginY = stroke.lastTileY;
    }
    if (!shiftKey && !stroke.lineLockAxis) {
      return { x: tileX, y: tileY };
    }
    const deltaX = tileX - stroke.lockOriginX;
    const deltaY = tileY - stroke.lockOriginY;
    if (!stroke.lineLockAxis && (deltaX !== 0 || deltaY !== 0)) {
      stroke.lineLockAxis = Math.abs(deltaX) >= Math.abs(deltaY) ? "x" : "y";
    }
    if (stroke.lineLockAxis === "x") {
      return { x: tileX, y: stroke.lockOriginY };
    }
    if (stroke.lineLockAxis === "y") {
      return { x: stroke.lockOriginX, y: tileY };
    }
    return { x: tileX, y: tileY };
  }

  function paintStrokeSegment(fromX, fromY, toX, toY) {
    let currentX = fromX;
    let currentY = fromY;
    const deltaX = Math.abs(toX - fromX);
    const deltaY = Math.abs(toY - fromY);
    const stepX = fromX < toX ? 1 : -1;
    const stepY = fromY < toY ? 1 : -1;
    let error = deltaX - deltaY;

    while (true) {
      applyStrokeCell(currentX, currentY, { deferDraw: true });
      if (currentX === toX && currentY === toY) {
        break;
      }
      const doubledError = error * 2;
      if (doubledError > -deltaY) {
        error -= deltaY;
        currentX += stepX;
      }
      if (doubledError < deltaX) {
        error += deltaX;
        currentY += stepY;
      }
    }
    scope.draw();
  }

  function paintStrokeAt(tileX, tileY, shiftKey) {
    if (!scope.paintingStroke) {
      return;
    }
    const stroke = scope.paintingStroke;
    if (stroke.shapeMode) {
      repaintShapeStroke(tileX, tileY);
      return;
    }
    const targetCell = getStrokeTargetCell(tileX, tileY, shiftKey);
    paintStrokeSegment(stroke.lastTileX, stroke.lastTileY, targetCell.x, targetCell.y);
    stroke.lastTileX = targetCell.x;
    stroke.lastTileY = targetCell.y;
  }

  function finalizePaintStroke() {
    if (!scope.paintingStroke) {
      return;
    }
    const stroke = scope.paintingStroke;
    scope.paintingStroke = null;
    scope.endTileInstanceMutationBatch();
    if (stroke.cells.length === 0) {
      return;
    }
    if (stroke.kind === "height") {
      const details = [
        "Height layer: " + (stroke.heightLayerName || stroke.heightLayerId || "Height Layer"),
        "Z Index: " + stroke.z,
        "Mode: " + (stroke.mode === "erase" ? "Remove" : "Assign"),
        "Shape: " + (
          stroke.shapeMode === "circle"
            ? "Circle fill"
            : (stroke.shapeMode === "rectangle" ? "Rectangle fill" : "Single / Freehand")
        ),
        "Cells changed: " + stroke.cells.length,
      ];
      scope.registerHistory(
        stroke.mode === "erase"
          ? (stroke.shapeMode === "circle" ? "Height circle clear" : (stroke.shapeMode === "rectangle" ? "Height rectangle clear" : "Height clear"))
          : (stroke.shapeMode === "circle" ? "Height circle paint" : (stroke.shapeMode === "rectangle" ? "Height rectangle paint" : "Height paint")),
        "cells:" + stroke.cells.length,
        "z:" + stroke.z,
        details,
      );
      scope.renderHeightLayerList?.();
      updateViewportCursor();
      return;
    }
    const previewCells = stroke.cells
      .slice(0, 6)
      .map((cell) => scope.formatCellCoord(cell) + " " + scope.describeTileSymbol(cell.before) + " -> " + scope.describeTileSymbol(cell.after));
    const details = [
      "Layer: " + stroke.layer,
      stroke.mode === "erase"
        ? "Brush: Eraser"
        : "Brush: " + scope.describeBrushTileId(stroke.brush),
      stroke.shapeMode
        ? "Shape: " + (stroke.shapeMode === "circle" ? "Circle outline" : "Rectangle outline")
        : "Shape: Freehand",
      "Cells changed: " + stroke.cells.length,
    ].concat(previewCells);
    scope.registerHistory(
      stroke.mode === "erase"
        ? (stroke.shapeMode === "circle" ? "Circle erase stroke" : (stroke.shapeMode === "rectangle" ? "Rectangle erase stroke" : "Erase stroke"))
        : (stroke.shapeMode === "circle" ? "Circle stroke" : (stroke.shapeMode === "rectangle" ? "Rectangle stroke" : "Brush stroke")),
      "cells:" + stroke.cells.length,
      stroke.mode === "erase" ? "eraser" : "brush:" + scope.describeBrushTileId(stroke.brush),
      details,
      {
        skipStateCheck: true,
        operation: buildTileCellsHistoryOperation(stroke.cells),
      },
    );
    updateViewportCursor();
  }

  function cancelPaintStroke() {
    if (!scope.paintingStroke) {
      return false;
    }
    const stroke = scope.paintingStroke;
    for (let index = stroke.cells.length - 1; index >= 0; index -= 1) {
      restoreStrokeCell(stroke, stroke.cells[index]);
    }
    scope.paintingStroke = null;
    scope.endTileInstanceMutationBatch();
    scope.pointerCandidate = null;
    if (stroke.kind === "height") {
      scope.renderHeightLayerList?.();
    }
    scope.draw();
    updateViewportCursor();
    return true;
  }

  function clearCanvasTargetSelection() {
    let changed = false;
    if (scope.pan.isPanning) {
      scope.pan.isPanning = false;
      changed = true;
    }
    if (scope.draggingNpc) {
      scope.draggingNpc = null;
      stopDragEdgePan();
      changed = true;
    }
    if (scope.pointerCandidate) {
      scope.pointerCandidate = null;
      changed = true;
    }
    if (cancelPaintStroke()) {
      changed = true;
    }
    if (scope.activeInstanceBrushId) {
      scope.activeInstanceBrushId = "";
      scope.renderInstancePalette();
      changed = true;
    }
    if (scope.selectedNpcId) {
      scope.selectedNpcId = "";
      scope.renderNpcList();
      changed = true;
    }
    if (scope.selectedTile) {
      scope.selectedTile = null;
      changed = true;
    }
    if (scope.activeSidebarTab === "tiles" && scope.activeBrushTileId) {
      scope.activeBrushTileId = "";
      scope.renderPaintPalette();
      changed = true;
    }
    if (changed) {
      scope.draw();
      updateViewportCursor();
      scope.setStatus("Cleared canvas selection.", false);
    }
    return changed;
  }

  function deleteFocusedCanvasSelection() {
    if (!canvasSelectionFocused) {
      return false;
    }
    if (scope.draggingNpc || scope.paintingStroke || scope.pan.isPanning) {
      return false;
    }
    if (scope.selectedNpcId) {
      return scope.removeNpcInstanceById(scope.selectedNpcId, "delete-key");
    }
    if (scope.selectedTile) {
      const targetLayer = Number(scope.selectedTile.layer);
      const tileX = Number(scope.selectedTile.x);
      const tileY = Number(scope.selectedTile.y);
      if (!Number.isFinite(targetLayer) || !Number.isFinite(tileX) || !Number.isFinite(tileY)) {
        return false;
      }
      const clearChar = getRowFillChar(targetLayer);
      const beforeState = getCellPaintState(targetLayer, tileX, tileY);
      const beforeChar = setTileCharAt(targetLayer, tileX, tileY, clearChar, "");
      if (beforeChar === null) {
        return false;
      }
      const afterState = getCellPaintState(targetLayer, tileX, tileY);
      scope.setLayerVisibility(targetLayer, true);
      scope.selectedTile = { x: tileX, y: tileY, layer: targetLayer };
      scope.selectedNpcId = "";
      scope.draw();
      scope.registerHistory("Tile cleared", beforeChar, clearChar, [
        "Layer: " + targetLayer,
        "Tile: (" + tileX + "," + tileY + ")",
        "Source: delete-key",
      ], {
        operation: buildTileCellsHistoryOperation([{
          layer: targetLayer,
          x: tileX,
          y: tileY,
          beforeStoredChar: beforeState ? beforeState.storedChar : ".",
          afterStoredChar: afterState ? afterState.storedChar : ".",
        }]),
      });
      scope.setStatus("Cleared tile (" + tileX + "," + tileY + ").", false);
      return true;
    }
    return false;
  }

  function handleAddLayer() {
    const beforeCount = scope.roomLayers.length;
    const maxLayer = scope.roomLayers.reduce((max, layer) => Math.max(max, Number(layer.layer) || 0), 0);
    const nextLayer = maxLayer + 1;
    scope.roomLayers.push({ layer: nextLayer, name: undefined, rows: scope.normalizeRows([], " "), instanceIds: [] });
    scope.activeLayer = nextLayer;
    scope.setLayerVisibility(nextLayer, true);
    scope.ensureBaseLayer();
    scope.rebuildVisibleWorldChunksFromDocument?.();
    scope.renderLayerList();
    scope.draw();
    scope.registerHistory("Layer added", "layers:" + beforeCount, "layers:" + scope.roomLayers.length, [
      "New layer: " + nextLayer,
      "Gameplay z-index: 0",
      "Layer count: " + beforeCount + " -> " + scope.roomLayers.length,
    ]);
  }

  function handleViewportMouseDown(event) {
    updateModifierKeyState(event);
    cursorState.pointerInsideViewport = true;
    setCanvasSelectionFocus(true);
    if (event.button === 1) {
      event.preventDefault();
      scope.pan.isPanning = true;
      scope.pan.startX = event.clientX;
      scope.pan.startY = event.clientY;
      scope.pan.scrollLeft = scope.viewport.scrollLeft;
      scope.pan.scrollTop = scope.viewport.scrollTop;
      updateViewportCursor();
      return;
    }
    if (event.button !== 0) {
      return;
    }
    const { x: canvasX, y: canvasY } = scope.getCanvasPoint(event);
    const tileX = Math.floor(canvasX / scope.tileSize);
    const tileY = Math.floor(canvasY / scope.tileSize);
    scope.pointerCandidate = {
      startX: event.clientX,
      startY: event.clientY,
      canvasX,
      canvasY,
      tileX,
      tileY,
      altKey: event.altKey === true,
      shapeMode: isElevationToolTabActive()
        ? getElevationBrushShapeMode()
        : getActiveShapeMode(cursorState.leftCtrlKey, cursorState.rightCtrlKey, event.ctrlKey),
      hitNpcIndex: -1,
    };
    if (!isElevationToolTabActive()) {
      for (let i = scope.npcOverlays.length - 1; i >= 0; i -= 1) {
        const npc = scope.npcOverlays[i];
        if (!scope.isLayerRendered(npc.layer)) {
          continue;
        }
        const nx = npc.x * scope.tileSize;
        const ny = npc.y * scope.tileSize;
        const drawWidth = getNpcDrawWidth(npc);
        const drawHeight = getNpcDrawHeight(npc);
        if (canvasX >= nx && canvasX < nx + drawWidth && canvasY >= ny && canvasY < ny + drawHeight) {
          scope.pointerCandidate.hitNpcIndex = i;
          break;
        }
      }
    }
    event.preventDefault();
    updateViewportCursor();
  }

  function handleViewportContextMenu(event) {
    event.preventDefault();
    setCanvasSelectionFocus(true);
    const { x: canvasX, y: canvasY } = scope.getCanvasPoint(event);
    const tileX = Math.floor(canvasX / scope.tileSize);
    const tileY = Math.floor(canvasY / scope.tileSize);
    const withinBounds = tileX >= 0 && tileX < scope.width && tileY >= 0 && tileY < scope.height;
    const npc = scope.findTopNpcAtCanvas(canvasX, canvasY);

    if (npc) {
      scope.selectedNpcId = npc.id;
      scope.selectedTile = null;
      scope.setSidebarTab("instances");
      scope.renderNpcList();
      scope.draw();
      if (scope.openPlacedEntityContextMenu?.(npc, event, {
        removeSource: "canvas",
        tooltipId: "canvas-entity-context:" + String(npc.id || ""),
      })) {
        return;
      }
      return;
    }

    const targetLayer = getEditingLayerNumber();
    if (withinBounds) {
      scope.setLayerVisibility(targetLayer, true);
      scope.selectedTile = { x: tileX, y: tileY, layer: targetLayer };
      scope.selectedNpcId = "";
      scope.setSidebarTab("tiles");
    }
    scope.renderNpcList();
    scope.draw();
    const tileId = withinBounds ? String(scope.getTileIdForCell?.(targetLayer, tileX, tileY) || "").trim() : "";
    if (tileId && scope.openTilePaletteContextMenu) {
      scope.openTilePaletteContextMenu(tileId, scope.getTileEntryById(tileId), event, {
        tooltipId: "canvas-tile-context:" + targetLayer + ":" + tileX + ":" + tileY,
        buildItems: () => {
          const items = [];
          if (targetLayer === 0 && scope.backgroundTileId) {
            items.push({
              kind: "item",
              innerHtml: "<span>Make Transparent</span>",
              onSelect: () => {
              const beforeState = getCellPaintState(targetLayer, tileX, tileY);
              const beforeChar = setTileCharAt(targetLayer, tileX, tileY, " ", "");
              if (beforeChar === null) {
                scope.atTooltip.close();
                return;
              }
              const afterState = getCellPaintState(targetLayer, tileX, tileY);
              scope.selectedTile = { x: tileX, y: tileY, layer: targetLayer };
              scope.draw();
              scope.registerHistory("Background hole", beforeChar, "transparent", [
                "Layer: " + targetLayer,
                "Tile: (" + tileX + "," + tileY + ")",
              ], {
                operation: buildTileCellsHistoryOperation([{
                  layer: targetLayer,
                  x: tileX,
                  y: tileY,
                  beforeStoredChar: beforeState ? beforeState.storedChar : ".",
                  afterStoredChar: afterState ? afterState.storedChar : ".",
                }]),
              });
              scope.atTooltip.close();
              },
            });
          }
          items.push({
            kind: "item",
            innerHtml: "<span>Remove from world</span>",
            onSelect: () => {
            if (scope.viewingAllLayers) {
              scope.setLayerVisibility(targetLayer, true);
            }
            const clearChar = getRowFillChar(targetLayer);
            const beforeState = getCellPaintState(targetLayer, tileX, tileY);
            const beforeChar = setTileCharAt(targetLayer, tileX, tileY, clearChar, "");
            if (beforeChar === null) {
              scope.atTooltip.close();
              return;
            }
            const afterState = getCellPaintState(targetLayer, tileX, tileY);
            scope.selectedTile = { x: tileX, y: tileY, layer: targetLayer };
            scope.draw();
            scope.registerHistory("Tile cleared", beforeChar, clearChar, [
              "Layer: " + targetLayer,
              "Tile: (" + tileX + "," + tileY + ")",
            ], {
              operation: buildTileCellsHistoryOperation([{
                layer: targetLayer,
                x: tileX,
                y: tileY,
                beforeStoredChar: beforeState ? beforeState.storedChar : ".",
                afterStoredChar: afterState ? afterState.storedChar : ".",
              }]),
            });
            scope.atTooltip.close();
            },
          });
          return items;
        },
      });
      return;
    }
    scope.atTooltip.openAtPoint(event.clientX, event.clientY, (panel) => {
      panel.appendChild(scope.atTooltip.makeItem("<span>Nothing here</span>", () => {}, "", { disabled: true }));
    }, "canvas-context");
    scope.setStatus("Opened tile context menu.", false);
  }

  function handleGlobalCtrlWheel(event) {
    if (event.ctrlKey !== true) {
      return;
    }
    event.preventDefault();
    if (!isClientPointInsideViewport(event.clientX, event.clientY)) {
      return;
    }
    const zoomFactor = event.deltaY < 0 ? 1.125 : (1 / 1.125);
    if (scope.applyZoomLevel(scope.zoomLevel * zoomFactor, event.clientX, event.clientY)) {
      scope.startZoomPreview(140);
      scope.syncWorldNeighborhoodForViewport?.();
      scope.draw();
      if (zoomRefreshTimer) {
        window.clearTimeout(zoomRefreshTimer);
      }
      zoomRefreshTimer = window.setTimeout(() => {
        zoomRefreshTimer = 0;
        scope.zoomPreviewUntil = 0;
        scope.syncWorldNeighborhoodForViewport?.();
        scope.drawNow();
      }, 150);
    }
  }

  function placeInstanceAt(tileX, tileY) {
    if (tileX < 0 || tileX >= scope.width || tileY < 0 || tileY >= scope.height) {
      return;
    }
    const templateEntry = scope.getNpcCatalogRecords().find((entry) => entry.id === scope.activeInstanceBrushId) || null;
    const placedTemplateId = String(scope.activeInstanceBrushId || "").trim();
    const entityType = scope.normalizeEntityType?.(templateEntry?.entityType || scope.activeEntityCategory, "friendly") || "friendly";
    const slotId = scope.runtimeUniqueId();
    const spawnLayer = getEditingLayerNumber();
    scope.setLayerVisibility(spawnLayer, true);
    const spriteId = templateEntry ? (templateEntry.spriteId || "") : "";
    const spriteEntry = scope.spriteCatalog[spriteId] || null;
    const worldPosition = (typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive())
      ? {
        x: Math.floor(Number(tileX) || 0) + Math.floor(Number(scope.worldTileOffsetX) || 0),
        y: Math.floor(Number(tileY) || 0) + Math.floor(Number(scope.worldTileOffsetY) || 0),
      }
      : {
        x: Math.floor(Number(tileX) || 0),
        y: Math.floor(Number(tileY) || 0),
      };
    const overlay = {
      id: slotId,
      layer: spawnLayer,
      name: templateEntry ? (templateEntry.name || templateEntry.id || "NPC") : "NPC",
      spriteId,
      isPlacementSlot: false,
      x: tileX,
      y: tileY,
      dataUrl: spriteEntry ? spriteEntry.dataUrl : null,
      spriteWidth: spriteEntry ? spriteEntry.spriteWidth : scope.baseTileSize,
      spriteHeight: spriteEntry ? spriteEntry.spriteHeight : scope.baseTileSize,
      record: {
        ...(templateEntry ? JSON.parse(JSON.stringify(templateEntry.record || {})) : {}),
        id: slotId,
        layer: spawnLayer,
        position: worldPosition,
        name: templateEntry ? (templateEntry.name || "") : "",
        templateId: placedTemplateId,
        entityType,
        spriteId,
        enabled: true,
      },
    };
    scope.ensureNpcImageLoaded(overlay);
    scope.npcOverlays.push(overlay);
    if (typeof scope.rebuildWorldChunksForLocalBounds === "function" && typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive()) {
      scope.rebuildWorldChunksForLocalBounds({ minX: tileX, minY: tileY, maxX: tileX, maxY: tileY });
    }
    scope.selectedNpcId = slotId;
    scope.renderNpcList();
    scope.renderInstancePalette();
    scope.draw();
    const afterSnapshot = scope.cloneNpcOverlays([overlay])[0];
    scope.registerHistory("Entity placed", placedTemplateId, "(" + tileX + "," + tileY + ")", [
      "Catalog source: " + placedTemplateId,
      "Type: " + (scope.getEntityTypeLabel?.(entityType) || entityType),
      "Position: (" + tileX + "," + tileY + ")",
      "Entity id: " + slotId,
    ], {
      operation: {
        type: "npc_entries",
        entries: [{
          before: null,
          after: afterSnapshot,
          beforeIndex: -1,
          afterIndex: scope.npcOverlays.length - 1,
        }],
      },
    });
    scope.setStatus("Placed entity " + (overlay.name || slotId) + " at (" + tileX + "," + tileY + ").", false);
  }

  function placeSelectedUnplacedNpcAt(tileX, tileY) {
    if (tileX < 0 || tileX >= scope.width || tileY < 0 || tileY >= scope.height) {
      return false;
    }
    const npc = getSelectedUnplacedNpc();
    if (!npc) {
      return false;
    }
    const beforePos = "unplaced";
    const beforeIndex = scope.npcOverlays.findIndex((entry) => entry.id === npc.id);
    const beforeSnapshot = scope.cloneNpcOverlays([npc])[0];
    npc.isPlacementSlot = false;
    npc.x = tileX;
    npc.y = tileY;
    npc.layer = Number(npc.layer) || getEditingLayerNumber();
    if (npc.record && typeof npc.record === "object" && !Array.isArray(npc.record)) {
      npc.record.layer = npc.layer;
      npc.record.position = (typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive())
        ? {
          x: Math.floor(Number(tileX) || 0) + Math.floor(Number(scope.worldTileOffsetX) || 0),
          y: Math.floor(Number(tileY) || 0) + Math.floor(Number(scope.worldTileOffsetY) || 0),
        }
        : { x: tileX, y: tileY };
    }
    if (typeof scope.rebuildWorldChunksForLocalBounds === "function" && typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive()) {
      scope.rebuildWorldChunksForLocalBounds({ minX: tileX, minY: tileY, maxX: tileX, maxY: tileY });
    }
    scope.setLayerVisibility(npc.layer, true);
    scope.ensureNpcImageLoaded(npc);
    scope.renderNpcList();
    scope.draw();
    const afterSnapshot = scope.cloneNpcOverlays([npc])[0];
    scope.registerHistory("Entity placed: " + (npc.name || npc.id), beforePos, "(" + tileX + "," + tileY + ")", [
      "Entity: " + (npc.name || npc.id),
      "Position: " + beforePos + " -> (" + tileX + "," + tileY + ")",
      "Layer: " + npc.layer,
    ], {
      operation: {
        type: "npc_entries",
        entries: [{
          before: beforeSnapshot,
          after: afterSnapshot,
          beforeIndex,
          afterIndex: beforeIndex,
        }],
      },
    });
    scope.setStatus("Placed entity " + (npc.name || npc.id) + " at (" + tileX + "," + tileY + ").", false);
    return true;
  }

  function handleWindowMouseMove(event) {
    updateModifierKeyState(event);
    cursorState.pointerInsideViewport = isClientPointInsideViewport(event.clientX, event.clientY);
    if (scope.pan.isPanning && (event.buttons & 4) === 0) {
      stopPanInteraction();
    }
    if (scope.pointerCandidate && !scope.draggingNpc && !scope.paintingStroke) {
      const deltaX = event.clientX - scope.pointerCandidate.startX;
      const deltaY = event.clientY - scope.pointerCandidate.startY;
      const movedEnough = Math.abs(deltaX) + Math.abs(deltaY) >= 4;
      if (movedEnough) {
        if (isTileSelectModeActive()) {
          scope.pointerCandidate = null;
          updateViewportCursor();
          return;
        }
        if (scope.pointerCandidate.hitNpcIndex >= 0) {
          const npc = scope.npcOverlays[scope.pointerCandidate.hitNpcIndex];
          const nx = npc.x * scope.tileSize;
          const ny = npc.y * scope.tileSize;
          scope.draggingNpc = {
            index: scope.pointerCandidate.hitNpcIndex,
            offsetX: scope.pointerCandidate.canvasX - nx,
            offsetY: scope.pointerCandidate.canvasY - ny,
          };
          scope.dragDrawX = nx;
          scope.dragDrawY = ny;
          updateViewportCursor();
        } else {
          if (scope.activeSidebarTab !== "tiles") {
            scope.pointerCandidate = null;
            return;
          }
          if (scope.activeSidebarTab === "tiles" && !scope.activeBrushTileId && !scope.pointerCandidate.altKey) {
            scope.pointerCandidate = null;
            updateViewportCursor();
            return;
          }
          beginPaintStroke(scope.pointerCandidate.tileX, scope.pointerCandidate.tileY, {
            shapeMode: scope.pointerCandidate.shapeMode,
          });
        }
        scope.pointerCandidate = null;
      }
    }
    if (scope.paintingStroke) {
      const { x: canvasX, y: canvasY } = scope.getCanvasPoint(event);
      paintStrokeAt(Math.floor(canvasX / scope.tileSize), Math.floor(canvasY / scope.tileSize), cursorState.leftShiftKey);
      updateViewportCursor();
      return;
    }
    if (scope.draggingNpc) {
      updateDraggingNpcDrawPositionFromClient(event.clientX, event.clientY);
      updateDragEdgePanFromPointer(event.clientX, event.clientY);
      scope.draw();
      return;
    }
    if (scope.pan.isPanning) {
      scope.viewport.scrollLeft = scope.pan.scrollLeft - (event.clientX - scope.pan.startX);
      scope.viewport.scrollTop = scope.pan.scrollTop - (event.clientY - scope.pan.startY);
      updateViewportCursor();
      return;
    }
    const rect = getInteractiveSurfaceRect();
    const localCanvasX = event.clientX - rect.left;
    const localCanvasY = event.clientY - rect.top;
    const { x: canvasX, y: canvasY } = scope.getCanvasPoint(event);
    const nextHoverTileX = Math.floor(canvasX / scope.tileSize);
    const nextHoverTileY = Math.floor(canvasY / scope.tileSize);
    let overNpc = false;
    let hitNpc = null;
    if (!isElevationToolTabActive()) {
      for (const npc of scope.npcOverlays) {
        if (!scope.isLayerRendered(npc.layer)) {
          continue;
        }
        const nx = npc.x * scope.tileSize;
        const ny = npc.y * scope.tileSize;
        const drawWidth = getNpcDrawWidth(npc);
        const drawHeight = getNpcDrawHeight(npc);
        if (canvasX >= nx && canvasX < nx + drawWidth && canvasY >= ny && canvasY < ny + drawHeight) {
          overNpc = true;
          hitNpc = npc;
          break;
        }
      }
    }
    const nextHoveredId = hitNpc ? String(hitNpc.id || "") : "";
    const ghostChanged = nextHoverTileX !== scope.hoverTileX || nextHoverTileY !== scope.hoverTileY;
    if (nextHoveredId !== scope.hoveredNpcId || scope.hoverCanvasX !== localCanvasX || scope.hoverCanvasY !== localCanvasY || ghostChanged) {
      scope.hoveredNpcId = nextHoveredId;
      scope.hoverCanvasX = localCanvasX;
      scope.hoverCanvasY = localCanvasY;
      scope.hoverTileX = nextHoverTileX;
      scope.hoverTileY = nextHoverTileY;
      scope.refreshInformationPanel?.();
      scope.draw();
    }
    cursorState.overNpc = overNpc;
    updateViewportCursor();
  }

  function handleViewportMouseLeave() {
    cursorState.pointerInsideViewport = false;
    cursorState.overNpc = false;
    if (!scope.draggingNpc) {
      stopDragEdgePan();
    }
    if (scope.hoverTileX >= 0 || scope.hoverTileY >= 0) {
      scope.hoverTileX = -1;
      scope.hoverTileY = -1;
      scope.refreshInformationPanel?.();
      scope.draw();
    }
    updateViewportCursor();
  }

  function isEditableKeyTarget(target) {
    if (!(target instanceof HTMLElement)) {
      return false;
    }
    if (target.isContentEditable) {
      return true;
    }
    const tagName = String(target.tagName || "").toLowerCase();
    return tagName === "input" || tagName === "textarea" || tagName === "select";
  }

  function handleWindowKeyChange(event) {
    if (event.type === "keydown" && event.ctrlKey && !event.altKey && !isEditableKeyTarget(event.target)) {
      const key = String(event.key || "").toLowerCase();
      if (key === "z" && !event.shiftKey) {
        event.preventDefault();
        scope.undo();
        return;
      }
      if (key === "y") {
        event.preventDefault();
        scope.redo();
        return;
      }
    }
    if (event.type === "keydown" && event.key === "Escape") {
      if (!event.repeat && !document.querySelector(".at-tooltip-panel")) {
        if (scope.importJsonModal && !scope.importJsonModal.classList.contains("hidden")) {
          event.preventDefault();
          scope.closeJsonImportModal();
          return;
        }
        if (!isEditableKeyTarget(event.target) && cursorState.pointerInsideViewport) {
          if (clearCanvasTargetSelection()) {
            event.preventDefault();
            return;
          }
        }
        if (!isEditableKeyTarget(event.target)) {
          const collapsed = typeof scope.collapseActiveSidebarTabSections === "function"
            ? scope.collapseActiveSidebarTabSections()
            : false;
          if (collapsed) {
            event.preventDefault();
          }
        }
      }
    }
    if (event.type === "keydown" && event.key === "Delete" && !event.repeat && !isEditableKeyTarget(event.target)) {
      if (!document.querySelector(".at-tooltip-panel") && deleteFocusedCanvasSelection()) {
        event.preventDefault();
        return;
      }
    }
    if (
      event.type === "keydown"
      && !event.repeat
      && !event.ctrlKey
      && !event.altKey
      && !event.metaKey
      && !isEditableKeyTarget(event.target)
      && String(event.key || "").toLowerCase() === "m"
    ) {
      event.preventDefault();
      scope.openWorldOverviewWindow?.();
      return;
    }
    if (
      event.type === "keydown"
      && !event.repeat
      && !event.ctrlKey
      && !event.altKey
      && !event.metaKey
      && !isEditableKeyTarget(event.target)
      && String(event.key || "").toLowerCase() === "o"
    ) {
      event.preventDefault();
      scope.showChunkBounds = !scope.showChunkBounds;
      scope.setStatus(scope.showChunkBounds ? "Chunk boundary overlay enabled." : "Chunk boundary overlay disabled.", false);
      return;
    }
    const modifierChanged = updateModifierKeyState(event);
    if (modifierChanged && (cursorState.pointerInsideViewport || scope.paintingStroke)) {
      scope.draw();
      updateViewportCursor();
    }
  }

  function handleWindowMouseUp(event) {
    updateModifierKeyState(event);
    cursorState.pointerInsideViewport = isClientPointInsideViewport(event.clientX, event.clientY);
    if (scope.pointerCandidate && !scope.draggingNpc && !scope.paintingStroke && event.target instanceof Node && !scope.viewport.contains(event.target)) {
      scope.pointerCandidate = null;
      updateViewportCursor();
      return;
    }
    if (scope.pointerCandidate && !scope.draggingNpc && !scope.paintingStroke) {
      const npc = scope.pointerCandidate.hitNpcIndex >= 0 ? scope.npcOverlays[scope.pointerCandidate.hitNpcIndex] : null;
      const wasAlreadySelectedNpc = npc && String(scope.selectedNpcId || "").trim() === String(npc.id || "").trim();
      if (isTileSelectModeActive()) {
        selectTile(scope.pointerCandidate.tileX, scope.pointerCandidate.tileY);
      } else if (npc) {
        scope.selectNpc(npc, { centerViewport: !wasAlreadySelectedNpc });
      } else if (scope.activeSidebarTab === "instances" && !scope.activeInstanceBrushId && getSelectedUnplacedNpc()) {
        const { x: releaseCanvasX, y: releaseCanvasY } = scope.getCanvasPoint(event);
        placeSelectedUnplacedNpcAt(Math.floor(releaseCanvasX / scope.tileSize), Math.floor(releaseCanvasY / scope.tileSize));
      } else if (scope.activeSidebarTab === "instances" && scope.activeInstanceBrushId) {
        const { x: releaseCanvasX, y: releaseCanvasY } = scope.getCanvasPoint(event);
        placeInstanceAt(Math.floor(releaseCanvasX / scope.tileSize), Math.floor(releaseCanvasY / scope.tileSize));
      } else if (scope.activeSidebarTab === "information" && getEditingLayerNumber() === 0) {
        applyBackgroundCellModeAt(scope.pointerCandidate.tileX, scope.pointerCandidate.tileY);
      } else if (scope.activeSidebarTab === "tiles") {
        if (scope.activeBrushTileId || scope.pointerCandidate.altKey) {
          beginPaintStroke(scope.pointerCandidate.tileX, scope.pointerCandidate.tileY, {
            shapeMode: scope.pointerCandidate.shapeMode,
          });
          finalizePaintStroke();
        }
      }
      scope.pointerCandidate = null;
      updateViewportCursor();
      return;
    }
    if (scope.draggingNpc) {
      stopDragEdgePan();
      const { x: canvasX, y: canvasY } = scope.getCanvasPoint(event);
      const snappedX = Math.max(0, Math.min(scope.width - 1, Math.floor((canvasX - scope.draggingNpc.offsetX + scope.tileSize / 2) / scope.tileSize)));
      const snappedY = Math.max(0, Math.min(scope.height - 1, Math.floor((canvasY - scope.draggingNpc.offsetY + scope.tileSize / 2) / scope.tileSize)));
      const npc = scope.npcOverlays[scope.draggingNpc.index];
      const beforePos = "(" + npc.x + "," + npc.y + ")";
      const beforeSnapshot = scope.cloneNpcOverlays([npc])[0];
      const beforeIndex = scope.draggingNpc.index;
      const previousX = Math.floor(Number(npc.x) || 0);
      const previousY = Math.floor(Number(npc.y) || 0);
      npc.x = snappedX;
      npc.y = snappedY;
      if (npc.record && typeof npc.record === "object" && !Array.isArray(npc.record)) {
        npc.record.position = (typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive())
          ? {
            x: Math.floor(Number(snappedX) || 0) + Math.floor(Number(scope.worldTileOffsetX) || 0),
            y: Math.floor(Number(snappedY) || 0) + Math.floor(Number(scope.worldTileOffsetY) || 0),
          }
          : { x: snappedX, y: snappedY };
      }
      if (typeof scope.rebuildWorldChunksForLocalBounds === "function" && typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive()) {
        scope.rebuildWorldChunksForLocalBounds({
          minX: Math.min(previousX, snappedX),
          minY: Math.min(previousY, snappedY),
          maxX: Math.max(previousX, snappedX),
          maxY: Math.max(previousY, snappedY),
        });
      }
      scope.draggingNpc = null;
      cursorState.overNpc = false;
      scope.renderNpcList();
      scope.draw();
      const afterPos = "(" + npc.x + "," + npc.y + ")";
      const afterSnapshot = scope.cloneNpcOverlays([npc])[0];
      scope.registerHistory("NPC moved: " + (npc.name || npc.id), beforePos, afterPos, [
        "NPC: " + (npc.name || npc.id),
        "Position: " + beforePos + " -> " + afterPos,
      ], {
        operation: {
          type: "npc_entries",
          entries: [{
            before: beforeSnapshot,
            after: afterSnapshot,
            beforeIndex,
            afterIndex: beforeIndex,
          }],
        },
      });
      scope.pointerCandidate = null;
      updateViewportCursor();
      return;
    }
    if (scope.paintingStroke) {
      finalizePaintStroke();
      scope.pointerCandidate = null;
      updateViewportCursor();
      return;
    }
    if (!scope.pan.isPanning) {
      updateViewportCursor();
      return;
    }
    stopPanInteraction();
  }

  function bindDomEvents() {
    if (domEventsBound) {
      return;
    }
    domEventsBound = true;
    const bindSectionToggle = (buttonEl, bodyEl) => {
      if (!buttonEl || !bodyEl) {
        return;
      }
      const sync = () => {
        const isExpanded = !bodyEl.classList.contains("hidden");
        buttonEl.setAttribute("aria-expanded", isExpanded ? "true" : "false");
      };
      sync();
      buttonEl.addEventListener("click", () => {
        bodyEl.classList.toggle("hidden");
        sync();
      });
    };

    const addLayerBtn = document.getElementById("addLayerBtn");
    addLayerBtn?.addEventListener("click", handleAddLayer);

    bindSectionToggle(scope.toggleInformationSettingsSectionBtn, scope.informationSettingsSectionBody);
    bindSectionToggle(scope.toggleInformationConfigurationSectionBtn, scope.informationConfigurationSectionBody);
    bindSectionToggle(scope.toggleInformationHotkeysSectionBtn, scope.informationHotkeysSectionBody);

    scope.viewport.addEventListener("mousedown", handleViewportMouseDown);
    scope.viewport.addEventListener("auxclick", (event) => {
      if (event.button === 1) {
        event.preventDefault();
      }
    });
    scope.viewport.addEventListener("contextmenu", handleViewportContextMenu);
    scope.viewport.addEventListener("scroll", () => {
      scheduleWorldSyncFromScroll();
      scope.refreshWorldOverviewWindow?.();
      if (!scope.pan.isPanning && !scope.draggingNpc && !scope.paintingStroke && !scope.isZoomPreviewActive()) {
        scheduleScrollPreview();
        return;
      }
      scope.draw();
    }, { passive: true });
    window.addEventListener("mousemove", handleWindowMouseMove);
    scope.viewport.addEventListener("mouseleave", handleViewportMouseLeave);
    window.addEventListener("mouseup", handleWindowMouseUp);
    window.addEventListener("wheel", handleGlobalCtrlWheel, { passive: false, capture: true });
    window.addEventListener("resize", () => {
      window.requestAnimationFrame(() => {
        scope.syncViewportDimensions();
        scope.syncWorldNeighborhoodForViewport?.();
        scope.drawNow();
        scope.refreshWorldOverviewWindow?.();
      });
    });
    window.addEventListener("keydown", handleWindowKeyChange);
    window.addEventListener("keyup", handleWindowKeyChange);
    document.addEventListener("pointerdown", (event) => {
      setCanvasSelectionFocus(event.target instanceof Node && scope.viewport.contains(event.target));
    }, true);
    window.addEventListener("blur", () => {
      cursorState.leftShiftKey = false;
      cursorState.rightShiftKey = false;
      cursorState.leftCtrlKey = false;
      cursorState.rightCtrlKey = false;
      cursorState.altKey = false;
      cursorState.currentCursor = "";
      setCanvasSelectionFocus(false);
      scope.hideTileGrid = false;
      stopDragEdgePan();
      cancelScheduledWorldSync();
      pendingWorldSyncAfterPan = false;
      if (scope.pan.isPanning) {
        stopPanInteraction({ skipWorldSync: true });
      }
      if (cursorState.pointerInsideViewport || scope.paintingStroke) {
        scope.draw();
        updateViewportCursor();
      }
    });

    scope.undoBtn.addEventListener("click", scope.undo);
    scope.redoBtn.addEventListener("click", scope.redo);
    scope.saveBtn.addEventListener("click", () => {
      if (!scope.hasUnsavedChanges()) {
        scope.setStatus("No unsaved changes.", false);
        return;
      }
      scope.saveCurrentState().catch((error) => scope.setStatus(String(error), true));
    });
    scope.testHeightBtn?.addEventListener("click", () => {
      scope.openHeightViewerWindow?.();
    });
    scope.canvasSelectToolBtn?.addEventListener("click", () => {
      toggleCanvasToolMode();
    });
    scope.menuLayerSelectEl?.addEventListener("change", () => {
      scope.handleMenuLayerSelectionChange();
    });
    scope.themePresetButtons.forEach((button) => {
      button.addEventListener("click", () => {
        scope.applyThemePreset(button.getAttribute("data-theme-preset") || "");
      });
    });
    scope.saveStatusEl?.addEventListener("contextmenu", (event) => {
      event.preventDefault();
      event.stopPropagation();
      scope.openStatusLogWindow?.();
    });
    scope.informationTabBtn.addEventListener("click", () => scope.handleSidebarTabButtonClick("information"));
    scope.layersTabBtn.addEventListener("click", () => scope.handleSidebarTabButtonClick("layers"));
    scope.tilesTabBtn.addEventListener("click", () => scope.handleSidebarTabButtonClick("tiles"));
    scope.instancesTabBtn.addEventListener("click", () => scope.handleSidebarTabButtonClick("instances"));
    scope.triggersTabBtn.addEventListener("click", () => scope.handleSidebarTabButtonClick("triggers"));
    scope.pathsTabBtn.addEventListener("click", () => scope.handleSidebarTabButtonClick("paths"));
    scope.transitionsTabBtn.addEventListener("click", () => scope.handleSidebarTabButtonClick("transitions"));
    scope.historyTabBtn.addEventListener("click", () => scope.handleSidebarTabButtonClick("history"));
    scope.newsTabBtn?.addEventListener("click", () => {
      scope.openNewsWindow?.({ center: true });
    });
    scope.newNpcBtn.addEventListener("click", scope.createNewNpc);
    scope.newTileFolderBtn?.addEventListener("click", () => {
      createTileFolder();
    });
    scope.newTileBtn?.addEventListener("click", () => {
      if (String(scope.activeGraphicsTab || "tiles") === "sprites") {
        void scope.createNewSpriteGraphic?.("sprite");
        return;
      }
      if (String(scope.activeGraphicsTab || "tiles") === "other") {
        void scope.createNewSpriteGraphic?.("other");
        return;
      }
      void scope.createNewTile?.();
    });
    scope.graphicsTilesBtn?.addEventListener("click", () => {
      scope.activeGraphicsTab = "tiles";
      scope.renderPaintPalette();
    });
    scope.graphicsSpritesBtn?.addEventListener("click", () => {
      scope.activeGraphicsTab = "sprites";
      scope.renderPaintPalette();
    });
    scope.graphicsOtherBtn?.addEventListener("click", () => {
      scope.activeGraphicsTab = "other";
      scope.renderPaintPalette();
    });
    scope.newTemplateFolderBtn?.addEventListener("click", () => {
      createTemplateFolder();
    });
    scope.newPlacedFolderBtn?.addEventListener("click", () => {
      createPlacedFolder();
    });
    scope.entityTypeFriendlyBtn?.addEventListener("click", () => {
      scope.setActiveEntityCategory?.("friendly");
    });
    scope.entityTypeHostileBtn?.addEventListener("click", () => {
      scope.setActiveEntityCategory?.("hostile");
    });
    scope.entityTypePropBtn?.addEventListener("click", () => {
      scope.setActiveEntityCategory?.("prop");
    });
    scope.newMonsterFolderBtn?.addEventListener("click", () => {
      scope.createPanelFolder("monsters", [], "Monsters");
      scope.renderMonsterList();
    });
    scope.newTriggerFolderBtn?.addEventListener("click", () => {
      scope.createPanelFolder("triggers", [], "Triggers");
      scope.renderTriggerList();
    });
    scope.newPathFolderBtn?.addEventListener("click", () => {
      scope.createPanelFolder("paths", [], "Paths");
      scope.renderPathList();
    });
    scope.newTransitionFolderBtn?.addEventListener("click", () => {
      scope.createPanelFolder("transitions", [], "Transitions");
      scope.renderTransitionList();
    });
    scope.toggleTemplateSectionBtn?.addEventListener("click", () => {
      scope.toggleTemplateSection();
    });
    scope.togglePlacedSectionBtn?.addEventListener("click", () => {
      scope.togglePlacedSection();
    });
    scope.toggleDrawLayerSectionBtn?.addEventListener("click", () => {
      scope.toggleDrawLayerSection?.();
    });
    scope.toggleHeightLayerSectionBtn?.addEventListener("click", () => {
      scope.toggleHeightLayerSection?.();
    });
    scope.confirmWidthBtn?.addEventListener("click", () => scope.applyInformationEdits({ width: true, height: false }));
    scope.cancelWidthBtn?.addEventListener("click", () => scope.cancelDimensionEdit("width"));
    scope.confirmHeightBtn?.addEventListener("click", () => scope.applyInformationEdits({ width: false, height: true }));
    scope.cancelHeightBtn?.addEventListener("click", () => scope.cancelDimensionEdit("height"));
    scope.restoreToolWindowsBtn?.addEventListener("click", () => {
      scope.restoreAllToolWindows?.();
    });
    scope.resetWorkspaceLayoutBtn?.addEventListener("click", () => {
      scope.resetWorkspaceLayout?.();
    });
    scope.engineOverridesBtn?.addEventListener("click", () => {
      scope.openEngineOverrideWindow?.();
    });
    scope.mapNameInputEl.addEventListener("input", () => {
      scope.mapName = String(scope.mapNameInputEl.value || "").trim() || scope.mapId;
      scope.draw();
    });
    scope.mapNameInputEl.addEventListener("change", () => {
      scope.applyInformationEdits({ width: false, height: false });
    });
    if (!scope.mapWidthInputEl?.readOnly) {
      scope.mapWidthInputEl.addEventListener("input", () => {
        scope.mapInfoDraft = { ...scope.mapInfoDraft, width: Math.max(1, Number(scope.mapWidthInputEl.value) || scope.width) };
        scope.refreshInformationDraftState();
      });
      scope.mapWidthInputEl.addEventListener("keydown", (event) => {
        scope.handleDimensionKeydown("width", event);
      });
    }
    if (!scope.mapHeightInputEl?.readOnly) {
      scope.mapHeightInputEl.addEventListener("input", () => {
        scope.mapInfoDraft = { ...scope.mapInfoDraft, height: Math.max(1, Number(scope.mapHeightInputEl.value) || scope.height) };
        scope.refreshInformationDraftState();
      });
      scope.mapHeightInputEl.addEventListener("keydown", (event) => {
        scope.handleDimensionKeydown("height", event);
      });
    }
    scope.mapBackgroundColorInputEl.addEventListener("input", () => {
      scope.backgroundColor = scope.normalizeMapBackgroundColor(scope.mapBackgroundColorInputEl.value);
      scope.draw();
    });
    scope.mapBackgroundColorInputEl.addEventListener("change", () => {
      scope.applyInformationEdits({ width: false, height: false });
    });
    scope.addHeightLayerBtn?.addEventListener("click", () => {
      scope.addHeightLayer?.();
    });
    scope.backgroundModeBtn?.addEventListener("click", () => {
      scope.cycleBackgroundCellMode();
    });
    scope.experimentalImportToggleBtn?.addEventListener("click", () => {
      scope.toggleExperimentalImportPanel();
    });
    scope.importSpritesBtn?.addEventListener("click", () => {
      scope.openImportDialog("sprites");
    });
    scope.importTilesBtn?.addEventListener("click", () => {
      scope.openImportDialog("tiles");
    });
    scope.importJsonBtn?.addEventListener("click", () => {
      scope.openJsonImportModal();
    });
    scope.importSpritesInputEl?.addEventListener("change", () => {
      void scope.handleImportSelection("sprites");
    });
    scope.importTilesInputEl?.addEventListener("change", () => {
      void scope.handleImportSelection("tiles");
    });
    scope.importJsonConfirmBtn?.addEventListener("click", () => {
      void scope.submitJsonImport();
    });
    scope.importJsonCancelBtn?.addEventListener("click", () => {
      scope.closeJsonImportModal();
    });
    scope.importJsonModal?.addEventListener("click", (event) => {
      if (event.target === scope.importJsonModal) {
        scope.closeJsonImportModal();
      }
    });
  }

  function initializeEditorState() {
    scope.ensureBaseLayer();
    const initialEntry = {
      id: scope.nextHistoryId,
      seq: scope.nextHistoryId,
      createdAt: Date.now(),
      label: "Initial state",
      before: "-",
      after: "-",
      details: ["Loaded initial map state."],
      state: scope.captureState(),
    };
    scope.historyEntries = [initialEntry];
    scope.historyIndex = 0;
    scope.historySelectionIndex = 0;
    scope.lastSavedHistoryId = initialEntry.id;
    scope.nextHistoryId += 1;

    const restoredHistory = scope.restoreHistoryState();
    if (!scope.applyHistorySnapshot(restoredHistory)) {
      scope.persistHistoryState();
    }
  }

  function initializeUi() {
    setCanvasToolMode(scope.canvasToolMode, { force: true, silent: true, skipDraw: true });
    updateViewportCursor();
    scope.refreshThemePresetButtons();
    scope.refreshInformationPanel();
    scope.refreshImportControls();
    scope.renderHeightLayerList();
    scope.refreshInstanceSectionState();
    scope.refreshEntityTypeTabs?.();
    scope.renderPaintPalette();
    scope.renderInstancePalette();
    scope.renderLayerList();
    scope.renderNpcList();
    scope.renderMonsterList();
    scope.renderTriggerList();
    scope.renderPathList();
    scope.renderTransitionList();
    scope.setSidebarTab(scope.activeSidebarTab || "information");
    scope.refreshToolbarState();
    scope.draw();
  }

  function createTileFolder() {
    const activeGraphicsTab = String(scope.activeGraphicsTab || "tiles");
    if (activeGraphicsTab === "sprites" || activeGraphicsTab === "other") {
      const panelKey = activeGraphicsTab === "other" ? "graphicsOther" : "graphicsSprites";
      const spritePayload = scope.ensureDocumentContentPayload?.("sprites", { schemaVersion: 1, sprites: [] }) || { schemaVersion: 1, sprites: [] };
      const itemIds = (Array.isArray(spritePayload?.sprites) ? spritePayload.sprites : [])
        .filter((entry) => {
          const role = String(entry?.graphicRole || "sprite").trim().toLowerCase() === "other" ? "other" : "sprites";
          return role === activeGraphicsTab;
        })
        .map((entry) => String(entry?.id || "").trim())
        .filter(Boolean);
      scope.createPanelFolder(panelKey, itemIds, "Graphics");
      scope.renderPaintPalette();
      return;
    }
    scope.createPanelFolder("tiles", scope.getPaintableTileIds(), "Graphics");
    scope.renderPaintPalette();
  }

  function createTemplateFolder() {
    const templateIds = scope.getNpcCatalogRecords().map((entry) => String(entry.id || "").trim()).filter(Boolean);
    scope.createPanelFolder("instanceTemplates", templateIds, "Entity Catalog");
    scope.renderInstancePalette();
  }

  function createPlacedFolder() {
    const instanceIds = scope.npcOverlays
      .slice()
      .sort((left, right) => String(left.name || left.id || "").localeCompare(String(right.name || right.id || "")))
      .map((entry) => String(entry.id || "").trim())
      .filter(Boolean);
    scope.createPanelFolder("instances", instanceIds, "Placed Entities");
    scope.renderNpcList();
  }

  return {
    getRowFillChar,
    selectTile,
    setTileCharAt,
    beginPaintStroke,
    paintStrokeAt,
    finalizePaintStroke,
    handleAddLayer,
    bindDomEvents,
    initializeEditorState,
    initializeUi,
  };
}
