//! The generation engine: [`GenContext`], [`Pass`], [`Pipeline`], [`Snapshot`]
//! (spec §4.6).
//!
//! This is the keystone contract every concrete pass (Tasks 7–11) consumes. The
//! shape of the data flow is:
//!
//! - A [`Pipeline`] is a recipe: a canvas size plus an ordered list of passes.
//! - [`Pipeline::run`] builds a fresh [`GenContext`] (an all-[`Tile::Wall`] grid
//!   and empty regions/graph/blackboard), then applies each [`Pass`] in order,
//!   mutating the shared context.
//! - The final context is packaged into a [`crate::map::Map`] — the library's
//!   sole output.
//!
//! ## RNG isolation is owned here, not by passes (spec §4.6, §7)
//!
//! [`Pipeline::run`] creates one root [`Rng`] from the seed and, for each pass,
//! derives a **dedicated** child stream via [`Rng::fork`] keyed by
//! `"<pass name>#<occurrence>"`, where `occurrence` is a *per-name* counter the
//! run loop maintains. Each pass receives only its own `&mut Rng` and never sees
//! a shared one.
//!
//! Because [`Rng::fork`] depends only on `(seed, key)` — not on draw history —
//! this makes determinism-under-pipeline-edits **structural**: in pipelines
//! `[A, B]` and `[A, X, B]`, both `A` and `B` still key to `A#0` / `B#0`, so they
//! receive identical streams regardless of the inserted `X`. Inserting or
//! reordering a pass never reshuffles another pass's randomness.
//!
//! ## `run` and `run_with_snapshots` cannot diverge (spec §7)
//!
//! Both public entry points delegate to the single private
//! [`Pipeline::run_inner`], so the generation path is literally shared. The only
//! difference is whether a [`Snapshot`] is cloned out after each pass — snapshot
//! capture only *reads* context state and never touches the RNG, so the
//! resulting [`Map`] is identical for a given seed with or without snapshots.

use crate::blackboard::Blackboard;
use crate::entity::Entity;
use crate::geometry::{Point, Rect};
use crate::grid::Grid;
use crate::map::{Map, Tile};
use crate::region::{ConnGraph, Edge, Region, RegionId, RegionKind};
use crate::rng::Rng;
use std::collections::HashMap;

/// The mutable working state threaded through every [`Pass`] (spec §4.6).
///
/// A pass reads and writes these fields directly — they are the only channel
/// through which passes communicate (passes never call each other). The starting
/// state for a run is an all-[`Tile::Wall`] grid with empty regions, graph, and
/// blackboard; each pass carves tiles, appends regions, adds edges, or stashes
/// scratch data on the [`Blackboard`].
pub struct GenContext {
    /// The terrain layer being carved. Starts entirely [`Tile::Wall`].
    pub tiles: Grid<Tile>,
    /// The semantic regions discovered/created so far, indexed by [`RegionId`].
    pub regions: Vec<Region>,
    /// The region connectivity graph (edges added by connector passes).
    pub graph: ConnGraph,
    /// Named typed scratch slots for cross-pass data that isn't tiles/regions.
    pub blackboard: Blackboard,
}

impl GenContext {
    /// Appends a new region and returns its [`RegionId`].
    ///
    /// The id is assigned as `RegionId(self.regions.len())` *before* the push, so
    /// it equals the region's index in [`regions`](GenContext::regions). Regions
    /// are append-only — never removed or reordered — which preserves the
    /// id-as-index invariant (Task 5 / [`RegionId`]) that edges and lookups rely
    /// on.
    pub fn add_region(&mut self, kind: RegionKind, bounds: Rect, cells: Vec<Point>) -> RegionId {
        let id = RegionId(self.regions.len());
        self.regions.push(Region {
            id,
            kind,
            bounds,
            cells,
            theme: None,
        });
        id
    }
}

/// A single generation step (spec §4.6).
///
/// Implementors are small, focused transforms (partition, carve, connect, place)
/// that mutate the shared [`GenContext`]. The contract is deliberately tight:
///
/// - [`name`](Pass::name) is the pass's stable identity. The pipeline uses it to
///   key this pass's dedicated RNG sub-stream (`"<name>#<occurrence>"`), so two
///   distinct pass *kinds* should return distinct names.
/// - [`apply`](Pass::apply) takes `&self`: a pass's configuration is immutable
///   during a run. The pipeline — not the pass — tracks how many times a given
///   name has occurred, so the same config can appear twice and still get
///   distinct, stable streams. A pass draws all its randomness from the `rng`
///   it is handed and from no other source.
pub trait Pass {
    /// This pass's stable name, used to key its dedicated RNG sub-stream.
    fn name(&self) -> &str;

    /// Applies this pass to `ctx`, drawing any randomness from `rng`.
    ///
    /// `rng` is a sub-stream dedicated to this pass occurrence; a pass must not
    /// reach for any other source of randomness if the run is to stay
    /// deterministic.
    fn apply(&self, ctx: &mut GenContext, rng: &mut Rng);
}

/// A point-in-time capture of generation state, taken *after* a pass runs
/// (spec §4.6).
///
/// Snapshots feed a visualization scrubber that steps through generation. Each
/// captures the tiles **and** the region bounds + graph edges, so passes that
/// don't change tiles (partition, connect) are still visible as state changes.
/// Snapshotting only clones context state — it never advances the RNG — so
/// collecting snapshots cannot alter the final [`Map`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Snapshot {
    /// The name of the pass that produced this state (its [`Pass::name`]).
    pub label: String,
    /// The tile grid as it stood just after the pass ran.
    pub tiles: Grid<Tile>,
    /// The regions as they stood just after the pass ran.
    pub regions: Vec<Region>,
    /// The connectivity edges as they stood just after the pass ran.
    pub edges: Vec<Edge>,
}

/// A generation recipe: a canvas size plus an ordered list of passes.
///
/// Build one with [`Pipeline::new`] and chain passes with the
/// [`then`](Pipeline::then) builder, then produce a map with
/// [`run`](Pipeline::run) (or [`run_with_snapshots`](Pipeline::run_with_snapshots)
/// for the viz scrubber). The pipeline owns the canvas dimensions: `run` builds
/// the initial all-[`Tile::Wall`] grid at that size.
pub struct Pipeline {
    /// Canvas width in cells.
    width: u32,
    /// Canvas height in cells.
    height: u32,
    /// The passes to apply, in order.
    passes: Vec<Box<dyn Pass>>,
}

impl Pipeline {
    /// Creates an empty pipeline for a `width` × `height` canvas.
    ///
    /// Add passes with [`then`](Pipeline::then).
    pub fn new(width: u32, height: u32) -> Self {
        Pipeline {
            width,
            height,
            passes: Vec::new(),
        }
    }

    /// Appends `pass` and returns the pipeline, for fluent chaining.
    ///
    /// `'static` is required because the pass is boxed and stored; any owned
    /// pass value (including one capturing `Rc<RefCell<_>>` for test
    /// observation) satisfies it. The pipeline is single-threaded, so no `Send`
    /// bound is needed.
    pub fn then(mut self, pass: impl Pass + 'static) -> Self {
        self.passes.push(Box::new(pass));
        self
    }

    /// Runs the pipeline for `seed`, returning the generated [`Map`].
    ///
    /// Builds a fresh [`GenContext`], applies each pass in order with its own
    /// forked RNG sub-stream, and packages the result. Equivalent to
    /// [`run_with_snapshots`](Pipeline::run_with_snapshots) minus the snapshots,
    /// and guaranteed to produce an identical map (both share
    /// [`run_inner`](Pipeline::run_inner)).
    pub fn run(&self, seed: u64) -> Map {
        let (map, _snapshots) = self.run_inner(seed, false);
        map
    }

    /// Runs the pipeline for `seed`, returning the [`Map`] **and** a [`Snapshot`]
    /// captured after each pass (in pass order).
    ///
    /// The returned map is byte-identical to [`run`](Pipeline::run) for the same
    /// seed — snapshot capture only reads context state and never touches the
    /// RNG. The snapshot count equals the number of passes.
    pub fn run_with_snapshots(&self, seed: u64) -> (Map, Vec<Snapshot>) {
        self.run_inner(seed, true)
    }

    /// The single shared generation path behind both public entry points.
    ///
    /// When `collect` is `true`, a [`Snapshot`] is cloned out after each pass;
    /// otherwise the returned snapshot vec is empty. Crucially, the RNG forking
    /// and pass application are identical in both modes, so the resulting [`Map`]
    /// cannot diverge between `run` and `run_with_snapshots` (spec §7).
    fn run_inner(&self, seed: u64, collect: bool) -> (Map, Vec<Snapshot>) {
        // Fresh context: solid-wall canvas, nothing else.
        let mut ctx = GenContext {
            tiles: Grid::new(self.width, self.height, Tile::Wall),
            regions: Vec::new(),
            graph: ConnGraph::new(),
            blackboard: Blackboard::new(),
        };

        // The root stream all per-pass sub-streams fork from.
        let root = Rng::from_seed(seed);
        // Per-NAME occurrence counter: two passes sharing a name get `name#0`,
        // `name#1`, … — distinct, stable streams. A *different* name keeps its
        // own count, so inserting a pass never shifts another's occurrence.
        let mut occurrences: HashMap<&str, u32> = HashMap::new();

        let mut snapshots = Vec::new();

        for pass in &self.passes {
            let name = pass.name();
            // Read-then-increment this name's occurrence counter.
            let occurrence = occurrences.entry(name).or_insert(0);
            let key = format!("{}#{}", name, *occurrence);
            *occurrence += 1;

            // Hand the pass *only* its own dedicated sub-stream.
            let mut rng = root.fork(&key);
            pass.apply(&mut ctx, &mut rng);

            if collect {
                // Read-only capture of post-pass state. Does not touch `rng`.
                snapshots.push(Snapshot {
                    label: name.to_string(),
                    tiles: ctx.tiles.clone(),
                    regions: ctx.regions.clone(),
                    edges: ctx.graph.edges().to_vec(),
                });
            }
        }

        // Harvest the entity layer the placer (if any) stashed on the blackboard
        // — the side channel for data that isn't a tile/region/edge (§4.6). A
        // recipe with no placer simply yields no entities.
        let entities = ctx
            .blackboard
            .take::<Vec<Entity>>(crate::entity::BLACKBOARD_KEY)
            .unwrap_or_default();
        let map = Map {
            tiles: ctx.tiles,
            regions: ctx.regions,
            graph: ctx.graph,
            entities,
            seed,
            width: self.width,
            height: self.height,
        };
        (map, snapshots)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;
    use std::rc::Rc;

    /// A trivial pass that fills the entire grid with [`Tile::Floor`]. Draws no
    /// randomness.
    struct FillFloor;
    impl Pass for FillFloor {
        fn name(&self) -> &str {
            "FillFloor"
        }
        fn apply(&self, ctx: &mut GenContext, _rng: &mut Rng) {
            let (w, h) = (ctx.tiles.width() as i32, ctx.tiles.height() as i32);
            for y in 0..h {
                for x in 0..w {
                    ctx.tiles.set(Point::new(x, y), Tile::Floor);
                }
            }
        }
    }

    /// A pass that records its first `range` draw into a shared cell, so a test
    /// can observe the sub-stream it was given despite `apply(&self)` and `run`
    /// returning only a `Map`. `name` is configurable so we can build same-named
    /// and differently-named instances.
    struct RecordDraw {
        name: String,
        sink: Rc<RefCell<Vec<i32>>>,
    }
    impl Pass for RecordDraw {
        fn name(&self) -> &str {
            &self.name
        }
        fn apply(&self, _ctx: &mut GenContext, rng: &mut Rng) {
            let v = rng.range(0, 1_000_000);
            self.sink.borrow_mut().push(v);
        }
    }

    /// A no-draw pass that does not touch the RNG or the context — used to verify
    /// that inserting it between two passes doesn't shift their streams.
    struct NoOp;
    impl Pass for NoOp {
        fn name(&self) -> &str {
            "NoOp"
        }
        fn apply(&self, _ctx: &mut GenContext, _rng: &mut Rng) {}
    }

    /// `add_region` assigns sequential ids equal to the region's index.
    #[test]
    fn add_region_assigns_index_as_id() {
        let mut ctx = GenContext {
            tiles: Grid::new(4, 4, Tile::Wall),
            regions: Vec::new(),
            graph: ConnGraph::new(),
            blackboard: Blackboard::new(),
        };
        let a = ctx.add_region(RegionKind::Room, Rect::new(0, 0, 2, 2), vec![]);
        let b = ctx.add_region(RegionKind::Corridor, Rect::new(2, 0, 1, 4), vec![]);
        assert_eq!(a, RegionId(0));
        assert_eq!(b, RegionId(1));
        assert_eq!(ctx.regions[0].id, RegionId(0));
        assert_eq!(ctx.regions[1].id, RegionId(1));
    }

    /// A trivial fill pass produces the expected grid and a `Map` with matching
    /// seed and dimensions.
    #[test]
    fn trivial_fill_floor_pass() {
        let map = Pipeline::new(3, 2).then(FillFloor).run(0x1234);

        assert_eq!(map.seed, 0x1234);
        assert_eq!((map.width, map.height), (3, 2));
        // Every cell is Floor.
        assert!(map.tiles.iter().all(|(_, &t)| t == Tile::Floor));
    }

    /// Same seed → equal maps.
    #[test]
    fn run_is_deterministic() {
        let build = || Pipeline::new(8, 8).then(FillFloor);
        let a = build().run(777);
        let b = build().run(777);
        assert_eq!(a, b);
    }

    /// `run` and `run_with_snapshots` produce equal maps; one snapshot per pass.
    #[test]
    fn snapshots_match_run_and_count_passes() {
        let build = || {
            Pipeline::new(6, 6)
                .then(FillFloor)
                .then(NoOp)
                .then(FillFloor)
        };
        let plain = build().run(2024);
        let (snapped, snapshots) = build().run_with_snapshots(2024);

        assert_eq!(plain, snapped, "snapshotting must not change the map");
        assert_eq!(snapshots.len(), 3, "one snapshot per pass");
        assert_eq!(snapshots[0].label, "FillFloor");
        assert_eq!(snapshots[1].label, "NoOp");
        assert_eq!(snapshots[2].label, "FillFloor");
    }

    /// RNG isolation: inserting a pass `X` between `A` and `B` does not change
    /// the streams `A` and `B` receive. We compare the first draws of `A` and
    /// `B` across pipelines `[A, B]` and `[A, X, B]`.
    #[test]
    fn inserting_a_pass_does_not_reshuffle_others() {
        let seed = 0xFEED_FACE;

        // Pipeline [A, B].
        let a_sink1 = Rc::new(RefCell::new(Vec::new()));
        let b_sink1 = Rc::new(RefCell::new(Vec::new()));
        Pipeline::new(4, 4)
            .then(RecordDraw {
                name: "A".to_string(),
                sink: a_sink1.clone(),
            })
            .then(RecordDraw {
                name: "B".to_string(),
                sink: b_sink1.clone(),
            })
            .run(seed);

        // Pipeline [A, X, B] — X draws then discards (proving it's about keying,
        // not draw history).
        let a_sink2 = Rc::new(RefCell::new(Vec::new()));
        let b_sink2 = Rc::new(RefCell::new(Vec::new()));
        let x_sink = Rc::new(RefCell::new(Vec::new()));
        Pipeline::new(4, 4)
            .then(RecordDraw {
                name: "A".to_string(),
                sink: a_sink2.clone(),
            })
            .then(RecordDraw {
                name: "X".to_string(),
                sink: x_sink.clone(),
            })
            .then(RecordDraw {
                name: "B".to_string(),
                sink: b_sink2.clone(),
            })
            .run(seed);

        assert_eq!(
            a_sink1.borrow().as_slice(),
            a_sink2.borrow().as_slice(),
            "A's stream must be unchanged by inserting X"
        );
        assert_eq!(
            b_sink1.borrow().as_slice(),
            b_sink2.borrow().as_slice(),
            "B's stream must be unchanged by inserting X"
        );
    }

    /// Occurrence keying: two instances of the same-named pass receive different
    /// sub-streams (`name#0` vs `name#1`), so their recorded draws differ.
    #[test]
    fn same_name_instances_get_distinct_streams() {
        let sink = Rc::new(RefCell::new(Vec::new()));
        Pipeline::new(4, 4)
            .then(RecordDraw {
                name: "Dup".to_string(),
                sink: sink.clone(),
            })
            .then(RecordDraw {
                name: "Dup".to_string(),
                sink: sink.clone(),
            })
            .run(123);

        let draws = sink.borrow();
        assert_eq!(draws.len(), 2);
        assert_ne!(
            draws[0], draws[1],
            "occurrence #0 and #1 must draw from distinct streams"
        );
    }
}
