// explore.js — turn a dungeon envelope into a navigable room graph for the
// pre-baked first-person explorer.
//
// The player occupies a ROOM and a cardinal FACING. Moving forward (W) steps to
// the neighbouring room in the faced direction; A/D rotate the facing. To support
// that we need, per room: a vantage cell (where the camera stands), and which
// room lies to its N/E/S/W. Rooms connect through corridors, so we contract the
// region graph (rooms = nodes, corridors = edges to traverse through) and bin
// each room-neighbour into the cardinal of its bearing.
//
// Renderer-side, like depth.js/fpv.js: derived from regions + edges, nothing
// stored in the core. Room ids here are the region ARRAY INDEX (== the wire id;
// edges already reference regions by that index), used as the atlas frame key.

import { getStage, distanceToWall } from './render.js';
import { mostOpenCell, centroidOf } from './fpv.js';

const WALL = 0;
const DIRV = [[0, -1, 'N'], [1, 0, 'E'], [0, 1, 'S'], [-1, 0, 'W']]; // y grows downward = south

// Build the room navigation graph from the ACTUAL tile openings (not centroid
// bearings, which disagree with what the views render). For each room we scan its
// boundary for real gaps, bin each gap by the cardinal it sits on, and flood the
// corridor behind it to find the room it truly leads to. nav + open then share a
// single source of truth with the rendered passages.
//
// Returns { seed, width, height, startRoom, rooms }; each room =
// { id, theme, vantage:[x,y], centroid:[x,y], bounds, nav:{N,E,S,W}, open:{N,E,S,W} }
// (nav = neighbour room id or null; open = a real gap exists that way).
export function dungeonGraph(env, opts = {}) {
  const { tiles, regions } = getStage(env, opts.stage);
  const w = env.width, h = env.height;
  const dist = distanceToWall(tiles, w, h);
  const isRoom = (i) => regions[i] && regions[i].kind === 'Room' && regions[i].cells && regions[i].cells.length > 0;
  const inBounds = (x, y) => x >= 0 && y >= 0 && x < w && y < h;
  const open = (x, y) => inBounds(x, y) && tiles[y][x] !== WALL; // floor/door/pillar/water/lava all pass

  // cell → owning room id (region index), -1 for corridors/walls.
  const cellRoom = new Int32Array(w * h).fill(-1);
  for (let i = 0; i < regions.length; i++) {
    if (!isRoom(i)) continue;
    for (const [x, y] of regions[i].cells) if (inBounds(x, y)) cellRoom[y * w + x] = i;
  }

  // From a gap cell just outside `home`, flood through non-room corridor cells
  // until we reach a different room; return its id (or null for a dead end/loop).
  const destFrom = (sx, sy, home) => {
    const seen = new Set([sx + ',' + sy]);
    const q = [[sx, sy]];
    while (q.length) {
      const [x, y] = q.shift();
      const rid = cellRoom[y * w + x];
      if (rid !== -1 && rid !== home) return rid;       // arrived at another room
      for (const [dx, dy] of DIRV) {
        const nx = x + dx, ny = y + dy, k = nx + ',' + ny;
        if (seen.has(k) || !open(nx, ny)) continue;
        if (cellRoom[ny * w + nx] === home) continue;   // never wander back into home
        seen.add(k); q.push([nx, ny]);
      }
    }
    return null;
  };

  const rooms = [];
  for (let i = 0; i < regions.length; i++) {
    if (!isRoom(i)) continue;
    const r = regions[i];
    const centroid = centroidOf(r.cells);
    const van = mostOpenCell(dist, w, r.cells);
    const cellSet = new Set(r.cells.map(([x, y]) => x + ',' + y));

    // Collect boundary gaps per cardinal: a room cell whose neighbour that way is
    // outside the room and not a wall.
    const gaps = { N: [], E: [], S: [], W: [] };
    for (const [x, y] of r.cells) {
      for (const [dx, dy, dir] of DIRV) {
        const nx = x + dx, ny = y + dy;
        if (!inBounds(nx, ny) || cellSet.has(nx + ',' + ny) || tiles[ny][nx] === WALL) continue;
        gaps[dir].push([nx, ny]);
      }
    }

    const nav = { N: null, E: null, S: null, W: null };
    const openDir = { N: false, E: false, S: false, W: false };
    for (const dir of ['N', 'E', 'S', 'W']) {
      if (gaps[dir].length === 0) continue;
      openDir[dir] = true;
      for (const [gx, gy] of gaps[dir]) {               // first gap that reaches a room wins
        const d = destFrom(gx, gy, i);
        if (d != null) { nav[dir] = d; break; }
      }
    }

    rooms.push({
      id: i,
      theme: r.theme || null,
      vantage: van,
      centroid: [Math.round(centroid[0]), Math.round(centroid[1])],
      bounds: r.bounds,
      nav,
      open: openDir,
    });
  }

  // Start room: the one holding the Entrance entity, else the largest.
  let startRoom = rooms.length ? rooms[0].id : null;
  const ent = (env.entities || []).find((e) => e.kind === 'Entrance');
  if (ent) {
    const hit = rooms.find((rm) => regions[rm.id].cells.some(([x, y]) => x === ent.at[0] && y === ent.at[1]));
    if (hit) startRoom = hit.id;
  } else {
    let bestArea = 0;
    for (const rm of rooms) {
      const a = regions[rm.id].cells.length;
      if (a > bestArea) { bestArea = a; startRoom = rm.id; }
    }
  }

  return { seed: env.seed, width: w, height: h, startRoom, rooms };
}

// Full manifest = the nav graph + the tile grid (for the viewer's minimap).
export function dungeonManifest(env, opts = {}) {
  const g = dungeonGraph(env, opts);
  const { tiles } = getStage(env, opts.stage);
  return { ...g, tiles };
}
