import { Application, Container, Graphics, Sprite, Texture } from "pixi.js";
import UPNG from "upng-js";

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

const TILE_SIZE = 32;
const MAP_OFFSET_X = 16;
const MAP_OFFSET_Y = 16;
const ENTITY_RADIUS = 4;
const MAX_SPEED = 2;
const ARRIVAL_RADIUS = TILE_SIZE * 1.5;
const ORBIT_STRENGTH = 0.8;
const SEPARATION_WEIGHT = 1.5;
const WALL_SIGHT_RADIUS = TILE_SIZE * 1.5;
const WALL_AVOIDANCE_WEIGHT = 2.0;

const SEPARATION_RADIUS_LINEAR = ENTITY_RADIUS * 5;
const SEPARATION_SIGMA = ENTITY_RADIUS * 3;
const SEPARATION_CUTOFF = SEPARATION_SIGMA * 4;

type SeparationKernel = (dist: number) => number;

const kernelLinear: SeparationKernel = (dist) =>
  dist < SEPARATION_RADIUS_LINEAR
    ? ((SEPARATION_RADIUS_LINEAR - dist) / SEPARATION_RADIUS_LINEAR) *
      SEPARATION_WEIGHT
    : 0;

const kernelExponential: SeparationKernel = (dist) =>
  dist < SEPARATION_CUTOFF
    ? SEPARATION_WEIGHT * Math.exp(-dist / SEPARATION_SIGMA)
    : 0;

const enum Tile {
  Ground = 0,
  Wall = 1,
}

interface World {
  cols: number;
  rows: number;
  tiles: Uint8Array;
}

interface Entity {
  x: number;
  y: number;
  targetX: number;
  targetY: number;
}

type Coord = { col: number; row: number };

function makeWorld(cols: number, rows: number): World {
  return { cols, rows, tiles: new Uint8Array(cols * rows) };
}

function getTile(world: World, col: number, row: number): Tile {
  return world.tiles[row * world.cols + col] as Tile;
}

function tileCenter(col: number, row: number): { x: number; y: number } {
  return {
    x: MAP_OFFSET_X + col * TILE_SIZE + TILE_SIZE / 2,
    y: MAP_OFFSET_Y + row * TILE_SIZE + TILE_SIZE / 2,
  };
}

function inBounds(world: World, col: number, row: number): boolean {
  return col >= 0 && col < world.cols && row >= 0 && row < world.rows;
}

function pixelToTile(world: World, px: number, py: number): Coord | null {
  const col = Math.floor((px - MAP_OFFSET_X) / TILE_SIZE);
  const row = Math.floor((py - MAP_OFFSET_Y) / TILE_SIZE);
  if (!inBounds(world, col, row)) return null;
  return { col, row };
}

function isPassable(
  world: World,
  px: number,
  py: number,
  radius: number,
): boolean {
  const probes: [number, number][] = [
    [0, 0],
    [radius, 0],
    [-radius, 0],
    [0, radius],
    [0, -radius],
  ];
  for (const [ox, oy] of probes) {
    const coord = pixelToTile(world, px + ox, py + oy);
    if (!coord || getTile(world, coord.col, coord.row) === Tile.Wall)
      return false;
  }
  return true;
}

// Tile↔color mapping for PNG I/O: Ground=black, Wall=white.
function tileToRgb(tile: Tile): [number, number, number] {
  return tile === Tile.Wall ? [255, 255, 255] : [0, 0, 0];
}

function rgbToTile(r: number, g: number, b: number): Tile {
  return r + g + b > 382 ? Tile.Wall : Tile.Ground;
}

function worldToIndexedPng(world: World): ArrayBuffer {
  const rgba = new Uint8Array(world.cols * world.rows * 4);
  for (let i = 0; i < world.tiles.length; i++) {
    const [r, g, b] = tileToRgb(world.tiles[i] as Tile);
    rgba[i * 4] = r;
    rgba[i * 4 + 1] = g;
    rgba[i * 4 + 2] = b;
    rgba[i * 4 + 3] = 255;
  }
  return UPNG.encode([rgba.buffer], world.cols, world.rows, 0, []);
}

function worldFromIndexedPng(buffer: ArrayBuffer): World | null {
  try {
    const img = UPNG.decode(buffer);
    const rgba = new Uint8Array(UPNG.toRGBA8(img)[0]);
    const world = makeWorld(img.width, img.height);
    for (let i = 0; i < world.tiles.length; i++) {
      world.tiles[i] = rgbToTile(rgba[i * 4], rgba[i * 4 + 1], rgba[i * 4 + 2]);
    }
    return world;
  } catch {
    return null;
  }
}

const BTN_STYLE =
  "padding:6px 12px;background:#333;color:#ccc;border:1px solid #666;cursor:pointer;font-family:monospace;font-size:13px;";

function makeBtn(label: string, onClick: () => void): HTMLButtonElement {
  const btn = document.createElement("button");
  btn.textContent = label;
  btn.style.cssText = BTN_STYLE;
  btn.addEventListener("click", onClick);
  return btn;
}

function mountIoButtons(
  getWorld: () => World,
  onLoad: (loaded: World) => void,
): void {
  const wrap = document.createElement("div");
  wrap.style.cssText =
    "position:fixed;top:16px;right:16px;display:flex;flex-direction:column;gap:8px;";

  const applyWorld = (buf: ArrayBuffer) => {
    const loaded = worldFromIndexedPng(buf);
    if (loaded) onLoad(loaded);
  };

  const fileInput = document.createElement("input");
  fileInput.type = "file";
  fileInput.accept = ".png,image/png";
  fileInput.style.display = "none";
  fileInput.addEventListener("change", () => {
    const file = fileInput.files?.[0];
    if (!file) return;
    file.arrayBuffer().then((buf) => applyWorld(buf));
    fileInput.value = "";
  });

  wrap.appendChild(
    makeBtn("Download PNG", () => {
      const buf = worldToIndexedPng(getWorld());
      const url = URL.createObjectURL(new Blob([buf], { type: "image/png" }));
      const a = document.createElement("a");
      a.href = url;
      a.download = "world.png";
      a.click();
      URL.revokeObjectURL(url);
    }),
  );
  wrap.appendChild(
    makeBtn("Round-trip", () => applyWorld(worldToIndexedPng(getWorld()))),
  );
  wrap.appendChild(makeBtn("Upload PNG", () => fileInput.click()));
  wrap.appendChild(fileInput);

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

  if (mapEntries.length > 0) {
    const sep = document.createElement("hr");
    sep.style.cssText = "border:none;border-top:1px solid #666;margin:4px 0;";
    wrap.appendChild(sep);

    const loadMap = (url: string) =>
      fetch(url)
        .then((r) => r.arrayBuffer())
        .then((buf) => applyWorld(buf));

    for (const { name, url } of mapEntries) {
      wrap.appendChild(makeBtn(name, () => loadMap(url)));
    }

    const hash = location.hash.replace(/^#/, "");
    const match = mapEntries.find((e) => e.name === hash);
    if (match) loadMap(match.url);
  }

  document.body.appendChild(wrap);
}

function makeTileTextures(app: Application): Record<Tile, Texture> {
  const wallGfx = new Graphics()
    .rect(0, 0, TILE_SIZE, TILE_SIZE)
    .fill(0x444444);
  const groundGfx = new Graphics()
    .rect(0, 0, TILE_SIZE, TILE_SIZE)
    .fill(0x222222)
    .rect(0, 0, TILE_SIZE, TILE_SIZE)
    .stroke({ color: 0x1a1a1a, width: 1, alignment: 1 });
  return {
    [Tile.Wall]: app.renderer.generateTexture(wallGfx),
    [Tile.Ground]: app.renderer.generateTexture(groundGfx),
  };
}

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

  const textures = makeTileTextures(app);

  const map0Buf = await fetch(stockMaps["./maps/map2.png"]).then((r) =>
    r.arrayBuffer(),
  );
  let world = worldFromIndexedPng(map0Buf) ?? makeWorld(1, 1);

  const mapContainer = new Container();
  const entityGfx = new Graphics();
  app.stage.addChild(mapContainer);
  app.stage.addChild(entityGfx);

  let editMode = false;
  let activeKernel: SeparationKernel = kernelExponential;
  let tileSprites: Sprite[][] = [];
  const entities: Entity[] = [];

  function onPointerDown(event: import("pixi.js").FederatedPointerEvent): void {
    const pos = event.getLocalPosition(app.stage);
    if (!editMode) {
      if (event.button === 0) {
        for (const e of entities) {
          e.targetX = pos.x;
          e.targetY = pos.y;
        }
      } else if (event.button === 2) {
        const spawnRadius = 3 * TILE_SIZE;
        for (let i = 0; i < 20; i++) {
          const angle = Math.random() * Math.PI * 2;
          const dist = Math.random() * spawnRadius;
          const sx = pos.x + Math.cos(angle) * dist;
          const sy = pos.y + Math.sin(angle) * dist;
          if (!isPassable(world, sx, sy, ENTITY_RADIUS)) continue;
          entities.push({ x: sx, y: sy, targetX: sx, targetY: sy });
        }
      }
      return;
    }
    const coord = pixelToTile(world, pos.x, pos.y);
    if (!coord) return;
    applyEditAt(
      coord.col,
      coord.row,
      event.button === 0 ? Tile.Wall : Tile.Ground,
    );
  }

  function onPointerMove(event: import("pixi.js").FederatedPointerEvent): void {
    if (!editMode || event.buttons === 0) return;
    const pos = event.getLocalPosition(app.stage);
    const coord = pixelToTile(world, pos.x, pos.y);
    if (!coord) return;
    applyEditAt(
      coord.col,
      coord.row,
      event.buttons & 1 ? Tile.Wall : Tile.Ground,
    );
  }

  function applyEditAt(col: number, row: number, tile: Tile): void {
    if (world.tiles[row * world.cols + col] === tile) return;
    world.tiles[row * world.cols + col] = tile;
    tileSprites[row][col].texture = textures[tile];
  }

  function buildMap(): void {
    for (const child of mapContainer.removeChildren()) child.destroy();
    tileSprites = Array.from(
      { length: world.rows },
      () => new Array<Sprite>(world.cols),
    );
    for (let row = 0; row < world.rows; row++) {
      for (let col = 0; col < world.cols; col++) {
        const sprite = new Sprite(textures[getTile(world, col, row)]);
        sprite.x = MAP_OFFSET_X + col * TILE_SIZE;
        sprite.y = MAP_OFFSET_Y + row * TILE_SIZE;
        tileSprites[row][col] = sprite;
        mapContainer.addChild(sprite);
      }
    }
    const mapW = world.cols * TILE_SIZE;
    const mapH = world.rows * TILE_SIZE;
    mapContainer.addChild(
      new Graphics()
        .rect(MAP_OFFSET_X, MAP_OFFSET_Y, mapW, mapH)
        .stroke({ width: 1, color: 0x888888 }),
    );
    const hit = new Graphics()
      .rect(MAP_OFFSET_X, MAP_OFFSET_Y, mapW, mapH)
      .fill({ color: 0x000000, alpha: 0 });
    hit.eventMode = "static";
    hit.cursor = "pointer";
    hit.on("pointerdown", onPointerDown);
    hit.on("pointermove", onPointerMove);
    mapContainer.addChild(hit);
  }

  buildMap();

  // --- Steering: outputs a desire vector (direction + magnitude 0–1) ---
  function steer(
    e: Entity,
    allEntities: Entity[],
    w: World,
  ): { dx: number; dy: number } {
    // Arrival with orbital blend near target
    const tdx = e.targetX - e.x;
    const tdy = e.targetY - e.y;
    const tDist = Math.hypot(tdx, tdy);
    const proximity = tDist > 0 ? Math.min(tDist / ARRIVAL_RADIUS, 1) : 0;
    let ax = 0;
    let ay = 0;
    if (tDist > 0) {
      const radX = tdx / tDist;
      const radY = tdy / tDist;
      const tanX = -radY;
      const tanY = radX;
      const orbitBlend = (1 - proximity) * ORBIT_STRENGTH;
      const dirX = radX * (1 - orbitBlend) + tanX * orbitBlend;
      const dirY = radY * (1 - orbitBlend) + tanY * orbitBlend;
      const dMag = Math.hypot(dirX, dirY);
      ax = (dirX / dMag) * proximity;
      ay = (dirY / dMag) * proximity;
    }

    // Separation + wall avoidance (unbounded magnitude)
    let lx = 0;
    let ly = 0;

    for (const other of allEntities) {
      if (other === e) continue;
      const odx = e.x - other.x;
      const ody = e.y - other.y;
      const oDist = Math.hypot(odx, ody);
      if (oDist > 0) {
        const strength = activeKernel(oDist);
        if (strength > 0) {
          lx += (odx / oDist) * strength;
          ly += (ody / oDist) * strength;
        }
      }
    }

    const tileRange = Math.ceil(WALL_SIGHT_RADIUS / TILE_SIZE);
    const eCoord = pixelToTile(w, e.x, e.y);
    if (eCoord) {
      for (let dr = -tileRange; dr <= tileRange; dr++) {
        for (let dc = -tileRange; dc <= tileRange; dc++) {
          const col = eCoord.col + dc;
          const row = eCoord.row + dr;
          if (!inBounds(w, col, row)) continue;
          if (getTile(w, col, row) !== Tile.Wall) continue;
          const { x: wx, y: wy } = tileCenter(col, row);
          const wdx = e.x - wx;
          const wdy = e.y - wy;
          const wDist = Math.hypot(wdx, wdy);
          if (wDist > 0 && wDist < WALL_SIGHT_RADIUS) {
            const strength =
              ((WALL_SIGHT_RADIUS - wDist) / WALL_SIGHT_RADIUS) *
              WALL_AVOIDANCE_WEIGHT;
            lx += (wdx / wDist) * strength;
            ly += (wdy / wDist) * strength;
          }
        }
      }
    }

    // Clamp local forces to unit magnitude
    const lMag = Math.hypot(lx, ly);
    const localScale = lMag > 0 ? Math.min(lMag, 1) / lMag : 0;

    return { dx: ax + lx * localScale, dy: ay + ly * localScale };
  }

  // --- Movement: converts steering desire into displacement ---
  function move(e: Entity, desire: { dx: number; dy: number }): void {
    const mag = Math.hypot(desire.dx, desire.dy);
    if (mag <= 0) return;
    const speed = Math.min(mag, 1) * MAX_SPEED;
    const nx = e.x + (desire.dx / mag) * speed;
    const ny = e.y + (desire.dy / mag) * speed;
    if (isPassable(world, nx, ny, ENTITY_RADIUS)) {
      e.x = nx;
      e.y = ny;
    }
  }

  app.ticker.add(() => {
    for (const e of entities) {
      move(e, steer(e, entities, world));
    }

    entityGfx.clear();
    for (const e of entities)
      entityGfx.circle(e.x, e.y, ENTITY_RADIUS).fill(0x44aaff);
  });

  app.canvas.addEventListener("contextmenu", (e) => e.preventDefault());

  const editBtn = makeBtn("Edit: OFF", () => {
    editMode = !editMode;
    editBtn.textContent = `Edit: ${editMode ? "ON" : "OFF"}`;
  });
  editBtn.style.cssText = BTN_STYLE + "position:fixed;bottom:16px;left:16px;";
  document.body.appendChild(editBtn);

  const ACTIVE_BTN_STYLE = BTN_STYLE + "background:#555;color:#fff;";
  const setKernel = (kernel: SeparationKernel) => {
    activeKernel = kernel;
    expBtn.style.cssText =
      (kernel === kernelExponential ? ACTIVE_BTN_STYLE : BTN_STYLE) +
      "position:fixed;bottom:16px;left:140px;";
    linBtn.style.cssText =
      (kernel === kernelLinear ? ACTIVE_BTN_STYLE : BTN_STYLE) +
      "position:fixed;bottom:16px;left:260px;";
  };
  const expBtn = makeBtn("Exponential", () => setKernel(kernelExponential));
  const linBtn = makeBtn("Linear", () => setKernel(kernelLinear));
  document.body.appendChild(expBtn);
  document.body.appendChild(linBtn);
  setKernel(kernelExponential);

  mountIoButtons(
    () => world,
    (loaded) => {
      world = loaded;
      entities.length = 0;
      buildMap();
    },
  );
})();