///|
/// A topology problem that can be surfaced before export or clash analysis.
pub(all) enum TopologyIssueKind {
  InvalidIndex
  DegenerateTriangle
  BoundaryEdge
  NonManifoldEdge
  DisconnectedComponent
  EmptyMesh
} derive(Debug, Eq)

///|
pub(all) struct TopologyIssue {
  kind : TopologyIssueKind
  triangle : Int?
  vertex_a : Int?
  vertex_b : Int?
  message : String
} derive(Debug)

///|
pub(all) struct TopologyReport {
  point_count : Int
  triangle_count : Int
  valid_triangle_count : Int
  edge_count : Int
  boundary_edge_count : Int
  non_manifold_edge_count : Int
  degenerate_triangle_count : Int
  invalid_index_count : Int
  component_count : Int
  euler_characteristic : Int
  surface_area : Double
  signed_volume : Double
  issues : Array[TopologyIssue]
} derive(Debug)

///|
pub fn topology_issue_name(kind : TopologyIssueKind) -> String {
  match kind {
    InvalidIndex => "invalid-index"
    DegenerateTriangle => "degenerate-triangle"
    BoundaryEdge => "boundary-edge"
    NonManifoldEdge => "non-manifold-edge"
    DisconnectedComponent => "disconnected-component"
    EmptyMesh => "empty-mesh"
  }
}

///|
fn topology_edge_key(left : Int, right : Int) -> String {
  if left < right {
    left.to_string() + ":" + right.to_string()
  } else {
    right.to_string() + ":" + left.to_string()
  }
}

///|
fn topology_edge(
  edges : Map[String, Int],
  adjacency : Map[Int, Array[Int]],
  left : Int,
  right : Int,
) -> Unit {
  let key = topology_edge_key(left, right)
  edges.update_or_default(key, 1, count => count + 1)
  adjacency.update_or_default(left, [right], values => {
    if !values.contains(right) {
      values.push(right)
    }
    values
  })
  adjacency.update_or_default(right, [left], values => {
    if !values.contains(left) {
      values.push(left)
    }
    values
  })
}

///|
fn topology_issue(
  issues : Array[TopologyIssue],
  kind : TopologyIssueKind,
  triangle : Int?,
  vertex_a : Int?,
  vertex_b : Int?,
  message : String,
) -> Unit {
  issues.push({ kind, triangle, vertex_a, vertex_b, message })
}

///|
fn topology_edge_vertex(key : String, wanted : Int) -> Int? {
  let mut index = 0
  for part in key.split(":") {
    if index == wanted {
      let text = part.to_owned()
      return Some(stream_parse_int(text, 0, text.length()))
    }
    index += 1
  }
  None
}

///|
fn topology_valid_index(index : Int, point_count : Int) -> Bool {
  index >= 0 && index < point_count
}

///|
fn topology_component_count(adjacency : Map[Int, Array[Int]]) -> Int {
  let visited : Map[Int, Bool] = Map([])
  let mut components = 0
  for start, _ in adjacency {
    if visited.contains(start) {
      continue
    }
    components += 1
    let queue : Array[Int] = [start]
    while queue.length() > 0 {
      let current = queue.remove(0)
      if visited.contains(current) {
        continue
      }
      visited[current] = true
      for neighbor in adjacency.get(current).unwrap_or([]) {
        if !visited.contains(neighbor) {
          queue.push(neighbor)
        }
      }
    }
  }
  components
}

///|
fn topology_component_issues(
  issues : Array[TopologyIssue],
  adjacency : Map[Int, Array[Int]],
  component_count : Int,
) -> Unit {
  if component_count > 1 {
    issues.push({
      kind: DisconnectedComponent,
      triangle: None,
      vertex_a: None,
      vertex_b: None,
      message: "mesh contains " +
      component_count.to_string() +
      " disconnected components",
    })
  }
  ignore(adjacency)
}

///|
/// Analyze edge incidence, manifoldness and connected components of a mesh.
pub fn topology_audit(mesh : Mesh) -> TopologyReport {
  let issues : Array[TopologyIssue] = []
  let edges : Map[String, Int] = Map([])
  let adjacency : Map[Int, Array[Int]] = Map([])
  let mut valid_triangles = 0
  let mut degenerate = 0
  let mut invalid = 0
  for index in 0.. 2 {
      non_manifold += 1
      topology_issue(
        issues,
        NonManifoldEdge,
        None,
        topology_edge_vertex(key, 0),
        topology_edge_vertex(key, 1),
        "edge is referenced by more than two triangles",
      )
    }
  }
  let components = topology_component_count(adjacency)
  topology_component_issues(issues, adjacency, components)
  if mesh.points.length() == 0 || mesh.triangles.length() == 0 {
    issues.push({
      kind: EmptyMesh,
      triangle: None,
      vertex_a: None,
      vertex_b: None,
      message: "mesh contains no renderable triangles",
    })
  }
  {
    point_count: mesh.points.length(),
    triangle_count: mesh.triangles.length(),
    valid_triangle_count: valid_triangles,
    edge_count: edges.length(),
    boundary_edge_count: boundary,
    non_manifold_edge_count: non_manifold,
    degenerate_triangle_count: degenerate,
    invalid_index_count: invalid,
    component_count: components,
    euler_characteristic: mesh.points.length() -
    edges.length() +
    valid_triangles,
    surface_area: mesh_area(mesh),
    signed_volume: mesh_volume(mesh),
    issues,
  }
}

///|
pub fn TopologyReport::is_manifold(self : TopologyReport) -> Bool {
  self.non_manifold_edge_count == 0 && self.invalid_index_count == 0
}

///|
pub fn TopologyReport::is_closed(self : TopologyReport) -> Bool {
  self.boundary_edge_count == 0 && self.valid_triangle_count > 0
}

///|
pub fn TopologyReport::is_renderable(self : TopologyReport) -> Bool {
  self.valid_triangle_count > 0 && self.invalid_index_count == 0
}

///|
pub fn TopologyReport::is_solid_candidate(self : TopologyReport) -> Bool {
  self.is_manifold() &&
  self.is_closed() &&
  self.component_count == 1 &&
  self.signed_volume().abs() > 0.000000001
}

///|
pub fn TopologyReport::issue_count(self : TopologyReport) -> Int {
  self.issues.length()
}

///|
pub fn TopologyReport::issues_of(
  self : TopologyReport,
  kind : TopologyIssueKind,
) -> Array[TopologyIssue] {
  self.issues.filter(issue => issue.kind == kind)
}

///|
pub fn TopologyReport::signed_volume(self : TopologyReport) -> Double {
  self.signed_volume
}

///|
pub fn TopologyReport::headline(self : TopologyReport) -> String {
  "points=" +
  self.point_count.to_string() +
  " triangles=" +
  self.triangle_count.to_string() +
  " edges=" +
  self.edge_count.to_string() +
  " components=" +
  self.component_count.to_string() +
  " boundary=" +
  self.boundary_edge_count.to_string() +
  " non_manifold=" +
  self.non_manifold_edge_count.to_string()
}

///|
pub fn TopologyIssue::headline(self : TopologyIssue) -> String {
  let triangle = match self.triangle {
    Some(value) => " triangle=" + value.to_string()
    None => ""
  }
  "[" + topology_issue_name(self.kind) + "]" + triangle + " " + self.message
}

///|
pub fn TopologyIssue::to_json(self : TopologyIssue) -> String {
  let triangle = match self.triangle {
    Some(value) => value.to_string()
    None => "null"
  }
  let left = match self.vertex_a {
    Some(value) => value.to_string()
    None => "null"
  }
  let right = match self.vertex_b {
    Some(value) => value.to_string()
    None => "null"
  }
  "{\"kind\":\"" +
  topology_issue_name(self.kind) +
  "\",\"triangle\":" +
  triangle +
  ",\"vertex_a\":" +
  left +
  ",\"vertex_b\":" +
  right +
  ",\"message\":\"" +
  self.message +
  "\"}"
}

///|
pub fn TopologyReport::to_json(self : TopologyReport) -> String {
  "{\"point_count\":" +
  self.point_count.to_string() +
  ",\"triangle_count\":" +
  self.triangle_count.to_string() +
  ",\"valid_triangle_count\":" +
  self.valid_triangle_count.to_string() +
  ",\"edge_count\":" +
  self.edge_count.to_string() +
  ",\"boundary_edge_count\":" +
  self.boundary_edge_count.to_string() +
  ",\"non_manifold_edge_count\":" +
  self.non_manifold_edge_count.to_string() +
  ",\"component_count\":" +
  self.component_count.to_string() +
  ",\"euler_characteristic\":" +
  self.euler_characteristic.to_string() +
  ",\"surface_area\":" +
  self.surface_area.to_string() +
  ",\"signed_volume\":" +
  self.signed_volume.to_string() +
  ",\"issues\":[" +
  self.issues.map(issue => issue.to_json()).join(",") +
  "]}"
}

///|
pub fn TopologyReport::to_markdown(self : TopologyReport) -> String {
  let builder = StringBuilder()
  builder <+ "# Mesh topology audit\n\n"
  builder <+ "- \{self.headline()}\n"
  builder <+ "- Renderable: \{self.is_renderable()}\n"
  builder <+ "- Manifold: \{self.is_manifold()}\n"
  builder <+ "- Closed: \{self.is_closed()}\n"
  builder <+ "- Solid candidate: \{self.is_solid_candidate()}\n"
  builder <+ "- Euler characteristic: \{self.euler_characteristic}\n"
  builder <+ "- Surface area: \{self.surface_area}\n"
  builder <+ "- Signed volume: \{self.signed_volume}\n\n"
  builder <+ "| Kind | Triangle | Message |\n| --- | ---: | --- |\n"
  for issue in self.issues {
    let triangle = match issue.triangle {
      Some(value) => value.to_string()
      None => ""
    }
    builder <+
      "|\{topology_issue_name(issue.kind)}|\{triangle}|\{issue.message}|\n"
  }
  builder.to_string()
}

///|
pub fn topology_issue_histogram(report : TopologyReport) -> Map[String, Int] {
  let result : Map[String, Int] = Map([])
  for issue in report.issues {
    result.update_or_default(topology_issue_name(issue.kind), 1, count => {
      count + 1
    })
  }
  result
}

///|
pub fn topology_reports_for_meshes(
  meshes : Array[Mesh],
) -> Array[TopologyReport] {
  meshes.map(mesh => topology_audit(mesh))
}

///|
pub fn topology_total_surface_area(reports : Array[TopologyReport]) -> Double {
  reports.fold(init=0.0, (sum, report) => sum + report.surface_area)
}

///|
pub fn topology_total_volume(reports : Array[TopologyReport]) -> Double {
  reports.fold(init=0.0, (sum, report) => sum + report.signed_volume.abs())
}

///|
pub fn topology_unhealthy(
  reports : Array[TopologyReport],
) -> Array[TopologyReport] {
  reports.filter(report => !report.is_renderable() || !report.is_manifold())
}

///|
pub fn topology_summary(reports : Array[TopologyReport]) -> Array[String] {
  [
    "meshes=" + reports.length().to_string(),
    "unhealthy=" + topology_unhealthy(reports).length().to_string(),
    "surface_area=" + topology_total_surface_area(reports).to_string(),
    "volume=" + topology_total_volume(reports).to_string(),
  ]
}