// 确定性表面采样与内法线射线距离估计。
// 样本最小值可能漏掉局部薄处,不能视为全局最小壁厚或可打印结论。
// 每条射线遍历所有面,时间约 O(samples*faces)。

///|
/// 壁厚检查的结果。
pub(all) struct ThicknessReport {
  /// 一共打了多少条射线
  samples : Int
  /// 有多少条打到了对面的表面。
  /// 明显偏少说明模型有开放面,或者朝向不一致,结论不可信
  hits : Int
  /// 采到的最薄处有多厚(打到对面的那些里最小的)
  min_thickness : Double
  /// 最薄处的位置
  min_at : Vec3?
  /// 平均厚度(只统计打到对面的)
  mean_thickness : Double
}

///|
/// 默认样本预算,不保证覆盖全部面或薄处。
/// 每条射线遍历所有面,运行成本取决于输入规模。
const DEFAULT_THICKNESS_SAMPLES : Int = 1000

///|
/// 用默认采样数检查壁厚。
pub fn check_thickness_default(mesh : Mesh) -> ThicknessReport {
  check_thickness(mesh, DEFAULT_THICKNESS_SAMPLES)
}

///|
/// 检查壁厚。
///
/// 采样是**确定性**的:同一个模型每次跑出来的数一样。
/// 用黄金比之类的低差异序列把采样点铺开,而不是伪随机数——
/// 「上次跑 0.42,这次跑 0.38」这种报告没法用来对比修复前后的变化。
pub fn check_thickness(mesh : Mesh, samples : Int) -> ThicknessReport {
  let n = mesh.triangle_count()
  if n == 0 || samples <= 0 {
    return {
      samples: 0,
      hits: 0,
      min_thickness: 0.0,
      min_at: None,
      mean_thickness: 0.0,
    }
  }

  // 射线起点往里挪一点点,免得打中自己所在的那个三角形。
  // 挪的量按模型尺度取,别用固定值——微米级的模型和米级的模型差着六个数量级
  let inset = probe_inset(mesh)

  let mut hits = 0
  let mut min_d = 1.0e300
  let mut min_at : Vec3? = None
  let mut total = 0.0

  for i = 0; i < samples; i = i + 1 {
    // 三角形编号:乘一个大奇数再取模,把相邻的采样打散到整个模型上。
    // 顺序扫的话前一半采样全落在模型的同一片区域
    let idx = (i.to_uint64() * 2654435761UL % n.to_uint64()).to_int()
    let tri = mesh.triangles[idx]

    // 三角形内的均匀采样点
    let r1 = frac(i.to_double() * 0.6180339887498949).sqrt()
    let r2 = frac(i.to_double() * 0.7548776662466927)
    let p = Vec3::new(
      tri.a.x * (1.0 - r1) + tri.b.x * (r1 * (1.0 - r2)) + tri.c.x * (r1 * r2),
      tri.a.y * (1.0 - r1) + tri.b.y * (r1 * (1.0 - r2)) + tri.c.y * (r1 * r2),
      tri.a.z * (1.0 - r1) + tri.b.z * (r1 * (1.0 - r2)) + tri.c.z * (r1 * r2),
    )

    // 往**里面**打。法线朝外,所以方向取反。
    // 退化面没有法线,跳过
    let normal = tri.computed_normal()
    if normal.is_zero(1.0e-12) {
      continue
    }
    let dir = Vec3::new(-normal.x, -normal.y, -normal.z)
    let origin = Vec3::new(
      p.x + dir.x * inset,
      p.y + dir.y * inset,
      p.z + dir.z * inset,
    )

    match mesh.ray_cast(origin, dir) {
      Some(hit) => {
        hits = hits + 1
        total = total + hit.distance
        if hit.distance < min_d {
          min_d = hit.distance
          min_at = Some(hit.point)
        }
      }
      None => ()
    }
  }

  if hits == 0 {
    // 一条都没打到对面。开放曲面(比如一张纸)就是这样,
    // 报「厚度 0」会误导——那根本不是「薄」,是没法用这个办法量
    return {
      samples,
      hits: 0,
      min_thickness: 0.0,
      min_at: None,
      mean_thickness: 0.0,
    }
  }
  {
    samples,
    hits,
    min_thickness: min_d,
    min_at,
    mean_thickness: total / hits.to_double(),
  }
}

///|
/// 取小数部分。
fn frac(v : Double) -> Double {
  v - v.floor()
}

///|
/// 射线起点往里挪多少。
///
/// 取包围盒对角线的一亿分之一:足够大到能躲开自己那个三角形,
/// 又小到不影响测出来的厚度。模型尺度差几个数量级也能自适应
fn probe_inset(mesh : Mesh) -> Double {
  match mesh.bounding_box() {
    Some(b) => {
      let s = b.size()
      let diag = (s.x * s.x + s.y * s.y + s.z * s.z).sqrt()
      if diag > 0.0 {
        diag * 1.0e-8
      } else {
        1.0e-9
      }
    }
    None => 1.0e-9
  }
}