//! The [`EntityPlacer`] pass: populate the dungeon with entities.
//!
//! A *placer* that drops [`Entity`](crate::entity::Entity)s onto open floor — the
//! final pass of the dungeon recipe, after all terrain (rooms, caves, corridors,
//! doors, pools, pillars) is settled. It changes **no** tiles or regions; it
//! produces an overlay layer the engine harvests into [`Map::entities`](crate::map::Map::entities).
//!
//! Because [`GenContext`] has no entity field (entities aren't tiles, regions, or
//! edges), this pass stashes its `Vec<Entity>` on the [`Blackboard`](crate::blackboard::Blackboard)
//! under [`crate::entity::BLACKBOARD_KEY`] — the side channel designed for exactly
//! this kind of cross-pass/-engine handoff (spec §4.6) — and
//! [`Pipeline`](crate::pass::Pipeline) takes it out into the final `Map`.
//!
//! Placement, in a fixed (deterministic) order:
//!
//! 1. **Entrance** — the way-in room chosen *purely from geometry* (no RNG) by
//!    [`entrance_exit_indices`]; the entity sits on the floor cell nearest its
//!    center.
//! 2. **Exit** — the room whose center is farthest (Manhattan) from the entrance,
//!    again on its central floor cell. Gives the dungeon a traversal goal. (The
//!    entrance/exit choice is RNG-free so a themer can reproduce it and land its
//!    `Throne`/`Threshold` themes on these very rooms.)
//! 3. **Treasure** — each room has a `treasure_chance` of holding one (scaled by
//!    the room's [`RegionTheme`] treasure bias), on a random open floor cell.
//! 4. **Monsters** — each room *except the entrance room* has a `monster_chance`
//!    (scaled by the room's theme monster bias) of holding 1..=`max_monsters`,
//!    scattered on open floor.
//!
//! Entities only ever land on plain [`Floor`](crate::map::Tile::Floor) (never a
//! door, pool, pillar, or wall), and never two on one cell, so a renderer or game
//! can treat `at` as a free walkable cell.

use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

use crate::entity::{Entity, EntityKind, BLACKBOARD_KEY};
use crate::geometry::Point;
use crate::map::Tile;
use crate::pass::{GenContext, Pass};
use crate::region::{Region, RegionKind, RegionTheme};
use crate::rng::Rng;

/// Picks `(entrance_index, exit_index)` into a room list purely from geometry —
/// **no RNG** — so any pass can reproduce the same choice without coupling to
/// another pass's random stream. `centers[i]` is room `i`'s center.
///
/// - The **entrance** is the room whose center is lexicographically smallest
///   (top-most row, then left-most column): a stable, corner-ward way in.
/// - The **exit** is the room whose center is farthest (Manhattan) from the
///   entrance — the natural traversal goal.
///
/// Returns `None` for an empty list; with a single room both indices are `0`.
/// [`RoomThemer`](crate::passes::room_themer::RoomThemer) calls this with the
/// same room set to land its `Throne`/`Threshold` themes on the rooms that
/// actually receive the `Exit`/`Entrance` markers — the two passes agree by
/// construction rather than by replaying each other's randomness.
pub(crate) fn entrance_exit_indices(centers: &[Point]) -> Option<(usize, usize)> {
    if centers.is_empty() {
        return None;
    }
    let entrance = (0..centers.len())
        .min_by_key(|&i| (centers[i].y, centers[i].x))
        .expect("non-empty");
    let from = centers[entrance];
    let exit = (0..centers.len())
        .max_by_key(|&i| centers[i].manhattan(from))
        .unwrap_or(entrance);
    Some((entrance, exit))
}

/// Configuration for an [`EntityPlacer`] pass.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct EntityConfig {
    /// Probability that a given room holds a treasure.
    pub treasure_chance: f64,
    /// Probability that a given room (other than the entrance room) holds
    /// monsters.
    pub monster_chance: f64,
    /// The most monsters a single monster-room may hold (each placed on its own
    /// floor cell). The actual count is `1..=max_monsters`.
    pub max_monsters: i32,
}

impl Default for EntityConfig {
    /// A sensible default: about a third of rooms hold treasure, half hold up to
    /// three monsters.
    fn default() -> Self {
        EntityConfig {
            treasure_chance: 0.35,
            monster_chance: 0.50,
            max_monsters: 3,
        }
    }
}

/// An entity-placing pass.
///
/// Construct one with [`EntityPlacer::new`]; it implements [`Pass`] with the
/// stable name `"entity_placer"`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct EntityPlacer {
    cfg: EntityConfig,
}

impl EntityPlacer {
    /// Creates an entity placer with the given configuration.
    pub fn new(cfg: EntityConfig) -> Self {
        EntityPlacer { cfg }
    }
}

/// A real room's data, snapshotted so placement doesn't hold a borrow on `ctx`.
struct RoomInfo {
    center: Point,
    /// The room's open-floor cells (Tile::Floor only), row-major.
    floor: Vec<Point>,
    /// The room's theme, if a themer classified it (drives content biases).
    theme: Option<RegionTheme>,
}

impl Pass for EntityPlacer {
    fn name(&self) -> &str {
        "entity_placer"
    }

    fn apply(&self, ctx: &mut GenContext, rng: &mut Rng) {
        // Snapshot each real room's center and its open-floor cells. Only plain
        // Floor counts — doors, pools, pillars and walls are never entity homes.
        let rooms: Vec<RoomInfo> = ctx
            .regions
            .iter()
            .filter(|r: &&Region| r.kind == RegionKind::Room && !r.cells.is_empty())
            .map(|r| RoomInfo {
                center: r.bounds.center(),
                floor: r
                    .cells
                    .iter()
                    .copied()
                    .filter(|&p| ctx.tiles.get(p) == Some(&Tile::Floor))
                    .collect(),
                theme: r.theme,
            })
            .collect();

        let mut entities: Vec<Entity> = Vec::new();
        let mut taken: BTreeSet<(i32, i32)> = BTreeSet::new();

        // Entrance/exit are chosen by the shared geometry helper (no RNG), so the
        // themer's Throne/Threshold land on these very rooms (see
        // [`entrance_exit_indices`]).
        let centers: Vec<Point> = rooms.iter().map(|r| r.center).collect();
        if let Some((entrance_idx, exit_idx)) = entrance_exit_indices(&centers) {
            // 1) Entrance — the way-in room, central floor cell.
            if let Some(p) = nearest_free(&rooms[entrance_idx], rooms[entrance_idx].center, &taken) {
                taken.insert((p.x, p.y));
                entities.push(Entity::new(EntityKind::Entrance, p));
            }

            // 2) Exit — the far room, central floor cell.
            if let Some(p) = nearest_free(&rooms[exit_idx], rooms[exit_idx].center, &taken) {
                taken.insert((p.x, p.y));
                entities.push(Entity::new(EntityKind::Exit, p));
            }

            // 3 & 4) Per-room treasure and monsters, in room order for determinism.
            // Each room's theme scales the base chances (no theme = neutral 1.0),
            // clamped to a valid probability — so a Vault is loot-rich and a Den
            // is monster-dense, while the entrance room stays monster-free.
            for (i, room) in rooms.iter().enumerate() {
                let (treasure_bias, monster_bias) =
                    room.theme.map(RegionTheme::biases).unwrap_or((1.0, 1.0));

                let treasure_p = (self.cfg.treasure_chance * treasure_bias).clamp(0.0, 1.0);
                if rng.chance(treasure_p) {
                    if let Some(p) = choose_free(room, &taken, rng) {
                        taken.insert((p.x, p.y));
                        entities.push(Entity::new(EntityKind::Treasure, p));
                    }
                }

                // Keep the entrance room clear of monsters.
                let monster_p = (self.cfg.monster_chance * monster_bias).clamp(0.0, 1.0);
                if i != entrance_idx && rng.chance(monster_p) {
                    let n = rng.range(1, self.cfg.max_monsters.max(1) + 1);
                    for _ in 0..n {
                        match choose_free(room, &taken, rng) {
                            Some(p) => {
                                taken.insert((p.x, p.y));
                                entities.push(Entity::new(EntityKind::Monster, p));
                            }
                            None => break, // room is full
                        }
                    }
                }
            }
        }

        ctx.blackboard.insert(BLACKBOARD_KEY, entities);
    }
}

/// The room's free floor cell nearest `target`, or [`None`] if none are free.
fn nearest_free(room: &RoomInfo, target: Point, taken: &BTreeSet<(i32, i32)>) -> Option<Point> {
    room.floor
        .iter()
        .copied()
        .filter(|p| !taken.contains(&(p.x, p.y)))
        .min_by_key(|p| (p.x - target.x).pow(2) + (p.y - target.y).pow(2))
}

/// A uniformly random free floor cell of the room, drawn from `rng`, or [`None`].
fn choose_free(room: &RoomInfo, taken: &BTreeSet<(i32, i32)>, rng: &mut Rng) -> Option<Point> {
    let free: Vec<Point> = room
        .floor
        .iter()
        .copied()
        .filter(|p| !taken.contains(&(p.x, p.y)))
        .collect();
    rng.choose(&free).copied()
}

#[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)`).
    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: EntityConfig, seed: u64) -> Vec<Entity> {
        let mut rng = Rng::from_seed(seed).fork("entity_placer#0");
        EntityPlacer::new(cfg).apply(ctx, &mut rng);
        ctx.blackboard.take::<Vec<Entity>>(BLACKBOARD_KEY).unwrap_or_default()
    }

    #[test]
    fn name_is_entity_placer() {
        assert_eq!(EntityPlacer::new(EntityConfig::default()).name(), "entity_placer");
    }

    /// A multi-room map gets exactly one entrance and one exit, on distinct floor
    /// cells, and every entity sits on a Floor cell with no two sharing a cell.
    #[test]
    fn places_one_entrance_one_exit_on_distinct_floor() {
        let rooms = [(0, 0, 8, 8), (20, 0, 8, 8), (0, 12, 8, 8)];
        let mut ctx = ctx_with_rooms(32, 24, &rooms);
        let entities = run(&mut ctx, EntityConfig::default(), 0x1234);

        let entrances = entities.iter().filter(|e| e.kind == EntityKind::Entrance).count();
        let exits = entities.iter().filter(|e| e.kind == EntityKind::Exit).count();
        assert_eq!(entrances, 1, "exactly one entrance");
        assert_eq!(exits, 1, "exactly one exit");

        // Every entity on Floor; all on distinct cells.
        let mut cells = BTreeSet::new();
        for e in &entities {
            assert_eq!(ctx.tiles.get(e.at), Some(&Tile::Floor), "entity {e:?} not on Floor");
            assert!(cells.insert((e.at.x, e.at.y)), "two entities share cell {:?}", e.at);
        }

        // Entrance and exit are different cells (different rooms here).
        let entrance = entities.iter().find(|e| e.kind == EntityKind::Entrance).unwrap();
        let exit = entities.iter().find(|e| e.kind == EntityKind::Exit).unwrap();
        assert_ne!(entrance.at, exit.at);
    }

    /// No monster shares the entrance room; the entrance room is a safe start.
    #[test]
    fn entrance_room_has_no_monsters() {
        let rooms = [(0, 0, 8, 8), (20, 0, 8, 8), (0, 12, 8, 8)];
        let mut ctx = ctx_with_rooms(32, 24, &rooms);
        // Force monsters everywhere they're allowed.
        let cfg = EntityConfig { treasure_chance: 0.0, monster_chance: 1.0, max_monsters: 3 };
        let entities = run(&mut ctx, cfg, 7);

        let entrance = entities.iter().find(|e| e.kind == EntityKind::Entrance).unwrap().at;
        // Which room contains the entrance?
        let in_room = |p: Point, r: (i32, i32, i32, i32)| Rect::new(r.0, r.1, r.2, r.3).contains(p);
        let entrance_room = rooms.iter().copied().find(|&r| in_room(entrance, r)).unwrap();
        for m in entities.iter().filter(|e| e.kind == EntityKind::Monster) {
            assert!(!in_room(m.at, entrance_room), "monster {m:?} spawned in the entrance room");
        }
    }

    /// Same seed reproduces the exact same entity layer.
    #[test]
    fn placement_is_deterministic() {
        let rooms = [(0, 0, 8, 8), (20, 0, 8, 8), (0, 12, 8, 8), (20, 12, 8, 8)];
        let cfg = EntityConfig::default();
        let mut a = ctx_with_rooms(32, 24, &rooms);
        let mut b = ctx_with_rooms(32, 24, &rooms);
        assert_eq!(run(&mut a, cfg, 0x5EED), run(&mut b, cfg, 0x5EED));
    }

    /// An empty map (no rooms) places no entities and does not panic.
    #[test]
    fn no_rooms_places_nothing() {
        let mut ctx = ctx_with_rooms(8, 8, &[]);
        assert!(run(&mut ctx, EntityConfig::default(), 1).is_empty());
    }

    /// Theme biases scale per-room density: a `Threshold` room (monster_bias 0.0)
    /// gets no monsters even at monster_chance 1.0, while a `Vault`
    /// (treasure_bias 2.5) is guaranteed treasure when its scaled chance saturates
    /// to 1.0. A `None` theme stays neutral.
    #[test]
    fn theme_biases_scale_treasure_and_monsters() {
        let rooms = [(0, 0, 8, 8), (20, 0, 8, 8), (0, 12, 8, 8)];
        let mut ctx = ctx_with_rooms(32, 24, &rooms);
        // Entrance (geometry) = room 0; exit = room 1. Theme room 1 a Threshold
        // (monster_bias 0.0) and room 2 a Vault (treasure_bias 2.5).
        ctx.regions[1].theme = Some(RegionTheme::Threshold);
        ctx.regions[2].theme = Some(RegionTheme::Vault);

        // 0.4 treasure * 2.5 Vault = 1.0 (guaranteed); 1.0 monster * 0.0 = 0.0.
        let cfg = EntityConfig { treasure_chance: 0.4, monster_chance: 1.0, max_monsters: 3 };
        let entities = run(&mut ctx, cfg, 0xBADF00D);

        let in_room = |p: Point, r: (i32, i32, i32, i32)| Rect::new(r.0, r.1, r.2, r.3).contains(p);

        // The Threshold room (room 1) holds no monsters despite monster_chance 1.0.
        let monsters_in_room1 = entities
            .iter()
            .filter(|e| e.kind == EntityKind::Monster && in_room(e.at, rooms[1]))
            .count();
        assert_eq!(monsters_in_room1, 0, "monster_bias 0.0 must suppress all monsters");

        // The Vault room (room 2) is guaranteed a treasure (scaled chance == 1.0).
        let treasure_in_room2 = entities
            .iter()
            .any(|e| e.kind == EntityKind::Treasure && in_room(e.at, rooms[2]));
        assert!(treasure_in_room2, "treasure_bias 2.5 must guarantee Vault treasure");
    }
}
