import {
  Application,
  Assets,
  BufferImageSource,
  Container,
  Graphics,
  Rectangle,
  Sprite,
  Texture,
} from "pixi.js";
import { settings as tilemapSettings, Tilemap } from "@pixi/tilemap";
import seedrandom from "seedrandom";

tilemapSettings.use32bitIndex = true;
import {
  parseGpl,
  decodeWorldMap,
  type GplEntry,
  type TileGrid,
} from "../srcUtils/worldMaps.ts";
import { buildTileAtlas, TileAtlas } from "../srcUtils/tileAtlas.ts";
import {
  generateTileset,
  NoiseAlgo,
  type TextureSpec,
} from "./scratchProceduralTexture.ts";
import paletteText from "./maps/palette2.gpl?raw";

const islandMaps = import.meta.glob("./maps/islands*.png", {
  eager: true,
  import: "default",
  query: "?url",
}) as Record<string, string>;

const TILE_SIZE = 32;
const TILE_VARIANTS = 16;

const FOREST_SPEC: TextureSpec = {
  rgb: [25, 110, 35],
  lum: 0.3,
  range: 0.16,
  fill: 0.2,
  algo: NoiseAlgo.corner,
};

const WATER_SPEC: TextureSpec = {
  rgb: [20, 35, 140],
  lum: 0.2,
  range: 0.12,
  fill: 0.15,
  algo: NoiseAlgo.domino,
};

const GROUND_SPEC: TextureSpec = {
  rgb: [90, 55, 0],
  lum: 0.3,
  range: 0.08,
  fill: 0.1,
  algo: NoiseAlgo.normal,
};

const MOUNTAIN_SPEC: TextureSpec = {
  rgb: [210, 210, 210],
  lum: 0.5,
  range: 0.12,
  fill: 0.1,
  algo: NoiseAlgo.corner,
};
const VIEWPORT_MARGIN = 16;
const BORDER_COLOR = 0x888888;
const BORDER_WIDTH = 2;
const PAN_STEP = TILE_SIZE;
const STORAGE_KEY = "scratchWorldMap";
const MINIMAP_FRACTION = 0.22;

enum TileType {
  Blank = 0,
  Floor = 1,
  Wall = 2,
  Forest = 3,
  Water = 4,
  Ground = 5,
  Mountain = 6,
}

const BEVEL = 2;

// Display colors — separate from palette save colors
const DISPLAY_COLORS: Record<TileType, [number, number, number]> = {
  [TileType.Blank]: [0, 0, 0],
  [TileType.Floor]: [34, 34, 34], // handled by makeFloorTexture
  [TileType.Wall]: [204, 204, 204], // handled by makeWallTexture
  [TileType.Forest]: [0, 100, 5], // crushed neon green
  [TileType.Water]: [0, 40, 100], // deep saturated navy
  [TileType.Ground]: [90, 55, 0], // rich dark amber
  [TileType.Mountain]: [50, 50, 70], // dark slate-blue
};

function makeFloorTexture(tileSize: number, app: Application): Texture {
  const gfx = new Graphics()
    .rect(0, 0, tileSize, tileSize)
    .fill(0x222222)
    .rect(0, 0, tileSize, tileSize)
    .stroke({ color: 0x1a1a1a, width: 1, alignment: 1 });
  const tex = app.renderer.generateTexture(gfx);
  gfx.destroy();
  return tex;
}

function makeWallTexture(tileSize: number, app: Application): Texture {
  const gfx = new Graphics()
    .rect(0, 0, tileSize, tileSize)
    .fill(0xcccccc)
    .rect(0, 0, tileSize, BEVEL)
    .fill(0xffffff)
    .rect(0, BEVEL, BEVEL, tileSize - BEVEL)
    .fill(0xffffff)
    .rect(0, tileSize - BEVEL, tileSize, BEVEL)
    .fill(0x666666)
    .rect(tileSize - BEVEL, BEVEL, BEVEL, tileSize - 2 * BEVEL)
    .fill(0x666666);
  const tex = app.renderer.generateTexture(gfx);
  gfx.destroy();
  return tex;
}

function pixelBufToTexture(buf: Uint8Array, size: number): Texture {
  return new Texture({
    source: new BufferImageSource({
      resource: buf,
      width: size,
      height: size,
      format: "rgba8unorm",
    }),
  });
}

function makeTileTextures(
  palette: GplEntry[],
  tileEnum: Record<string, number>,
  app: Application,
  tileSize: number,
): Record<string, Texture> {
  const textures: Record<string, Texture> = {};
  for (const entry of palette) {
    const index = tileEnum[entry.name];
    if (index === undefined) continue;
    if (entry.name === "Floor") {
      textures[entry.name] = makeFloorTexture(tileSize, app);
      continue;
    }
    if (entry.name === "Wall") {
      textures[entry.name] = makeWallTexture(tileSize, app);
      continue;
    }
    if (entry.name === "Forest") {
      const bufs = generateTileset(FOREST_SPEC, tileSize, TILE_VARIANTS);
      bufs.forEach((buf, v) => {
        textures[`Forest_${v}`] = pixelBufToTexture(buf, tileSize);
      });
      continue;
    }
    if (entry.name === "Water") {
      const bufs = generateTileset(WATER_SPEC, tileSize, TILE_VARIANTS);
      bufs.forEach((buf, v) => {
        textures[`Water_${v}`] = pixelBufToTexture(buf, tileSize);
      });
      continue;
    }
    if (entry.name === "Ground") {
      const bufs = generateTileset(GROUND_SPEC, tileSize, TILE_VARIANTS);
      bufs.forEach((buf, v) => {
        textures[`Ground_${v}`] = pixelBufToTexture(buf, tileSize);
      });
      continue;
    }
    if (entry.name === "Mountain") {
      const bufs = generateTileset(MOUNTAIN_SPEC, tileSize, TILE_VARIANTS);
      bufs.forEach((buf, v) => {
        textures[`Mountain_${v}`] = pixelBufToTexture(buf, tileSize);
      });
      continue;
    }
    const [r, g, b] = DISPLAY_COLORS[index as TileType] ?? [
      entry.r,
      entry.g,
      entry.b,
    ];
    const color = (r << 16) | (g << 8) | b;
    const gfx = new Graphics().rect(0, 0, tileSize, tileSize).fill(color);
    textures[entry.name] = app.renderer.generateTexture(gfx);
    gfx.destroy();
  }
  return textures;
}

function drawTilemap(
  tilemap: Tilemap,
  grid: TileGrid,
  getTextureName: (tile: number, row: number, col: number) => string,
  atlas: TileAtlas,
) {
  for (let row = 0; row < grid.rows; row++) {
    for (let col = 0; col < grid.cols; col++) {
      const tile = grid.tiles[row * grid.cols + col];
      tilemap.tile(
        atlas.get(getTextureName(tile, row, col)),
        col * TILE_SIZE,
        row * TILE_SIZE,
      );
    }
  }
}

async function setup(app: Application) {
  await app.init({ background: "#111111", resizeTo: window, antialias: true });
  document.getElementById("pixi-container")!.appendChild(app.canvas);

  const palette = parseGpl(paletteText);

  const mapEntries = Object.entries(islandMaps)
    .map(([path, url]) => ({
      name: path.replace(/^.*\//, "").replace(/\.png$/, ""),
      url,
    }))
    .sort((a, b) => a.name.localeCompare(b.name));

  const firstBuf = await fetch(mapEntries[0].url).then((r) => r.arrayBuffer());
  let grid = decodeWorldMap(palette, firstBuf, TileType);

  // Scene: [M][Viewport][M][Panel column][M]
  const viewport = new Container();
  app.stage.addChild(viewport);
  const world = new Container();
  viewport.addChild(world);
  const maskShape = new Graphics();
  viewport.addChild(maskShape);
  world.mask = maskShape;
  const border = new Graphics();
  viewport.addChild(border);

  // Minimap sits at the bottom of the panel column
  const minimap = new Container();
  app.stage.addChild(minimap);
  const minimapSprite = new Sprite();
  minimap.addChild(minimapSprite);
  const minimapBorder = new Graphics();
  minimap.addChild(minimapBorder);
  const minimapViewRect = new Graphics();
  minimap.addChild(minimapViewRect);

  let panelContentW = 0;
  let wrap: HTMLDivElement | null = null;
  let vW = 0;
  let vH = 0;
  let mmW = 0;
  let mmH = 0;

  function updateMinimapRect() {
    const worldW = grid.cols * TILE_SIZE;
    const worldH = grid.rows * TILE_SIZE;
    const scaleX = mmW / worldW;
    const scaleY = mmH / worldH;
    minimapViewRect
      .clear()
      .rect(-world.x * scaleX, -world.y * scaleY, vW * scaleX, vH * scaleY)
      .stroke({ color: 0xffffff, width: 1 });
  }

  function updateViewport() {
    const worldW = grid.cols * TILE_SIZE;
    const worldH = grid.rows * TILE_SIZE;
    const isLandscape = window.innerWidth > window.innerHeight;

    if (isLandscape) {
      // Panel column on the right: [M][Viewport][M][Panel][M]
      mmW = Math.floor(window.innerWidth * MINIMAP_FRACTION);
      mmH = Math.round((mmW * worldH) / worldW);
      const maxMmH = window.innerHeight - VIEWPORT_MARGIN * 2;
      if (mmH > maxMmH) {
        mmH = maxMmH;
        mmW = Math.round((mmH * worldW) / worldH);
      }
      const panelW = Math.max(panelContentW, mmW);
      const panelLeft = window.innerWidth - VIEWPORT_MARGIN - panelW;
      vW = Math.min(worldW, panelLeft - VIEWPORT_MARGIN * 2);
      vH = Math.min(worldH, window.innerHeight - VIEWPORT_MARGIN * 2);
      minimap.x = window.innerWidth - VIEWPORT_MARGIN - mmW;
      minimap.y = window.innerHeight - VIEWPORT_MARGIN - mmH;
      if (wrap)
        wrap.style.cssText = `position:fixed;top:${VIEWPORT_MARGIN}px;right:${VIEWPORT_MARGIN}px;display:flex;flex-direction:column;gap:8px;`;
    } else {
      // Panel row on the bottom: [M][Viewport][M][Panel][M]
      mmH = Math.floor(window.innerHeight * MINIMAP_FRACTION);
      mmW = Math.round((mmH * worldW) / worldH);
      const panelTop = window.innerHeight - VIEWPORT_MARGIN - mmH;
      vW = Math.min(worldW, window.innerWidth - VIEWPORT_MARGIN * 2);
      vH = Math.min(worldH, panelTop - VIEWPORT_MARGIN * 2);
      minimap.x = window.innerWidth - VIEWPORT_MARGIN - mmW;
      minimap.y = panelTop;
      if (wrap)
        wrap.style.cssText = `position:fixed;bottom:${VIEWPORT_MARGIN}px;left:${VIEWPORT_MARGIN}px;display:flex;flex-direction:column;gap:8px;`;
    }

    viewport.x = VIEWPORT_MARGIN;
    viewport.y = VIEWPORT_MARGIN;
    maskShape.clear().rect(0, 0, vW, vH).fill(0xffffff);
    border
      .clear()
      .rect(0, 0, vW, vH)
      .stroke({ color: BORDER_COLOR, width: BORDER_WIDTH });
    minimap.hitArea = new Rectangle(0, 0, mmW, mmH);
    minimapSprite.width = mmW;
    minimapSprite.height = mmH;
    minimapBorder
      .clear()
      .rect(0, 0, mmW, mmH)
      .stroke({ color: BORDER_COLOR, width: BORDER_WIDTH });

    clampWorld();
    updateMinimapRect();
  }

  function clampWorld() {
    world.x = Math.min(0, Math.max(-(grid.cols * TILE_SIZE - vW), world.x));
    world.y = Math.min(0, Math.max(-(grid.rows * TILE_SIZE - vH), world.y));
  }

  function saveState() {
    localStorage.setItem(
      STORAGE_KEY,
      JSON.stringify({
        mapUrl: currentMapUrl,
        worldX: world.x,
        worldY: world.y,
      }),
    );
  }

  function pan(dx: number, dy: number) {
    world.x += dx;
    world.y += dy;
    clampWorld();
    updateMinimapRect();
    saveState();
  }

  app.renderer.on("resize", updateViewport);

  window.addEventListener("keydown", (e) => {
    switch (e.key) {
      case "ArrowLeft":
        pan(PAN_STEP, 0);
        break;
      case "ArrowRight":
        pan(-PAN_STEP, 0);
        break;
      case "ArrowUp":
        pan(0, PAN_STEP);
        break;
      case "ArrowDown":
        pan(0, -PAN_STEP);
        break;
    }
  });

  minimap.eventMode = "static";
  minimap.cursor = "grab";

  let dragActive = false;
  let dragStartMm = { x: 0, y: 0 };
  let dragStartWorld = { x: 0, y: 0 };

  minimap.on("pointerdown", (e) => {
    dragActive = true;
    const local = minimap.toLocal(e.global);
    dragStartMm = { x: local.x, y: local.y };
    dragStartWorld = { x: world.x, y: world.y };
    minimap.cursor = "grabbing";
  });

  minimap.on("pointermove", (e) => {
    if (!dragActive) return;
    const local = minimap.toLocal(e.global);
    const dx = local.x - dragStartMm.x;
    const dy = local.y - dragStartMm.y;
    world.x = dragStartWorld.x - dx * ((grid.cols * TILE_SIZE) / mmW);
    world.y = dragStartWorld.y - dy * ((grid.rows * TILE_SIZE) / mmH);
    clampWorld();
    updateMinimapRect();
  });

  minimap.on("pointerup", () => {
    dragActive = false;
    minimap.cursor = "grab";
    saveState();
  });
  minimap.on("pointerupoutside", () => {
    dragActive = false;
    minimap.cursor = "grab";
    saveState();
  });

  let currentMapUrl = mapEntries[0].url;
  let currentTilemap: Tilemap | null = null;
  let currentTileTextures: Record<string, Texture> | null = null;
  let currentAtlas: TileAtlas | null = null;

  function rebuild() {
    if (currentTilemap) {
      world.removeChild(currentTilemap);
      currentTilemap.destroy();
    }
    if (currentTileTextures) {
      for (const tex of Object.values(currentTileTextures)) tex.destroy(true);
    }
    if (currentAtlas) currentAtlas.source.destroy();

    const tileTextures = makeTileTextures(palette, TileType, app, TILE_SIZE);
    const atlas = buildTileAtlas(app.renderer, tileTextures, TILE_SIZE);
    currentTileTextures = tileTextures;
    currentAtlas = atlas;

    const tilemap = new Tilemap(atlas.source);
    const rng = seedrandom("worldmap-tiles");
    drawTilemap(
      tilemap,
      grid,
      (tile) => {
        const name = TileType[tile];
        if (name === undefined)
          throw new Error(`No TileType entry for value ${tile}`);
        if (name === "Wall" || name === "Floor" || name === "Blank")
          return name;
        return `${name}_${Math.floor(rng() * TILE_VARIANTS)}`;
      },
      atlas,
    );
    world.addChild(tilemap);
    currentTilemap = tilemap;
  }

  async function loadMap(url: string) {
    const [buf, tex] = await Promise.all([
      fetch(url).then((r) => r.arrayBuffer()),
      Assets.load<Texture>(url),
    ]);
    currentMapUrl = url;
    world.x = 0;
    world.y = 0;
    grid = decodeWorldMap(palette, buf, TileType);
    minimapSprite.texture = tex;
    updateViewport();
    rebuild();
    saveState();
  }

  const btnStyle =
    "padding:6px 12px;background:#333;color:#ccc;border:1px solid #666;cursor:pointer;font-family:monospace;font-size:13px;";
  wrap = document.createElement("div");

  for (const { name, url } of mapEntries) {
    const btn = document.createElement("button");
    btn.textContent = name;
    btn.style.cssText = btnStyle;
    btn.addEventListener("click", () => loadMap(url));
    wrap.appendChild(btn);
  }

  wrap.style.cssText = "display:inline-flex;flex-direction:column;gap:8px;";
  document.body.appendChild(wrap);
  panelContentW = wrap.offsetWidth;

  const saved = (() => {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      return raw ? JSON.parse(raw) : null;
    } catch {
      return null;
    }
  })();

  const defaultUrl =
    mapEntries.find((e) => e.name === "islands2")?.url ?? mapEntries[0].url;
  const initialUrl =
    saved?.mapUrl && mapEntries.some((e) => e.url === saved.mapUrl)
      ? saved.mapUrl
      : defaultUrl;
  currentMapUrl = initialUrl;

  if (initialUrl !== mapEntries[0].url) {
    const [savedBuf, savedTex] = await Promise.all([
      fetch(initialUrl).then((r) => r.arrayBuffer()),
      Assets.load<Texture>(initialUrl),
    ]);
    grid = decodeWorldMap(palette, savedBuf, TileType);
    minimapSprite.texture = savedTex;
  } else {
    minimapSprite.texture = await Assets.load<Texture>(initialUrl);
  }

  updateViewport();

  if (saved?.mapUrl === initialUrl) {
    world.x = saved.worldX ?? 0;
    world.y = saved.worldY ?? 0;
    clampWorld();
    updateMinimapRect();
  }

  rebuild();
}

export async function main() {
  const app = new Application();
  await setup(app);
}