///|
pub(all) struct BoundaryGroup {
  tag : Int
  facets : Int
} derive(ToJson)

///|
pub extend BoundaryGroup with ToJson::{to_json}

///|
/// A label inventory, not a finite-element boundary-condition certificate.
/// Physical tags are scoped to boundary_dimension. Volume labels never count.
pub(all) struct BoundaryCoverage {
  boundary_dimension : Int
  facets : Int
  groups : Array[BoundaryGroup]
  unlabeled : Array[BoundarySource]
  multiple : Array[BoundarySource]
  missing_groups : Array[Int]
} derive(ToJson)

///|
pub extend BoundaryCoverage with ToJson::{to_json}

///|
/// required_groups is a caller-supplied contract, not inferred from group names.
/// Multiple labels can be intentional; consumers choose whether to reject them.
/// Inspection does not authorize the separate lossy derived-mesh export.
pub fn Boundary::coverage(
  self : Boundary,
  required_groups? : Array[Int] = [],
) -> BoundaryCoverage raise {
  if required_groups.length() > 100000 {
    bad("required_groups exceeds 100000")
  }
  let required : Map[Int, Bool] = Map([])
  for tag in required_groups {
    if tag <= 0 || required.contains(tag) {
      bad("required_groups must contain distinct positive tags")
    }
    required[tag] = true
  }
  let counts : Map[Int, Int] = Map([])
  let unlabeled : Array[BoundarySource] = []
  let multiple : Array[BoundarySource] = []
  for i, element in self.output.es {
    if element.physical.is_empty() {
      unlabeled.push(self.summary.facets[i])
    }
    if element.physical.length() > 1 {
      multiple.push(self.summary.facets[i])
    }
    for tag in element.physical {
      counts[tag] = counts.get(tag).unwrap_or(0) + 1
    }
  }
  let tags = counts.keys().to_array()
  tags.sort()
  let missing = required_groups.filter(tag => !counts.contains(tag))
  missing.sort()
  {
    boundary_dimension: self.summary.boundary_dimension,
    facets: self.output.es.length(),
    groups: tags.map(tag => { tag, facets: counts[tag], }),
    unlabeled,
    multiple,
    missing_groups: missing,
  }
}