///|
/// Real face/edge counts. Exterior loops are reported separately.
pub(all) struct TopologyStats {
  vertices : Int
  edges : Int
  faces : Int
  boundary_edges : Int
  boundary_loops : Int
  components : Int
  euler : Int
} derive(Eq, Debug)

///|
pub extend TopologyStats with Eq::{equal, not_equal}

///|
pub extend TopologyStats with @debug.Debug::{to_repr}

///|
/// Export independent storage. Positions and triangle indices retain their ids.
pub fn Mesh::to_indexed(self : Mesh) -> IndexedMesh {
  let triangles = self.faces.map(fn(h) {
    (
      self.halfedges[h].origin,
      self.halfedges[self.halfedges[h].next].origin,
      self.halfedges[self.halfedges[self.halfedges[h].next].next].origin,
    )
  })
  { vertices: self.vertices.copy(), triangles, }
}

///|
/// Is this undirected edge adjacent to an exterior halfedge?
pub fn Mesh::is_boundary_edge(
  self : Mesh,
  edge : EdgeId,
) -> Bool raise SurfaceError {
  let EdgeId(e) = edge
  ignore(self.edge_vertices(edge))
  self.halfedges[2 * e].face < 0 || self.halfedges[2 * e + 1].face < 0
}

///|
/// Exterior-oriented loops, without a repeated endpoint, ordered by halfedge id.
pub fn Mesh::boundary_loops(self : Mesh) -> Array[Array[Int]] {
  let visited = Array::make(self.halfedges.length(), false)
  let loops : Array[Array[Int]] = []
  for start, he in self.halfedges {
    if he.face >= 0 || visited[start] {
      continue
    }
    let ring : Array[Int] = []
    let mut h = start
    while !visited[h] {
      visited[h] = true
      ring.push(self.halfedges[h].origin)
      h = self.halfedges[h].next
    }
    loops.push(ring)
  }
  loops
}

///|
/// Count connected components and boundary loops; Euler is summed over components.
pub fn Mesh::stats(self : Mesh) -> TopologyStats {
  let visited = Array::make(self.vertices.length(), false)
  let mut components = 0
  for start in 0.. Unit raise SurfaceError {
  let n = self.vertices.length()
  let nh = self.halfedges.length()
  if nh % 2 != 0 ||
    self.outgoing.length() != n ||
    self.degrees.length() != n ||
    self.edge_lookup.length() != nh / 2 {
    raise TopologyInvariant("array counts")
  }
  let degrees = Array::make(n, 0)
  for he in self.halfedges {
    if he.origin < 0 ||
      he.origin >= n ||
      he.next < 0 ||
      he.next >= nh ||
      he.face < -1 ||
      he.face >= self.faces.length() {
      raise TopologyInvariant("halfedge range")
    }
    degrees[he.origin] += 1
  }
  let covered = Array::make(nh, false)
  for f, start in self.faces {
    if start < 0 || start >= nh {
      raise TopologyInvariant("face range")
    }
    let mut h = start
    for _ in 0..<3 {
      if covered[h] || self.halfedges[h].face != f {
        raise TopologyInvariant("face ownership")
      }
      covered[h] = true
      h = self.halfedges[h].next
    }
    if h != start {
      raise TopologyInvariant("triangle cycle")
    }
  }
  for h, he in self.halfedges {
    let other = self.halfedges[opposite(h)]
    if he.origin == other.origin ||
      self.halfedges[he.next].origin != other.origin ||
      (he.face >= 0 && !covered[h]) ||
      (he.face < 0 && self.halfedges[he.next].face >= 0) {
      raise TopologyInvariant("halfedge connectivity")
    }
    if self.edge_lookup.get(edge_key(he.origin, other.origin)) != Some(h / 2) {
      raise TopologyInvariant("edge lookup")
    }
  }
  for v, start in self.outgoing {
    if start < 0 || start >= nh || degrees[v] != self.degrees[v] {
      raise TopologyInvariant("vertex incidence")
    }
    let mut h = start
    for i in 0.. 0 && h == start) {
        raise TopologyInvariant("vertex fan")
      }
      h = self.halfedges[opposite(h)].next
    }
    if h != start {
      raise TopologyInvariant("vertex closure")
    }
  }
  let indexed = self.to_indexed()
  let limits = {
    ..self.limits,
    max_input_faces: Int::max(
      self.limits.max_input_faces,
      self.limits.max_output_faces,
    ),
  }
  ignore(IndexedMesh::new(indexed.vertices, indexed.triangles, limits~))
}