//! Semantic regions and connectivity (spec §4.5).
//!
//! Where [`crate::grid::Grid`] answers *what tile is at a cell*, this module
//! answers *what areas exist and how they connect*. A [`Region`] is a named
//! semantic area (a room, a corridor) with a bounding box and the exact set of
//! cells it occupies; a [`ConnGraph`] records which regions are linked and,
//! once doors are placed, where the link physically sits.
//!
//! The shared vocabulary here is deliberately map-type agnostic: a dungeon is
//! rooms joined by corridor edges, while a future town would be districts and
//! buildings joined by road edges — same types, different passes producing
//! them.
//!
//! All types derive `serde::{Serialize, Deserialize}` so they can travel inside
//! a [`crate::map::Map`] through JSON (spec §3).

use serde::{Deserialize, Serialize};

use crate::geometry::{Point, Rect};

/// A handle to a [`Region`], equal to its index in the region list.
///
/// **Invariant:** `RegionId(n)` is always the index `n` of that region in
/// `ctx.regions` / [`crate::map::Map::regions`]. Regions are append-only — never
/// removed or reordered — so this id-as-index relationship holds for the life of
/// the map. Edges in the [`ConnGraph`] and any lookup that turns an id back into
/// a region rely on it.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct RegionId(pub usize);

/// What kind of semantic area a [`Region`] represents.
///
/// Starts minimal; new variants are additive (later: `District`, `Plaza`,
/// `Building`). Passes match only on the kinds they care about.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum RegionKind {
    /// A room: a carved, typically rectangular open area.
    Room,
    /// A corridor: a thin passage carved to connect rooms.
    Corridor,
}

/// An optional *flavor* a recipe may assign to a region — a layer of meaning
/// **orthogonal** to [`RegionKind`] (kind says *room vs corridor*; theme says
/// *what kind of place this room is*).
///
/// A themer pass classifies each room into one of these (e.g. a room flooded
/// with lava reads as a [`Forge`](RegionTheme::Forge)); renderers tint the room
/// accordingly and a populator biases its contents. Like [`RegionKind`] and
/// [`crate::map::Tile`], the set is **additive** — a future town recipe could add
/// `Market`/`Temple`/`Slum` without disturbing existing matches — and entirely
/// optional: a region with `theme: None` is simply unthemed (the neutral look).
///
/// Each variant carries a pair of *content biases* via [`biases`](RegionTheme::biases),
/// the only gameplay knob the core attaches to a theme. All color/lighting is a
/// renderer concern and lives in the renderer, keyed by the theme's wire tag.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RegionTheme {
    /// A magma smithy — terrain-driven by lava. Loot-rich, a touch dangerous.
    Forge,
    /// A flooded reservoir — terrain-driven by water. Damp, low loot.
    Cistern,
    /// A pillared ceremonial hall — terrain-driven by pillars. Stately.
    Hall,
    /// The boss seat at the dungeon's far end (the exit room). High risk, high reward.
    Throne,
    /// The safe entrance chamber. Understated, kept monster-free.
    Threshold,
    /// A sealed treasury — the loot jackpot, lightly guarded.
    Vault,
    /// A scriptorium of tomes and scrolls. Loot-rich, low danger.
    Library,
    /// A monster lair / guardroom. Combat-dense, little loot.
    Den,
    /// A cold tomb of the restless dead. Undead-heavy, moderate grave-goods.
    Crypt,
    /// Plain torch-lit stone — the neutral baseline (all biases 1.0).
    Stone,
}

impl RegionTheme {
    /// The `(treasure_bias, monster_bias)` multipliers a populator applies to its
    /// base per-room treasure/monster chances. `Stone` is the `(1.0, 1.0)`
    /// neutral baseline; biases above 1 saturate the chance (a near-certain
    /// "this room always has loot / a boss" telegraph), below 1 thin it out.
    pub fn biases(self) -> (f64, f64) {
        match self {
            RegionTheme::Forge => (1.4, 1.2),
            RegionTheme::Cistern => (0.7, 1.1),
            RegionTheme::Hall => (1.1, 0.85),
            RegionTheme::Throne => (2.4, 2.3),
            RegionTheme::Threshold => (0.5, 0.0),
            RegionTheme::Vault => (2.5, 0.4),
            RegionTheme::Library => (1.6, 0.5),
            RegionTheme::Den => (0.7, 2.4),
            RegionTheme::Crypt => (1.2, 1.8),
            RegionTheme::Stone => (1.0, 1.0),
        }
    }
}

/// A semantic area of the map: an id, a kind, a bounding box, and its cells.
///
/// `bounds` is the axis-aligned bounding box; `cells` is the exact set of
/// occupied cells, which lets non-rectangular regions (e.g. cave blobs) be
/// represented faithfully rather than only as rectangles. Fields are `pub`
/// because passes and visualization read them directly across modules.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Region {
    /// This region's stable id, equal to its index in the region list.
    pub id: RegionId,
    /// The semantic category of this region.
    pub kind: RegionKind,
    /// The axis-aligned bounding box enclosing all of `cells`.
    pub bounds: Rect,
    /// The exact cells this region occupies, in the order they were added.
    pub cells: Vec<Point>,
    /// An optional flavor for this region (see [`RegionTheme`]). `None` until a
    /// themer pass classifies it; corridors and unthemed rooms stay `None`.
    #[serde(default)]
    pub theme: Option<RegionTheme>,
}

/// A connection between two regions in the [`ConnGraph`].
///
/// `a` and `b` are the linked regions. `at` is the physical location of the
/// connection (the door cell): it is [`None`] when the edge is first created by
/// `MstConnect`, and is filled in later by `DoorPlacer`. Refines spec §4.5,
/// where `at` was sketched as a bare `Point` — the door location simply is not
/// known until placement runs. Fields are `pub` because `DoorPlacer` writes `at`
/// and visualization reads all three.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Edge {
    /// One endpoint region.
    pub a: RegionId,
    /// The other endpoint region.
    pub b: RegionId,
    /// Where the connection physically sits, once known (e.g. the door cell).
    pub at: Option<Point>,
}

/// The region connectivity graph: nodes are [`RegionId`]s, edges are [`Edge`]s.
///
/// Built incrementally — `MstConnect` adds the edges that should exist (with no
/// location yet), then `DoorPlacer` populates each edge's `at`. The graph stores
/// edges in insertion order, which is deterministic and the order
/// [`ConnGraph::edges`] yields them in.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConnGraph {
    edges: Vec<Edge>,
}

impl ConnGraph {
    /// Creates an empty connectivity graph.
    pub fn new() -> Self {
        ConnGraph::default()
    }

    /// Records that regions `a` and `b` are connected.
    ///
    /// The new edge's location is left as `at: None`; `DoorPlacer` fills it in
    /// later. Edges are appended, so the order they were added is preserved.
    pub fn add_edge(&mut self, a: RegionId, b: RegionId) {
        self.edges.push(Edge { a, b, at: None });
    }

    /// Returns the edges in insertion order.
    pub fn edges(&self) -> &[Edge] {
        &self.edges
    }

    /// Returns the edges mutably, so `DoorPlacer` can populate each `at`.
    pub fn edges_mut(&mut self) -> &mut [Edge] {
        &mut self.edges
    }

    /// Iterates the regions directly connected to `id`.
    ///
    /// For every edge touching `id`, yields the *other* endpoint. An undirected
    /// link `{a, b}` therefore appears from both sides: `neighbors(a)` yields `b`
    /// and `neighbors(b)` yields `a`. Self-loops (`a == b == id`) yield `id`. The
    /// iteration order follows the edges' insertion order.
    pub fn neighbors(&self, id: RegionId) -> impl Iterator<Item = RegionId> + '_ {
        self.edges.iter().filter_map(move |e| {
            if e.a == id {
                Some(e.b)
            } else if e.b == id {
                Some(e.a)
            } else {
                None
            }
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_edge_stores_none_location() {
        let mut g = ConnGraph::new();
        g.add_edge(RegionId(0), RegionId(1));

        assert_eq!(g.edges().len(), 1);
        let e = g.edges()[0];
        assert_eq!(e.a, RegionId(0));
        assert_eq!(e.b, RegionId(1));
        assert_eq!(e.at, None, "MstConnect leaves the door location unknown");
    }

    #[test]
    fn neighbors_returns_both_endpoints_counterparts() {
        let mut g = ConnGraph::new();
        g.add_edge(RegionId(0), RegionId(1));
        g.add_edge(RegionId(1), RegionId(2));

        // From region 1 we can reach both of its neighbors.
        let n1: Vec<RegionId> = g.neighbors(RegionId(1)).collect();
        assert_eq!(n1, vec![RegionId(0), RegionId(2)]);

        // The link is undirected: each endpoint sees the other.
        let n0: Vec<RegionId> = g.neighbors(RegionId(0)).collect();
        assert_eq!(n0, vec![RegionId(1)]);
        let n2: Vec<RegionId> = g.neighbors(RegionId(2)).collect();
        assert_eq!(n2, vec![RegionId(1)]);
    }

    #[test]
    fn edges_mut_lets_door_placer_populate_location() {
        let mut g = ConnGraph::new();
        g.add_edge(RegionId(0), RegionId(1));

        // Simulate DoorPlacer writing the door cell into the edge.
        g.edges_mut()[0].at = Some(Point::new(4, 7));
        assert_eq!(g.edges()[0].at, Some(Point::new(4, 7)));
    }
}
