///|
pub fn default_mesh_pipeline_options() -> MeshPipelineOptions {
  {
    weld_tolerance: 0.000001,
    degenerate_tolerance: 0.000000001,
    maximum_triangles: 0,
    recenter: false,
    scale: 1.0,
    flip_winding: false,
    generate_normals: true,
    compact_vertices: true,
  }
}

///|
pub fn MeshPipelineOptions::with_weld_tolerance(
  self : MeshPipelineOptions,
  value : Double,
) -> MeshPipelineOptions {
  { ..self, weld_tolerance: if value < 0.0 { 0.0 } else { value } }
}

///|
pub fn MeshPipelineOptions::with_degenerate_tolerance(
  self : MeshPipelineOptions,
  value : Double,
) -> MeshPipelineOptions {
  { ..self, degenerate_tolerance: if value < 0.0 { 0.0 } else { value } }
}

///|
pub fn MeshPipelineOptions::with_maximum_triangles(
  self : MeshPipelineOptions,
  value : Int,
) -> MeshPipelineOptions {
  { ..self, maximum_triangles: if value < 0 { 0 } else { value } }
}

///|
pub fn MeshPipelineOptions::recentered(
  self : MeshPipelineOptions,
  value : Bool,
) -> MeshPipelineOptions {
  { ..self, recenter: value }
}

///|
pub fn MeshPipelineOptions::scaled(
  self : MeshPipelineOptions,
  value : Double,
) -> MeshPipelineOptions {
  { ..self, scale: if value == 0.0 { 1.0 } else { value } }
}

///|
pub fn MeshPipelineOptions::flipped(
  self : MeshPipelineOptions,
  value : Bool,
) -> MeshPipelineOptions {
  { ..self, flip_winding: value }
}

///|
pub fn MeshPipelineOptions::with_normals(
  self : MeshPipelineOptions,
  value : Bool,
) -> MeshPipelineOptions {
  { ..self, generate_normals: value }
}

///|
pub fn MeshPipelineOptions::compacting(
  self : MeshPipelineOptions,
  value : Bool,
) -> MeshPipelineOptions {
  { ..self, compact_vertices: value }
}

///|
fn mesh_zero_vector() -> Vector3 {
  { x: 0.0, y: 0.0, z: 0.0 }
}

///|
fn mesh_valid_index(mesh : Mesh, index : Int) -> Bool {
  index >= 0 && index < mesh.points.length()
}

///|
fn mesh_triangle_valid(mesh : Mesh, triangle : (Int, Int, Int)) -> Bool {
  let (a, b, c) = triangle
  mesh_valid_index(mesh, a) &&
  mesh_valid_index(mesh, b) &&
  mesh_valid_index(mesh, c) &&
  a != b &&
  b != c &&
  a != c
}

///|
/// Check index safety, duplicate vertices and geometric degeneracy before export.
pub fn validate_mesh(
  mesh : Mesh,
  tolerance? : Double = 0.000000001,
) -> MeshValidation {
  let issues : Array[MeshIssue] = []
  let referenced : Map[Int, Bool] = Map([])
  let mut degenerate = 0
  for point_index, _ in mesh.points {
    ignore(point_index)
  }
  for triangle_index, triangle in mesh.triangles {
    let (a, b, c) = triangle
    if !mesh_triangle_valid(mesh, triangle) {
      issues.push({
        code: "MESH_INDEX",
        message: "triangle references an invalid or repeated point",
        point_index: None,
        triangle_index: Some(triangle_index),
      })
      continue
    }
    referenced[a] = true
    referenced[b] = true
    referenced[c] = true
    if triangle_area(mesh, triangle) <= tolerance {
      degenerate += 1
      issues.push({
        code: "MESH_DEGENERATE",
        message: "triangle area is below tolerance",
        point_index: None,
        triangle_index: Some(triangle_index),
      })
    }
  }
  let mut unreferenced = 0
  for index in 0.. Mesh {
  {
    points: mesh.points,
    triangles: mesh.triangles.map(triangle => {
      let (a, b, c) = triangle
      (a, c, b)
    }),
  }
}

///|
fn mesh_transform_points(mesh : Mesh, recenter : Bool, scale : Double) -> Mesh {
  let bounds = bounds_of_mesh(mesh)
  let center = if recenter {
    bounds.center()
  } else {
    { x: 0.0, y: 0.0, z: 0.0 }
  }
  let safe_scale = if scale == 0.0 { 1.0 } else { scale }
  {
    points: mesh.points.map(point => {
      x: (point.x - center.x) * safe_scale,
      y: (point.y - center.y) * safe_scale,
      z: (point.z - center.z) * safe_scale,
    }),
    triangles: mesh.triangles,
  }
}

///|
fn mesh_compact_vertices(mesh : Mesh) -> Mesh {
  let remap : Map[Int, Int] = Map([])
  let points : Array[Point3] = []
  for triangle in mesh.triangles {
    let (a, b, c) = triangle
    for old_index in [a, b, c] {
      if !remap.contains(old_index) {
        match mesh.points.get(old_index) {
          Some(point) => {
            remap[old_index] = points.length()
            points.push(point)
          }
          None => ()
        }
      }
    }
  }
  let triangles : Array[(Int, Int, Int)] = []
  for triangle in mesh.triangles {
    let (a, b, c) = triangle
    match (remap.get(a), remap.get(b), remap.get(c)) {
      (Some(left), Some(middle), Some(right)) if left != middle &&
        middle != right &&
        left != right => triangles.push((left, middle, right))
      _ => ()
    }
  }
  { points, triangles }
}

///|
fn mesh_remove_duplicate_triangles(mesh : Mesh) -> Mesh {
  let seen : Map[String, Bool] = Map([])
  let triangles : Array[(Int, Int, Int)] = []
  for triangle in mesh.triangles {
    let (a, b, c) = triangle
    let mut first = a
    let mut second = b
    let mut third = c
    if second < first {
      let swap = first
      first = second
      second = swap
    }
    if third < second {
      let swap = second
      second = third
      third = swap
    }
    if second < first {
      let swap = first
      first = second
      second = swap
    }
    let key = first.to_string() +
      ":" +
      second.to_string() +
      ":" +
      third.to_string()
    if !seen.contains(key) {
      seen[key] = true
      triangles.push(triangle)
    }
  }
  { points: mesh.points, triangles }
}

///|
fn mesh_pipeline_normals(mesh : Mesh) -> Array[Vector3] {
  let normals : Array[Vector3] = []
  for _ in mesh.points {
    normals.push(mesh_zero_vector())
  }
  for triangle in mesh.triangles {
    let (a, b, c) = triangle
    if mesh_triangle_valid(mesh, triangle) {
      let normal = triangle_normal(mesh, triangle)
      for index in [a, b, c] {
        match normals.get(index) {
          Some(value) => normals[index] = value.add(normal)
          None => ()
        }
      }
    }
  }
  normals.map(normal => normal.normalized())
}

///|
fn mesh_pipeline_stats(
  input : Mesh,
  output : Mesh,
  valid : Bool,
) -> MeshPipelineStats {
  let analysis = mesh_stats(output)
  {
    input_points: input.points.length(),
    input_triangles: input.triangles.length(),
    output_points: output.points.length(),
    output_triangles: output.triangles.length(),
    removed_points: if input.points.length() > output.points.length() {
      input.points.length() - output.points.length()
    } else {
      0
    },
    removed_triangles: if input.triangles.length() > output.triangles.length() {
      input.triangles.length() - output.triangles.length()
    } else {
      0
    },
    surface_area: analysis.surface_area,
    volume: analysis.volume(),
    bounds: bounds_of_mesh(output),
    valid,
  }
}

///|
/// Apply deterministic cleanup, welding, orientation and optional LOD limits.
pub fn process_mesh(
  mesh : Mesh,
  options? : MeshPipelineOptions = default_mesh_pipeline_options(),
) -> MeshPipelineResult {
  let initial = mesh
  let valid_input = validate_mesh(
    initial,
    tolerance=options.degenerate_tolerance,
  )
  let cleaned = remove_degenerate_triangles(
    initial,
    options.degenerate_tolerance,
  )
  let welded = if options.weld_tolerance > 0.0 {
    weld_mesh(cleaned, options.weld_tolerance)
  } else {
    cleaned
  }
  let unique = mesh_remove_duplicate_triangles(welded)
  let limited = limit_mesh_triangles(unique, options.maximum_triangles)
  let oriented = if options.flip_winding { mesh_flip(limited) } else { limited }
  let transformed = mesh_transform_points(
    oriented,
    options.recenter,
    options.scale,
  )
  let compacted = if options.compact_vertices {
    mesh_compact_vertices(transformed)
  } else {
    transformed
  }
  let validation = validate_mesh(
    compacted,
    tolerance=options.degenerate_tolerance,
  )
  {
    mesh: compacted,
    normals: if options.generate_normals {
      mesh_pipeline_normals(compacted)
    } else {
      []
    },
    stats: mesh_pipeline_stats(
      initial,
      compacted,
      valid_input.valid && validation.valid,
    ),
  }
}

///|
pub fn mesh_pipeline(
  mesh : Mesh,
  options? : MeshPipelineOptions = default_mesh_pipeline_options(),
) -> MeshPipelineResult {
  process_mesh(mesh, options~)
}

///|
pub fn mesh_for_product_pipeline(
  index : ModelIndex,
  product : Product,
  options? : MeshPipelineOptions = default_mesh_pipeline_options(),
) -> MeshPipelineResult raise ParseError {
  process_mesh(mesh_for_product_advanced(index, product), options~)
}

///|
pub fn MeshPipelineResult::is_valid(self : MeshPipelineResult) -> Bool {
  self.stats.valid
}

///|
pub fn MeshPipelineResult::point_count(self : MeshPipelineResult) -> Int {
  self.mesh.points.length()
}

///|
pub fn MeshPipelineResult::triangle_count(self : MeshPipelineResult) -> Int {
  self.mesh.triangles.length()
}

///|
pub fn MeshPipelineResult::bounds(self : MeshPipelineResult) -> BoundingBox {
  self.stats.bounds
}

///|
pub fn MeshPipelineResult::headline(self : MeshPipelineResult) -> String {
  "points=" +
  self.mesh.points.length().to_string() +
  " triangles=" +
  self.mesh.triangles.length().to_string() +
  " area=" +
  self.stats.surface_area.to_string() +
  " volume=" +
  self.stats.volume.to_string() +
  " valid=" +
  self.stats.valid.to_string()
}

///|
pub fn MeshPipelineStats::headline(self : MeshPipelineStats) -> String {
  "input=" +
  self.input_triangles.to_string() +
  " output=" +
  self.output_triangles.to_string() +
  " removed=" +
  self.removed_triangles.to_string() +
  " area=" +
  self.surface_area.to_string() +
  " volume=" +
  self.volume.to_string()
}

///|
pub fn MeshValidation::headline(self : MeshValidation) -> String {
  "valid=" +
  self.valid.to_string() +
  " issues=" +
  self.issues.length().to_string() +
  " referenced_points=" +
  self.referenced_point_count.to_string() +
  " unreferenced_points=" +
  self.unreferenced_point_count.to_string()
}

///|
pub fn MeshIssue::headline(self : MeshIssue) -> String {
  self.code + ": " + self.message
}

///|
pub fn mesh_validation(mesh : Mesh) -> MeshValidation {
  validate_mesh(mesh)
}

///|
pub fn mesh_is_renderable(mesh : Mesh) -> Bool {
  let report = validate_mesh(mesh)
  report.valid && mesh.points.length() > 0 && mesh.triangles.length() > 0
}

///|
pub fn mesh_normals(mesh : Mesh) -> Array[Vector3] {
  mesh_pipeline_normals(mesh)
}

///|
pub fn mesh_transform_scale(mesh : Mesh, scale : Double) -> Mesh {
  mesh_transform_points(mesh, false, scale)
}

///|
pub fn mesh_centered(mesh : Mesh) -> Mesh {
  mesh_transform_points(mesh, true, 1.0)
}

///|
pub fn mesh_flip_winding(mesh : Mesh) -> Mesh {
  mesh_flip(mesh)
}

///|
pub fn mesh_compact(mesh : Mesh) -> Mesh {
  mesh_compact_vertices(mesh)
}

///|
pub fn mesh_unique_triangles(mesh : Mesh) -> Mesh {
  mesh_remove_duplicate_triangles(mesh)
}

///|
fn mesh_lod_cell_size(mesh : Mesh, target_triangles : Int) -> Double {
  let safe_target = if target_triangles < 1 { 1 } else { target_triangles }
  let bounds = bounds_of_mesh(mesh)
  let volume = bounds.volume()
  if volume <= 0.0 || mesh.triangles.length() <= safe_target {
    0.0
  } else {
    let ratio = mesh.triangles.length().to_double() / safe_target.to_double()
    let base = (volume / ratio).abs()
    mesh_cube_root(base)
  }
}

///|
fn mesh_cube_root(value : Double) -> Double {
  if value <= 0.0 {
    return 0.0
  }
  let mut guess = if value > 1.0 { value / 3.0 } else { 1.0 }
  for _ in 0..<16 {
    guess = (2.0 * guess + value / (guess * guess)) / 3.0
  }
  guess
}

///|
/// Generate a coarse level of detail by welding vertices into a deterministic grid.
pub fn mesh_lod(mesh : Mesh, target_triangles : Int) -> Mesh {
  let target = if target_triangles < 1 { 1 } else { target_triangles }
  if mesh.triangles.length() <= target {
    return mesh
  }
  let cell = mesh_lod_cell_size(mesh, target)
  let welded = weld_mesh(mesh, if cell <= 0.0 { 0.000001 } else { cell })
  let compacted = mesh_compact_vertices(mesh_remove_duplicate_triangles(welded))
  limit_mesh_triangles(compacted, target)
}

///|
pub fn mesh_lod_result(
  mesh : Mesh,
  target_triangles : Int,
  options? : MeshPipelineOptions = default_mesh_pipeline_options(),
) -> MeshPipelineResult {
  process_mesh(mesh_lod(mesh, target_triangles), options~)
}

///|
pub fn mesh_area(mesh : Mesh) -> Double {
  mesh_stats(mesh).surface_area
}

///|
pub fn mesh_volume(mesh : Mesh) -> Double {
  mesh_stats(mesh).volume().abs()
}

///|
pub fn mesh_bounds_json(mesh : Mesh) -> String {
  let bounds = bounds_of_mesh(mesh)
  "{\"min\":[" +
  bounds.min.x.to_string() +
  "," +
  bounds.min.y.to_string() +
  "," +
  bounds.min.z.to_string() +
  "],\"max\":[" +
  bounds.max.x.to_string() +
  "," +
  bounds.max.y.to_string() +
  "," +
  bounds.max.z.to_string() +
  "],\"empty\":" +
  bounds.empty.to_string() +
  "}"
}

///|
pub fn MeshPipelineResult::to_json(self : MeshPipelineResult) -> String {
  "{\"points\":" +
  self.mesh.points.length().to_string() +
  ",\"triangles\":" +
  self.mesh.triangles.length().to_string() +
  ",\"normals\":" +
  self.normals.length().to_string() +
  ",\"surface_area\":" +
  self.stats.surface_area.to_string() +
  ",\"volume\":" +
  self.stats.volume.to_string() +
  ",\"valid\":" +
  self.stats.valid.to_string() +
  ",\"bounds\":" +
  mesh_bounds_json(self.mesh) +
  "}"
}

///|
pub fn MeshPipelineResult::to_obj(self : MeshPipelineResult) -> String {
  let builder = StringBuilder()
  builder <+ "# moonbit-ifc mesh pipeline\n"
  for point in self.mesh.points {
    builder <+ "v \{point.x} \{point.y} \{point.z}\n"
  }
  for normal in self.normals {
    builder <+ "vn \{normal.x} \{normal.y} \{normal.z}\n"
  }
  for triangle in self.mesh.triangles {
    let (a, b, c) = triangle
    builder <+ "f \{a + 1} \{b + 1} \{c + 1}\n"
  }
  builder.to_string()
}

///|
pub fn mesh_pipeline_summary(result : MeshPipelineResult) -> Array[String] {
  [
    result.headline(),
    result.stats.headline(),
    result.bounds().volume().to_string(),
  ]
}