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

import { createOverlayRenderer } from "./overlayRenderer";
import { buildChunkFileName } from "../worldChunking";

export function createRenderController(scope) {
  const documentScope = scope.documentScope || scope;
  const uiScope = scope.uiScope || scope;
  const sessionScope = scope.sessionScope || scope;
  const tileImages = {};
  let renderAssetsInitialized = false;
  let pendingDrawFrame = 0;
  let lastMetaMainText = "";
  let lastMetaStatsText = "";
  const renderDebugState = {
    panelEl: null,
    textEl: null,
    lastText: "",
  };
  const invalidationState = {
    lastFullReason: "startup",
    fullCount: 0,
    lastPatchReason: "",
    patchCount: 0,
    recent: [],
  };
  const profilerState = {
    fps: 0,
    fpsFrameCount: 0,
    fpsWindowStartedAt: performance.now(),
    metrics: {
      draw: { last: 0, avg: 0, max: 0 },
      tileStage: { last: 0, avg: 0, max: 0 },
      tileSurfaceRefresh: { last: 0, avg: 0, max: 0 },
      tileSurfacePatch: { last: 0, avg: 0, max: 0 },
      overlay: { last: 0, avg: 0, max: 0 },
    },
  };
  const overlayRenderer = createOverlayRenderer(scope, {
    tileImages,
    draw,
    rectIntersects,
    drawSelectionReticle,
  });
  let pixiTileStageController = null;
  let pixiTileStageControllerPromise = null;

  function isPixiTileStageActive() {
    return !!pixiTileStageController?.isReady();
  }

  function ensurePixiTileStageController() {
    if (pixiTileStageController) {
      return Promise.resolve(pixiTileStageController);
    }
    if (pixiTileStageControllerPromise) {
      return pixiTileStageControllerPromise;
    }
    pixiTileStageControllerPromise = import("./pixiTileStageController")
      .then(({ createPixiTileStageController }) => {
        pixiTileStageController = createPixiTileStageController(scope, {
          tileImages,
          recordMetric: recordProfileMetric,
        });
        return pixiTileStageController;
      })
      .catch((error) => {
        pixiTileStageControllerPromise = null;
        console.error("Failed to load the Pixi world renderer for the Worldshaper Studio.", error);
        throw error;
      });
    return pixiTileStageControllerPromise;
  }

  function initializeRenderAssets() {
    if (renderAssetsInitialized) {
      refreshRendererDebugState();
      updateMetaBar();
      return;
    }
    renderAssetsInitialized = true;
    refreshRendererDebugState();
    preloadUiImages();
    preloadNpcImages();
    preloadTileImages();
    void ensurePixiTileStageController()
      .then((controller) => controller.initialize().then((ready) => ({ controller, ready })))
      .then(({ controller, ready }) => {
        if (ready) {
          trackInvalidation("full", "renderer-ready");
          controller.invalidateAll();
        }
        drawNow();
      })
      .catch(() => {
        drawNow();
      });
    updateMetaBar();
  }

  function formatFixed2(value) {
    return Number(value || 0).toFixed(2);
  }

  function recordProfileMetric(metricName, value) {
    const bucket = profilerState.metrics[metricName];
    const duration = Math.max(0, Number(value) || 0);
    if (!bucket) {
      return duration;
    }
    bucket.last = duration;
    bucket.avg = bucket.avg > 0 ? ((bucket.avg * 0.82) + (duration * 0.18)) : duration;
    bucket.max = Math.max(bucket.max || 0, duration);
    return duration;
  }

  function measureProfileMetric(metricName, callback) {
    const startedAt = performance.now();
    const result = callback();
    recordProfileMetric(metricName, performance.now() - startedAt);
    return result;
  }

  function trackInvalidation(kind, reason) {
    const normalizedKind = kind === "patch" ? "patch" : "full";
    const normalizedReason = String(reason || "unspecified").trim() || "unspecified";
    if (normalizedKind === "patch") {
      invalidationState.lastPatchReason = normalizedReason;
      invalidationState.patchCount += 1;
    } else {
      invalidationState.lastFullReason = normalizedReason;
      invalidationState.fullCount += 1;
    }
    invalidationState.recent.unshift(normalizedKind + ":" + normalizedReason);
    if (invalidationState.recent.length > 6) {
      invalidationState.recent.length = 6;
    }
  }

  function updateMeasuredFps(now) {
    const currentNow = Number(now) || performance.now();
    profilerState.fpsFrameCount += 1;
    const elapsed = currentNow - profilerState.fpsWindowStartedAt;
    if (elapsed < 240) {
      return;
    }
    profilerState.fps = (profilerState.fpsFrameCount * 1000) / Math.max(1, elapsed);
    profilerState.fpsFrameCount = 0;
    profilerState.fpsWindowStartedAt = currentNow;
  }

  function getHeapUsageMb() {
    const heapBytes = Number(performance?.memory?.usedJSHeapSize) || 0;
    if (!heapBytes) {
      return 0;
    }
    return heapBytes / (1024 * 1024);
  }

  function formatWorldCoordLabel(label, x, y) {
    return label + " X: " + Math.round(Number(x) || 0) + " Y: " + Math.round(Number(y) || 0);
  }

  function toDisplayedWorldCoords(tileX, tileY) {
    const safeTileX = Number(tileX);
    const safeTileY = Number(tileY);
    if (!Number.isFinite(safeTileX) || !Number.isFinite(safeTileY)) {
      return null;
    }
    if (typeof scope.isWorldModeActive === "function" && scope.isWorldModeActive()) {
      return {
        x: (Number(scope.worldTileOffsetX) || 0) + safeTileX,
        y: (Number(scope.worldTileOffsetY) || 0) + safeTileY,
      };
    }
    return {
      x: safeTileX,
      y: safeTileY,
    };
  }

  function buildMetaMainText() {
    const activeLayerLabel = sessionScope.viewingAllLayers ? "all" : documentScope.getLayerDisplayName(sessionScope.activeLayer);
    const activeHeightLayer = documentScope.getActiveHeightLayer ? documentScope.getActiveHeightLayer() : null;
    const drawTargetLabel = sessionScope.editingTargetKind === "height" && activeHeightLayer
      ? (documentScope.getHeightLayerDisplayName(activeHeightLayer) + " @ Z" + Math.max(1, Number(activeHeightLayer.z) || 1))
      : documentScope.getLayerDisplayName(documentScope.getEditableLayerNumber());
    const viewportCenter = typeof scope.getViewportCenterWorldTile === "function"
      ? scope.getViewportCenterWorldTile()
      : null;
    const viewportCoordLabel = viewportCenter
      ? (" | " + formatWorldCoordLabel("view", viewportCenter.worldTileX, viewportCenter.worldTileY))
      : "";
    const hoverCoords = toDisplayedWorldCoords(sessionScope.hoverTileX, sessionScope.hoverTileY);
    const hoverCoordLabel = hoverCoords && sessionScope.hoverTileX >= 0 && sessionScope.hoverTileY >= 0
      ? (" | " + formatWorldCoordLabel("hover", hoverCoords.x, hoverCoords.y))
      : "";
    const preferredChunk = scope.isWorldModeActive?.() && typeof scope.getPreferredWorldChunkCoord === "function"
      ? scope.getPreferredWorldChunkCoord()
      : null;
    const chunkCoordLabel = preferredChunk
      ? (" | chunk " + Math.floor(Number(preferredChunk.chunkX) || 0) + "," + Math.floor(Number(preferredChunk.chunkY) || 0))
      : "";
    const selectedCoords = sessionScope.selectedTile
      ? toDisplayedWorldCoords(sessionScope.selectedTile.x, sessionScope.selectedTile.y)
      : null;
    return (
      "World: " + documentScope.mapName + " | " + documentScope.width + "x" + documentScope.height + " | tile " + scope.tileSize +
      "px | zoom " + scope.getZoomPercent() + "%" + viewportCoordLabel + hoverCoordLabel + chunkCoordLabel + " | active layer " + activeLayerLabel + " | draw target " + drawTargetLabel +
      (selectedCoords ? (" | " + formatWorldCoordLabel("sel", selectedCoords.x, selectedCoords.y)) : "") +
      (sessionScope.selectedNpcId ? " | selected npc " + sessionScope.selectedNpcId : "")
    );
  }

  function buildMetaStatsText() {
    const drawMetric = profilerState.metrics.draw;
    const tileMetric = profilerState.metrics.tileStage;
    const buildMetric = profilerState.metrics.tileSurfaceRefresh;
    const overlayMetric = profilerState.metrics.overlay;
    const heapUsageMb = getHeapUsageMb();
    return (
      "FPS: " + formatFixed2(profilerState.fps) +
      " | Draw: " + formatFixed2(drawMetric.avg) + "ms" +
      " | Tiles: " + formatFixed2(tileMetric.avg) + "ms" +
      " | Build: " + formatFixed2(buildMetric.avg) + "ms" +
      " | Overlay: " + formatFixed2(overlayMetric.avg) + "ms" +
      (heapUsageMb > 0 ? (" | Heap: " + formatFixed2(heapUsageMb) + "MB") : "")
    );
  }

  function updateMetaBar() {
    const nextMainText = buildMetaMainText();
    const nextStatsText = buildMetaStatsText();
    if (uiScope.metaMainEl) {
      if (nextMainText !== lastMetaMainText) {
        uiScope.metaMainEl.textContent = nextMainText;
      }
    } else if (uiScope.metaEl && (nextMainText !== lastMetaMainText || nextStatsText !== lastMetaStatsText)) {
      uiScope.metaEl.textContent = nextMainText + " | " + nextStatsText;
    }
    if (uiScope.metaStatsEl && nextStatsText !== lastMetaStatsText) {
      uiScope.metaStatsEl.textContent = nextStatsText;
    }
    lastMetaMainText = nextMainText;
    lastMetaStatsText = nextStatsText;
  }

  function ensureRenderDebugPanel() {
    if (renderDebugState.panelEl || !scope.stageEl) {
      return;
    }
    const panelEl = document.createElement("aside");
    panelEl.setAttribute("aria-hidden", "true");
    panelEl.style.position = "absolute";
    panelEl.style.right = "12px";
    panelEl.style.bottom = "12px";
    panelEl.style.zIndex = "12";
    panelEl.style.pointerEvents = "none";
    panelEl.style.maxWidth = "320px";
    panelEl.style.padding = "10px 12px";
    panelEl.style.border = "1px solid rgba(110, 160, 235, 0.45)";
    panelEl.style.borderRadius = "8px";
    panelEl.style.background = "rgba(7, 12, 22, 0.84)";
    panelEl.style.boxShadow = "0 10px 24px rgba(0, 0, 0, 0.24)";
    panelEl.style.backdropFilter = "blur(4px)";
    panelEl.style.color = "#d9ebff";
    panelEl.style.font = "11px/1.45 Consolas, 'SFMono-Regular', Menlo, monospace";

    const titleEl = document.createElement("div");
    titleEl.textContent = "Renderer Debug";
    titleEl.style.marginBottom = "6px";
    titleEl.style.fontWeight = "700";
    titleEl.style.letterSpacing = "0.03em";
    titleEl.style.textTransform = "uppercase";
    titleEl.style.color = "#9fd0ff";

    const textEl = document.createElement("pre");
    textEl.style.margin = "0";
    textEl.style.whiteSpace = "pre-wrap";
    textEl.style.wordBreak = "break-word";

    panelEl.appendChild(titleEl);
    panelEl.appendChild(textEl);
    scope.stageEl.appendChild(panelEl);
    renderDebugState.panelEl = panelEl;
    renderDebugState.textEl = textEl;
  }

  function updateRenderDebugPanel(viewportRect, canvasWidth, canvasHeight) {
    const renderDebugEnabled = scope.isRendererDebugEnabled?.() === true;
    if (!renderDebugEnabled) {
      if (renderDebugState.panelEl) {
        renderDebugState.panelEl.style.display = "none";
      }
      return;
    }
    ensureRenderDebugPanel();
    if (!renderDebugState.textEl) {
      return;
    }
    if (renderDebugState.panelEl) {
      renderDebugState.panelEl.style.display = "";
    }
    const pixiDebug = pixiTileStageController?.getDebugSnapshot?.() || null;
    const lines = [
      "mode: " + (isPixiTileStageActive() ? "pixi-world + canvas-overlay" : "loading"),
      "canvas: " + canvasWidth + "x" + canvasHeight + " | dpr " + (Number(window.devicePixelRatio) || 1),
      "viewport: " + viewportRect.width + "x" + viewportRect.height + " @ (" + viewportRect.left + "," + viewportRect.top + ")",
      "draw avg: " + formatFixed2(profilerState.metrics.draw.avg) + "ms | tiles " + formatFixed2(profilerState.metrics.tileStage.avg) + "ms | overlay " + formatFixed2(profilerState.metrics.overlay.avg) + "ms",
      "rebuild avg: " + formatFixed2(profilerState.metrics.tileSurfaceRefresh.avg) + "ms | patch " + formatFixed2(profilerState.metrics.tileSurfacePatch.avg) + "ms",
      "invalidate: full " + invalidationState.fullCount + " (" + invalidationState.lastFullReason + ") | patch " + invalidationState.patchCount + (invalidationState.lastPatchReason ? (" (" + invalidationState.lastPatchReason + ")") : ""),
    ];
    if (pixiDebug) {
      lines.push(
        "pixi: ready=" + (pixiDebug.ready ? "yes" : "no") + " dirty=" + (pixiDebug.dirty ? "yes" : "no") + " failed=" + (pixiDebug.failed ? "yes" : "no"),
        "chunks: " + pixiDebug.visibleChunkCount + "/" + pixiDebug.chunkCount + " visible | size " + pixiDebug.chunkSize + " | layers " + pixiDebug.layerRootCount,
        "textures: tiles " + pixiDebug.textureCacheSize + " | npc " + pixiDebug.npcTextureCacheSize + " | npc sprites " + pixiDebug.npcSpriteCount + " | height patches " + pixiDebug.heightPatchCount,
        "renderer: " + pixiDebug.rendererWidth + "x" + pixiDebug.rendererHeight + " @ " + formatFixed2(pixiDebug.rendererResolution),
      );
    }
    if (invalidationState.recent.length > 0) {
      lines.push("recent: " + invalidationState.recent.join(" | "));
    }
    const nextText = lines.join("\n");
    if (nextText === renderDebugState.lastText) {
      return;
    }
    renderDebugState.textEl.textContent = nextText;
    renderDebugState.lastText = nextText;
  }

  function refreshRendererDebugState() {
    const renderDebugEnabled = scope.isRendererDebugEnabled?.() === true;
    if (!renderDebugEnabled) {
      if (renderDebugState.panelEl) {
        renderDebugState.panelEl.style.display = "none";
      }
      if (renderDebugState.textEl) {
        renderDebugState.textEl.textContent = "";
      }
      renderDebugState.lastText = "";
      return;
    }
    ensureRenderDebugPanel();
    if (renderDebugState.panelEl) {
      renderDebugState.panelEl.style.display = "";
    }
  }

  function preloadUiImages() {
    try {
      fetch(documentScope.apiBase + "/api/images")
        .then((response) => (response.ok ? response.json() : null))
        .then((data) => {
          if (!data || !Array.isArray(data.images)) return;
          data.images.forEach((entry) => {
            const slug = String(entry.name || "").replace(/\.[^.]+$/, "");
            if (!slug) return;
            const img = new Image();
            img.src = scope.apiBase + entry.url;
            img.onload = () => {
              sessionScope.uiImageCache[slug] = img;
              uiScope.renderNpcList();
            };
          });
        })
        .catch(() => {});
    } catch {
      // Ignore image-catalog fetch issues and keep fallback icon rendering.
    }
  }

  function preloadNpcImages() {
    documentScope.npcOverlays.forEach((npc) => {
      if (npc.dataUrl) {
        const img = new Image();
        img.src = npc.dataUrl;
        sessionScope.npcImages[npc.id] = img;
      }
    });
  }

  function preloadTileImages() {
    Object.entries(documentScope.tileCatalog).forEach(([symbol, tile]) => {
      const normalizedSymbol = String(symbol || "").charAt(0);
      const nextDataUrl = tile && tile.dataUrl ? String(tile.dataUrl) : "";
      const existing = normalizedSymbol ? tileImages[normalizedSymbol] : null;
      if (!normalizedSymbol) {
        return;
      }
      if (!nextDataUrl) {
        if (existing) {
          delete tileImages[normalizedSymbol];
          pixiTileStageController?.invalidateTileTexture?.(normalizedSymbol);
        }
        return;
      }
      if (existing && existing.src === nextDataUrl) {
        if (existing.complete && existing.naturalWidth > 0) {
          return;
        }
        existing.onload = () => {
          pixiTileStageController?.invalidateTileTexture?.(normalizedSymbol);
          invalidateTileSurface("tile-image-loaded:" + normalizedSymbol);
          draw();
        };
        return;
      }
      const img = new Image();
      img.onload = () => {
        pixiTileStageController?.invalidateTileTexture?.(normalizedSymbol);
        invalidateTileSurface("tile-image-loaded:" + normalizedSymbol);
        draw();
      };
      img.src = nextDataUrl;
      tileImages[normalizedSymbol] = img;
    });
  }

  function uiIconEl(slug, fallbackText, size) {
    const img = sessionScope.uiImageCache[slug];
    if (img && img.complete && img.naturalWidth > 0) {
      const el = document.createElement("img");
      el.src = img.src;
      el.alt = fallbackText;
      el.width = size || 16;
      el.height = size || 16;
      el.style.imageRendering = "pixelated";
      el.style.pointerEvents = "none";
      return el;
    }
    const el = document.createElement("span");
    el.textContent = fallbackText;
    return el;
  }

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

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

  function findTopNpcAtCanvas(canvasX, canvasY) {
    const visible = documentScope.getVisibleNpcOverlays().slice().reverse();
    for (const npc of visible) {
      const nx = npc.x * scope.tileSize;
      const ny = npc.y * scope.tileSize;
      const drawWidth = scope.getScaledSize(npc.spriteWidth, scope.baseTileSize);
      const drawHeight = scope.getScaledSize(npc.spriteHeight, scope.baseTileSize);
      if (canvasX >= nx && canvasX < nx + drawWidth && canvasY >= ny && canvasY < ny + drawHeight) {
        return npc;
      }
    }
    return null;
  }

  function drawSelectionReticle(drawX, drawY, drawW, drawH) {
    const pad = Math.max(2, Math.min(8, scope.tileSize * 0.18));
    const left = drawX - pad;
    const top = drawY - pad;
    const width = drawW + pad * 2;
    const height = drawH + pad * 2;
    const right = left + width;
    const bottom = top + height;
    const minDim = Math.max(8, Math.min(width, height));
    const lineWidth = Math.max(1.25, Math.min(3, scope.tileSize * 0.08));
    const outlineWidth = lineWidth + 2;
    const cornerLen = Math.max(4, Math.min(minDim * 0.28, scope.tileSize * 0.46));
    const markerLen = Math.max(4, Math.min(minDim * 0.24, scope.tileSize * 0.34));
    const arrowSize = Math.max(2.5, Math.min(minDim * 0.16, scope.tileSize * 0.2));
    const centerX = left + width / 2;
    const centerY = top + height / 2;
    const primaryColor = "rgba(72, 244, 226, 0.98)";
    const accentColor = "rgba(255, 80, 96, 0.98)";
    const outlineColor = "rgba(8, 16, 28, 0.96)";
    const cornerSegments = [
      [left, top + cornerLen, left, top],
      [left, top, left + cornerLen, top],
      [right - cornerLen, top, right, top],
      [right, top, right, top + cornerLen],
      [left, bottom - cornerLen, left, bottom],
      [left, bottom, left + cornerLen, bottom],
      [right - cornerLen, bottom, right, bottom],
      [right, bottom - cornerLen, right, bottom],
    ];
    const markerSegments = [
      [centerX, top - markerLen, centerX, top - arrowSize],
      [centerX, bottom + arrowSize, centerX, bottom + markerLen],
      [left - markerLen, centerY, left - arrowSize, centerY],
      [right + arrowSize, centerY, right + markerLen, centerY],
    ];

    function strokeSegments(segments, strokeStyle, widthValue) {
      scope.ctx.strokeStyle = strokeStyle;
      scope.ctx.lineWidth = widthValue;
      scope.ctx.beginPath();
      segments.forEach(([x1, y1, x2, y2]) => {
        scope.ctx.moveTo(x1, y1);
        scope.ctx.lineTo(x2, y2);
      });
      scope.ctx.stroke();
    }

    function fillArrow(points, fillStyle, strokeStyle, widthValue) {
      scope.ctx.fillStyle = fillStyle;
      scope.ctx.strokeStyle = strokeStyle;
      scope.ctx.lineWidth = widthValue;
      scope.ctx.beginPath();
      scope.ctx.moveTo(points[0][0], points[0][1]);
      for (let i = 1; i < points.length; i += 1) {
        scope.ctx.lineTo(points[i][0], points[i][1]);
      }
      scope.ctx.closePath();
      scope.ctx.fill();
      scope.ctx.stroke();
    }

    scope.ctx.save();
    scope.ctx.lineCap = "round";
    scope.ctx.lineJoin = "round";
    strokeSegments(cornerSegments, outlineColor, outlineWidth);
    strokeSegments(cornerSegments, primaryColor, lineWidth);
    strokeSegments(markerSegments, outlineColor, outlineWidth);
    strokeSegments(markerSegments, accentColor, lineWidth);
    fillArrow([
      [centerX, top + 0.5],
      [centerX - arrowSize, top - arrowSize * 1.15],
      [centerX + arrowSize, top - arrowSize * 1.15],
    ], accentColor, outlineColor, Math.max(1, lineWidth * 0.9));
    fillArrow([
      [centerX, bottom - 0.5],
      [centerX - arrowSize, bottom + arrowSize * 1.15],
      [centerX + arrowSize, bottom + arrowSize * 1.15],
    ], accentColor, outlineColor, Math.max(1, lineWidth * 0.9));
    fillArrow([
      [left + 0.5, centerY],
      [left - arrowSize * 1.15, centerY - arrowSize],
      [left - arrowSize * 1.15, centerY + arrowSize],
    ], accentColor, outlineColor, Math.max(1, lineWidth * 0.9));
    fillArrow([
      [right - 0.5, centerY],
      [right + arrowSize * 1.15, centerY - arrowSize],
      [right + arrowSize * 1.15, centerY + arrowSize],
    ], accentColor, outlineColor, Math.max(1, lineWidth * 0.9));
    scope.ctx.restore();
  }

  function getViewportRenderRect(canvasWidth, canvasHeight) {
    const viewportWidth = Math.max(1, Math.ceil(Number(scope.viewport?.clientWidth) || canvasWidth));
    const viewportHeight = Math.max(1, Math.ceil(Number(scope.viewport?.clientHeight) || canvasHeight));
    const left = Math.max(0, Math.min(canvasWidth, Math.floor(Number(scope.viewport?.scrollLeft) || 0)));
    const top = Math.max(0, Math.min(canvasHeight, Math.floor(Number(scope.viewport?.scrollTop) || 0)));
    const right = Math.max(left + 1, Math.min(canvasWidth, Math.ceil((Number(scope.viewport?.scrollLeft) || 0) + viewportWidth)));
    const bottom = Math.max(top + 1, Math.min(canvasHeight, Math.ceil((Number(scope.viewport?.scrollTop) || 0) + viewportHeight)));
    return {
      left,
      top,
      right,
      bottom,
      width: right - left,
      height: bottom - top,
    };
  }

  function rectIntersects(rect, x, y, width, height) {
    return x + width > rect.left && x < rect.right && y + height > rect.top && y < rect.bottom;
  }

  function invalidateTileSurface(reason, options) {
    const config = options && typeof options === "object" ? options : {};
    if (config.refreshTileImages === true) {
      preloadTileImages();
    }
    trackInvalidation("full", reason);
    pixiTileStageController?.invalidateAll(config);
  }

  function patchTileSurfaceCell(tileX, tileY, reason) {
    const normalizedTileX = Number(tileX);
    const normalizedTileY = Number(tileY);
    if (!Number.isFinite(normalizedTileX) || !Number.isFinite(normalizedTileY)) {
      return false;
    }
    if (normalizedTileX < 0 || normalizedTileY < 0 || normalizedTileX >= scope.width || normalizedTileY >= scope.height) {
      return false;
    }
    if (!isPixiTileStageActive()) {
      invalidateTileSurface(reason || "patch-fallback-full");
      return false;
    }
    const patched = pixiTileStageController.patchTileAt(normalizedTileX, normalizedTileY);
    if (patched) {
      trackInvalidation("patch", reason || "cell-patch");
    }
    return patched;
  }

  function drawTileGridOverlay(viewportRect) {
    if (scope.hideTileGrid || scope.tileSize < 12) {
      return;
    }
    const startTileX = Math.max(0, Math.floor(viewportRect.left / scope.tileSize));
    const endTileX = Math.min(scope.width, Math.ceil(viewportRect.right / scope.tileSize));
    const startTileY = Math.max(0, Math.floor(viewportRect.top / scope.tileSize));
    const endTileY = Math.min(scope.height, Math.ceil(viewportRect.bottom / scope.tileSize));
    scope.ctx.save();
    scope.ctx.strokeStyle = "rgba(0,0,0,0.23)";
    scope.ctx.lineWidth = 0.5;
    scope.ctx.beginPath();
    for (let tileX = startTileX; tileX <= endTileX; tileX += 1) {
      const drawX = Math.round((tileX * scope.tileSize) - viewportRect.left) + 0.5;
      scope.ctx.moveTo(drawX, 0);
      scope.ctx.lineTo(drawX, viewportRect.height);
    }
    for (let tileY = startTileY; tileY <= endTileY; tileY += 1) {
      const drawY = Math.round((tileY * scope.tileSize) - viewportRect.top) + 0.5;
      scope.ctx.moveTo(0, drawY);
      scope.ctx.lineTo(viewportRect.width, drawY);
    }
    scope.ctx.stroke();
    scope.ctx.restore();
  }

  function drawChunkBoundsOverlay(viewportRect) {
    if (!scope.showChunkBounds || !scope.isWorldModeActive?.()) {
      return;
    }
    const chunkWidth = Math.max(1, Number(scope.worldChunkWidth) || 0);
    const chunkHeight = Math.max(1, Number(scope.worldChunkHeight) || 0);
    if (!chunkWidth || !chunkHeight) {
      return;
    }
    const startTileX = Math.max(0, Math.floor(viewportRect.left / scope.tileSize));
    const endTileX = Math.min(scope.width, Math.ceil(viewportRect.right / scope.tileSize));
    const startTileY = Math.max(0, Math.floor(viewportRect.top / scope.tileSize));
    const endTileY = Math.min(scope.height, Math.ceil(viewportRect.bottom / scope.tileSize));
    const firstBoundaryChunkX = Math.max(0, Math.floor(startTileX / chunkWidth));
    const lastBoundaryChunkX = Math.ceil(endTileX / chunkWidth);
    const firstBoundaryChunkY = Math.max(0, Math.floor(startTileY / chunkHeight));
    const lastBoundaryChunkY = Math.ceil(endTileY / chunkHeight);
    const originChunkX = Math.floor(Number(scope.worldOriginChunkX) || 0);
    const originChunkY = Math.floor(Number(scope.worldOriginChunkY) || 0);

    scope.ctx.save();
    scope.ctx.strokeStyle = "rgba(255, 209, 102, 0.95)";
    scope.ctx.lineWidth = Math.max(1, Math.min(2, scope.tileSize * 0.08));
    scope.ctx.setLineDash([Math.max(4, Math.round(scope.tileSize * 0.24)), Math.max(3, Math.round(scope.tileSize * 0.16))]);
    scope.ctx.beginPath();
    for (let chunkX = firstBoundaryChunkX; chunkX <= lastBoundaryChunkX; chunkX += 1) {
      const drawX = Math.round(((chunkX * chunkWidth) * scope.tileSize) - viewportRect.left) + 0.5;
      scope.ctx.moveTo(drawX, 0);
      scope.ctx.lineTo(drawX, viewportRect.height);
    }
    for (let chunkY = firstBoundaryChunkY; chunkY <= lastBoundaryChunkY; chunkY += 1) {
      const drawY = Math.round(((chunkY * chunkHeight) * scope.tileSize) - viewportRect.top) + 0.5;
      scope.ctx.moveTo(0, drawY);
      scope.ctx.lineTo(viewportRect.width, drawY);
    }
    scope.ctx.stroke();

    const fontSize = Math.max(10, Math.min(15, Math.round(scope.tileSize * 0.42)));
    const labelPadX = Math.max(4, Math.round(scope.tileSize * 0.18));
    const labelPadY = Math.max(4, Math.round(scope.tileSize * 0.16));
    scope.ctx.setLineDash([]);
    scope.ctx.font = `600 ${fontSize}px monospace`;
    scope.ctx.textAlign = "left";
    scope.ctx.textBaseline = "top";
    for (let chunkY = firstBoundaryChunkY; chunkY < lastBoundaryChunkY; chunkY += 1) {
      for (let chunkX = firstBoundaryChunkX; chunkX < lastBoundaryChunkX; chunkX += 1) {
        const worldChunkX = originChunkX + chunkX;
        const worldChunkY = originChunkY + chunkY;
        const label = buildChunkFileName(worldChunkX, worldChunkY);
        const drawLeft = Math.round(((chunkX * chunkWidth) * scope.tileSize) - viewportRect.left);
        const drawTop = Math.round(((chunkY * chunkHeight) * scope.tileSize) - viewportRect.top);
        const metrics = scope.ctx.measureText(label);
        const textWidth = Math.ceil(metrics.width);
        const labelHeight = fontSize + 4;
        scope.ctx.fillStyle = "rgba(8, 16, 28, 0.78)";
        scope.ctx.fillRect(
          drawLeft + 2,
          drawTop + 2,
          textWidth + (labelPadX * 2),
          labelHeight + (labelPadY * 2) - 2,
        );
        scope.ctx.fillStyle = "rgba(255, 238, 184, 0.98)";
        scope.ctx.fillText(
          label,
          drawLeft + 2 + labelPadX,
          drawTop + 2 + labelPadY,
        );
      }
    }
    scope.ctx.restore();
  }

  function drawSelectedChunkOverlay(viewportRect) {
    if (!scope.showChunkBounds || !scope.isWorldModeActive?.() || typeof scope.getSelectedWorldChunkCoord !== "function") {
      return;
    }
    const selectedChunk = scope.getSelectedWorldChunkCoord();
    if (!selectedChunk) {
      return;
    }
    const chunkWidth = Math.max(1, Number(scope.worldChunkWidth) || 0);
    const chunkHeight = Math.max(1, Number(scope.worldChunkHeight) || 0);
    const originChunkX = Math.floor(Number(scope.worldOriginChunkX) || 0);
    const originChunkY = Math.floor(Number(scope.worldOriginChunkY) || 0);
    const drawX = (((Math.floor(Number(selectedChunk.chunkX) || 0) - originChunkX) * chunkWidth) * scope.tileSize) - viewportRect.left;
    const drawY = (((Math.floor(Number(selectedChunk.chunkY) || 0) - originChunkY) * chunkHeight) * scope.tileSize) - viewportRect.top;
    const drawWidth = chunkWidth * scope.tileSize;
    const drawHeight = chunkHeight * scope.tileSize;
    if (!rectIntersects(viewportRect, drawX + viewportRect.left, drawY + viewportRect.top, drawWidth, drawHeight)) {
      return;
    }
    scope.ctx.save();
    scope.ctx.strokeStyle = "rgba(95, 195, 255, 0.98)";
    scope.ctx.lineWidth = Math.max(2, Math.min(5, scope.tileSize * 0.16));
    scope.ctx.setLineDash([]);
    scope.ctx.strokeRect(
      Math.round(drawX) + 0.5,
      Math.round(drawY) + 0.5,
      Math.max(0, Math.round(drawWidth) - 1),
      Math.max(0, Math.round(drawHeight) - 1),
    );
    scope.ctx.restore();
  }

  function drawSelectedTileOverlay(viewportRect) {
    if (
      scope.selectedTile &&
      scope.isLayerRendered(scope.selectedTile.layer) &&
      rectIntersects(viewportRect, scope.selectedTile.x * scope.tileSize, scope.selectedTile.y * scope.tileSize, scope.tileSize, scope.tileSize)
    ) {
      drawSelectionReticle(
        (scope.selectedTile.x * scope.tileSize) - viewportRect.left,
        (scope.selectedTile.y * scope.tileSize) - viewportRect.top,
        scope.tileSize,
        scope.tileSize,
      );
    }
  }

  function drawTiles(viewportRect) {
    measureProfileMetric("tileStage", () => {
      if (isPixiTileStageActive()) {
        pixiTileStageController.render(false);
      }
      drawTileGridOverlay(viewportRect);
      drawChunkBoundsOverlay(viewportRect);
      drawSelectedChunkOverlay(viewportRect);
      drawSelectedTileOverlay(viewportRect);
    });
  }

  function drawRendererLoadingState(canvasWidth, canvasHeight) {
    if (isPixiTileStageActive()) {
      return;
    }
    scope.ctx.save();
    scope.ctx.fillStyle = "rgba(7, 12, 22, 0.72)";
    scope.ctx.fillRect(0, 0, canvasWidth, canvasHeight);
    scope.ctx.fillStyle = "rgba(217, 235, 255, 0.94)";
    scope.ctx.font = "600 14px Segoe UI, Arial, sans-serif";
    scope.ctx.textAlign = "center";
    scope.ctx.textBaseline = "middle";
    scope.ctx.fillText("Loading world renderer...", canvasWidth / 2, canvasHeight / 2);
    scope.ctx.restore();
  }

  function performDraw() {
    const drawStartedAt = performance.now();
    if (typeof scope.syncViewportDimensions === "function") {
      scope.syncViewportDimensions();
    }
    const canvasWidth = Math.max(1, Math.ceil(Number(scope.viewport?.clientWidth) || 0));
    const canvasHeight = Math.max(1, Math.ceil(Number(scope.viewport?.clientHeight) || 0));
    if (scope.canvas.width !== canvasWidth || scope.canvas.height !== canvasHeight) {
      scope.canvas.width = canvasWidth;
      scope.canvas.height = canvasHeight;
    }
    const viewportRect = getViewportRenderRect(Math.max(1, scope.width * scope.tileSize), Math.max(1, scope.height * scope.tileSize));
    scope.ctx.setTransform(1, 0, 0, 1, 0, 0);
    scope.ctx.clearRect(0, 0, canvasWidth, canvasHeight);
    if (!isPixiTileStageActive()) {
      scope.ctx.fillStyle = scope.normalizeMapBackgroundColor(scope.backgroundColor);
      scope.ctx.fillRect(0, 0, canvasWidth, canvasHeight);
    }
    drawTiles(viewportRect);
    drawRendererLoadingState(canvasWidth, canvasHeight);

    updateMetaBar();

    measureProfileMetric("overlay", () => {
      scope.ctx.save();
      scope.ctx.setTransform(1, 0, 0, 1, -viewportRect.left, -viewportRect.top);
      if (isPixiTileStageActive()) {
        overlayRenderer.drawNpcUiOverlay(viewportRect);
      }
      overlayRenderer.drawGhostCursor();
      scope.ctx.restore();
    });
    overlayRenderer.drawNpcHoverLabel();
    updateRenderDebugPanel(viewportRect, canvasWidth, canvasHeight);
    updateMeasuredFps(performance.now());
    recordProfileMetric("draw", performance.now() - drawStartedAt);
  }

  function drawNow() {
    if (pendingDrawFrame) {
      window.cancelAnimationFrame(pendingDrawFrame);
      pendingDrawFrame = 0;
    }
    performDraw();
  }

  function draw() {
    if (pendingDrawFrame) {
      return;
    }
    pendingDrawFrame = window.requestAnimationFrame(() => {
      pendingDrawFrame = 0;
      performDraw();
    });
  }

  return {
    initializeRenderAssets,
    refreshRendererDebugState,
    uiIconEl,
    getCanvasPoint,
    findTopNpcAtCanvas,
    draw,
    drawNow,
    invalidateTileSurface,
    patchTileSurfaceCell,
  };
}

