//! Parse the atlas manifest the browser baked (`tools/out/atlas/<seed>/manifest.json`).
//!
//! The manifest is the game's *only* source of dungeon truth: it was written at
//! bake time by `explore.js::dungeonManifest`, so the nav graph here is exactly
//! the graph the pre-rendered frames depict. The game layer never re-runs
//! generation — if it did, a drifted config could disagree with the baked art.

use serde::Deserialize;
use std::collections::HashMap;

/// A cardinal facing. Order matches the JS explorer (`['N','E','S','W']`) so
/// turn arithmetic is index rotation.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Dir {
    N,
    E,
    S,
    W,
}

pub const DIRS: [Dir; 4] = [Dir::N, Dir::E, Dir::S, Dir::W];

impl Dir {
    pub fn index(self) -> usize {
        DIRS.iter().position(|d| *d == self).unwrap()
    }

    pub fn turned(self, delta: i32) -> Dir {
        let i = (self.index() as i32 + delta).rem_euclid(4) as usize;
        DIRS[i]
    }

    pub fn opposite(self) -> Dir {
        self.turned(2)
    }

    pub fn letter(self) -> &'static str {
        match self {
            Dir::N => "N",
            Dir::E => "E",
            Dir::S => "S",
            Dir::W => "W",
        }
    }
}

/// Per-direction values as the manifest stores them (`{"N": .., "E": ..}`).
#[derive(Deserialize, Clone, Debug, Default)]
pub struct ByDir<T> {
    #[serde(rename = "N")]
    pub n: T,
    #[serde(rename = "E")]
    pub e: T,
    #[serde(rename = "S")]
    pub s: T,
    #[serde(rename = "W")]
    pub w: T,
}

impl<T: Copy> ByDir<T> {
    pub fn get(&self, d: Dir) -> T {
        match d {
            Dir::N => self.n,
            Dir::E => self.e,
            Dir::S => self.s,
            Dir::W => self.w,
        }
    }
}

/// One room node of the baked nav graph. Ids are region indices — sparse, and
/// also the atlas frame key (`r{id}_{dir}.png`).
#[derive(Deserialize, Clone, Debug)]
pub struct Room {
    pub id: usize,
    #[serde(default)]
    pub theme: Option<String>,
    pub nav: ByDir<Option<usize>>,
    pub open: ByDir<bool>,
}

#[derive(Deserialize, Clone, Debug)]
pub struct Manifest {
    pub seed: u64,
    #[serde(rename = "startRoom")]
    pub start_room: usize,
    pub rooms: Vec<Room>,
}

impl Manifest {
    pub fn parse(json: &str) -> Result<Manifest, String> {
        serde_json::from_str(json).map_err(|e| format!("bad manifest json: {e}"))
    }

    pub fn by_id(&self) -> HashMap<usize, &Room> {
        self.rooms.iter().map(|r| (r.id, r)).collect()
    }

    /// BFS distances over the nav graph from `from`. Unreachable rooms are absent.
    pub fn distances(&self, from: usize) -> HashMap<usize, u32> {
        let by_id = self.by_id();
        let mut dist = HashMap::new();
        let mut queue = std::collections::VecDeque::new();
        dist.insert(from, 0u32);
        queue.push_back(from);
        while let Some(id) = queue.pop_front() {
            let d = dist[&id];
            let Some(room) = by_id.get(&id) else { continue };
            for dir in DIRS {
                if let Some(next) = room.nav.get(dir) {
                    if let std::collections::hash_map::Entry::Vacant(e) = dist.entry(next) {
                        e.insert(d + 1);
                        queue.push_back(next);
                    }
                }
            }
        }
        dist
    }

    /// The room the run's stairs (or the final floor's boss) occupy: the
    /// reachable room farthest from the entrance — the deepest point of the
    /// floor. Deterministic tie-break on the higher id.
    pub fn exit_room(&self) -> usize {
        let dist = self.distances(self.start_room);
        dist.iter()
            .max_by_key(|(id, d)| (**d, **id))
            .map(|(id, _)| *id)
            .unwrap_or(self.start_room)
    }
}
