///|
/// # Breadth-First Search (BFS)
///
/// BFS explores a graph level by level — first all vertices at distance 1
/// from the start, then distance 2, etc. This gives the shortest-path
/// property: the first time BFS visits a vertex, it has found the shortest
/// path (in number of edges) from the start.
///
/// ## Implementation: array-based queue
///
/// Uses an `Array` as a FIFO queue with a `head` index pointer.
/// Dequeue is O(1) (just increment `head`). The array grows but is never
/// compacted — acceptable since BFS visits each vertex at most once,
/// so the queue never exceeds V entries.
///
/// ## BFS vs DFS
///
/// - BFS gives shortest paths and level-order traversal
/// - DFS gives deeper exploration first, useful for topological sort
/// - Both are O(V + E) time and O(V) space
/// - BFS uses a queue (FIFO), DFS uses a stack (LIFO)
///
/// BFS is generally preferred for "how far is X from Y" questions,
/// while DFS is preferred for "is X reachable from Y" and structural
/// decomposition (SCC, topological sort).

///|
/// BFS fold over vertices reachable from `start`.
///
/// Same interface as `dfs_fold` but visits vertices in breadth-first
/// (level) order. Vertices at distance d from `start` are all visited
/// before any vertex at distance d+1.
///
/// Time: O(V + E) where V and E are the reachable vertices and edges.
pub fn[G : Successors, Acc] bfs_fold(
  graph : G,
  start : Int,
  init : Acc,
  f : (Acc, Int) -> (Acc, Bool),
) -> Acc {
  graph.bfs_fold(start, init, f)
}

///|
/// Multi-source BFS fold over vertices reachable from any vertex in `starts`.
///
/// Seeds the BFS queue with all start vertices instead of one, enabling
/// frontier-based traversal. This is the natural fit for problems where
/// traversal begins from a SET of vertices rather than a single root —
/// for example, CRDT event-graph-walkers that walk forward from a set
/// of frontier LVs, or multi-root dependency resolution.
///
/// ## Why multi-source instead of looping single-source?
///
/// Calling `bfs_fold` in a loop for each start would revisit vertices
/// reachable from multiple starts. Multi-source BFS visits each vertex
/// at most once across ALL starts, giving true O(V+E) for the union.
///
/// ## Seeding
///
/// - Invalid starts (not in the graph) are silently skipped via `has_vertex`
/// - Duplicate starts are deduplicated before traversal begins
/// - Starts are enqueued in input order — `starts[0]` is visited first
///
/// Time: O(V + E) where V and E are the reachable vertices and edges.
/// Seed validation calls `has_vertex` — override it for O(1) on custom types.
pub fn[G : DirectedGraph, Acc] bfs_fold_multi(
  graph : G,
  starts : Array[Int],
  init : Acc,
  f : (Acc, Int) -> (Acc, Bool),
) -> Acc {
  let visited : Map[Int, Bool] = Map([])
  let queue : Array[Int] = []
  for s in starts {
    if !visited.contains(s) && G::has_vertex(graph, s) {
      visited[s] = true
      queue.push(s)
    }
  }
  let mut acc = init
  let mut head = 0
  while head < queue.length() {
    let v = queue[head]
    head = head + 1
    let result = f(acc, v)
    acc = result.0
    if !result.1 {
      break
    }
    G::each_successor(graph, v, fn(w) {
      if !visited.contains(w) {
        visited[w] = true
        queue.push(w)
      }
    })
  }
  acc
}