// 网格修复。
//
// 校验负责指出问题,这里负责修。修完的网格会重新跑一遍校验,报告里
// 能直接看到「修之前 3 条边界边,修之后 0 条」。
//
// 四件事,从简单到麻烦:
//   1. 删退化面        三个点粘在一起、没有面积的面,删掉
//   2. 删重复面        同一个三角形出现两次,删掉后面的
//   3. 补洞            校验简单平面边界环后用耳切三角化补面
//   4. 重算法线        按顶点顺序重算,把文件里写错的法线覆盖掉
//
// 补洞是最有价值的一项,也是最容易出错的:新补的三角形绕向必须和原来
// 的面一致,否则补完还是水密不了(甚至更糟)。

///|
/// 修复选项。
pub(all) struct RepairOptions {
  /// 顶点焊接容差。判断「两个点是不是同一个」用的,和校验用同一个值
  tolerance : Double
  /// 删除退化面
  drop_degenerate : Bool
  /// 删除重复面
  drop_duplicates : Bool
  /// 补洞
  fill_holes : Bool
  /// 重算法线
  recompute_normals : Bool
  /// 统一相邻绕向,并将包围盒互不重叠的独立闭合分量调整为正体积方向。
  unify_winding : Bool
}

///|
/// 默认修复:什么都做。
pub fn RepairOptions::default() -> RepairOptions {
  {
    tolerance: default_weld_tolerance(),
    drop_degenerate: true,
    drop_duplicates: true,
    fill_holes: true,
    recompute_normals: true,
    unify_winding: true,
  }
}

///|
/// 只修法线,不动拓扑。适合「模型本来是好的,只是法线写错了」的情况。
pub fn RepairOptions::normals_only() -> RepairOptions {
  {
    tolerance: default_weld_tolerance(),
    drop_degenerate: false,
    drop_duplicates: false,
    fill_holes: false,
    recompute_normals: true,
    // 「只修法线」不该动拓扑,绕向也属于拓扑,一起关掉
    unify_winding: false,
  }
}

///|
/// 修复结果。除了修好的网格,还报告每一项具体做了多少。
///
/// 报告做了什么很重要:如果跑了修复但边界边从 3 变成 3,用户需要知道
/// 是「补洞没生效」还是「补了三处又破了三处」。光给一个网格说不清楚。
pub(all) struct RepairResult {
  /// 修好的网格
  mesh : Mesh
  /// 删掉的退化面数量
  removed_degenerate : Int32
  /// 删掉的重复面数量
  removed_duplicates : Int32
  /// 补上的洞的数量(按边界环计)
  filled_holes : Int32
  /// 为了补洞新增的三角形数量
  added_triangles : Int32
  /// 重算过法线的三角形数量
  recomputed_normals : Int32
  /// 相邻绕向一致性调整的面数,不含分量整体翻转。
  flipped_faces : Int32
  /// Isolated closed components whose entire direction was reversed.
  reoriented_components : Int32
}

///|
/// 做了什么修改。
pub fn RepairResult::changed(self : RepairResult) -> Bool {
  self.removed_degenerate > 0 ||
  self.removed_duplicates > 0 ||
  self.filled_holes > 0 ||
  self.recomputed_normals > 0 ||
  self.flipped_faces > 0 ||
  self.reoriented_components > 0
}

///|
/// 修复一个网格。
fn repair_inner(
  mesh : Mesh,
  options : RepairOptions,
  limits : RepairLimits,
) -> Result[RepairResult, String] {
  let mut removed_degenerate = 0
  let mut removed_duplicates = 0

  // 先用焊接编号把每个三角形转成三个整数下标。后面删退化面、找重复面、
  // 找边界边都要用这个,算一次就够了。
  let weld = weld_vertices(mesh, options.tolerance)
  let kept : Array[Triangle] = []
  let kept_ids : Array[(Int32, Int32, Int32)] = []
  let seen : Map[String, Unit] = Map([])

  for i, triangle in mesh.triangles {
    let ia = weld.ids[i * 3]
    let ib = weld.ids[i * 3 + 1]
    let ic = weld.ids[i * 3 + 2]

    // 退化面:焊接后有两个顶点是同一个点,这个面没有面积
    if options.drop_degenerate &&
      (ia == ib || ib == ic || ia == ic || triangle_is_degenerate(triangle)) {
      removed_degenerate = removed_degenerate + 1
      continue
    }

    // 重复面:三个顶点下标排序后相同的,就是同一个面。
    // 排序是为了让 (1,2,3) 和 (2,3,1) 被认成同一个——它们顶点顺序不同
    // 但是同一个三角形。绕向相反的 (1,3,2) 也会被认出来,那种是「同一块
    // 面贴了两遍,一片朝外一片朝里」,同样该删。
    if options.drop_duplicates {
      let key = canonical_key(ia, ib, ic)
      if seen.contains(key) {
        removed_duplicates = removed_duplicates + 1
        continue
      }
      seen.set(key, ())
    }

    kept.push(triangle)
    kept_ids.push((ia, ib, ic))
  }

  // 补洞。用「删完之后」的网格来找边界,不然会去补一个马上要删掉的面
  // 留下的洞。
  let mut filled_holes = 0
  let mut added_triangles = 0
  let mut result = kept
  if options.fill_holes {
    let before = result.length()
    let (patched, count) = match fill_holes_safe(result, kept_ids, limits) {
      Ok(value) => value
      Err(message) => return Err(message)
    }
    result = patched
    added_triangles = result.length() - before
    filled_holes = count
  }

  // 统一绕向。必须排在重算法线**之前**:绕向一变,法线跟着变,
  // 反过来做的话刚算好的法线会被绕向翻转作废。
  //
  // 这一步是「重算法线」修不了的那部分:顶点顺序写反的面,按它自己
  // 的顺序重算法线是**自洽的**(法线和绕向一致),但几何还是错的——
  // 体积求和时它的贡献符号反了。
  let mut flipped_faces = 0
  let mut reoriented_components = 0
  if options.unify_winding {
    let before = check_winding(Mesh::new(result), options.tolerance)
    flipped_faces = before.flipped_faces
    // 分量整体翻转单独计数;可能嵌套/重叠的壳层保留符号。
    let (aligned, count) = unify_winding_details(
      Mesh::new(result),
      options.tolerance,
    )
    result = aligned.triangles
    reoriented_components = count
  }

  // 重算法线。放在最后——删面、补洞、翻转绕向都会改变法线,先算就白算了。
  let mut recomputed = 0
  let final_triangles : Array[Triangle] = []
  for triangle in result {
    let computed = triangle.computed_normal()
    if options.recompute_normals {
      let changed = !same_vec(triangle.normal, computed)
      if changed {
        recomputed = recomputed + 1
      }
      final_triangles.push({ ..triangle, normal: computed, })
    } else {
      final_triangles.push(triangle)
    }
  }

  Ok({
    mesh: Mesh::new(final_triangles),
    removed_degenerate,
    removed_duplicates,
    filled_holes,
    added_triangles,
    recomputed_normals: recomputed,
    flipped_faces,
    reoriented_components,
  })
}

///|
/// 三个下标排序后拼成字符串。用来判断两个面是不是同一个。
fn canonical_key(a : Int32, b : Int32, c : Int32) -> String {
  let (x, y, z) = if a <= b && a <= c {
    if b <= c {
      (a, b, c)
    } else {
      (a, c, b)
    }
  } else if b <= a && b <= c {
    if a <= c {
      (b, a, c)
    } else {
      (b, c, a)
    }
  } else if a <= b {
    (c, a, b)
  } else {
    (c, b, a)
  }
  "\{x},\{y},\{z}"
}