///|
fn replace_nested_graph(
  graph : Graph,
  steps : Array[GraphStep],
  position : Int32,
  replacement : Graph,
) -> Graph raise GraphError {
  if position == steps.length() {
    return replacement
  }
  match steps[position] {
    NodeGraph(index) => {
      bounded_index(index, graph.nodes.length(), "replace.node")
      let node = graph.nodes[index]
      let nested = node.graph.unwrap_or_else(() => {
        raise InvalidGraph(path="replace.node", reason="missing nested graph")
      })
      let updated = replace_nested_graph(
        nested,
        steps,
        position + 1,
        replacement,
      )
      let nodes = graph.nodes.copy()
      nodes[index] = { ..node, graph: Some(updated), }
      { ..graph, nodes, }
    }
    EdgeGraph(index) => {
      bounded_index(index, graph.edges.length(), "replace.edge")
      let edge = graph.edges[index]
      let nested = edge.graph.unwrap_or_else(() => {
        raise InvalidGraph(path="replace.edge", reason="missing nested graph")
      })
      let updated = replace_nested_graph(
        nested,
        steps,
        position + 1,
        replacement,
      )
      let edges = graph.edges.copy()
      edges[index] = { ..edge, graph: Some(updated), }
      { ..graph, edges, }
    }
    HyperedgeGraph(index) => {
      bounded_index(index, graph.hyperedges.length(), "replace.hyperedge")
      let hyperedge = graph.hyperedges[index]
      let nested = hyperedge.graph.unwrap_or_else(() => {
        raise InvalidGraph(
          path="replace.hyperedge",
          reason="missing nested graph",
        )
      })
      let updated = replace_nested_graph(
        nested,
        steps,
        position + 1,
        replacement,
      )
      let hyperedges = graph.hyperedges.copy()
      hyperedges[index] = { ..hyperedge, graph: Some(updated), }
      { ..graph, hyperedges, }
    }
  }
}

///|
/// Replace a graph atomically. Ancestor references and global IDs are revalidated.
pub fn Document::replace_graph(
  self : Document,
  address : GraphAddress,
  replacement : Graph,
  limits~ : Limits = Limits::default(),
) -> Document raise GraphError {
  bounded_index(address.root, self.graphs.length(), "replace.root")
  check_bound("graph_address_depth", address.steps.length(), limits.xml_depth)
  let graphs = self.graphs.copy()
  graphs[address.root] = replace_nested_graph(
    graphs[address.root],
    address.steps,
    0,
    replacement,
  )
  Document::new(
    self.keys,
    graphs,
    data=self.data,
    description?=self.description,
    extra=self.extra,
    namespaces=self.namespaces,
    limits~,
  )
}

///|
/// Add a top-level graph without merging ID scopes or changing existing addresses.
pub fn Document::append_graph(
  self : Document,
  graph : Graph,
  limits~ : Limits = Limits::default(),
) -> Document raise GraphError {
  let graphs = self.graphs.copy()
  graphs.push(graph)
  Document::new(
    self.keys,
    graphs,
    data=self.data,
    description?=self.description,
    extra=self.extra,
    namespaces=self.namespaces,
    limits~,
  )
}

///|
/// Keep selected top-level graphs, retaining the shared key catalog and document attributes.
pub fn Document::select_graphs(
  self : Document,
  roots : Array[Int32],
  limits~ : Limits = Limits::default(),
) -> Document raise GraphError {
  let graphs = []
  let seen : Map[Int32, Bool] = Map([])
  for root in roots {
    bounded_index(root, self.graphs.length(), "select.root")
    require(!seen.contains(root), "select.root", "duplicate root selection")
    seen[root] = true
    graphs.push(self.graphs[root])
  }
  Document::new(
    self.keys,
    graphs,
    data=self.data,
    description?=self.description,
    extra=self.extra,
    namespaces=self.namespaces,
    limits~,
  )
}