import type { World, Vec2 } from "../../core/types";
import { key } from "../../core/utils";
import { manhattan } from "../../core/math";
import { inBounds, isWall, isBlocked, idx } from "./world-logic";
import { type EntityManager } from "../EntityManager";

/**
 * Simple 4-dir A* pathfinding.
 * Returns an array of positions INCLUDING start and end. If no path, returns [].
 *
 * Exploration rule:
 * - You cannot path THROUGH unseen tiles.
 * - You cannot path TO an unseen target tile.
 */
export function findPathAStar(w: World, seen: Uint8Array, start: Vec2, end: Vec2, options: { ignoreBlockedTarget?: boolean; ignoreSeen?: boolean; em?: EntityManager } = {}): Vec2[] {
  if (!inBounds(w, end.x, end.y)) return [];
  if (isWall(w, end.x, end.y)) return [];
  
  // If not ignoring target block, fail if blocked
  if (!options.ignoreBlockedTarget && isBlocked(w, end.x, end.y, options.em)) return [];
  
  if (!options.ignoreSeen && seen[idx(w, end.x, end.y)] !== 1) return [];

  const open: Vec2[] = [start];
  const cameFrom = new Map<string, string>();

  const gScore = new Map<string, number>();
  const fScore = new Map<string, number>();

  const startK = key(start.x, start.y);
  gScore.set(startK, 0);
  fScore.set(startK, manhattan(start, end));

  const inOpen = new Set<string>([startK]);

  const dirs = [
    { x: 1, y: 0 },
    { x: -1, y: 0 },
    { x: 0, y: 1 },
    { x: 0, y: -1 }
  ];

  while (open.length > 0) {
    // Pick node with lowest fScore
    let bestIdx = 0;
    let bestF = Infinity;
    for (let i = 0; i < open.length; i++) {
      const k = key(open[i].x, open[i].y);
      const f = fScore.get(k) ?? Infinity;
      if (f < bestF) {
        bestF = f;
        bestIdx = i;
      }
    }

    const current = open.splice(bestIdx, 1)[0];
    const currentK = key(current.x, current.y);
    inOpen.delete(currentK);

    if (current.x === end.x && current.y === end.y) {
      // Reconstruct path
      const path: Vec2[] = [end];
      let k = currentK;
      while (cameFrom.has(k)) {
        const prevK = cameFrom.get(k)!;
        const [px, py] = prevK.split(",").map(Number);
        path.push({ x: px, y: py });
        k = prevK;
      }
      path.reverse();
      return path;
    }

    for (const d of dirs) {
      const nx = current.x + d.x;
      const ny = current.y + d.y;
      if (!inBounds(w, nx, ny)) continue;
      if (isWall(w, nx, ny)) continue;

      // Exploration rule: cannot path through unseen (except start, or if ignoreSeen is set)
      if (!options.ignoreSeen && !(nx === start.x && ny === start.y) && seen[idx(w, nx, ny)] !== 1) continue;

      // Avoid walking through other actors (except standing on start, OR if it is the target and we ignore block)
      const isTarget = nx === end.x && ny === end.y;
      if (!isTarget && !(nx === start.x && ny === start.y) && isBlocked(w, nx, ny, options.em)) continue;

      const nK = key(nx, ny);
      const tentativeG = (gScore.get(currentK) ?? Infinity) + 1;

      if (tentativeG < (gScore.get(nK) ?? Infinity)) {
        cameFrom.set(nK, currentK);
        gScore.set(nK, tentativeG);
        fScore.set(nK, tentativeG + manhattan({ x: nx, y: ny }, end));

        if (!inOpen.has(nK)) {
          open.push({ x: nx, y: ny });
          inOpen.add(nK);
        }
      }
    }
  }

  return [];
}
