//! The [`RoomThemer`] classifier pass: give every room a [`RegionTheme`].
//!
//! A *classifier* — the one pass that reads the finished terrain and labels what
//! each room **is** without changing a single tile. It runs after every shaper
//! and decorator (rooms, caves, corridors, doors, pools, pillars) and before
//! [`EntityPlacer`](crate::passes::entity_placer::EntityPlacer), writing a
//! [`RegionTheme`] onto each real [`Room`](crate::region::RegionKind::Room)
//! region's [`theme`](crate::region::Region::theme) field. Because it mutates no
//! tiles, regions' shapes, or edges, it carries **zero connectivity risk**.
//!
//! Two consumers read the theme it assigns: the renderer (which tints a themed
//! room's floor/walls) and `EntityPlacer` (whose treasure/monster density is
//! scaled by the theme's [`biases`](RegionTheme::biases)).
//!
//! ## Assignment (deterministic, first match wins)
//!
//! For each real room, in region-id order, the first matching rule applies:
//!
//! 1. **Forge** — the room contains any [`Lava`](crate::map::Tile::Lava) tile.
//! 2. **Cistern** — it contains any [`Water`](crate::map::Tile::Water) tile.
//! 3. **Hall** — it contains any [`Pillar`](crate::map::Tile::Pillar) tile.
//! 4. **Throne** — it is the dungeon's *exit* room (far end / boss seat).
//! 5. **Threshold** — it is the *entrance* room (the safe way in).
//! 6. otherwise a weighted-random pick from the *plain* pool
//!    ({Vault, Library, Den, Crypt, Stone}, weights from [`ThemeConfig`]).
//!
//! Rules 1–5 draw **no** randomness, so only plain rooms consume a draw from this
//! pass's stream — keeping the weighted pool reproducible regardless of how many
//! terrain/special rooms precede a given room. Entrance and exit are chosen by the
//! same geometry helper [`EntityPlacer`](crate::passes::entity_placer::EntityPlacer)
//! uses ([`entrance_exit_indices`](crate::passes::entity_placer::entrance_exit_indices)),
//! so the `Throne`/`Threshold` themes always land on the rooms that actually get
//! the `Exit`/`Entrance` markers — the two passes agree by construction.

use serde::{Deserialize, Serialize};

use crate::geometry::Point;
use crate::map::Tile;
use crate::pass::{GenContext, Pass};
use crate::passes::entity_placer::entrance_exit_indices;
use crate::region::{RegionKind, RegionTheme};
use crate::rng::Rng;

/// Configuration for a [`RoomThemer`] pass: the relative weights of the *plain*
/// (non-terrain, non-entrance/exit) room themes.
///
/// Terrain-driven themes (Forge/Cistern/Hall) and the special Throne/Threshold are
/// not weighted — they are assigned with certainty when their condition holds.
/// Only a plain room rolls against these weights. A `0.0` weight disables a theme;
/// an all-zero set degenerates to [`Stone`](RegionTheme::Stone).
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct ThemeConfig {
    /// Weight of the loot-jackpot [`Vault`](RegionTheme::Vault) — rare by default.
    pub vault: f64,
    /// Weight of the [`Library`](RegionTheme::Library).
    pub library: f64,
    /// Weight of the monster-dense [`Den`](RegionTheme::Den).
    pub den: f64,
    /// Weight of the undead-heavy [`Crypt`](RegionTheme::Crypt).
    pub crypt: f64,
    /// Weight of plain [`Stone`](RegionTheme::Stone) — the heaviest, so most rooms
    /// stay neutral and the special themes read as discoveries.
    pub stone: f64,
}

impl Default for ThemeConfig {
    /// A sensible mix: mostly bare stone, a healthy spread of crypts and dens,
    /// some libraries, and the occasional vault.
    fn default() -> Self {
        ThemeConfig {
            vault: 1.0,
            library: 2.0,
            den: 3.0,
            crypt: 3.0,
            stone: 6.0,
        }
    }
}

/// A room-theming classifier pass.
///
/// Construct one with [`RoomThemer::new`]; it implements [`Pass`] with the stable
/// name `"room_themer"`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RoomThemer {
    cfg: ThemeConfig,
}

impl RoomThemer {
    /// Creates a room themer with the given configuration.
    pub fn new(cfg: ThemeConfig) -> Self {
        RoomThemer { cfg }
    }

    /// Draws one weighted plain-pool theme from `rng`. Weights are scaled to
    /// integer "milli-weights" so the pick is a single [`Rng::range`] draw; an
    /// all-zero (or negative) set degenerates to [`Stone`](RegionTheme::Stone).
    fn pick_pool_theme(&self, rng: &mut Rng) -> RegionTheme {
        let c = self.cfg;
        let table = [
            (RegionTheme::Vault, c.vault),
            (RegionTheme::Library, c.library),
            (RegionTheme::Den, c.den),
            (RegionTheme::Crypt, c.crypt),
            (RegionTheme::Stone, c.stone),
        ];
        let scaled: [(RegionTheme, i32); 5] =
            table.map(|(t, w)| (t, (w.max(0.0) * 1000.0) as i32));
        let total: i32 = scaled.iter().map(|(_, n)| n).sum();
        if total <= 0 {
            return RegionTheme::Stone;
        }
        let mut pick = rng.range(0, total);
        for (t, n) in scaled {
            if pick < n {
                return t;
            }
            pick -= n;
        }
        RegionTheme::Stone
    }
}

/// A real room's data, snapshotted so theming doesn't hold a borrow on `ctx`.
struct RoomData {
    /// Index of this room's region in `ctx.regions` (its `RegionId`).
    idx: usize,
    center: Point,
    has_lava: bool,
    has_water: bool,
    has_pillar: bool,
}

impl Pass for RoomThemer {
    fn name(&self) -> &str {
        "room_themer"
    }

    fn apply(&self, ctx: &mut GenContext, rng: &mut Rng) {
        // Snapshot every real room (kind==Room, non-empty cells) with a cheap
        // terrain summary. Read-only over regions+tiles; we mutate themes after.
        let rooms: Vec<RoomData> = ctx
            .regions
            .iter()
            .enumerate()
            .filter(|(_, r)| r.kind == RegionKind::Room && !r.cells.is_empty())
            .map(|(idx, r)| {
                let mut has_lava = false;
                let mut has_water = false;
                let mut has_pillar = false;
                for &p in &r.cells {
                    match ctx.tiles.get(p) {
                        Some(&Tile::Lava) => has_lava = true,
                        Some(&Tile::Water) => has_water = true,
                        Some(&Tile::Pillar) => has_pillar = true,
                        _ => {}
                    }
                }
                RoomData {
                    idx,
                    center: r.bounds.center(),
                    has_lava,
                    has_water,
                    has_pillar,
                }
            })
            .collect();

        // Entrance/exit by the same geometry helper EntityPlacer uses, so
        // Throne/Threshold match the rooms that get the Exit/Entrance markers.
        let centers: Vec<Point> = rooms.iter().map(|r| r.center).collect();
        let (entrance_k, exit_k) = match entrance_exit_indices(&centers) {
            Some(pair) => pair,
            None => return, // no real rooms — nothing to theme
        };

        // Resolve each room's theme by the priority walk, then write them back.
        // Only the plain-pool branch draws from `rng`, in room (id) order.
        let assignments: Vec<(usize, RegionTheme)> = rooms
            .iter()
            .enumerate()
            .map(|(k, room)| {
                let theme = if room.has_lava {
                    RegionTheme::Forge
                } else if room.has_water {
                    RegionTheme::Cistern
                } else if room.has_pillar {
                    RegionTheme::Hall
                } else if k == exit_k {
                    RegionTheme::Throne
                } else if k == entrance_k {
                    RegionTheme::Threshold
                } else {
                    self.pick_pool_theme(rng)
                };
                (room.idx, theme)
            })
            .collect();

        for (idx, theme) in assignments {
            ctx.regions[idx].theme = Some(theme);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::blackboard::Blackboard;
    use crate::geometry::Rect;
    use crate::grid::Grid;
    use crate::region::ConnGraph;

    /// Builds a context with `rooms` rectangular Floor rooms (each `(x,y,w,h)`),
    /// the post-decorator state a classifier would see.
    fn ctx_with_rooms(w: u32, h: u32, rooms: &[(i32, i32, i32, i32)]) -> GenContext {
        let mut ctx = GenContext {
            tiles: Grid::new(w, h, Tile::Wall),
            regions: Vec::new(),
            graph: ConnGraph::new(),
            blackboard: Blackboard::new(),
        };
        for &(x, y, rw, rh) in rooms {
            let bounds = Rect::new(x, y, rw, rh);
            let cells: Vec<Point> = bounds.iter().collect();
            for &p in &cells {
                ctx.tiles.set(p, Tile::Floor);
            }
            ctx.add_region(RegionKind::Room, bounds, cells);
        }
        ctx
    }

    fn run(ctx: &mut GenContext, cfg: ThemeConfig, seed: u64) {
        let mut rng = Rng::from_seed(seed).fork("room_themer#0");
        RoomThemer::new(cfg).apply(ctx, &mut rng);
    }

    fn themes(ctx: &GenContext) -> Vec<Option<RegionTheme>> {
        ctx.regions.iter().map(|r| r.theme).collect()
    }

    /// Force the pool to always pick Stone, so non-terrain/non-special rooms are
    /// deterministic in tests that probe the special/terrain rules.
    fn stone_only() -> ThemeConfig {
        ThemeConfig {
            vault: 0.0,
            library: 0.0,
            den: 0.0,
            crypt: 0.0,
            stone: 1.0,
        }
    }

    #[test]
    fn name_is_room_themer() {
        assert_eq!(RoomThemer::new(ThemeConfig::default()).name(), "room_themer");
    }

    /// Terrain wins, in priority order: a room with lava is a Forge, with water a
    /// Cistern, with a pillar a Hall — regardless of entrance/exit position.
    #[test]
    fn terrain_drives_forge_cistern_hall() {
        // Three rooms, each seeded with one terrain tile inside its interior.
        let mut ctx = ctx_with_rooms(40, 12, &[(0, 0, 8, 8), (12, 0, 8, 8), (24, 0, 8, 8)]);
        ctx.tiles.set(Point::new(3, 3), Tile::Lava); // room 0
        ctx.tiles.set(Point::new(15, 3), Tile::Water); // room 1
        ctx.tiles.set(Point::new(27, 3), Tile::Pillar); // room 2
        run(&mut ctx, stone_only(), 1);

        assert_eq!(ctx.regions[0].theme, Some(RegionTheme::Forge));
        assert_eq!(ctx.regions[1].theme, Some(RegionTheme::Cistern));
        assert_eq!(ctx.regions[2].theme, Some(RegionTheme::Hall));
    }

    /// Lava beats water beats pillar when a room holds more than one (priority).
    #[test]
    fn terrain_priority_lava_beats_water_beats_pillar() {
        let mut ctx = ctx_with_rooms(16, 12, &[(0, 0, 10, 10)]);
        ctx.tiles.set(Point::new(2, 2), Tile::Pillar);
        ctx.tiles.set(Point::new(3, 3), Tile::Water);
        ctx.tiles.set(Point::new(4, 4), Tile::Lava);
        run(&mut ctx, stone_only(), 1);
        assert_eq!(ctx.regions[0].theme, Some(RegionTheme::Forge));
    }

    /// With no terrain, the geometric entrance (top-left-most) is the Threshold
    /// and the farthest room is the Throne; the rest fall to the pool (Stone here).
    #[test]
    fn entrance_is_threshold_and_far_room_is_throne() {
        // Centers: (4,4) top-left, (24,4), (4,16). Entrance = room 0; farthest
        // (Manhattan from (4,4)) is room 1 (dist 20) > room 2 (dist 12).
        let mut ctx = ctx_with_rooms(32, 24, &[(0, 0, 8, 8), (20, 0, 8, 8), (0, 12, 8, 8)]);
        run(&mut ctx, stone_only(), 7);
        assert_eq!(ctx.regions[0].theme, Some(RegionTheme::Threshold), "entrance room");
        assert_eq!(ctx.regions[1].theme, Some(RegionTheme::Throne), "far/exit room");
        assert_eq!(ctx.regions[2].theme, Some(RegionTheme::Stone), "plain room");
    }

    /// The pass changes no tiles — it is a pure classifier.
    #[test]
    fn changes_no_tiles() {
        let mut ctx = ctx_with_rooms(32, 16, &[(0, 0, 10, 10), (16, 0, 10, 10)]);
        let before = ctx.tiles.clone();
        run(&mut ctx, ThemeConfig::default(), 42);
        assert_eq!(ctx.tiles, before, "RoomThemer must not touch any tile");
        // Every real room got a theme; corridors/none stay None (there are none here).
        assert!(ctx.regions.iter().all(|r| r.theme.is_some()));
    }

    /// Same seed reproduces the same themes.
    #[test]
    fn theming_is_deterministic() {
        let layout = [(0, 0, 9, 9), (14, 0, 9, 9), (0, 12, 9, 9), (14, 12, 9, 9)];
        let cfg = ThemeConfig::default();
        let mut a = ctx_with_rooms(28, 24, &layout);
        let mut b = ctx_with_rooms(28, 24, &layout);
        run(&mut a, cfg, 0x5EED);
        run(&mut b, cfg, 0x5EED);
        assert_eq!(themes(&a), themes(&b));
    }

    /// Corridors and empty-`cells` placeholder rooms are never themed.
    #[test]
    fn corridors_and_empty_rooms_stay_unthemed() {
        let mut ctx = ctx_with_rooms(40, 12, &[(0, 0, 9, 9), (20, 0, 9, 9)]);
        // An uncarved placeholder room (empty cells) and a corridor.
        ctx.add_region(RegionKind::Room, Rect::new(31, 0, 4, 4), Vec::new());
        ctx.add_region(RegionKind::Corridor, Rect::new(9, 4, 11, 1), vec![Point::new(9, 4)]);
        run(&mut ctx, ThemeConfig::default(), 3);

        assert!(ctx.regions[0].theme.is_some());
        assert!(ctx.regions[1].theme.is_some());
        assert_eq!(ctx.regions[2].theme, None, "empty placeholder room unthemed");
        assert_eq!(ctx.regions[3].theme, None, "corridor unthemed");
    }
}
