import { fetchJsonOrThrow } from "../editorCore";
import {
  menuItem,
  menuLabel,
  menuSeparator,
  openContextMenuAtPoint,
} from "./contextMenuSchema";
import { clampFloatingWindowRect } from "./floatingWindowUtils";

const OVERVIEW_WINDOW_KEY = "worldOverview";
const MIN_WIDTH = 320;
const MIN_HEIGHT = 264;
const DEFAULT_WIDTH = 420;
const DEFAULT_HEIGHT = 364;
const MIN_ZOOM = 0.25;
const MAX_ZOOM = 12;

type ColorTuple = [number, number, number, number];

type LayerRectLike = {
  left?: number;
  top?: number;
  width?: number;
  height?: number;
  right?: number;
  bottom?: number;
};

type PersistedToolWindowState = {
  visible?: boolean;
  x?: number;
  y?: number;
  width?: number;
  height?: number;
};

type OverviewRoomLayer = {
  layer?: number;
  rows?: string[];
};

type OverviewChunk = {
  chunkX?: number;
  chunkY?: number;
  width?: number;
  height?: number;
  backgroundTileId?: string;
  roomLayers?: OverviewRoomLayer[];
  instances?: unknown[];
};

type OverviewPayload = {
  world?: {
    defaultBackgroundTileId?: string;
  };
  chunks?: OverviewChunk[];
};

type OverviewBookmark = {
  id?: string;
  label?: string;
  x?: number;
  y?: number;
  isSynthetic?: boolean;
};

type TileEntryLike = {
  id?: string;
  symbol?: string;
  color?: string;
  dataUrl?: string | null;
};

type DragPanState = {
  pointerId: number;
  startX: number;
  startY: number;
  originPanX: number;
  originPanY: number;
  moved: boolean;
};

type ChunkSurfaceCacheEntry = {
  signature: string;
  canvas: HTMLCanvasElement;
};

type PendingChunkAction = {
  type: "move" | "duplicate";
  sourceChunkKey: string;
  sourceChunkX: number;
  sourceChunkY: number;
};

type WorldBounds = {
  minTileX: number;
  minTileY: number;
  maxTileX: number;
  maxTileY: number;
  widthTiles: number;
  heightTiles: number;
};

type ChunkWorldRect = {
  chunkX: number;
  chunkY: number;
  chunkWidth: number;
  chunkHeight: number;
  leftTileX: number;
  topTileY: number;
  rightTileX: number;
  bottomTileY: number;
  centerTileX: number;
  centerTileY: number;
};

type ViewportWorldRect = {
  leftTileX: number;
  topTileY: number;
  rightTileX: number;
  bottomTileY: number;
  widthTiles: number;
  heightTiles: number;
};

type OverviewUiScope = {
  toolWindowLayerEl?: HTMLElement | null;
  editorBodyEl?: HTMLElement | null;
};

type OverviewSessionScope = {
  getPersistedToolWindowState?: (key: string) => PersistedToolWindowState | null;
  setPersistedToolWindowState?: (key: string, value: Record<string, unknown>) => void;
  persistPopupSessionLayout?: () => void;
};

type WorldOverviewScope = {
  uiScope?: OverviewUiScope;
  sessionScope?: OverviewSessionScope;
  worldId?: string;
  mapId?: string;
  backgroundTileId?: string;
  backgroundColor?: string;
  apiBase?: string;
  worldChunkWidth?: number;
  worldChunkHeight?: number;
  worldTileOffsetX?: number;
  worldTileOffsetY?: number;
  worldSpawnX?: number;
  worldSpawnY?: number;
  tileSize?: number;
  defaultTileColor?: string;
  viewport?: HTMLElement | null;
  persistPopupSessionLayout?: () => void;
  atTooltip?: {
    close?: () => void;
    openAtPoint?: (clientX: number, clientY: number, builder: (panel: HTMLDivElement) => void, tag?: string) => void;
    makeLabel?: (text: string) => HTMLElement;
    makeItem?: (innerHtml: string, onClick: () => void, extraClass?: string, options?: { disabled?: boolean }) => HTMLElement;
    makeSeparator?: () => HTMLElement;
  };
  getTileEntryById?: (tileId: string) => TileEntryLike | null | undefined;
  getTileEntry?: (symbol: string) => TileEntryLike | null | undefined;
  normalizeMapBackgroundColor?: (value?: string) => string;
  getCachedWorldChunkPayloads?: () => OverviewChunk[];
  getWorldBookmarks?: () => OverviewBookmark[];
  createWorldBookmark?: (worldTileX: number, worldTileY: number, label?: string) => OverviewBookmark | null;
  renameWorldBookmark?: (bookmarkId: string, nextLabel: string) => OverviewBookmark | null;
  deleteWorldBookmark?: (bookmarkId: string) => OverviewBookmark | null;
  applyWorldChunkBackgroundTileAt?: (chunkX: number, chunkY: number, tileId?: string) => Promise<boolean> | boolean;
  moveWorldChunkContent?: (
    sourceChunkX: number,
    sourceChunkY: number,
    targetChunkX: number,
    targetChunkY: number,
  ) => Promise<{ ok?: boolean; reason?: string } | boolean> | { ok?: boolean; reason?: string } | boolean;
  duplicateWorldChunkContent?: (
    sourceChunkX: number,
    sourceChunkY: number,
    targetChunkX: number,
    targetChunkY: number,
  ) => Promise<{ ok?: boolean; reason?: string } | boolean> | { ok?: boolean; reason?: string } | boolean;
  transformWorldChunkAt?: (
    chunkX: number,
    chunkY: number,
    operation: string,
  ) => Promise<{ ok?: boolean; reason?: string } | boolean> | { ok?: boolean; reason?: string } | boolean;
  clearWorldChunkAt?: (
    chunkX: number,
    chunkY: number,
  ) => Promise<{ ok?: boolean; reason?: string } | boolean> | { ok?: boolean; reason?: string } | boolean;
  activeBrushTileId?: string;
  describeBrushTileId?: (tileId: string) => string;
  registerHistory?: (label: string, before?: unknown, after?: unknown, details?: string[]) => void;
  refreshToolbarState?: (preserveCurrentStatus?: boolean) => void;
  getViewportCenterWorldTile?: () => { worldTileX?: number; worldTileY?: number };
  centerViewportOnWorldTile?: (worldTileX: number, worldTileY: number) => boolean;
  loadWorldNeighborhoodAtChunk?: (
    chunkX: number,
    chunkY: number,
    options?: {
      preserveWorldCenter?: {
        worldTileX: number;
        worldTileY: number;
      };
    },
  ) => Promise<boolean> | boolean;
  isWorldModeActive?: () => boolean;
  setStatus?: (message: string, isError?: boolean) => void;
};

type OverviewState = {
  visible: boolean;
  x: number;
  y: number;
  width: number;
  height: number;
  zoom: number;
  panX: number;
  panY: number;
  needsFit: boolean;
  renderFrame: number;
  fetchPromise: Promise<boolean> | null;
  serverWorldId: string;
  defaultBackgroundTileId: string;
  serverChunks: Map<string, OverviewChunk>;
  chunkSurfaceCache: Map<string, ChunkSurfaceCacheEntry>;
  tileColorCache: Map<string, ColorTuple>;
  tileColorPending: Set<string>;
  tileVisualRevision: number;
  shellEl: HTMLDivElement | null;
  titlebarEl: HTMLDivElement | null;
  bodyEl: HTMLDivElement | null;
  viewportEl: HTMLDivElement | null;
  canvasEl: HTMLCanvasElement | null;
  sidebarEl: HTMLDivElement | null;
  poiListEl: HTMLDivElement | null;
  poiMetaEl: HTMLSpanElement | null;
  actionBannerEl: HTMLDivElement | null;
  metaEl: HTMLDivElement | null;
  emptyEl: HTMLDivElement | null;
  resizeEl: HTMLDivElement | null;
  closeBtnEl: HTMLButtonElement | null;
  poiListSignature: string;
  dragPan: DragPanState | null;
  dragWindow: null;
  dragResize: null;
  nextZIndex: number;
  selectedBookmarkId: string;
  selectedChunkKey: string;
  pendingChunkAction: PendingChunkAction | null;
  hoveredChunkKey: string;
};

const DEFAULT_POI_ID = "__default_poi__";

function buildChunkKey(chunkX: unknown, chunkY: unknown) {
  return String(Math.floor(Number(chunkX) || 0)) + ":" + String(Math.floor(Number(chunkY) || 0));
}

function clampWindowRect(layerRect: LayerRectLike | null | undefined, left: unknown, top: unknown, width: unknown, height: unknown) {
  return clampFloatingWindowRect(layerRect, left, top, width, height, MIN_WIDTH, MIN_HEIGHT, DEFAULT_WIDTH, DEFAULT_HEIGHT);
}

function parseHexColor(value: unknown, fallback: ColorTuple = [6, 10, 20, 255]): ColorTuple {
  const raw = String(value || "").trim();
  if (!/^#[0-9a-fA-F]{6}$/.test(raw)) {
    return [fallback[0], fallback[1], fallback[2], fallback[3]];
  }
  return [
    Number.parseInt(raw.slice(1, 3), 16),
    Number.parseInt(raw.slice(3, 5), 16),
    Number.parseInt(raw.slice(5, 7), 16),
    255,
  ];
}

export function createWorldOverviewWindowController(scope: WorldOverviewScope) {
  let initialized = false;
  const uiScope: OverviewUiScope = (scope.uiScope || scope) as OverviewUiScope;
  const sessionScope: OverviewSessionScope = (scope.sessionScope || scope) as OverviewSessionScope;
  const persistedState: PersistedToolWindowState | null = typeof sessionScope.getPersistedToolWindowState === "function"
    ? sessionScope.getPersistedToolWindowState(OVERVIEW_WINDOW_KEY)
    : null;
  const state: OverviewState = {
    visible: persistedState?.visible === true,
    x: Number(persistedState?.x) || 24,
    y: Number(persistedState?.y) || 24,
    width: Number(persistedState?.width) || DEFAULT_WIDTH,
    height: Math.max(Number(persistedState?.height) || DEFAULT_HEIGHT, DEFAULT_HEIGHT),
    zoom: 1,
    panX: 0,
    panY: 0,
    needsFit: true,
    renderFrame: 0,
    fetchPromise: null,
    serverWorldId: "",
    defaultBackgroundTileId: "",
    serverChunks: new Map(),
    chunkSurfaceCache: new Map(),
    tileColorCache: new Map(),
    tileColorPending: new Set(),
    tileVisualRevision: 0,
    shellEl: null,
    titlebarEl: null,
    bodyEl: null,
    viewportEl: null,
    canvasEl: null,
    sidebarEl: null,
    poiListEl: null,
    poiMetaEl: null,
    actionBannerEl: null,
    metaEl: null,
    emptyEl: null,
    resizeEl: null,
    closeBtnEl: null,
    poiListSignature: "",
    dragPan: null,
    dragWindow: null,
    dragResize: null,
    nextZIndex: 90,
    selectedBookmarkId: "",
    selectedChunkKey: "",
    pendingChunkAction: null,
    hoveredChunkKey: "",
  };

  function getLayerRect() {
    return uiScope.toolWindowLayerEl?.getBoundingClientRect() || uiScope.editorBodyEl?.getBoundingClientRect() || {
      left: 0,
      top: 0,
      width: DEFAULT_WIDTH,
      height: DEFAULT_HEIGHT,
      right: DEFAULT_WIDTH,
      bottom: DEFAULT_HEIGHT,
    };
  }

  function getCurrentWorldId() {
    return String(scope.worldId || scope.mapId || "").trim();
  }

  function normalizeBookmarkEntry(entry: OverviewBookmark | null | undefined, fallbackIndex: number) {
    const fallbackId = `poi_${Math.max(1, fallbackIndex)}`;
    const fallbackLabel = `POI ${Math.max(1, fallbackIndex)}`;
    return {
      id: String(entry?.id || fallbackId).trim() || fallbackId,
      label: String(entry?.label || entry?.id || fallbackLabel).trim() || fallbackLabel,
      x: Math.floor(Number(entry?.x) || 0),
      y: Math.floor(Number(entry?.y) || 0),
      isSynthetic: entry?.isSynthetic === true,
    };
  }

  function getDefaultBookmark() {
    return normalizeBookmarkEntry({
      id: DEFAULT_POI_ID,
      label: "Default POI",
      x: Math.floor(Number(scope.worldSpawnX) || 0),
      y: Math.floor(Number(scope.worldSpawnY) || 0),
      isSynthetic: true,
    }, 0);
  }

  function getBookmarks() {
    const savedBookmarks = (typeof scope.getWorldBookmarks === "function" ? scope.getWorldBookmarks() : [])
      .map((entry, index) => normalizeBookmarkEntry(entry, index + 1));
    return [getDefaultBookmark(), ...savedBookmarks];
  }

  function getSavedBookmarkCount() {
    return typeof scope.getWorldBookmarks === "function"
      ? (Array.isArray(scope.getWorldBookmarks()) ? scope.getWorldBookmarks().length : 0)
      : 0;
  }

  function invalidateChunkSurfaces(chunkKeys?: string[] | null, options?: { refreshTileVisuals?: boolean }) {
    const config = options && typeof options === "object" ? options : {};
    const keys = Array.isArray(chunkKeys)
      ? Array.from(new Set(chunkKeys.map((entry) => String(entry || "").trim()).filter(Boolean)))
      : [];
    if (keys.length > 0) {
      keys.forEach((chunkKey) => {
        state.chunkSurfaceCache.delete(chunkKey);
      });
    } else {
      state.chunkSurfaceCache = new Map();
    }
    if (config.refreshTileVisuals === true || keys.length <= 0) {
      state.tileVisualRevision += 1;
    }
  }

  function buildTileColorCacheKey(entry: TileEntryLike | null | undefined, fallbackSymbol: string) {
    const tileId = String(entry?.id || fallbackSymbol || "").trim() || fallbackSymbol || ".";
    const tileDataUrl = String(entry?.dataUrl || "");
    const tileColor = String(entry?.color || "");
    return `${tileId}::${tileColor}::${tileDataUrl}`;
  }

  function computeAverageTileColorFromImage(entry: TileEntryLike, cacheKey: string, fallback: ColorTuple) {
    if (!entry?.dataUrl || state.tileColorPending.has(cacheKey)) {
      return;
    }
    state.tileColorPending.add(cacheKey);
    const image = new Image();
    image.decoding = "async";
    image.onload = () => {
      try {
        const width = Math.max(1, Math.floor(Number(image.naturalWidth) || Number(image.width) || 1));
        const height = Math.max(1, Math.floor(Number(image.naturalHeight) || Number(image.height) || 1));
        const canvas = document.createElement("canvas");
        canvas.width = width;
        canvas.height = height;
        const context = canvas.getContext("2d", { willReadFrequently: true });
        if (!context) {
          state.tileColorCache.set(cacheKey, fallback.slice() as ColorTuple);
          return;
        }
        context.clearRect(0, 0, width, height);
        context.drawImage(image, 0, 0, width, height);
        const pixels = context.getImageData(0, 0, width, height).data;
        let totalRed = 0;
        let totalGreen = 0;
        let totalBlue = 0;
        let totalAlpha = 0;
        let count = 0;
        for (let index = 0; index < pixels.length; index += 4) {
          const alpha = pixels[index + 3];
          if (alpha <= 0) {
            continue;
          }
          totalRed += pixels[index];
          totalGreen += pixels[index + 1];
          totalBlue += pixels[index + 2];
          totalAlpha += alpha;
          count += 1;
        }
        const nextColor: ColorTuple = count > 0
          ? [
            Math.round(totalRed / count),
            Math.round(totalGreen / count),
            Math.round(totalBlue / count),
            Math.max(1, Math.round(totalAlpha / count)),
          ]
          : fallback.slice() as ColorTuple;
        state.tileColorCache.set(cacheKey, nextColor);
      } catch {
        state.tileColorCache.set(cacheKey, fallback.slice() as ColorTuple);
      } finally {
        state.tileColorPending.delete(cacheKey);
        invalidateChunkSurfaces();
        scheduleRender();
      }
    };
    image.onerror = () => {
      state.tileColorPending.delete(cacheKey);
      state.tileColorCache.set(cacheKey, fallback.slice() as ColorTuple);
    };
    image.src = String(entry.dataUrl || "");
  }

  function getRepresentativeTileColor(entry: TileEntryLike | null | undefined, fallbackSymbol: string) {
    const fallback = parseHexColor(String(entry?.color || scope.defaultTileColor || "#000000"));
    if (!entry) {
      return fallback;
    }
    const cacheKey = buildTileColorCacheKey(entry, fallbackSymbol);
    const cached = state.tileColorCache.get(cacheKey);
    if (cached) {
      return cached;
    }
    if (!entry.dataUrl) {
      state.tileColorCache.set(cacheKey, fallback.slice() as ColorTuple);
      return fallback;
    }
    computeAverageTileColorFromImage(entry, cacheKey, fallback);
    return fallback;
  }

  function buildVirtualChunk(chunkX: number, chunkY: number): OverviewChunk {
    return {
      chunkX,
      chunkY,
      width: Math.max(1, Number(scope.worldChunkWidth) || 32),
      height: Math.max(1, Number(scope.worldChunkHeight) || 32),
      roomLayers: [],
    };
  }

  function getChunkCoordForWorldTile(worldTileX: number, worldTileY: number) {
    const chunkWidth = Math.max(1, Number(scope.worldChunkWidth) || 32);
    const chunkHeight = Math.max(1, Number(scope.worldChunkHeight) || 32);
    return {
      chunkX: Math.floor(Number(worldTileX) / chunkWidth),
      chunkY: Math.floor(Number(worldTileY) / chunkHeight),
    };
  }

  function getChunkWorldRect(chunk: OverviewChunk | null | undefined): ChunkWorldRect {
    const chunkWidth = Math.max(1, Number(chunk?.width) || Number(scope.worldChunkWidth) || 32);
    const chunkHeight = Math.max(1, Number(chunk?.height) || Number(scope.worldChunkHeight) || 32);
    const chunkX = Math.floor(Number(chunk?.chunkX) || 0);
    const chunkY = Math.floor(Number(chunk?.chunkY) || 0);
    const leftTileX = chunkX * chunkWidth;
    const topTileY = chunkY * chunkHeight;
    return {
      chunkX,
      chunkY,
      chunkWidth,
      chunkHeight,
      leftTileX,
      topTileY,
      rightTileX: leftTileX + chunkWidth,
      bottomTileY: topTileY + chunkHeight,
      centerTileX: leftTileX + (chunkWidth / 2),
      centerTileY: topTileY + (chunkHeight / 2),
    };
  }

  function getViewportWorldRect(): ViewportWorldRect | null {
    const viewportEl = scope.viewport;
    const tileSize = Math.max(1, Number(scope.tileSize) || 1);
    if (!viewportEl || tileSize <= 0) {
      return null;
    }
    const leftTileX = (Number(viewportEl.scrollLeft) || 0) / tileSize + (Number(scope.worldTileOffsetX) || 0);
    const topTileY = (Number(viewportEl.scrollTop) || 0) / tileSize + (Number(scope.worldTileOffsetY) || 0);
    const widthTiles = Math.max(1 / tileSize, (Number(viewportEl.clientWidth) || 0) / tileSize);
    const heightTiles = Math.max(1 / tileSize, (Number(viewportEl.clientHeight) || 0) / tileSize);
    return {
      leftTileX,
      topTileY,
      rightTileX: leftTileX + widthTiles,
      bottomTileY: topTileY + heightTiles,
      widthTiles,
      heightTiles,
    };
  }

  function getCurrentViewportChunkCoord() {
    const chunkWidth = Math.max(1, Number(scope.worldChunkWidth) || 32);
    const chunkHeight = Math.max(1, Number(scope.worldChunkHeight) || 32);
    if (typeof scope.getViewportCenterWorldTile === "function") {
      const center = scope.getViewportCenterWorldTile();
      return {
        chunkX: Math.floor((Number(center?.worldTileX) || 0) / chunkWidth),
        chunkY: Math.floor((Number(center?.worldTileY) || 0) / chunkHeight),
      };
    }
    const viewportRect = getViewportWorldRect();
    if (!viewportRect) {
      return null;
    }
    return {
      chunkX: Math.floor((viewportRect.leftTileX + (viewportRect.widthTiles / 2)) / chunkWidth),
      chunkY: Math.floor((viewportRect.topTileY + (viewportRect.heightTiles / 2)) / chunkHeight),
    };
  }

  function getWorldTileFromViewportClientPoint(clientX: number, clientY: number) {
    if (!state.viewportEl) {
      return null;
    }
    const rect = state.viewportEl.getBoundingClientRect();
    return {
      worldTileX: (clientX - rect.left - state.panX) / Math.max(MIN_ZOOM, state.zoom),
      worldTileY: (clientY - rect.top - state.panY) / Math.max(MIN_ZOOM, state.zoom),
    };
  }

  function findChunkAtWorldTile(worldTileX: number, worldTileY: number, chunks: OverviewChunk[]) {
    for (let index = 0; index < chunks.length; index += 1) {
      const chunkRect = getChunkWorldRect(chunks[index]);
      if (
        worldTileX >= chunkRect.leftTileX
        && worldTileX < chunkRect.rightTileX
        && worldTileY >= chunkRect.topTileY
        && worldTileY < chunkRect.bottomTileY
      ) {
        return chunks[index];
      }
    }
    return null;
  }

  function getChunkAtWorldTile(worldTileX: number, worldTileY: number, chunks: OverviewChunk[]) {
    const existingChunk = findChunkAtWorldTile(worldTileX, worldTileY, chunks);
    if (existingChunk) {
      return existingChunk;
    }
    const chunkCoord = getChunkCoordForWorldTile(worldTileX, worldTileY);
    return buildVirtualChunk(chunkCoord.chunkX, chunkCoord.chunkY);
  }

  async function navigateToChunk(chunk: OverviewChunk | null | undefined) {
    if (!chunk) {
      return false;
    }
    const chunkRect = getChunkWorldRect(chunk);
    state.selectedChunkKey = buildChunkKey(chunkRect.chunkX, chunkRect.chunkY);
    const currentChunk = getCurrentViewportChunkCoord();
    const targetWorldTileX = chunkRect.centerTileX;
    const targetWorldTileY = chunkRect.centerTileY;
    try {
      if (
        typeof scope.loadWorldNeighborhoodAtChunk === "function"
        && (
          !currentChunk
          || currentChunk.chunkX !== chunkRect.chunkX
          || currentChunk.chunkY !== chunkRect.chunkY
        )
      ) {
        await scope.loadWorldNeighborhoodAtChunk(chunkRect.chunkX, chunkRect.chunkY, {
          preserveWorldCenter: {
            worldTileX: targetWorldTileX,
            worldTileY: targetWorldTileY,
          },
        });
      }
      if (typeof scope.centerViewportOnWorldTile === "function") {
        scope.centerViewportOnWorldTile(targetWorldTileX, targetWorldTileY);
      }
      scheduleRender();
      return true;
    } catch (error) {
      if (typeof scope.setStatus === "function") {
        scope.setStatus("World overview navigation failed: " + String(error), true);
      }
      return false;
    }
  }

  async function navigateToWorldTile(worldTileX: number, worldTileY: number) {
    const chunkCoord = getChunkCoordForWorldTile(worldTileX, worldTileY);
    try {
      if (typeof scope.loadWorldNeighborhoodAtChunk === "function") {
        await scope.loadWorldNeighborhoodAtChunk(chunkCoord.chunkX, chunkCoord.chunkY, {
          preserveWorldCenter: {
            worldTileX,
            worldTileY,
          },
        });
      }
      if (typeof scope.centerViewportOnWorldTile === "function") {
        scope.centerViewportOnWorldTile(worldTileX, worldTileY);
      }
      state.selectedChunkKey = buildChunkKey(chunkCoord.chunkX, chunkCoord.chunkY);
      scheduleRender();
      return true;
    } catch (error) {
      if (typeof scope.setStatus === "function") {
        scope.setStatus("World overview navigation failed: " + String(error), true);
      }
      return false;
    }
  }

  function persistState() {
    if (typeof sessionScope.setPersistedToolWindowState === "function") {
      sessionScope.setPersistedToolWindowState(OVERVIEW_WINDOW_KEY, {
        visible: state.visible === true,
        mode: "floating",
        x: state.x,
        y: state.y,
        width: state.width,
        height: state.height,
        order: 999,
      });
    }
    if (typeof scope.persistPopupSessionLayout === "function") {
      scope.persistPopupSessionLayout();
    } else if (typeof sessionScope.persistPopupSessionLayout === "function") {
      sessionScope.persistPopupSessionLayout();
    }
  }

  function focusWindow() {
    if (!state.shellEl || state.visible !== true) {
      return;
    }
    state.nextZIndex += 1;
    state.shellEl.style.zIndex = String(state.nextZIndex);
    state.shellEl.classList.add("is-focused");
    state.shellEl.focus?.();
  }

  function clearFocus() {
    state.shellEl?.classList.remove("is-focused");
  }

  function applyWindowRect() {
    if (!state.shellEl) {
      return;
    }
    state.shellEl.style.left = Math.round(state.x) + "px";
    state.shellEl.style.top = Math.round(state.y) + "px";
    state.shellEl.style.width = Math.round(state.width) + "px";
    state.shellEl.style.height = Math.round(state.height) + "px";
  }

  function setEmptyState(message: unknown) {
    if (!state.emptyEl) {
      return;
    }
    const text = String(message || "").trim();
    state.emptyEl.textContent = text;
    state.emptyEl.classList.toggle("hidden", !text);
  }

  function updateMeta(message: unknown) {
    if (state.metaEl) {
      state.metaEl.textContent = String(message || "");
    }
  }

  function refreshPoiList() {
    const poiListEl = state.poiListEl;
    if (!poiListEl) {
      return;
    }
    const bookmarks = getBookmarks();
    const savedBookmarkCount = Math.max(0, getSavedBookmarkCount());
    const nextSignature = JSON.stringify({
      savedBookmarkCount,
      selectedBookmarkId: String(state.selectedBookmarkId || ""),
      bookmarks: bookmarks.map((bookmark) => ({
        id: String(bookmark.id || ""),
        label: String(bookmark.label || ""),
        x: Math.floor(Number(bookmark.x) || 0),
        y: Math.floor(Number(bookmark.y) || 0),
        isSynthetic: bookmark.isSynthetic === true,
      })),
    });
    if (state.poiMetaEl) {
      state.poiMetaEl.textContent = `${savedBookmarkCount} saved + default`;
    }
    if (state.poiListSignature === nextSignature) {
      return;
    }
    state.poiListSignature = nextSignature;
    poiListEl.innerHTML = "";
    bookmarks.forEach((bookmark) => {
      const row = document.createElement("button");
      row.type = "button";
      row.className = "world-overview-poi-row" + (state.selectedBookmarkId === bookmark.id ? " is-active" : "");
      const titleEl = document.createElement("span");
      titleEl.className = "world-overview-poi-title";
      titleEl.textContent = String(bookmark.label || bookmark.id || "POI");
      const coordsEl = document.createElement("span");
      coordsEl.className = "world-overview-poi-coords";
      coordsEl.textContent = `${bookmark.x}, ${bookmark.y}`;
      row.appendChild(titleEl);
      row.appendChild(coordsEl);
      row.addEventListener("click", () => {
        state.selectedBookmarkId = String(bookmark.id || "").trim();
        void navigateToWorldTile(bookmark.x, bookmark.y);
        refreshPoiList();
      });
      row.addEventListener("contextmenu", (event: MouseEvent) => {
        openPoiContextMenu(bookmark, event);
      });
      poiListEl.appendChild(row);
    });
  }

  function openPoiContextMenu(bookmark: OverviewBookmark, event: MouseEvent) {
    event.preventDefault();
    event.stopPropagation();
    const tooltip = scope.atTooltip;
    if (!tooltip?.openAtPoint) {
      return false;
    }
    const menuItems = [
      menuLabel(String(bookmark.label || bookmark.id || "POI")),
      menuItem("<span>Go to POI</span>", () => {
        state.selectedBookmarkId = String(bookmark.id || "").trim();
        void navigateToWorldTile(Number(bookmark.x) || 0, Number(bookmark.y) || 0);
        refreshPoiList();
        tooltip.close?.();
      }),
    ];
      if (bookmark.isSynthetic !== true) {
        menuItems.push(menuItem("<span>Rename POI</span>", () => {
          const nextRaw = window.prompt("POI name", String(bookmark.label || bookmark.id || "POI"));
          if (nextRaw === null) {
            return;
          }
          const nextLabel = String(nextRaw || "").trim();
          const updated = typeof scope.renameWorldBookmark === "function"
            ? scope.renameWorldBookmark(String(bookmark.id || ""), nextLabel)
            : null;
          if (updated) {
            state.selectedBookmarkId = String(updated.id || bookmark.id || "").trim();
            scope.registerHistory?.("POI renamed", bookmark.label || bookmark.id || "POI", updated.label || updated.id || "POI", [
              "POI: " + String(updated.id || bookmark.id || ""),
              "Label: " + String(bookmark.label || bookmark.id || "POI") + " -> " + String(updated.label || updated.id || "POI"),
              "Coordinates: " + updated.x + "," + updated.y,
            ]);
            scope.refreshToolbarState?.();
            refreshPoiList();
            scheduleRender();
            scope.setStatus?.("Renamed POI to " + String(updated.label || updated.id || "POI") + ".", false);
            tooltip.close?.();
          }
        }));
        menuItems.push(menuItem("<span>Delete POI</span>", () => {
          const removed = typeof scope.deleteWorldBookmark === "function"
            ? scope.deleteWorldBookmark(String(bookmark.id || ""))
            : null;
          if (removed) {
            if (state.selectedBookmarkId === String(removed.id || "").trim()) {
              state.selectedBookmarkId = DEFAULT_POI_ID;
            }
            scope.registerHistory?.("POI deleted", removed.label || removed.id || "POI", "removed", [
              "POI: " + String(removed.label || removed.id || "POI"),
              "Coordinates: " + removed.x + "," + removed.y,
            ]);
            scope.refreshToolbarState?.();
            refreshPoiList();
            scheduleRender();
            scope.setStatus?.("Deleted POI " + String(removed.label || removed.id || "POI") + ".", false);
            tooltip.close?.();
          }
        }));
      }
    return openContextMenuAtPoint(tooltip, event.clientX, event.clientY, menuItems, "world-overview-poi:" + String(bookmark.id || ""));
  }

  function updateActionBanner() {
    const bannerEl = state.actionBannerEl;
    if (!bannerEl) {
      return;
    }
    const pendingAction = state.pendingChunkAction;
    if (!pendingAction) {
      bannerEl.textContent = "Ready. Right-click a chunk for actions, click a POI to jump, and press Esc to cancel any active chunk move or duplicate mode.";
      bannerEl.classList.remove("hidden", "is-move", "is-duplicate");
      bannerEl.classList.add("is-idle");
      return;
    }
    const actionLabel = pendingAction.type === "move" ? "Move" : "Duplicate";
    const contentLabel = pendingAction.type === "move" ? "tiles, height data, and entities" : "tiles and height data";
    bannerEl.textContent =
      actionLabel
      + " mode: chunk "
      + pendingAction.sourceChunkX + "," + pendingAction.sourceChunkY
      + " is selected. Click an empty destination chunk to move "
      + contentLabel
      + ". Press Esc to cancel.";
    bannerEl.classList.remove("hidden", "is-idle", "is-move", "is-duplicate");
    bannerEl.classList.add(pendingAction.type === "move" ? "is-move" : "is-duplicate");
  }

  function clearPendingChunkAction() {
    state.pendingChunkAction = null;
    state.hoveredChunkKey = "";
    updateActionBanner();
    scheduleRender();
  }

  function beginPendingChunkAction(type: "move" | "duplicate", chunkRect: ChunkWorldRect, chunkKey: string, chunk?: OverviewChunk | null) {
    const entityCount = Array.isArray(chunk?.instances) ? chunk.instances.length : 0;
    if (type === "move" && entityCount > 0) {
      const confirmed = window.confirm(
        "Move chunk " + chunkRect.chunkX + "," + chunkRect.chunkY + "?\n\nThis will also move " + entityCount + " placed entit" + (entityCount === 1 ? "y" : "ies") + ".",
      );
      if (!confirmed) {
        return false;
      }
    }
    state.pendingChunkAction = {
      type,
      sourceChunkKey: chunkKey,
      sourceChunkX: chunkRect.chunkX,
      sourceChunkY: chunkRect.chunkY,
    };
    state.selectedChunkKey = chunkKey;
    state.hoveredChunkKey = "";
    updateActionBanner();
    scope.setStatus?.(
      type === "move"
        ? ("Move mode active for chunk " + chunkRect.chunkX + "," + chunkRect.chunkY + ". Click an empty destination chunk.")
        : ("Duplicate mode active for chunk " + chunkRect.chunkX + "," + chunkRect.chunkY + ". Click an empty destination chunk."),
      false,
    );
    scheduleRender();
    return true;
  }

  function openChunkTransformMenu(event: MouseEvent, chunkRect: ChunkWorldRect, chunkKey: string) {
    const tooltip = scope.atTooltip;
    if (!tooltip?.openAtPoint) {
      return false;
    }
    const executeTransform = (operation: string) => {
      if (typeof scope.transformWorldChunkAt !== "function") {
        return;
      }
      state.selectedChunkKey = chunkKey;
      void Promise.resolve(scope.transformWorldChunkAt(chunkRect.chunkX, chunkRect.chunkY, operation)).then((result) => {
        const succeeded = result === true || (result && typeof result === "object" && result.ok !== false);
        if (succeeded) {
          scheduleRender();
        }
      });
      tooltip.close?.();
    };
    return openContextMenuAtPoint(tooltip, event.clientX + 12, event.clientY + 12, [
      menuLabel(`Transform ${chunkRect.chunkX},${chunkRect.chunkY}`),
      menuItem("<span>Rotate 90° clockwise</span>", () => { executeTransform("rotate90cw"); }),
      menuItem("<span>Rotate 90° counterclockwise</span>", () => { executeTransform("rotate90ccw"); }),
      menuItem("<span>Rotate 180°</span>", () => { executeTransform("rotate180"); }),
      menuItem("<span>Flip horizontally</span>", () => { executeTransform("flipHorizontal"); }),
      menuItem("<span>Flip vertically</span>", () => { executeTransform("flipVertical"); }),
    ], "world-overview-chunk-transform:" + chunkKey);
  }

  function updatePendingChunkHover(clientX: number, clientY: number) {
    if (!state.pendingChunkAction) {
      if (state.hoveredChunkKey) {
        state.hoveredChunkKey = "";
        scheduleRender();
      }
      return null;
    }
    const worldPoint = getWorldTileFromViewportClientPoint(clientX, clientY);
    if (!worldPoint) {
      if (state.hoveredChunkKey) {
        state.hoveredChunkKey = "";
        scheduleRender();
      }
      return null;
    }
    const chunk = getChunkAtWorldTile(worldPoint.worldTileX, worldPoint.worldTileY, getCombinedChunks());
    const chunkRect = getChunkWorldRect(chunk);
    const chunkKey = buildChunkKey(chunkRect.chunkX, chunkRect.chunkY);
    if (state.hoveredChunkKey !== chunkKey) {
      state.hoveredChunkKey = chunkKey;
      scheduleRender();
    }
    return {
      chunk,
      chunkRect,
      chunkKey,
    };
  }

  async function executePendingChunkAction(chunkRect: ChunkWorldRect) {
    const pendingAction = state.pendingChunkAction;
    if (!pendingAction) {
      return false;
    }
    if (pendingAction.sourceChunkKey === buildChunkKey(chunkRect.chunkX, chunkRect.chunkY)) {
      scope.setStatus?.("Choose a different destination chunk.", true);
      return false;
    }
    try {
      const result = pendingAction.type === "move"
        ? await Promise.resolve(scope.moveWorldChunkContent?.(
          pendingAction.sourceChunkX,
          pendingAction.sourceChunkY,
          chunkRect.chunkX,
          chunkRect.chunkY,
        ))
        : await Promise.resolve(scope.duplicateWorldChunkContent?.(
          pendingAction.sourceChunkX,
          pendingAction.sourceChunkY,
          chunkRect.chunkX,
          chunkRect.chunkY,
        ));
      const succeeded = result === true || (result && typeof result === "object" && result.ok !== false);
      if (!succeeded) {
        return false;
      }
      state.selectedChunkKey = buildChunkKey(chunkRect.chunkX, chunkRect.chunkY);
      clearPendingChunkAction();
      return true;
    } catch (error) {
      scope.setStatus?.("Chunk action failed: " + String(error), true);
      return false;
    }
  }

  function openChunkContextMenu(event: MouseEvent, chunk: OverviewChunk | null | undefined, chunkRect: ChunkWorldRect, chunkKey: string) {
    event.preventDefault();
    event.stopPropagation();
    focusWindow();
    const tooltip = scope.atTooltip;
    if (!tooltip?.openAtPoint) {
      return false;
    }
    const activeTileId = String(scope.activeBrushTileId || "").trim();
    const menuItems = [
      menuLabel(`Chunk ${chunkRect.chunkX},${chunkRect.chunkY}`),
      menuItem("<span>Go to chunk</span>", () => {
        clearPendingChunkAction();
        state.selectedChunkKey = chunkKey;
        void navigateToChunk(chunk);
        tooltip.close?.();
      }),
      menuSeparator(),
      menuItem("<span>Move chunk</span>", () => {
        beginPendingChunkAction("move", chunkRect, chunkKey, chunk);
        tooltip.close?.();
      }, "", { disabled: typeof scope.moveWorldChunkContent !== "function" }),
      menuItem("<span>Duplicate chunk tiles</span>", () => {
        beginPendingChunkAction("duplicate", chunkRect, chunkKey, chunk);
        tooltip.close?.();
      }, "", { disabled: typeof scope.duplicateWorldChunkContent !== "function" }),
      menuItem("<span>Flip / Rotate</span>", () => {
        openChunkTransformMenu(event, chunkRect, chunkKey);
      }, "", { disabled: typeof scope.transformWorldChunkAt !== "function" }),
      menuItem("<span>Delete chunk</span>", () => {
        if (typeof scope.clearWorldChunkAt !== "function") {
          return;
        }
        const confirmed = window.confirm(
          "Delete chunk " + chunkRect.chunkX + "," + chunkRect.chunkY + "?\n\nThis clears the chunk's tiles, height data, and placed entities.",
        );
        if (!confirmed) {
          return;
        }
        state.selectedChunkKey = chunkKey;
        clearPendingChunkAction();
        void Promise.resolve(scope.clearWorldChunkAt(chunkRect.chunkX, chunkRect.chunkY)).then((result) => {
          const succeeded = result === true || (result && typeof result === "object" && result.ok !== false);
          if (succeeded) {
            scheduleRender();
          }
        });
        tooltip.close?.();
      }, "", { disabled: typeof scope.clearWorldChunkAt !== "function" }),
      ...(state.pendingChunkAction ? [
        menuItem("<span>Cancel pending chunk action</span>", () => {
          clearPendingChunkAction();
          tooltip.close?.();
        }),
      ] : []),
      menuSeparator(),
      menuItem(
        `<span>Paint chunk with ${activeTileId && typeof scope.describeBrushTileId === "function" ? scope.describeBrushTileId(activeTileId) : "selected tile"}</span>`,
        () => {
          if (!activeTileId || typeof scope.applyWorldChunkBackgroundTileAt !== "function") {
            return;
          }
          state.selectedChunkKey = chunkKey;
          void Promise.resolve(scope.applyWorldChunkBackgroundTileAt(chunkRect.chunkX, chunkRect.chunkY, activeTileId)).then(() => {
            scheduleRender();
          });
          tooltip.close?.();
        },
        "",
        { disabled: !activeTileId || typeof scope.applyWorldChunkBackgroundTileAt !== "function" },
      ),
      menuItem("<span>Restore chunk background</span>", () => {
        if (typeof scope.applyWorldChunkBackgroundTileAt !== "function") {
          return;
        }
        state.selectedChunkKey = chunkKey;
        void Promise.resolve(scope.applyWorldChunkBackgroundTileAt(chunkRect.chunkX, chunkRect.chunkY, "")).then(() => {
          scheduleRender();
        });
        tooltip.close?.();
      }),
      menuSeparator(),
      menuItem("<span>Set POI</span>", () => {
        const nextBookmark = typeof scope.createWorldBookmark === "function"
          ? scope.createWorldBookmark(chunkRect.centerTileX, chunkRect.centerTileY, `POI ${chunkRect.chunkX},${chunkRect.chunkY}`)
          : null;
        if (nextBookmark) {
          state.selectedBookmarkId = String(nextBookmark.id || "").trim();
          scope.registerHistory?.("POI added", "none", nextBookmark.label || nextBookmark.id || "POI", [
            "POI: " + String(nextBookmark.label || nextBookmark.id || "POI"),
            "Coordinates: " + Math.floor(Number(nextBookmark.x) || 0) + "," + Math.floor(Number(nextBookmark.y) || 0),
            "Chunk: " + chunkRect.chunkX + "," + chunkRect.chunkY,
          ]);
          scope.refreshToolbarState?.();
          refreshPoiList();
          scheduleRender();
          scope.setStatus?.("Added POI " + String(nextBookmark.label || nextBookmark.id || "POI") + ".", false);
          tooltip.close?.();
        }
      }),
    ];
    openContextMenuAtPoint(tooltip, event.clientX, event.clientY, menuItems, "world-overview-chunk:" + chunkKey);
    state.selectedChunkKey = chunkKey;
    scheduleRender();
    return true;
  }

  function ensureShell() {
    if (state.shellEl && state.shellEl.isConnected) {
      return state.shellEl;
    }
    const shellEl = document.createElement("div");
    shellEl.className = "tool-popout-window world-overview-window";
    shellEl.setAttribute("data-panel-key", OVERVIEW_WINDOW_KEY);
    shellEl.tabIndex = -1;

    const titlebarEl = document.createElement("div");
    titlebarEl.className = "tool-popout-titlebar";
    titlebarEl.innerHTML =
      '<div class="tool-popout-title">World Overview</div>' +
      '<div class="tool-popout-hint">Drag to move</div>' +
      '<button type="button" class="tool-popout-close-btn" title="Close overview" aria-label="Close overview">X</button>';

    const bodyEl = document.createElement("div");
    bodyEl.className = "tool-popout-body world-overview-body";

    const actionBannerEl = document.createElement("div");
    actionBannerEl.className = "world-overview-action-banner";

    const sidebarEl = document.createElement("div");
    sidebarEl.className = "world-overview-sidebar";

    const sidebarHeadEl = document.createElement("div");
    sidebarHeadEl.className = "world-overview-sidebar-head";
    const sidebarTitleEl = document.createElement("span");
    sidebarTitleEl.textContent = "POIs";
    const poiMetaEl = document.createElement("span");
    poiMetaEl.className = "world-overview-sidebar-meta";
    poiMetaEl.textContent = "0 saved";
    sidebarHeadEl.appendChild(sidebarTitleEl);
    sidebarHeadEl.appendChild(poiMetaEl);

    const poiListEl = document.createElement("div");
    poiListEl.className = "world-overview-poi-list";

    const mainEl = document.createElement("div");
    mainEl.className = "world-overview-main";

    const viewportEl = document.createElement("div");
    viewportEl.className = "world-overview-viewport";

    const canvasEl = document.createElement("canvas");
    canvasEl.className = "world-overview-canvas";
    viewportEl.appendChild(canvasEl);

    const emptyEl = document.createElement("div");
    emptyEl.className = "world-overview-empty hidden";
    viewportEl.appendChild(emptyEl);

    const metaEl = document.createElement("div");
    metaEl.className = "world-overview-meta";

    const resizeEl = document.createElement("div");
    resizeEl.className = "tool-popout-resize";

    sidebarEl.appendChild(sidebarHeadEl);
    sidebarEl.appendChild(poiListEl);
    mainEl.appendChild(viewportEl);
    mainEl.appendChild(metaEl);
    bodyEl.appendChild(sidebarEl);
    bodyEl.appendChild(mainEl);
    shellEl.appendChild(titlebarEl);
    shellEl.appendChild(actionBannerEl);
    shellEl.appendChild(bodyEl);
    shellEl.appendChild(resizeEl);

    const closeBtnEl = titlebarEl.querySelector(".tool-popout-close-btn") as HTMLButtonElement | null;

    shellEl.addEventListener("pointerdown", () => {
      focusWindow();
    });
    shellEl.addEventListener("keydown", (event: KeyboardEvent) => {
      if (event.key !== "Escape" || !state.pendingChunkAction) {
        return;
      }
      event.preventDefault();
      event.stopPropagation();
      clearPendingChunkAction();
      scope.setStatus?.("Cancelled pending chunk action.", false);
    });

    titlebarEl.addEventListener("pointerdown", (event: PointerEvent) => {
      if (closeBtnEl && closeBtnEl.contains(event.target as Node | null)) {
        return;
      }
      if (event.button !== 0) {
        return;
      }
      event.preventDefault();
      focusWindow();
      const layerRect = getLayerRect();
      const originLeft = Number(state.x) || 0;
      const originTop = Number(state.y) || 0;
      const startX = event.clientX;
      const startY = event.clientY;
      const move = (moveEvent: PointerEvent) => {
        const nextRect = clampWindowRect(
          layerRect,
          originLeft + (moveEvent.clientX - startX),
          originTop + (moveEvent.clientY - startY),
          state.width,
          state.height,
        );
        state.x = nextRect.left;
        state.y = nextRect.top;
        applyWindowRect();
      };
      const up = () => {
        window.removeEventListener("pointermove", move);
        window.removeEventListener("pointerup", up);
        persistState();
      };
      window.addEventListener("pointermove", move);
      window.addEventListener("pointerup", up);
    });

    closeBtnEl?.addEventListener("click", (event: MouseEvent) => {
      event.preventDefault();
      event.stopPropagation();
      close();
    });

    resizeEl.addEventListener("pointerdown", (event: PointerEvent) => {
      if (event.button !== 0) {
        return;
      }
      event.preventDefault();
      focusWindow();
      const layerRect = getLayerRect();
      const startX = event.clientX;
      const startY = event.clientY;
      const originWidth = Number(state.width) || DEFAULT_WIDTH;
      const originHeight = Number(state.height) || DEFAULT_HEIGHT;
      const move = (moveEvent: PointerEvent) => {
        const nextRect = clampWindowRect(
          layerRect,
          state.x,
          state.y,
          Math.max(MIN_WIDTH, originWidth + (moveEvent.clientX - startX)),
          Math.max(MIN_HEIGHT, originHeight + (moveEvent.clientY - startY)),
        );
        state.width = nextRect.width;
        state.height = nextRect.height;
        applyWindowRect();
        scheduleRender();
      };
      const up = () => {
        window.removeEventListener("pointermove", move);
        window.removeEventListener("pointerup", up);
        persistState();
      };
      window.addEventListener("pointermove", move);
      window.addEventListener("pointerup", up);
    });

    viewportEl.addEventListener("pointerdown", (event: PointerEvent) => {
      if (event.button !== 0) {
        return;
      }
      event.preventDefault();
      focusWindow();
      viewportEl.setPointerCapture?.(event.pointerId);
      state.dragPan = {
        pointerId: event.pointerId,
        startX: event.clientX,
        startY: event.clientY,
        originPanX: state.panX,
        originPanY: state.panY,
        moved: false,
      };
      viewportEl.classList.add("is-panning");
    });

    viewportEl.addEventListener("pointermove", (event: PointerEvent) => {
      if (!state.dragPan || state.dragPan.pointerId !== event.pointerId) {
        updatePendingChunkHover(event.clientX, event.clientY);
        return;
      }
      const deltaX = event.clientX - state.dragPan.startX;
      const deltaY = event.clientY - state.dragPan.startY;
      if (!state.dragPan.moved && ((Math.abs(deltaX) + Math.abs(deltaY)) >= 4)) {
        state.dragPan.moved = true;
      }
      state.panX = state.dragPan.originPanX + (event.clientX - state.dragPan.startX);
      state.panY = state.dragPan.originPanY + (event.clientY - state.dragPan.startY);
      updatePendingChunkHover(event.clientX, event.clientY);
      scheduleRender();
    });

    const endPan = (event?: PointerEvent) => {
      const dragPan = state.dragPan;
      if (!dragPan || (event && dragPan.pointerId !== event.pointerId)) {
        return;
      }
      if (event && dragPan.moved !== true) {
        const worldPoint = getWorldTileFromViewportClientPoint(event.clientX, event.clientY);
        if (worldPoint) {
          const chunk = getChunkAtWorldTile(worldPoint.worldTileX, worldPoint.worldTileY, getCombinedChunks());
          const chunkRect = getChunkWorldRect(chunk);
          if (state.pendingChunkAction) {
            void executePendingChunkAction(chunkRect).then((succeeded) => {
              if (succeeded) {
                scheduleRender();
              }
            });
          } else {
            void navigateToChunk(chunk);
          }
        }
      }
      viewportEl.releasePointerCapture?.(dragPan.pointerId);
      state.dragPan = null;
      viewportEl.classList.remove("is-panning");
    };

    viewportEl.addEventListener("pointerup", endPan);
    viewportEl.addEventListener("pointercancel", endPan);
    viewportEl.addEventListener("pointerleave", () => {
      if (state.hoveredChunkKey) {
        state.hoveredChunkKey = "";
        scheduleRender();
      }
    });

    viewportEl.addEventListener("wheel", (event: WheelEvent) => {
      event.preventDefault();
      const rect = viewportEl.getBoundingClientRect();
      const anchorX = event.clientX - rect.left;
      const anchorY = event.clientY - rect.top;
      const worldX = (anchorX - state.panX) / Math.max(MIN_ZOOM, state.zoom);
      const worldY = (anchorY - state.panY) / Math.max(MIN_ZOOM, state.zoom);
      const zoomFactor = event.deltaY < 0 ? 1.15 : (1 / 1.15);
      const nextZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, state.zoom * zoomFactor));
      state.zoom = nextZoom;
      state.panX = anchorX - (worldX * nextZoom);
      state.panY = anchorY - (worldY * nextZoom);
      scheduleRender();
    }, { passive: false });

    viewportEl.addEventListener("contextmenu", (event: MouseEvent) => {
      updatePendingChunkHover(event.clientX, event.clientY);
      const worldPoint = getWorldTileFromViewportClientPoint(event.clientX, event.clientY);
      if (!worldPoint) {
        return;
      }
      const chunk = getChunkAtWorldTile(worldPoint.worldTileX, worldPoint.worldTileY, getCombinedChunks());
      const chunkRect = getChunkWorldRect(chunk);
      const chunkKey = buildChunkKey(chunkRect.chunkX, chunkRect.chunkY);
      openChunkContextMenu(event, chunk, chunkRect, chunkKey);
    });

    state.shellEl = shellEl;
    state.titlebarEl = titlebarEl;
    state.bodyEl = bodyEl;
    state.viewportEl = viewportEl;
    state.canvasEl = canvasEl;
    state.sidebarEl = sidebarEl;
    state.poiListEl = poiListEl;
    state.poiMetaEl = poiMetaEl;
    state.actionBannerEl = actionBannerEl;
    state.metaEl = metaEl;
    state.emptyEl = emptyEl;
    state.resizeEl = resizeEl;
    state.closeBtnEl = closeBtnEl;
    uiScope.toolWindowLayerEl?.appendChild(shellEl);
    applyWindowRect();
    shellEl.classList.toggle("hidden", state.visible !== true);
    updateActionBanner();
    refreshPoiList();
    return shellEl;
  }

  function syncCanvasSize() {
    if (!state.canvasEl || !state.viewportEl) {
      return { width: 1, height: 1, dpr: 1 };
    }
    const viewportWidth = Math.max(1, Math.floor(Number(state.viewportEl.clientWidth) || 1));
    const viewportHeight = Math.max(1, Math.floor(Number(state.viewportEl.clientHeight) || 1));
    const dpr = Math.max(1, Number(window.devicePixelRatio) || 1);
    const pixelWidth = Math.max(1, Math.round(viewportWidth * dpr));
    const pixelHeight = Math.max(1, Math.round(viewportHeight * dpr));
    if (state.canvasEl.width !== pixelWidth || state.canvasEl.height !== pixelHeight) {
      state.canvasEl.width = pixelWidth;
      state.canvasEl.height = pixelHeight;
    }
    state.canvasEl.style.width = viewportWidth + "px";
    state.canvasEl.style.height = viewportHeight + "px";
    return { width: viewportWidth, height: viewportHeight, dpr };
  }

  function getDefaultBackgroundTileId(chunk: OverviewChunk | null | undefined) {
    return String(chunk?.backgroundTileId || state.defaultBackgroundTileId || scope.backgroundTileId || "").trim();
  }

  function getBackgroundSymbol(chunk: OverviewChunk | null | undefined) {
    const tileId = getDefaultBackgroundTileId(chunk);
    if (!tileId || typeof scope.getTileEntryById !== "function") {
      return "";
    }
    const entry = scope.getTileEntryById(tileId);
    return String(entry?.symbol || ".").charAt(0) || ".";
  }

  function getTopVisibleSymbol(chunk: OverviewChunk | null | undefined, localX: number, localY: number) {
    const backgroundSymbol = getBackgroundSymbol(chunk);
    const roomLayers = Array.isArray(chunk?.roomLayers)
      ? chunk.roomLayers.slice().sort((left: OverviewRoomLayer, right: OverviewRoomLayer) => (Number(left?.layer) || 0) - (Number(right?.layer) || 0))
      : [];
    let resolvedSymbol = backgroundSymbol || "";
    roomLayers.forEach((layer: OverviewRoomLayer) => {
      const layerNumber = Number(layer?.layer) || 0;
      const fillChar = layerNumber === 0 ? "." : " ";
      const row = Array.isArray(layer?.rows) ? String(layer.rows[localY] || "") : "";
      const symbol = String(row.charAt(localX) || fillChar).charAt(0) || fillChar;
      if (layerNumber === 0) {
        if (symbol !== "." && symbol !== " ") {
          resolvedSymbol = symbol;
        }
        return;
      }
      if (symbol !== "." && symbol !== " ") {
        resolvedSymbol = symbol;
      }
    });
    return resolvedSymbol;
  }

  function getSymbolColor(symbol: string, chunk: OverviewChunk | { backgroundTileId?: string } | null | undefined) {
    const normalizedSymbol = String(symbol || "").charAt(0);
    if (!normalizedSymbol || normalizedSymbol === "." || normalizedSymbol === " ") {
      const backgroundSymbol = getBackgroundSymbol(chunk);
      if (backgroundSymbol && backgroundSymbol !== "." && backgroundSymbol !== " ") {
        return getSymbolColor(backgroundSymbol, { backgroundTileId: "" });
      }
      return parseHexColor(scope.normalizeMapBackgroundColor ? scope.normalizeMapBackgroundColor(scope.backgroundColor || "#060A14") : "#060A14");
    }
    const entry = typeof scope.getTileEntry === "function" ? scope.getTileEntry(normalizedSymbol) : null;
    return getRepresentativeTileColor(entry, normalizedSymbol);
  }

  function buildChunkSurfaceSignature(chunk: OverviewChunk | null | undefined) {
    const roomLayerSig = (Array.isArray(chunk?.roomLayers) ? chunk.roomLayers : [])
      .map((layer: OverviewRoomLayer) => String(Number(layer?.layer) || 0) + ":" + (Array.isArray(layer?.rows) ? layer.rows.join("|") : ""))
      .join("~");
    return [
      String(chunk?.backgroundTileId || ""),
      String(chunk?.width || ""),
      String(chunk?.height || ""),
      String(state.tileVisualRevision || 0),
      roomLayerSig,
    ].join("::");
  }

  function getChunkSurface(chunk: OverviewChunk | null | undefined) {
    const chunkKey = buildChunkKey(chunk?.chunkX, chunk?.chunkY);
    const signature = buildChunkSurfaceSignature(chunk);
    const cached = state.chunkSurfaceCache.get(chunkKey);
    if (cached && cached.signature === signature) {
      return cached.canvas;
    }
    const chunkWidth = Math.max(1, Number(chunk?.width) || Number(scope.worldChunkWidth) || 32);
    const chunkHeight = Math.max(1, Number(chunk?.height) || Number(scope.worldChunkHeight) || 32);
    const canvas = document.createElement("canvas");
    canvas.width = chunkWidth;
    canvas.height = chunkHeight;
    const ctx = canvas.getContext("2d");
    if (ctx) {
      const imageData = ctx.createImageData(chunkWidth, chunkHeight);
      for (let localY = 0; localY < chunkHeight; localY += 1) {
        for (let localX = 0; localX < chunkWidth; localX += 1) {
          const color = getSymbolColor(getTopVisibleSymbol(chunk, localX, localY), chunk);
          const pixelIndex = (localY * chunkWidth + localX) * 4;
          imageData.data[pixelIndex] = color[0];
          imageData.data[pixelIndex + 1] = color[1];
          imageData.data[pixelIndex + 2] = color[2];
          imageData.data[pixelIndex + 3] = color[3];
        }
      }
      ctx.putImageData(imageData, 0, 0);
    }
    state.chunkSurfaceCache.set(chunkKey, { signature, canvas });
    return canvas;
  }

  function getCombinedChunks() {
    const worldId = getCurrentWorldId();
    const chunksByKey = new Map<string, OverviewChunk>();
    if (state.serverWorldId === worldId) {
      state.serverChunks.forEach((chunk, key) => {
        chunksByKey.set(key, chunk);
      });
    }
    const cachedChunks = typeof scope.getCachedWorldChunkPayloads === "function"
      ? scope.getCachedWorldChunkPayloads()
      : [];
    cachedChunks.forEach((chunk) => {
      if (!chunk || typeof chunk !== "object" || Array.isArray(chunk)) {
        return;
      }
      chunksByKey.set(buildChunkKey(chunk.chunkX, chunk.chunkY), chunk);
    });
    return Array.from(chunksByKey.values());
  }

  function computeWorldBounds(chunks: OverviewChunk[]) {
    if (!Array.isArray(chunks) || chunks.length === 0) {
      return null;
    }
    let minTileX = Infinity;
    let minTileY = Infinity;
    let maxTileX = -Infinity;
    let maxTileY = -Infinity;
    chunks.forEach((chunk) => {
      const chunkWidth = Math.max(1, Number(chunk?.width) || Number(scope.worldChunkWidth) || 32);
      const chunkHeight = Math.max(1, Number(chunk?.height) || Number(scope.worldChunkHeight) || 32);
      const chunkX = Math.floor(Number(chunk?.chunkX) || 0);
      const chunkY = Math.floor(Number(chunk?.chunkY) || 0);
      minTileX = Math.min(minTileX, chunkX * chunkWidth);
      minTileY = Math.min(minTileY, chunkY * chunkHeight);
      maxTileX = Math.max(maxTileX, ((chunkX + 1) * chunkWidth) - 1);
      maxTileY = Math.max(maxTileY, ((chunkY + 1) * chunkHeight) - 1);
    });
    return {
      minTileX,
      minTileY,
      maxTileX,
      maxTileY,
      widthTiles: Math.max(1, (maxTileX - minTileX) + 1),
      heightTiles: Math.max(1, (maxTileY - minTileY) + 1),
    };
  }

  function fitToBounds(bounds: WorldBounds | null, viewportWidth: number, viewportHeight: number) {
    if (!bounds) {
      return;
    }
    const padding = 18;
    const availableWidth = Math.max(1, viewportWidth - (padding * 2));
    const availableHeight = Math.max(1, viewportHeight - (padding * 2));
    const nextZoom = Math.max(
      MIN_ZOOM,
      Math.min(
        MAX_ZOOM,
        Math.min(
          availableWidth / Math.max(1, bounds.widthTiles),
          availableHeight / Math.max(1, bounds.heightTiles),
        ),
      ),
    );
    state.zoom = nextZoom;
    state.panX = padding + ((availableWidth - (bounds.widthTiles * nextZoom)) / 2) - (bounds.minTileX * nextZoom);
    state.panY = padding + ((availableHeight - (bounds.heightTiles * nextZoom)) / 2) - (bounds.minTileY * nextZoom);
  }

  function render() {
    state.renderFrame = 0;
    if (!state.visible || !state.canvasEl || !state.viewportEl) {
      return;
    }
    refreshPoiList();
    const { width: viewportWidth, height: viewportHeight, dpr } = syncCanvasSize();
    const ctx = state.canvasEl.getContext("2d");
    if (!ctx) {
      return;
    }
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.clearRect(0, 0, viewportWidth, viewportHeight);
    ctx.fillStyle = "#08101d";
    ctx.fillRect(0, 0, viewportWidth, viewportHeight);
    ctx.imageSmoothingEnabled = false;

    if (!scope.isWorldModeActive || !scope.isWorldModeActive()) {
      setEmptyState("World overview is only available while editing a world.");
      updateMeta("World mode inactive");
      return;
    }

    const chunks = getCombinedChunks();
    const bounds = computeWorldBounds(chunks);
    if (!bounds) {
      setEmptyState("No world chunks are available yet.");
      updateMeta("0 chunks");
      return;
    }
    setEmptyState("");
    if (state.needsFit) {
      fitToBounds(bounds, viewportWidth, viewportHeight);
      state.needsFit = false;
    }

    const currentChunkCoord = getCurrentViewportChunkCoord();
    const viewportWorldRect = getViewportWorldRect();

    chunks.forEach((chunk) => {
      const chunkRect = getChunkWorldRect(chunk);
      const screenX = state.panX + (chunkRect.leftTileX * state.zoom);
      const screenY = state.panY + (chunkRect.topTileY * state.zoom);
      const screenWidth = chunkRect.chunkWidth * state.zoom;
      const screenHeight = chunkRect.chunkHeight * state.zoom;
      if (screenX > viewportWidth || screenY > viewportHeight || screenX + screenWidth < 0 || screenY + screenHeight < 0) {
        return;
      }
      const surface = getChunkSurface(chunk);
      ctx.drawImage(surface, 0, 0, chunkRect.chunkWidth, chunkRect.chunkHeight, screenX, screenY, screenWidth, screenHeight);
      ctx.strokeStyle = "rgba(168, 201, 255, 0.28)";
      ctx.lineWidth = 1;
      ctx.strokeRect(Math.round(screenX) + 0.5, Math.round(screenY) + 0.5, Math.max(1, Math.round(screenWidth)), Math.max(1, Math.round(screenHeight)));
    });

    if (currentChunkCoord) {
      const chunkWidth = Math.max(1, Number(scope.worldChunkWidth) || 32);
      const chunkHeight = Math.max(1, Number(scope.worldChunkHeight) || 32);
      const currentChunkScreenX = state.panX + ((currentChunkCoord.chunkX * chunkWidth) * state.zoom);
      const currentChunkScreenY = state.panY + ((currentChunkCoord.chunkY * chunkHeight) * state.zoom);
      const currentChunkScreenWidth = chunkWidth * state.zoom;
      const currentChunkScreenHeight = chunkHeight * state.zoom;
      ctx.fillStyle = "rgba(79, 240, 209, 0.1)";
      ctx.fillRect(currentChunkScreenX, currentChunkScreenY, currentChunkScreenWidth, currentChunkScreenHeight);
      ctx.strokeStyle = "rgba(79, 240, 209, 0.92)";
      ctx.lineWidth = 2;
      ctx.strokeRect(
        Math.round(currentChunkScreenX) + 0.5,
        Math.round(currentChunkScreenY) + 0.5,
        Math.max(1, Math.round(currentChunkScreenWidth)),
        Math.max(1, Math.round(currentChunkScreenHeight)),
      );
    }

    if (state.pendingChunkAction) {
      const chunkWidth = Math.max(1, Number(scope.worldChunkWidth) || 32);
      const chunkHeight = Math.max(1, Number(scope.worldChunkHeight) || 32);
      const sourceScreenX = state.panX + ((state.pendingChunkAction.sourceChunkX * chunkWidth) * state.zoom);
      const sourceScreenY = state.panY + ((state.pendingChunkAction.sourceChunkY * chunkHeight) * state.zoom);
      const sourceScreenWidth = chunkWidth * state.zoom;
      const sourceScreenHeight = chunkHeight * state.zoom;
      ctx.fillStyle = state.pendingChunkAction.type === "move"
        ? "rgba(255, 92, 92, 0.1)"
        : "rgba(255, 209, 102, 0.1)";
      ctx.fillRect(sourceScreenX, sourceScreenY, sourceScreenWidth, sourceScreenHeight);
      ctx.strokeStyle = state.pendingChunkAction.type === "move"
        ? "rgba(255, 92, 92, 0.9)"
        : "rgba(255, 209, 102, 0.92)";
      ctx.lineWidth = 2;
      ctx.strokeRect(
        Math.round(sourceScreenX) + 0.5,
        Math.round(sourceScreenY) + 0.5,
        Math.max(1, Math.round(sourceScreenWidth)),
        Math.max(1, Math.round(sourceScreenHeight)),
      );
    }

    if (state.hoveredChunkKey && state.pendingChunkAction) {
      const [hoverChunkXRaw, hoverChunkYRaw] = state.hoveredChunkKey.split(":");
      const hoverChunkX = Math.floor(Number(hoverChunkXRaw) || 0);
      const hoverChunkY = Math.floor(Number(hoverChunkYRaw) || 0);
      const chunkWidth = Math.max(1, Number(scope.worldChunkWidth) || 32);
      const chunkHeight = Math.max(1, Number(scope.worldChunkHeight) || 32);
      const hoverScreenX = state.panX + ((hoverChunkX * chunkWidth) * state.zoom);
      const hoverScreenY = state.panY + ((hoverChunkY * chunkHeight) * state.zoom);
      const hoverScreenWidth = chunkWidth * state.zoom;
      const hoverScreenHeight = chunkHeight * state.zoom;
      ctx.fillStyle = "rgba(96, 244, 149, 0.1)";
      ctx.fillRect(hoverScreenX, hoverScreenY, hoverScreenWidth, hoverScreenHeight);
      ctx.strokeStyle = "rgba(96, 244, 149, 0.92)";
      ctx.lineWidth = 2;
      ctx.strokeRect(
        Math.round(hoverScreenX) + 0.5,
        Math.round(hoverScreenY) + 0.5,
        Math.max(1, Math.round(hoverScreenWidth)),
        Math.max(1, Math.round(hoverScreenHeight)),
      );
    }

    if (state.selectedChunkKey) {
      const [selectedChunkXRaw, selectedChunkYRaw] = state.selectedChunkKey.split(":");
      const selectedChunkX = Math.floor(Number(selectedChunkXRaw) || 0);
      const selectedChunkY = Math.floor(Number(selectedChunkYRaw) || 0);
      const chunkWidth = Math.max(1, Number(scope.worldChunkWidth) || 32);
      const chunkHeight = Math.max(1, Number(scope.worldChunkHeight) || 32);
      const selectedScreenX = state.panX + ((selectedChunkX * chunkWidth) * state.zoom);
      const selectedScreenY = state.panY + ((selectedChunkY * chunkHeight) * state.zoom);
      const selectedScreenWidth = chunkWidth * state.zoom;
      const selectedScreenHeight = chunkHeight * state.zoom;
      ctx.strokeStyle = "rgba(255, 120, 120, 0.98)";
      ctx.lineWidth = 2;
      ctx.strokeRect(
        Math.round(selectedScreenX) + 0.5,
        Math.round(selectedScreenY) + 0.5,
        Math.max(1, Math.round(selectedScreenWidth)),
        Math.max(1, Math.round(selectedScreenHeight)),
      );
    }

    if (viewportWorldRect) {
      const viewportScreenX = state.panX + (viewportWorldRect.leftTileX * state.zoom);
      const viewportScreenY = state.panY + (viewportWorldRect.topTileY * state.zoom);
      const viewportScreenWidth = viewportWorldRect.widthTiles * state.zoom;
      const viewportScreenHeight = viewportWorldRect.heightTiles * state.zoom;
      ctx.fillStyle = "rgba(255, 209, 102, 0.08)";
      ctx.fillRect(viewportScreenX, viewportScreenY, viewportScreenWidth, viewportScreenHeight);
      ctx.strokeStyle = "rgba(255, 209, 102, 0.95)";
      ctx.lineWidth = 2;
      ctx.strokeRect(
        Math.round(viewportScreenX) + 0.5,
        Math.round(viewportScreenY) + 0.5,
        Math.max(1, Math.round(viewportScreenWidth)),
        Math.max(1, Math.round(viewportScreenHeight)),
      );
    }

    const bookmarks = getBookmarks();
    bookmarks.forEach((bookmark) => {
      const screenX = state.panX + (bookmark.x * state.zoom);
      const screenY = state.panY + (bookmark.y * state.zoom);
      const radius = Math.max(3, Math.min(7, state.zoom * 0.75));
      if (screenX < -12 || screenY < -12 || screenX > viewportWidth + 12 || screenY > viewportHeight + 12) {
        return;
      }
      ctx.beginPath();
      ctx.moveTo(screenX, screenY - radius);
      ctx.lineTo(screenX + radius, screenY);
      ctx.lineTo(screenX, screenY + radius);
      ctx.lineTo(screenX - radius, screenY);
      ctx.closePath();
      ctx.fillStyle = state.selectedBookmarkId === String(bookmark.id || "").trim()
        ? "rgba(255, 209, 102, 0.96)"
        : "rgba(255, 128, 128, 0.94)";
      ctx.fill();
      ctx.strokeStyle = "rgba(16, 24, 38, 0.95)";
      ctx.lineWidth = 1;
      ctx.stroke();
    });

    updateMeta(
      String(chunks.length) + " chunks"
      + " | zoom " + Math.round(state.zoom * 100) + "%"
      + (currentChunkCoord ? (" | chunk " + currentChunkCoord.chunkX + "," + currentChunkCoord.chunkY) : "")
      + " | view " + Math.round((-state.panX) / Math.max(0.001, state.zoom)) + "," + Math.round((-state.panY) / Math.max(0.001, state.zoom)),
    );
  }

  function scheduleRender() {
    if (!state.visible) {
      return;
    }
    if (state.renderFrame) {
      return;
    }
    state.renderFrame = window.requestAnimationFrame(render);
  }

  async function loadServerOverview(force = false) {
    if (!scope.isWorldModeActive || !scope.isWorldModeActive()) {
      return false;
    }
    const worldId = getCurrentWorldId();
    if (!worldId) {
      return false;
    }
    if (!force && state.fetchPromise && state.serverWorldId === worldId) {
      return state.fetchPromise;
    }
    const requestUrl = new URL(`/api/world/${encodeURIComponent(worldId)}/overview`, scope.apiBase || window.location.origin);
    const requestPromise = fetchJsonOrThrow(requestUrl.toString())
      .then((payload) => {
        const typedPayload = payload as OverviewPayload;
        if (worldId !== getCurrentWorldId()) {
          return false;
        }
        state.serverWorldId = worldId;
        state.defaultBackgroundTileId = String(typedPayload?.world?.defaultBackgroundTileId || "").trim();
        state.serverChunks = new Map(
          (Array.isArray(typedPayload?.chunks) ? typedPayload.chunks : [])
            .filter((chunk): chunk is OverviewChunk => Boolean(chunk) && typeof chunk === "object" && !Array.isArray(chunk))
            .map((chunk) => [buildChunkKey(chunk.chunkX, chunk.chunkY), chunk]),
        );
        invalidateChunkSurfaces();
        state.needsFit = true;
        refreshPoiList();
        scheduleRender();
        return true;
      })
      .catch((error) => {
        if (typeof scope.setStatus === "function") {
          scope.setStatus("World overview load failed: " + String(error), true);
        }
        return false;
      })
      .finally(() => {
        if (state.fetchPromise === requestPromise) {
          state.fetchPromise = null;
        }
      });
    state.fetchPromise = requestPromise;
    return requestPromise;
  }

  function open() {
    ensureShell();
    const worldId = getCurrentWorldId();
    if (state.serverWorldId && state.serverWorldId !== worldId) {
      state.serverWorldId = "";
      state.defaultBackgroundTileId = "";
      state.serverChunks = new Map();
      invalidateChunkSurfaces();
      state.needsFit = true;
    }
    const nextRect = clampWindowRect(getLayerRect(), state.x, state.y, state.width, state.height);
    state.x = nextRect.left;
    state.y = nextRect.top;
    state.width = nextRect.width;
    state.height = nextRect.height;
    state.visible = true;
    state.shellEl?.classList.remove("hidden");
    applyWindowRect();
    focusWindow();
    persistState();
    if (!state.selectedBookmarkId) {
      state.selectedBookmarkId = DEFAULT_POI_ID;
    }
    refreshPoiList();
    scheduleRender();
    void loadServerOverview(false);
    return true;
  }

  function close() {
    state.visible = false;
    clearFocus();
    state.shellEl?.classList.add("hidden");
    persistState();
    return true;
  }

  function refresh() {
    if (!state.visible) {
      return false;
    }
    refreshPoiList();
    scheduleRender();
    return true;
  }

  function handleWorldContextChanged() {
    state.serverWorldId = "";
    state.defaultBackgroundTileId = "";
    state.serverChunks = new Map();
    invalidateChunkSurfaces();
    state.needsFit = true;
    state.poiListSignature = "";
    state.selectedChunkKey = "";
    state.selectedBookmarkId = DEFAULT_POI_ID;
    if (state.visible) {
      refreshPoiList();
      scheduleRender();
      void loadServerOverview(true);
    }
  }

  function initialize() {
    if (initialized) {
      return;
    }
    initialized = true;
    ensureShell();
    window.addEventListener("resize", () => {
      const nextRect = clampWindowRect(getLayerRect(), state.x, state.y, state.width, state.height);
      state.x = nextRect.left;
      state.y = nextRect.top;
      state.width = nextRect.width;
      state.height = nextRect.height;
      applyWindowRect();
      scheduleRender();
      persistState();
    });
    if (state.visible) {
      open();
    }
  }

  return {
    initialize,
    open,
    close,
    refresh,
    invalidateChunkSurfaces,
    handleWorldContextChanged,
    isOpen: () => state.visible === true,
  };
}
