import { resolveUnifiedColorSymbol } from "../editorCore";
import type { JsonObject } from "../contracts/json";

export const TILE_COLORS: Record<string, string> = {
  "#": resolveUnifiedColorSymbol("L", "#3d4f6a"),
  ".": resolveUnifiedColorSymbol("1", "#12182a"),
  "+": resolveUnifiedColorSymbol("K", "#7a5a2a"),
  "N": resolveUnifiedColorSymbol("S", "#1d4ed8"),
  "@": resolveUnifiedColorSymbol("J", "#065f46"),
  "!": resolveUnifiedColorSymbol("O", "#991b1b"),
  " ": "#060a14",
};

export const DEFAULT_TILE_COLOR = resolveUnifiedColorSymbol("C", "#4338ca");
export const DEFAULT_MAP_BACKGROUND_COLOR = "#060A14";

export type RoomLayerPayload = {
  layer: number;
  name?: string;
  zIndex?: number;
  rows: string[];
  instanceIds: string[];
};

export type HeightLayerPatchPayload = {
  id: string;
  name?: string;
  z: number;
  x: number;
  y: number;
  rows: string[];
};

export type SpriteCatalogEntry = {
  dataUrl: string | null;
  spriteWidth: number;
  spriteHeight: number;
  opacity?: number;
};

export type TileCatalogEntry = {
  id: string;
  symbol: string;
  name: string;
  color: string;
  dataUrl: string | null;
  width: number;
  height: number;
  pixelScale?: number;
  opacity?: number;
  rows?: string[];
  tags?: string[];
};

export type NpcOverlay = {
  id: string;
  layer: number;
  name: string;
  spriteId: string;
  isPlacementSlot?: boolean;
  x: number;
  y: number;
  dataUrl: string | null;
  spriteWidth: number;
  spriteHeight: number;
  opacity?: number;
  record: JsonObject;
};

export function getMapRows(record: JsonObject): string[] {
  const rawRows = record.rows;
  if (!Array.isArray(rawRows)) return [];
  return rawRows.map((row) => String(row ?? ""));
}

export function getMapDims(record: JsonObject) {
  const rows = getMapRows(record);
  const height = typeof record.height === "number" ? record.height : rows.length;
  const width = typeof record.width === "number"
    ? record.width
    : rows.reduce((max, row) => Math.max(max, row.length), 0);
  const tileSize = typeof record.tileSize === "number" ? record.tileSize : 32;
  return {
    rows,
    width: Math.max(1, Number(width) || 1),
    height: Math.max(1, Number(height) || 1),
    tileSize: Math.max(8, Math.min(128, Number(tileSize) || 32)),
  };
}

export function resizeRows(rows: string[], width: number, height: number, fillChar = "."): string[] {
  return Array.from({ length: Math.max(0, height) }, (_, y) => {
    const src = String(rows[y] ?? "");
    if (src.length >= width) return src.slice(0, width);
    return src + fillChar.repeat(Math.max(0, width - src.length));
  });
}

export function normalizeMapBackgroundColor(value: unknown, fallback = DEFAULT_MAP_BACKGROUND_COLOR): string {
  const raw = String(value || "").trim();
  return /^#[0-9a-fA-F]{6}$/.test(raw) ? raw.toUpperCase() : fallback;
}

export function getMapBackgroundTileId(record: JsonObject): string {
  const topLevel = String(record.backgroundTileId || "").trim();
  if (topLevel) {
    return topLevel;
  }
  const rawTiles = (record as Record<string, unknown>).tiles;
  if (!rawTiles || typeof rawTiles !== "object" || Array.isArray(rawTiles)) {
    return "";
  }
  return String((rawTiles as Record<string, unknown>).backgroundTileId || "").trim();
}

export function parseRoomLayers(record: JsonObject, width: number, height: number): RoomLayerPayload[] {
  const raw = record.roomLayers;
  const parsed: RoomLayerPayload[] = Array.isArray(raw)
    ? raw.flatMap((entry) => {
      if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
      const obj = entry as Record<string, unknown>;
      const layer = Number(obj.layer);
      if (!Number.isFinite(layer)) return [];
      const name = typeof obj.name === "string" ? String(obj.name).trim() : "";
      const zIndex = Number(layer) === 0 ? 0 : Math.max(0, Math.min(5, Number(obj.zIndex) || 0));
      const rows = Array.isArray(obj.rows) ? obj.rows.map((r) => String(r ?? "")) : [];
      const instanceIds = Array.isArray(obj.instanceIds) ? obj.instanceIds.map((id) => String(id ?? "").trim()).filter(Boolean) : [];
      return [{
        layer,
        name: name || undefined,
        zIndex,
        rows: resizeRows(rows, width, height, layer === 0 ? "." : " "),
        instanceIds,
      }];
    })
    : [];

  const sorted = parsed.sort((a, b) => a.layer - b.layer);
  if (!sorted.some((entry) => entry.layer === 0)) {
    sorted.unshift({
      layer: 0,
      name: undefined,
      zIndex: 0,
      rows: resizeRows(getMapRows(record), width, height, "."),
      instanceIds: [],
    });
  }
  if (sorted.length === 0) {
    return [{ layer: 0, name: undefined, zIndex: 0, rows: resizeRows(getMapRows(record), width, height, "."), instanceIds: [] }];
  }
  return sorted;
}

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

function normalizeHeightPatchRowsToMapBounds(
  rows: string[],
  x: number,
  y: number,
  mapWidth: number,
  mapHeight: number,
): { x: number; y: number; rows: string[] } {
  const safeMapWidth = Math.max(1, Number(mapWidth) || 1);
  const safeMapHeight = Math.max(1, Number(mapHeight) || 1);
  let nextX = Math.floor(Number(x) || 0);
  let nextY = Math.floor(Number(y) || 0);
  let nextRows = Array.isArray(rows) ? rows.map((row) => String(row || "").replace(/\./g, " ")) : [];

  if (nextY < 0) {
    nextRows = nextRows.slice(-nextY);
    nextY = 0;
  }
  if (nextX < 0) {
    nextRows = nextRows.map((row) => row.slice(-nextX));
    nextX = 0;
  }
  if (nextY >= safeMapHeight || nextX >= safeMapWidth) {
    return { x: nextX, y: nextY, rows: [] };
  }
  nextRows = nextRows.slice(0, Math.max(0, safeMapHeight - nextY));
  nextRows = nextRows.map((row) => row.slice(0, Math.max(0, safeMapWidth - nextX)));
  return trimHeightPatchRows(nextRows, nextX, nextY);
}

export function parseHeightLayers(record: JsonObject, width: number, height: number): HeightLayerPatchPayload[] {
  const raw = record.heightLayers;
  if (!Array.isArray(raw)) {
    return [];
  }
  const seenIds = new Set<string>();
  return raw
    .flatMap((entry, index) => {
      if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
        return [];
      }
      const obj = entry as Record<string, unknown>;
      const fallbackId = "height_" + String(index + 1);
      const id = String(obj.id || fallbackId).trim() || fallbackId;
      if (seenIds.has(id)) {
        return [];
      }
      seenIds.add(id);
      const normalized = normalizeHeightPatchRowsToMapBounds(
        Array.isArray(obj.rows) ? obj.rows.map((row) => String(row ?? "")) : [],
        Number(obj.x) || 0,
        Number(obj.y) || 0,
        width,
        height,
      );
      return [{
        id,
        name: typeof obj.name === "string" && String(obj.name).trim() ? String(obj.name).trim() : undefined,
        z: Math.max(1, Math.floor(Number(obj.z) || 1)),
        x: normalized.x,
        y: normalized.y,
        rows: normalized.rows,
      }];
    })
    .sort((a, b) => {
      if (a.z !== b.z) {
        return a.z - b.z;
      }
      return String(a.name || a.id).localeCompare(String(b.name || b.id));
    });
}

export function buildSpriteCatalog(
  allSpriteRecords: JsonObject[],
  buildSpriteDataUrl: (record: JsonObject, pixelSize: number) => string | null,
): Record<string, SpriteCatalogEntry> {
  const catalog: Record<string, SpriteCatalogEntry> = {};
  allSpriteRecords.forEach((spriteRec) => {
    const spriteId = String(spriteRec.id || "").trim();
    if (!spriteId) {
      return;
    }
    const rawPixelScale = Math.max(1, Number(spriteRec.pixelScale) || 1);
    catalog[spriteId] = {
      dataUrl: buildSpriteDataUrl(spriteRec, rawPixelScale),
      spriteWidth: Math.max(8, (Number(spriteRec.width) || 16) * rawPixelScale),
      spriteHeight: Math.max(8, (Number(spriteRec.height) || 16) * rawPixelScale),
      opacity: Number.isFinite(Number(spriteRec.opacity)) ? Math.max(0, Math.min(1, Number(spriteRec.opacity))) : 1,
    };
  });
  return catalog;
}

export function buildTileCatalogById(
  allTileRecords: JsonObject[],
  buildSpriteDataUrl: (record: JsonObject, pixelSize: number) => string | null,
): Record<string, TileCatalogEntry> {
  const catalog: Record<string, TileCatalogEntry> = {};
  allTileRecords.forEach((tileRec) => {
    const id = String(tileRec.id || "").trim();
    const symbol = String(tileRec.symbol || "").trim().charAt(0);
    if (!id || !symbol) {
      return;
    }
    const rawPixelScale = Math.max(1, Number(tileRec.pixelScale) || 1);
    const rows = Array.isArray(tileRec.rows) ? tileRec.rows.map((row) => String(row || "")) : [];
    const firstRow = rows[0] || "";
    const firstSymbol = String(firstRow.charAt(0) || "").toUpperCase();
    const fallbackColor = firstSymbol && firstSymbol !== "."
      ? resolveUnifiedColorSymbol(firstSymbol, DEFAULT_TILE_COLOR)
      : DEFAULT_TILE_COLOR;
    catalog[id] = {
      id,
      symbol,
      name: String(tileRec.name || id),
      color: fallbackColor,
      dataUrl: buildSpriteDataUrl(tileRec, rawPixelScale),
      width: Math.max(1, Number(tileRec.width) || 1),
      height: Math.max(1, Number(tileRec.height) || 1),
      pixelScale: rawPixelScale,
      opacity: Number.isFinite(Number(tileRec.opacity)) ? Math.max(0, Math.min(1, Number(tileRec.opacity))) : 1,
      rows,
      tags: Array.isArray(tileRec.tags) ? tileRec.tags.map((tag) => String(tag || "").trim()).filter(Boolean) : [],
    };
  });
  return catalog;
}

export function buildNpcOverlays(
  mapId: string,
  allNpcRecords: JsonObject[],
  spriteCatalog: Record<string, SpriteCatalogEntry>,
): NpcOverlay[] {
  const recordsToRender = allNpcRecords.filter((npc) => String(npc.mapId || "").trim() === mapId);

  return recordsToRender
    .map((npc) => {
      const pos = (npc.position && typeof npc.position === "object" && !Array.isArray(npc.position))
        ? npc.position as Record<string, unknown>
        : {};
      const x = typeof pos.x === "number" ? pos.x : (typeof npc.x === "number" ? npc.x : 0);
      const y = typeof pos.y === "number" ? pos.y : (typeof npc.y === "number" ? npc.y : 0);
      const layer = Number(npc.layer ?? 0) || 0;
      const spriteId = String(npc.spriteIdOverride || npc.spriteId || "").trim();
      const spriteEntry = spriteCatalog[spriteId] || null;
      const name = String(npc.nameOverride || npc.name || npc.id || "NPC");

      return {
        id: String(npc.id || ""),
        layer,
        name,
        spriteId,
        x,
        y,
        dataUrl: spriteEntry ? spriteEntry.dataUrl : null,
        spriteWidth: spriteEntry ? spriteEntry.spriteWidth : 28,
        spriteHeight: spriteEntry ? spriteEntry.spriteHeight : 28,
        opacity: spriteEntry ? spriteEntry.opacity : 1,
        record: { ...npc },
      };
    });
}
