//! Built-in builders for common topologies. Each constructor returns
//! a fully-populated `BoardBuilder { name, graph }`. The `graph`
//! field is mutable, so plugins can compose: `square_grid(8, 8)`
//! followed by procedural mutation for chutes/ladders, etc.
#![allow(clippy::indexing_slicing)]

use crate::board::BoardGraph;
use crate::board::ids::BoardNodeId;

pub struct Branch {
    pub fork_at: u32,
    pub length: u32,
}

#[derive(Debug, Clone)]
pub struct BoardBuilder {
    pub name: String,
    pub graph: BoardGraph,
}

impl BoardBuilder {
    /// Override the auto-generated name from a built-in builder.
    pub fn renamed(mut self, name: impl Into<String>) -> Self {
        self.name = name.into();
        self
    }

    /// Linear chain of `length` nodes connected by bidirectional edges.
    /// Useful for path-style games; directed jumps are added on top.
    pub fn linear_path(length: u32) -> Self {
        let mut graph = BoardGraph::new();
        let mut prev = None;
        for _ in 0..length {
            let n = graph.add_node();
            if let Some(p) = prev {
                graph.add_bidirectional_edge(p, n);
            }
            prev = Some(n);
        }
        Self { name: format!("Linear path · {length} nodes"), graph }
    }

    /// Square tiling — `cols × rows` cells. Nodes at corners,
    /// bidirectional edges along sides, faces are the cells.
    #[expect(clippy::expect_used, reason = "invariant: lattice nodes are always connected; try_add_face cannot fail for a well-formed square grid")]
    pub fn square_grid(cols: u32, rows: u32) -> Self {
        let mut graph = BoardGraph::new();
        // Corner nodes laid out as a (cols+1) × (rows+1) lattice.
        let nrows = rows + 1;
        let ncols = cols + 1;
        let idx = |c: u32, r: u32| -> usize { (r * ncols + c) as usize };
        let mut node_ids = Vec::with_capacity((nrows * ncols) as usize);
        for _ in 0..(nrows * ncols) {
            node_ids.push(graph.add_node());
        }
        // Horizontal edges.
        for r in 0..nrows {
            for c in 0..cols {
                graph.add_bidirectional_edge(node_ids[idx(c, r)], node_ids[idx(c + 1, r)]);
            }
        }
        // Vertical edges.
        for r in 0..rows {
            for c in 0..ncols {
                graph.add_bidirectional_edge(node_ids[idx(c, r)], node_ids[idx(c, r + 1)]);
            }
        }
        // Faces (cell at row r, col c bounded by 4 corner nodes, CW).
        // AR-1: register Cartesian face coords as we go so distance
        // metrics work without stride math.
        for r in 0..rows {
            for c in 0..cols {
                let a = node_ids[idx(c, r)];
                let b = node_ids[idx(c + 1, r)];
                let cc = node_ids[idx(c + 1, r + 1)];
                let d = node_ids[idx(c, r + 1)];
                let face = graph.try_add_face(vec![a, b, cc, d]).expect("square corners are connected");
                graph.register_face_coord(
                    face,
                    crate::board::FaceCoord::Cartesian {
                        col: c as i32,
                        row: r as i32,
                    },
                );
            }
        }
        Self { name: format!("Square grid · {cols}×{rows}"), graph }
    }

    #[expect(clippy::expect_used, reason = "invariant: hex corners are deduped before adding the face; try_add_face cannot fail for a valid hex boundary")]
    /// Hex tiling with the given radius using axial coordinates.
    /// radius 0 → 1 hex, radius 1 → 7 hexes, radius 2 → 19 hexes, etc.
    ///
    /// Corners and edges shared by adjacent hexes are deduped so the
    /// resulting graph is a proper planar graph — no duplicate nodes or
    /// parallel edges.
    pub fn hex_grid(radius: u32) -> Self {
        use std::collections::HashMap;

        // Axial neighbour direction offsets, CW from NE.
        // Corner c of hex (q,r) is shared by the hex itself, its
        // neighbour in direction c, and its neighbour in direction (c+5)%6.
        const DQ: [i32; 6] = [1,  0, -1, -1,  0,  1];
        const DR: [i32; 6] = [0,  1,  1,  0, -1, -1];

        // Canonical key for the corner that sits between hex (q,r) and
        // its two neighbours in directions c and (c+5)%6.
        // Sort the three hex coords and pack as a fixed triple.
        let corner_key = |q: i32, r: i32, c: usize| -> (i64, i64, i64) {
            let (q1, r1) = (q + DQ[c], r + DR[c]);
            let (q2, r2) = (q + DQ[(c + 5) % 6], r + DR[(c + 5) % 6]);
            let mut hexes = [(q as i64, r as i64), (q1 as i64, r1 as i64), (q2 as i64, r2 as i64)];
            hexes.sort();
            (hexes[0].0 * 10000 + hexes[0].1,
             hexes[1].0 * 10000 + hexes[1].1,
             hexes[2].0 * 10000 + hexes[2].1)
        };

        // Canonical key for an edge between two node-keys (sorted).
        let edge_key = |a: (i64, i64, i64), b: (i64, i64, i64)| -> ((i64, i64, i64), (i64, i64, i64)) {
            if a <= b { (a, b) } else { (b, a) }
        };

        // Collect hex coords in the axial grid.
        let r_i = radius as i32;
        let mut hex_coords: Vec<(i32, i32)> = Vec::new();
        for q in -r_i..=r_i {
            for r in -r_i..=r_i {
                if q.abs().max(r.abs()).max((q + r).abs()) <= r_i {
                    hex_coords.push((q, r));
                }
            }
        }

        let mut graph = BoardGraph::new();
        // corner-key → BoardNodeId
        let mut corner_to_node: HashMap<(i64, i64, i64), BoardNodeId> = HashMap::new();
        // edge-key → unit (HashSet shape avoids the clippy type_complexity lint).
        type EdgeKey = ((i64, i64, i64), (i64, i64, i64));
        let mut edge_set: std::collections::HashSet<EdgeKey> = std::collections::HashSet::new();

        for (q, r) in hex_coords {
            // Resolve (or create) the 6 corner nodes for this hex.
            let mut corners: Vec<BoardNodeId> = Vec::with_capacity(6);
            let mut keys: Vec<(i64, i64, i64)> = Vec::with_capacity(6);
            for c in 0..6usize {
                let k = corner_key(q, r, c);
                keys.push(k);
                let node = corner_to_node.entry(k).or_insert_with(|| graph.add_node());
                corners.push(*node);
            }

            // Add the 6 perimeter edges (consecutive corners, wrapping).
            for i in 0..6usize {
                let j = (i + 1) % 6;
                let ek = edge_key(keys[i], keys[j]);
                if edge_set.insert(ek) {
                    graph.add_bidirectional_edge(corners[i], corners[j]);
                }
            }

            // Register the hex face (corners in CW order).
            let face = graph.try_add_face(corners).expect("hex boundary is closed");
            // UC-04-3: register Hex face coords so DistanceMetric::Hex
            // (and any future hex-coord-aware queries) can find them
            // via `graph.face_coord(face)`.
            graph.register_face_coord(face, crate::board::FaceCoord::Hex { q, r });
        }

        Self { name: format!("Hex grid · radius {radius}"), graph }
    }

    /// Equilateral triangle tiling — each cell of an `(rows × cols)`
    /// rectangular grid contains an upward + downward triangle pair.
    #[expect(clippy::expect_used, reason = "invariant: lattice nodes are always connected; try_add_face cannot fail for well-formed triangle faces")]
    pub fn triangle_grid(rows: u32, cols: u32) -> Self {
        // Build over a (rows+1) × (cols+1) lattice; each cell gives 2 triangles.
        let mut graph = BoardGraph::new();
        let nrows = rows + 1;
        let ncols = cols + 1;
        let idx = |c: u32, r: u32| -> usize { (r * ncols + c) as usize };
        let mut node_ids = Vec::with_capacity((nrows * ncols) as usize);
        for _ in 0..(nrows * ncols) {
            node_ids.push(graph.add_node());
        }
        // Horizontal + vertical + one diagonal per cell.
        for r in 0..nrows {
            for c in 0..cols {
                graph.add_bidirectional_edge(node_ids[idx(c, r)], node_ids[idx(c + 1, r)]);
            }
        }
        for r in 0..rows {
            for c in 0..ncols {
                graph.add_bidirectional_edge(node_ids[idx(c, r)], node_ids[idx(c, r + 1)]);
            }
        }
        for r in 0..rows {
            for c in 0..cols {
                graph.add_bidirectional_edge(node_ids[idx(c, r)], node_ids[idx(c + 1, r + 1)]);
            }
        }
        // Faces: per cell, upper-left triangle (a, b, cc) and lower-right (a, cc, d).
        for r in 0..rows {
            for c in 0..cols {
                let a = node_ids[idx(c, r)];
                let b = node_ids[idx(c + 1, r)];
                let cc = node_ids[idx(c + 1, r + 1)];
                let d = node_ids[idx(c, r + 1)];
                graph.try_add_face(vec![a, b, cc]).expect("upper triangle closed");
                graph.try_add_face(vec![a, cc, d]).expect("lower triangle closed");
            }
        }
        Self { name: format!("Triangle grid · {rows}×{cols}"), graph }
    }

    /// Branching path — main trunk of `trunk_length` nodes, plus
    /// branches forking off at specified indices.
    pub fn branching_path(trunk_length: u32, branches: Vec<Branch>) -> Self {
        let mut graph = BoardGraph::new();
        let mut trunk_ids: Vec<BoardNodeId> = Vec::with_capacity(trunk_length as usize);
        let mut prev = None;
        for _ in 0..trunk_length {
            let n = graph.add_node();
            if let Some(p) = prev {
                graph.add_bidirectional_edge(p, n);
            }
            trunk_ids.push(n);
            prev = Some(n);
        }
        for br in &branches {
            let fork = trunk_ids[br.fork_at as usize];
            let mut last = fork;
            for _ in 0..br.length {
                let n = graph.add_node();
                graph.add_bidirectional_edge(last, n);
                last = n;
            }
        }
        Self {
            name: format!("Branching path · trunk {trunk_length}, {} branches", branches.len()),
            graph,
        }
    }
}

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

    #[test]
    fn linear_path_has_expected_counts() {
        let b = BoardBuilder::linear_path(5);
        assert_eq!(b.graph.node_count(), 5);
        assert_eq!(b.graph.edge_count(), 4);
        assert_eq!(b.graph.face_count(), 0);
        assert!(b.name.contains("Linear path"));
    }

    #[test]
    fn square_grid_8x8_has_expected_counts() {
        let b = BoardBuilder::square_grid(8, 8);
        assert_eq!(b.graph.node_count(), 9 * 9);  // (cols+1) × (rows+1)
        assert_eq!(b.graph.edge_count(), 8 * 9 + 9 * 8);  // horizontal + vertical
        assert_eq!(b.graph.face_count(), 8 * 8);  // cells
    }

    #[test]
    fn renamed_overrides_auto_name() {
        let b = BoardBuilder::square_grid(8, 8).renamed("Chess board");
        assert_eq!(b.name, "Chess board");
    }

    #[test]
    fn hex_grid_radius0_is_single_hex() {
        let b = BoardBuilder::hex_grid(0);
        // Single hexagon: 6 corner nodes, 6 edges, 1 face.
        assert_eq!(b.graph.node_count(), 6);
        assert_eq!(b.graph.edge_count(), 6);
        assert_eq!(b.graph.face_count(), 1);
    }

    #[test]
    fn triangle_grid_2x2_has_8_triangles() {
        // 2 rows × 2 cols of triangles = 8 small triangles
        // (each cell is split into upward + downward triangles).
        let b = BoardBuilder::triangle_grid(2, 2);
        assert_eq!(b.graph.face_count(), 8);
    }

    #[test]
    fn hex_grid_radius1_has_7_hexes() {
        let b = BoardBuilder::hex_grid(1);
        assert_eq!(b.graph.face_count(), 7);
    }

    #[test]
    fn hex_grid_radius2_has_19_hexes() {
        let b = BoardBuilder::hex_grid(2);
        assert_eq!(b.graph.face_count(), 19);
    }

    #[test]
    fn hex_grid_radius1_corners_are_deduped() {
        let b = BoardBuilder::hex_grid(1);
        // Without dedup: 7 hexes × 6 corners = 42 nodes.
        // With dedup there are far fewer unique vertices.
        let naive_corners = 7 * 6;
        assert!(
            b.graph.node_count() < naive_corners,
            "expected dedup; got {} corners",
            b.graph.node_count()
        );
    }

    #[test]
    fn uc_04_3_hex_grid_registers_hex_face_coords() {
        use crate::board::FaceCoord;
        let b = BoardBuilder::hex_grid(2);
        // Every face must carry a registered FaceCoord::Hex { q, r }.
        let mut seen: Vec<(i32, i32)> = Vec::new();
        for face in b.graph.faces_iter() {
            match b.graph.face_coord(face) {
                Some(FaceCoord::Hex { q, r }) => seen.push((q, r)),
                other => panic!("face {face:?} expected Hex coord, got {other:?}"),
            }
        }
        // All 19 axial coords inside radius 2 should be present, exactly once.
        assert_eq!(seen.len(), 19);
        seen.sort();
        let mut expected: Vec<(i32, i32)> = Vec::new();
        for q in -2i32..=2 {
            for r in -2i32..=2 {
                if q.abs().max(r.abs()).max((q + r).abs()) <= 2 {
                    expected.push((q, r));
                }
            }
        }
        expected.sort();
        assert_eq!(seen, expected);
    }

    #[test]
    fn hex_grid_radius2_face_boundaries_are_closed() {
        // try_add_face validates closed boundaries during construction.
        // Panic-free completion means all 19 faces are valid.
        let _b = BoardBuilder::hex_grid(2);
    }

    #[test]
    fn branching_path_extends_trunk_with_branches() {
        let b = BoardBuilder::branching_path(
            5,
            vec![Branch { fork_at: 2, length: 3 }],
        );
        // 5-node trunk + 3-node branch attached at trunk index 2.
        assert_eq!(b.graph.node_count(), 5 + 3);
        // 4 trunk edges + 1 connector + 2 branch internal edges.
        assert_eq!(b.graph.edge_count(), 4 + 1 + 2);
    }
}
