/* eslint-disable @typescript-eslint/ban-ts-comment, no-empty */
// @ts-nocheck

export function createHistoryController(scope) {
  const documentScope = scope.documentScope || scope;
  const renderScope = scope.renderScope || scope;
  const historyScope = scope.historyScope || scope;
  const uiScope = scope.uiScope || scope;
  const sessionScope = scope.sessionScope || scope;
  const MAX_HISTORY_ENTRIES = 40;
  const MAX_PERSISTED_HISTORY_CHARS = 1_500_000;
  const OPERATION_CHECKPOINT_INTERVAL = 12;
  let pendingPersistTimer = 0;

  function cloneValue(value) {
    if (typeof structuredClone === "function") {
      return structuredClone(value);
    }
    return value == null ? value : JSON.parse(JSON.stringify(value));
  }

  function clearPendingPersistTimer() {
    if (!pendingPersistTimer) {
      return;
    }
    window.clearTimeout(pendingPersistTimer);
    pendingPersistTimer = 0;
  }

  function persistHistoryState() {
    clearPendingPersistTimer();
    try {
      const savedIndex = Math.max(0, Math.min(
        scope.historyEntries.findIndex((entry) => Number(entry?.id) === Number(historyScope.lastSavedHistoryId)),
        scope.historyEntries.length - 1,
      ));
      const savedState = scope.historyEntries.length > 0
        ? captureHistoryStateAtIndex(savedIndex >= 0 ? savedIndex : scope.historyIndex)
        : captureState();
      const payload = {
        mapId: String(documentScope.mapId || scope.mapId || ""),
        savedStateSignature: getStateSignature(savedState),
        historyEntries: historyScope.historyEntries,
        historyIndex: historyScope.historyIndex,
        historySelectionIndex: historyScope.historySelectionIndex,
        nextHistoryId: historyScope.nextHistoryId,
        lastSavedHistoryId: historyScope.lastSavedHistoryId,
      };
      const serialized = JSON.stringify(payload);
      if (serialized.length > MAX_PERSISTED_HISTORY_CHARS) {
      window.localStorage.removeItem(historyScope.historyStorageKey);
        return false;
      }
      window.localStorage.setItem(historyScope.historyStorageKey, serialized);
      return true;
    } catch {}
    return false;
  }

  function schedulePersistHistoryState() {
    clearPendingPersistTimer();
    pendingPersistTimer = window.setTimeout(() => {
      pendingPersistTimer = 0;
      persistHistoryState();
    }, 120);
    return true;
  }

  function restoreHistoryState() {
    try {
      const raw = window.localStorage.getItem(historyScope.historyStorageKey);
      if (!raw) {
        return null;
      }
      const parsed = JSON.parse(raw);
      if (!parsed || typeof parsed !== "object") {
        return null;
      }
      return parsed;
    } catch {
      return null;
    }
  }

  function applyHistorySnapshot(snapshot) {
    if (!snapshot || typeof snapshot !== "object") {
      return false;
    }
    const snapshotMapId = String(snapshot.mapId || "").trim();
    const currentMapId = String(documentScope.mapId || scope.mapId || "").trim();
    if (snapshotMapId && currentMapId && snapshotMapId !== currentMapId) {
      return false;
    }
    const entries = Array.isArray(snapshot.historyEntries) ? snapshot.historyEntries : null;
    if (!entries || entries.length === 0) {
      return false;
    }
    const savedId = Number(snapshot.lastSavedHistoryId) || 0;
    const currentId = Number(snapshot.historyEntries?.[Number(snapshot.historyIndex) || 0]?.id) || 0;
    if (!savedId || !currentId || savedId !== currentId) {
      return false;
    }
    const savedStateSignature = String(snapshot.savedStateSignature || "").trim();
    if (savedStateSignature) {
      const currentLoadedSignature = getStateSignature(captureState());
      if (savedStateSignature !== currentLoadedSignature) {
        return false;
      }
    }
    historyScope.historyEntries = entries;
    historyScope.historyIndex = Math.max(0, Math.min(Number(snapshot.historyIndex) || 0, historyScope.historyEntries.length - 1));
    historyScope.historySelectionIndex = Math.max(0, Math.min(Number(snapshot.historySelectionIndex) || historyScope.historyIndex, historyScope.historyEntries.length - 1));
    historyScope.nextHistoryId = Math.max(1, Number(snapshot.nextHistoryId) || (historyScope.historyEntries[historyScope.historyEntries.length - 1]?.seq || 0) + 1);
    historyScope.lastSavedHistoryId = Math.max(1, Number(snapshot.lastSavedHistoryId) || historyScope.historyEntries[historyScope.historyIndex]?.id || 1);
    if (!restoreToHistoryIndex(historyScope.historyIndex)) {
      const currentState = historyScope.historyEntries[historyScope.historyIndex] && historyScope.historyEntries[historyScope.historyIndex].state ? historyScope.historyEntries[historyScope.historyIndex].state : null;
      if (currentState) {
        applyState(currentState);
      }
    }
    return true;
  }

  function captureState() {
    return {
      width: Number(documentScope.width) || 1,
      height: Number(documentScope.height) || 1,
      mapName: String(documentScope.mapName || scope.mapId || ""),
      backgroundColor: documentScope.normalizeMapBackgroundColor(documentScope.backgroundColor),
      backgroundTileId: String(documentScope.backgroundTileId || "").trim(),
      heightBlurStep: Math.max(0, Math.min(1, Number(documentScope.heightBlurStep ?? documentScope.heightDetailStep) || 0.1)),
      layers: documentScope.cloneLayers(documentScope.roomLayers),
      heightLayers: documentScope.cloneHeightLayers(documentScope.heightLayers),
      npcs: documentScope.cloneNpcOverlays(documentScope.npcOverlays),
      worldChunkBackgrounds: typeof scope.captureWorldChunkBackgroundState === "function"
        ? scope.captureWorldChunkBackgroundState()
        : {},
      worldBookmarks: typeof scope.captureWorldBookmarkState === "function"
        ? scope.captureWorldBookmarkState()
        : [],
      editorUi: documentScope.cloneEditorUiState(),
    };
  }

  function refreshUiAfterHistoryMutation() {
    documentScope.ensureBaseLayer();
    sessionScope.activeLayer = documentScope.roomLayers.some((layer) => layer.layer === sessionScope.activeLayer) ? sessionScope.activeLayer : 0;
    if (!documentScope.npcOverlays.some((npc) => npc.id === sessionScope.selectedNpcId)) {
      sessionScope.selectedNpcId = documentScope.npcOverlays[0] ? String(documentScope.npcOverlays[0].id || "") : "";
    }
    if (uiScope.refreshInstanceSectionState) {
      uiScope.refreshInstanceSectionState();
    }
    uiScope.renderPaintPalette();
    if (uiScope.renderHeightLayerList) {
      uiScope.renderHeightLayerList();
    }
    uiScope.renderInstancePalette();
    uiScope.renderLayerList();
    uiScope.renderNpcList();
    if (uiScope.renderTriggerList) {
      uiScope.renderTriggerList();
    }
    if (uiScope.renderMonsterList) {
      uiScope.renderMonsterList();
    }
    if (uiScope.renderPathList) {
      uiScope.renderPathList();
    }
    if (uiScope.renderTransitionList) {
      uiScope.renderTransitionList();
    }
    uiScope.refreshInformationPanel();
    if (typeof scope.refreshWorldOverviewWindow === "function") {
      scope.refreshWorldOverviewWindow();
    }
    renderScope.draw();
  }

  function applyState(state, options) {
    const config = options && typeof options === "object" ? options : {};
    documentScope.width = Math.max(1, Number(state?.width) || documentScope.width || 1);
    documentScope.height = Math.max(1, Number(state?.height) || documentScope.height || 1);
    documentScope.mapName = String(state?.mapName || scope.mapId || documentScope.mapName || "");
    documentScope.backgroundColor = documentScope.normalizeMapBackgroundColor(state?.backgroundColor || documentScope.backgroundColor);
    documentScope.backgroundTileId = documentScope.normalizeBackgroundTileId(state?.backgroundTileId);
    documentScope.heightBlurStep = Math.max(0, Math.min(1, Number(state?.heightBlurStep ?? state?.heightDetailStep) || documentScope.heightBlurStep || documentScope.heightDetailStep || 0.1));
    documentScope.roomLayers = documentScope.cloneLayers(Array.isArray(state.layers) ? state.layers : []);
    documentScope.heightLayers = documentScope.cloneHeightLayers(Array.isArray(state.heightLayers) ? state.heightLayers : []);
    const nextNpcs = documentScope.cloneNpcOverlays(Array.isArray(state.npcs) ? state.npcs : []);
    sessionScope.editorUiState = state && state.editorUi ? documentScope.cloneEditorUiState(state.editorUi) : { panelLayouts: {} };
    if (!documentScope.getHeightLayerById(sessionScope.activeHeightLayerId)) {
      sessionScope.activeHeightLayerId = String(documentScope.heightLayers[0]?.id || "").trim();
    }
    if (sessionScope.editingTargetKind === "height" && !sessionScope.activeHeightLayerId) {
      sessionScope.editingTargetKind = "room";
    }
    nextNpcs.forEach((npc) => documentScope.syncNpcOverlayFromRecord(npc));
    documentScope.npcOverlays.length = 0;
    nextNpcs.forEach((npc) => documentScope.npcOverlays.push(npc));
    if (typeof scope.applyWorldChunkBackgroundState === "function" && scope.isWorldModeActive?.()) {
      scope.applyWorldChunkBackgroundState(state?.worldChunkBackgrounds || {});
    }
    if (typeof scope.applyWorldBookmarkState === "function" && scope.isWorldModeActive?.()) {
      scope.applyWorldBookmarkState(state?.worldBookmarks || []);
    }
    if (typeof scope.rebuildVisibleWorldChunksFromDocument === "function" && typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive()) {
      scope.rebuildVisibleWorldChunksFromDocument();
    }
    if (!config.deferRefresh) {
      refreshUiAfterHistoryMutation();
    }
  }

  function ensureLayerForOperation(layerNumber) {
    const normalizedLayer = Number(layerNumber) || 0;
    let layerEntry = scope.roomLayers.find((layer) => Number(layer.layer) === normalizedLayer) || null;
    if (layerEntry) {
      return layerEntry;
    }
    layerEntry = {
      layer: normalizedLayer,
      name: undefined,
      zIndex: 0,
      rows: scope.normalizeRows([], normalizedLayer === 0 ? "." : " "),
      instanceIds: [],
    };
    scope.roomLayers.push(layerEntry);
    scope.roomLayers = scope.roomLayers
      .slice()
      .sort((left, right) => Number(left.layer) - Number(right.layer));
    return scope.roomLayers.find((layer) => Number(layer.layer) === normalizedLayer) || layerEntry;
  }

  function setStoredTileCharAt(layerNumber, tileX, tileY, nextStoredChar) {
    if (tileX < 0 || tileX >= scope.width || tileY < 0 || tileY >= scope.height) {
      return false;
    }
    const normalizedLayer = Number(layerNumber) || 0;
    const layerEntry = ensureLayerForOperation(normalizedLayer);
    const fillChar = normalizedLayer === 0 ? "." : " ";
    const rows = scope.normalizeRows(layerEntry.rows, fillChar);
    const row = rows[tileY] || fillChar.repeat(scope.width);
    const safeChar = String(nextStoredChar || fillChar).charAt(0) || fillChar;
    if ((row.charAt(tileX) || fillChar) === safeChar) {
      return false;
    }
    rows[tileY] = row.slice(0, tileX) + safeChar + row.slice(tileX + 1);
    layerEntry.rows = rows;
    if (typeof scope.syncWorldChunkCellFromLocalTile === "function" && typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive()) {
      scope.syncWorldChunkCellFromLocalTile(normalizedLayer, tileX, tileY, safeChar);
    }
    return true;
  }

  function resolveTileOperationCellCoord(cell) {
    if (typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive()) {
      const worldX = Number(cell?.worldX);
      const worldY = Number(cell?.worldY);
      if (Number.isFinite(worldX) && Number.isFinite(worldY)) {
        return {
          x: Math.floor(worldX - (Number(scope.worldTileOffsetX) || 0)),
          y: Math.floor(worldY - (Number(scope.worldTileOffsetY) || 0)),
        };
      }
    }
    return {
      x: Math.floor(Number(cell?.x) || 0),
      y: Math.floor(Number(cell?.y) || 0),
    };
  }

  function applyTileCellsOperation(operation, direction) {
    const isRedo = direction !== "undo";
    const nextBackgroundTileId = isRedo
      ? operation.afterBackgroundTileId
      : operation.beforeBackgroundTileId;
    if (nextBackgroundTileId !== undefined) {
      scope.backgroundTileId = scope.normalizeBackgroundTileId(nextBackgroundTileId);
    }
    const cells = Array.isArray(operation.cells) ? operation.cells : [];
    cells.forEach((cell) => {
      const resolvedCoord = resolveTileOperationCellCoord(cell, scope.width, scope.height);
      const nextStoredChar = isRedo ? cell.afterStoredChar : cell.beforeStoredChar;
      setStoredTileCharAt(cell.layer, resolvedCoord.x, resolvedCoord.y, nextStoredChar);
    });
    if (nextBackgroundTileId !== undefined && typeof scope.rebuildVisibleWorldChunksFromDocument === "function" && typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive()) {
      scope.rebuildVisibleWorldChunksFromDocument();
    }
    scope.invalidateTileSurface();
  }

  function buildNpcTargetEntries(operation, direction) {
    const useAfter = direction !== "undo";
    const rawEntries = Array.isArray(operation.entries) ? operation.entries : [];
    return rawEntries
      .map((entry) => {
        const snapshot = useAfter ? entry.after : entry.before;
        const targetIndex = useAfter ? entry.afterIndex : entry.beforeIndex;
        if (!snapshot || typeof snapshot !== "object") {
          return null;
        }
        const cloned = scope.cloneNpcOverlays([cloneValue(snapshot)])[0];
        if (!cloned) {
          return null;
        }
        scope.syncNpcOverlayFromRecord(cloned);
        return {
          npc: cloned,
          index: Math.max(0, Number(targetIndex) || 0),
        };
      })
      .filter((entry) => entry !== null)
      .sort((left, right) => left.index - right.index);
  }

  function applyNpcEntriesOperation(operation, direction) {
    const rawEntries = Array.isArray(operation.entries) ? operation.entries : [];
    const touchedPositions = [];
    rawEntries.forEach((entry) => {
      const beforePos = entry?.before && typeof entry.before === "object" ? entry.before : null;
      const afterPos = entry?.after && typeof entry.after === "object" ? entry.after : null;
      if (beforePos) {
        const x = Math.floor(Number(beforePos.x));
        const y = Math.floor(Number(beforePos.y));
        if (Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0) {
          touchedPositions.push({ x, y });
        }
      }
      if (afterPos) {
        const x = Math.floor(Number(afterPos.x));
        const y = Math.floor(Number(afterPos.y));
        if (Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0) {
          touchedPositions.push({ x, y });
        }
      }
    });
    const affectedIds = new Set(
      rawEntries.flatMap((entry) => {
        const ids = [];
        const beforeId = String(entry?.before?.id || "").trim();
        const afterId = String(entry?.after?.id || "").trim();
        if (beforeId) {
          ids.push(beforeId);
        }
        if (afterId) {
          ids.push(afterId);
        }
        return ids;
      }),
    );
    if (affectedIds.size === 0) {
      return;
    }
    const remainingNpcs = scope.npcOverlays.filter((npc) => !affectedIds.has(String(npc.id || "").trim()));
    scope.npcOverlays.length = 0;
    remainingNpcs.forEach((npc) => scope.npcOverlays.push(npc));
    affectedIds.forEach((npcId) => {
      delete scope.npcImages[npcId];
    });
    const targetEntries = buildNpcTargetEntries(operation, direction);
    targetEntries.forEach((entry) => {
      const nextIndex = Math.max(0, Math.min(scope.npcOverlays.length, entry.index));
      scope.ensureNpcImageLoaded(entry.npc);
      scope.npcOverlays.splice(nextIndex, 0, entry.npc);
    });
    if (typeof scope.rebuildWorldChunksForLocalBounds === "function" && typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive() && touchedPositions.length > 0) {
      const xs = touchedPositions.map((entry) => entry.x);
      const ys = touchedPositions.map((entry) => entry.y);
      scope.rebuildWorldChunksForLocalBounds({
        minX: Math.min(...xs),
        minY: Math.min(...ys),
        maxX: Math.max(...xs),
        maxY: Math.max(...ys),
      });
    }
  }

  function applyOperation(operation, direction, options) {
    const config = options && typeof options === "object" ? options : {};
    if (!operation || typeof operation !== "object") {
      return false;
    }
    if (operation.type === "tile_cells") {
      applyTileCellsOperation(operation, direction);
    } else if (operation.type === "npc_entries") {
      applyNpcEntriesOperation(operation, direction);
    } else {
      return false;
    }
    if (!config.deferRefresh) {
      refreshUiAfterHistoryMutation();
    }
    return true;
  }

  function cloneHistoryState(state) {
    if (!state || typeof state !== "object") {
      return captureState();
    }
    return {
      width: Math.max(1, Number(state.width) || 1),
      height: Math.max(1, Number(state.height) || 1),
      mapName: String(state.mapName || scope.mapId || ""),
      backgroundColor: documentScope.normalizeMapBackgroundColor(state.backgroundColor),
      backgroundTileId: documentScope.normalizeBackgroundTileId(state.backgroundTileId),
      heightBlurStep: Math.max(0, Math.min(1, Number(state.heightBlurStep ?? state.heightDetailStep) || 0.1)),
      layers: documentScope.cloneLayers(Array.isArray(state.layers) ? state.layers : []),
      heightLayers: documentScope.cloneHeightLayers(Array.isArray(state.heightLayers) ? state.heightLayers : []),
      npcs: documentScope.cloneNpcOverlays(Array.isArray(state.npcs) ? state.npcs : []),
      editorUi: documentScope.cloneEditorUiState(state.editorUi || {}),
    };
  }

  function ensureLayerForStateOperation(state, layerNumber) {
    const normalizedLayer = Number(layerNumber) || 0;
    let layerEntry = state.layers.find((layer) => Number(layer.layer) === normalizedLayer) || null;
    if (layerEntry) {
      return layerEntry;
    }
    layerEntry = {
      layer: normalizedLayer,
      name: undefined,
      zIndex: 0,
      rows: scope.normalizeRows([], normalizedLayer === 0 ? "." : " "),
      instanceIds: [],
    };
    state.layers.push(layerEntry);
    state.layers = state.layers
      .slice()
      .sort((left, right) => Number(left.layer) - Number(right.layer));
    return state.layers.find((layer) => Number(layer.layer) === normalizedLayer) || layerEntry;
  }

  function setStoredTileCharAtInState(state, layerNumber, tileX, tileY, nextStoredChar) {
    if (tileX < 0 || tileX >= state.width || tileY < 0 || tileY >= state.height) {
      return false;
    }
    const normalizedLayer = Number(layerNumber) || 0;
    const layerEntry = ensureLayerForStateOperation(state, normalizedLayer);
    const fillChar = normalizedLayer === 0 ? "." : " ";
    const rows = scope.normalizeRows(layerEntry.rows, fillChar);
    const row = rows[tileY] || fillChar.repeat(state.width);
    const safeChar = String(nextStoredChar || fillChar).charAt(0) || fillChar;
    if ((row.charAt(tileX) || fillChar) === safeChar) {
      return false;
    }
    rows[tileY] = row.slice(0, tileX) + safeChar + row.slice(tileX + 1);
    layerEntry.rows = rows;
    return true;
  }

  function applyTileCellsOperationToState(state, operation, direction) {
    const nextState = cloneHistoryState(state);
    const isRedo = direction !== "undo";
    const nextBackgroundTileId = isRedo
      ? operation.afterBackgroundTileId
      : operation.beforeBackgroundTileId;
    if (nextBackgroundTileId !== undefined) {
      nextState.backgroundTileId = documentScope.normalizeBackgroundTileId(nextBackgroundTileId);
    }
    const cells = Array.isArray(operation.cells) ? operation.cells : [];
    cells.forEach((cell) => {
      const resolvedCoord = resolveTileOperationCellCoord(cell, nextState.width, nextState.height);
      const nextStoredChar = isRedo ? cell.afterStoredChar : cell.beforeStoredChar;
      setStoredTileCharAtInState(nextState, cell.layer, resolvedCoord.x, resolvedCoord.y, nextStoredChar);
    });
    return nextState;
  }

  function applyNpcEntriesOperationToState(state, operation, direction) {
    const nextState = cloneHistoryState(state);
    const rawEntries = Array.isArray(operation.entries) ? operation.entries : [];
    const affectedIds = new Set(
      rawEntries.flatMap((entry) => {
        const ids = [];
        const beforeId = String(entry?.before?.id || "").trim();
        const afterId = String(entry?.after?.id || "").trim();
        if (beforeId) {
          ids.push(beforeId);
        }
        if (afterId) {
          ids.push(afterId);
        }
        return ids;
      }),
    );
    if (affectedIds.size === 0) {
      return nextState;
    }
    const useAfter = direction !== "undo";
    const remainingNpcs = nextState.npcs.filter((npc) => !affectedIds.has(String(npc.id || "").trim()));
    const targetEntries = rawEntries
      .map((entry) => {
        const snapshot = useAfter ? entry.after : entry.before;
        const targetIndex = useAfter ? entry.afterIndex : entry.beforeIndex;
        if (!snapshot || typeof snapshot !== "object") {
          return null;
        }
        const cloned = documentScope.cloneNpcOverlays([cloneValue(snapshot)])[0];
        if (!cloned) {
          return null;
        }
        documentScope.syncNpcOverlayFromRecord(cloned);
        return {
          npc: cloned,
          index: Math.max(0, Number(targetIndex) || 0),
        };
      })
      .filter((entry) => entry !== null)
      .sort((left, right) => left.index - right.index);
    nextState.npcs = remainingNpcs;
    targetEntries.forEach((entry) => {
      const nextIndex = Math.max(0, Math.min(nextState.npcs.length, entry.index));
      nextState.npcs.splice(nextIndex, 0, entry.npc);
    });
    return nextState;
  }

  function captureHistoryStateAtIndex(targetIndex) {
    const normalizedTargetIndex = Math.max(0, Math.min(Number(targetIndex) || 0, scope.historyEntries.length - 1));
    const targetEntry = scope.historyEntries[normalizedTargetIndex] || null;
    if (!targetEntry) {
      return captureState();
    }
    if (targetEntry.state) {
      return cloneHistoryState(targetEntry.state);
    }
    const snapshotIndex = findNearestSnapshotIndex(normalizedTargetIndex);
    if (snapshotIndex < 0) {
      return captureState();
    }
    let nextState = cloneHistoryState(scope.historyEntries[snapshotIndex].state);
    for (let index = snapshotIndex + 1; index <= normalizedTargetIndex; index += 1) {
      const entry = scope.historyEntries[index];
      if (!entry) {
        continue;
      }
      if (entry.state) {
        nextState = cloneHistoryState(entry.state);
        continue;
      }
      if (!entry.operation || typeof entry.operation !== "object") {
        continue;
      }
      if (entry.operation.type === "tile_cells") {
        nextState = applyTileCellsOperationToState(nextState, entry.operation, "redo");
      } else if (entry.operation.type === "npc_entries") {
        nextState = applyNpcEntriesOperationToState(nextState, entry.operation, "redo");
      }
    }
    return nextState;
  }

  function findNearestSnapshotIndex(targetIndex) {
    for (let index = Math.max(0, Number(targetIndex) || 0); index >= 0; index -= 1) {
      if (scope.historyEntries[index] && scope.historyEntries[index].state) {
        return index;
      }
    }
    return -1;
  }

  function restoreToHistoryIndex(targetIndex) {
    const normalizedTargetIndex = Math.max(0, Math.min(Number(targetIndex) || 0, scope.historyEntries.length - 1));
    const snapshotIndex = findNearestSnapshotIndex(normalizedTargetIndex);
    if (snapshotIndex < 0) {
      return false;
    }
    applyState(scope.historyEntries[snapshotIndex].state, { deferRefresh: true });
    for (let index = snapshotIndex + 1; index <= normalizedTargetIndex; index += 1) {
      const entry = scope.historyEntries[index];
      if (!entry) {
        continue;
      }
      if (entry.state) {
        applyState(entry.state, { deferRefresh: true });
        continue;
      }
      if (entry.operation) {
        applyOperation(entry.operation, "redo", { deferRefresh: true });
      }
    }
    refreshUiAfterHistoryMutation();
    return true;
  }

  function getStateSignature(state) {
    const layerSig = scope.cloneLayers(state.layers)
      .sort((a, b) => a.layer - b.layer)
      .map((layer) => ({
        layer: layer.layer,
        name: typeof layer.name === "string" ? layer.name : "",
        rows: scope.normalizeRows(layer.rows, layer.layer === 0 ? "." : " "),
      }));
    const heightLayerSig = scope.cloneHeightLayers(state.heightLayers)
      .sort((a, b) => String(a.id || "").localeCompare(String(b.id || "")))
      .map((entry) => ({
        id: String(entry.id || ""),
        name: typeof entry.name === "string" ? entry.name : "",
        z: Number(entry.z) || 1,
        x: Number(entry.x) || 0,
        y: Number(entry.y) || 0,
        rows: Array.isArray(entry.rows) ? entry.rows.map((row) => String(row || "")) : [],
      }));
    const npcSig = scope.cloneNpcOverlays(state.npcs)
      .sort((a, b) => a.id.localeCompare(b.id))
      .map((entry) => ({
        id: entry.id,
        layer: Number(entry.layer) || 0,
        name: entry.name,
        spriteId: entry.spriteId,
        x: entry.x,
        y: entry.y,
      }));
    return JSON.stringify({
      width: Number(state.width) || 1,
      height: Number(state.height) || 1,
      mapName: String(state.mapName || ""),
      backgroundColor: scope.normalizeMapBackgroundColor(state.backgroundColor),
      backgroundTileId: scope.normalizeBackgroundTileId(state.backgroundTileId),
      heightBlurStep: Math.max(0, Math.min(1, Number(state.heightBlurStep ?? state.heightDetailStep) || 0.1)),
      layerSig,
      heightLayerSig,
      npcSig,
      worldChunkBackgrounds: state && state.worldChunkBackgrounds && typeof state.worldChunkBackgrounds === "object" && !Array.isArray(state.worldChunkBackgrounds)
        ? state.worldChunkBackgrounds
        : {},
      editorUi: scope.cloneEditorUiState(state.editorUi || {}),
    });
  }

  function formatCellCoord(cell) {
    return "(" + cell.x + "," + cell.y + ")";
  }

  function formatHistoryLabel(entry) {
    const pairText = (entry.before || entry.after)
      ? (" (" + (entry.before || "?") + " -> " + (entry.after || "?") + ")")
      : "";
    return entry.label + pairText;
  }

  function renderHistoryPreview() {
    const selectedEntry = scope.historyEntries[scope.historySelectionIndex] || null;
    if (!selectedEntry) {
      scope.historyPreviewEl.innerHTML = '<h4>Change Preview</h4><div class="history-preview-empty">Select a history entry to inspect it.</div>';
      return;
    }

    const details = Array.isArray(selectedEntry.details) ? selectedEntry.details : [];
    const detailHtml = details.length > 0
      ? "<ul>" + details.map((detail) => "<li>" + detail + "</li>").join("") + "</ul>"
      : '<div class="history-preview-empty">No additional details recorded.</div>';
    const currentText = scope.historySelectionIndex === scope.historyIndex ? "Current state" : "Selected step " + selectedEntry.seq;
    scope.historyPreviewEl.innerHTML =
      "<h4>" + currentText + "</h4>" +
      '<div style="margin-bottom:6px;">' + formatHistoryLabel(selectedEntry) + "</div>" +
      detailHtml +
      '<button class="mini-btn" id="jumpHistoryBtn" type="button" style="margin-top:8px;">Restore To Selected</button>';

    const nextJumpBtn = document.getElementById("jumpHistoryBtn");
    nextJumpBtn.disabled = scope.isSaving || scope.historySelectionIndex === scope.historyIndex;
    nextJumpBtn.addEventListener("click", () => {
      if (scope.historySelectionIndex === scope.historyIndex) {
        return;
      }
      scope.historyIndex = scope.historySelectionIndex;
      restoreToHistoryIndex(scope.historyIndex);
      refreshToolbarState();
      scope.setStatus("Restored to history step " + scope.historyEntries[scope.historyIndex].seq + ".", false);
    });
  }

  function renderHistoryList() {
    scope.historyListEl.innerHTML = "";
    if (scope.historyCurrentEl) {
      const currentEntry = scope.historyEntries[scope.historyIndex] || null;
      scope.historyCurrentEl.innerHTML = currentEntry
        ? (
          '<div class="history-current-label">Current State</div>' +
          '<button type="button" class="history-row current-row">' +
            "<span>" + String(currentEntry.seq) + ". " + formatHistoryLabel(currentEntry) + "</span>" +
            '<span class="history-meta">' + new Date(currentEntry.createdAt).toLocaleTimeString() + "</span>" +
          "</button>"
        )
        : '<div class="history-current-label">Current State</div><div class="history-current-empty">No history yet.</div>';
    }
    scope.historyEntries.forEach((entry, index) => {
      if (index === scope.historyIndex) {
        return;
      }
      const row = document.createElement("button");
      row.type = "button";
      row.className = "history-row" + (index === scope.historySelectionIndex ? " active" : "");
      const timeText = new Date(entry.createdAt).toLocaleTimeString();
      row.innerHTML =
        "<span>" + String(entry.seq) + ". " + formatHistoryLabel(entry) + "</span>" +
        '<span class="history-meta">' + timeText + "</span>";
      row.addEventListener("click", () => {
        if (index === scope.historySelectionIndex) {
          return;
        }
        scope.historySelectionIndex = index;
        renderHistoryList();
        renderHistoryPreview();
      });
      scope.historyListEl.appendChild(row);
    });
  }

  function refreshToolbarState(preserveCurrentStatus) {
    const canUndo = scope.historyIndex > 0;
    const canRedo = scope.historyIndex < scope.historyEntries.length - 1;
    const currentHistoryId = scope.historyEntries[scope.historyIndex] ? scope.historyEntries[scope.historyIndex].id : 0;
    const isDirtyFromSaved = currentHistoryId !== scope.lastSavedHistoryId;

    scope.undoBtn.disabled = scope.isSaving || !canUndo;
    scope.redoBtn.disabled = scope.isSaving || !canRedo;
    scope.saveBtn.disabled = scope.isSaving || !isDirtyFromSaved;

    renderHistoryList();
    renderHistoryPreview();

    if (preserveCurrentStatus) {
      return;
    }

    if (scope.isSaving) {
      scope.setStatus("Saving...", false);
    } else if (canRedo) {
      scope.setStatus("History branch active. New edits will replace future steps.", false);
    } else if (isDirtyFromSaved) {
      scope.setStatus("Unsaved history changes.", false);
    } else {
      scope.setStatus("All changes saved.", false);
    }
  }

  function registerHistory(label, before, after, details, options) {
    const config = options && typeof options === "object" ? options : {};
    const operation = config.operation ? cloneValue(config.operation) : null;
    if (operation && operation.type === "tile_cells" && (!Array.isArray(operation.cells) || operation.cells.length === 0)) {
      return;
    }
    if (operation && operation.type === "npc_entries" && (!Array.isArray(operation.entries) || operation.entries.length === 0)) {
      return;
    }
    const shouldStoreOperationOnly = Boolean(operation);
    const nextState = shouldStoreOperationOnly ? null : (config.nextState || captureState());
    const currentEntry = scope.historyEntries[scope.historyIndex] || null;
    const currentState = currentEntry && currentEntry.state
      ? currentEntry.state
      : captureHistoryStateAtIndex(scope.historyIndex);
    if (!config.skipStateCheck && nextState && currentState && getStateSignature(nextState) === getStateSignature(currentState)) {
      return;
    }

    if (scope.historyIndex < scope.historyEntries.length - 1) {
      scope.historyEntries = scope.historyEntries.slice(0, scope.historyIndex + 1);
    }

    let operationEntriesSinceSnapshot = 0;
    if (shouldStoreOperationOnly) {
      for (let index = scope.historyEntries.length - 1; index >= 0; index -= 1) {
        const entry = scope.historyEntries[index];
        if (!entry) {
          continue;
        }
        if (entry.state) {
          break;
        }
        if (entry.operation) {
          operationEntriesSinceSnapshot += 1;
        }
      }
    }
    const checkpointState = shouldStoreOperationOnly && operationEntriesSinceSnapshot + 1 >= OPERATION_CHECKPOINT_INTERVAL
      ? captureState()
      : null;

    const entry = {
      id: scope.nextHistoryId,
      seq: scope.nextHistoryId,
      createdAt: Date.now(),
      label,
      before,
      after,
      details: Array.isArray(details) ? details : [],
      state: nextState || checkpointState,
      operation,
    };
    scope.nextHistoryId += 1;

    scope.historyEntries.push(entry);
    scope.historyIndex = scope.historyEntries.length - 1;
    scope.historySelectionIndex = scope.historyIndex;
    if (scope.historyEntries.length > MAX_HISTORY_ENTRIES) {
      const trimmedCount = scope.historyEntries.length - MAX_HISTORY_ENTRIES;
      scope.historyEntries = scope.historyEntries.slice(trimmedCount);
      scope.historyIndex = Math.max(0, scope.historyIndex - trimmedCount);
      scope.historySelectionIndex = Math.max(0, scope.historySelectionIndex - trimmedCount);
    }

    schedulePersistHistoryState();
    refreshToolbarState();
  }

  function undo() {
    if (scope.historyIndex <= 0) {
      return;
    }
    scope.historyIndex -= 1;
    scope.historySelectionIndex = scope.historyIndex;
    restoreToHistoryIndex(scope.historyIndex);
    schedulePersistHistoryState();
    refreshToolbarState();
    scope.setStatus("Undo to step " + scope.historyEntries[scope.historyIndex].seq + ".", false);
  }

  function redo() {
    if (scope.historyIndex >= scope.historyEntries.length - 1) {
      return;
    }
    scope.historyIndex += 1;
    scope.historySelectionIndex = scope.historyIndex;
    restoreToHistoryIndex(scope.historyIndex);
    schedulePersistHistoryState();
    refreshToolbarState();
    scope.setStatus("Redo to step " + scope.historyEntries[scope.historyIndex].seq + ".", false);
  }

  return {
    persistHistoryState,
    schedulePersistHistoryState,
    restoreHistoryState,
    applyHistorySnapshot,
    captureState,
    applyState,
    applyOperation,
    restoreToHistoryIndex,
    getStateSignature,
    formatCellCoord,
    formatHistoryLabel,
    renderHistoryPreview,
    renderHistoryList,
    refreshToolbarState,
    registerHistory,
    undo,
    redo,
  };
}
