///|
pub(all) struct Statistics {
  nonterminal_nodes : Int32
  edges : Int32
  height : Int32
  support : Array[Int32]
  nodes_per_variable : Array[Int32]
}

///|
/// Structural statistics do not enumerate members or require a representable count.
pub fn Family::statistics(self : Family) -> Statistics {
  let depths = [0, 0]
  let widths = Array::make(self.universe, 0)
  for id in 2.. 0 {
      support.push(variable)
    }
  }
  let nonterminal_nodes = self.nodes.length() - 2
  {
    nonterminal_nodes,
    edges: 2 * nonterminal_nodes,
    height: depths[self.root],
    support,
    nodes_per_variable: widths,
  }
}

///|
/// Diagnostic DOT. Integer-only labels are safe to render without escaping user strings.
/// Refuses oversized output instead of emitting an unlabeled partial graph.
pub fn Family::to_dot(
  self : Family,
  max_nodes~ : Int32 = 5000,
) -> String raise ZddError {
  if max_nodes < 0 || max_nodes > 100000 {
    raise Invalid("DOT budget must be 0..100000")
  }
  if self.nodes.length() - 2 > max_nodes {
    raise BudgetExceeded
  }
  let output = StringBuilder()
  output.write_string("digraph ZDD {\n  rankdir=TB;\n  entry [shape=point];\n")
  output.write_string("  entry -> n\{self.root};\n")
  output.write_string(
    "  n0 [shape=box,label=\"empty family\"];\n  n1 [shape=box,label=\"{empty set}\"];\n",
  )
  for id in 2.. n\{node.low} [style=dashed,label=\"0\"];\n",
    )
    output.write_string("  n\{id} -> n\{node.high} [label=\"1\"];\n")
  }
  output.write_string("}\n")
  output.to_string()
}