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

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

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

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

///|
/// 检查壁厚。
///
/// 采样是**确定性**的:同一个模型每次跑出来的数一样。
/// 用黄金比之类的低差异序列把采样点铺开,而不是伪随机数——
/// 「上次跑 0.42,这次跑 0.38」这种报告没法用来对比修复前后的变化。
fn thickness_samples(
  mesh : Mesh,
  samples : Int32,
  weighted : Bool,
  seed : Int32,
) -> 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,
    }
  }

  // Start at the sampled surface and exclude its source triangle. A global
  // bounding-box inset can jump across a locally thin wall.
  let cumulative : Array[Double] = []
  let area_total = MomentSum::new()
  if weighted {
    let mut max_area = 0.0
    for triangle in mesh.triangles {
      max_area = max_area.max(triangle.area())
    }
    if max_area > 0.0 && !not_finite(max_area) {
      for triangle in mesh.triangles {
        area_total.add(triangle.area() / max_area)
        cumulative.push(area_total.value())
      }
    }
  }

  let mut hits = 0
  let mut min_d = 1.0 / 0.0
  let mut min_at : Vec3? = None
  let total = MomentSum::new()

  for i = 0; i < samples; i = i + 1 {
    // 三角形编号:乘一个大奇数再取模,把相邻的采样打散到整个模型上。
    // 顺序扫的话前一半采样全落在模型的同一片区域
    let idx = if weighted {
      if cumulative.length() == 0 {
        continue
      }
      let target = frac(
          (i.to_double() + 0.5) * 0.6180339887498949 +
          seed.to_double() * 0.4142135623730951,
        ) *
        area_total.value()
      let mut lo = 0
      let mut hi = cumulative.length()
      while lo < hi {
        let mid = lo + (hi - lo) / 2
        if cumulative[mid] <= target {
          lo = mid + 1
        } else {
          hi = mid
        }
      }
      lo.min(n - 1)
    } else {
      (i.to_uint64() * 2654435761UL % n.to_uint64()).to_int()
    }
    let tri = mesh.triangles[idx]

    // 三角形内的均匀采样点
    let sample = i.to_double() + (if weighted { 0.5 } else { 0.0 })
    let r1 = frac(sample * 0.6180339887498949).sqrt()
    let r2 = frac(sample * 0.7548776662466927)
    let ab = tri.b.sub(tri.a)
    let ac = tri.c.sub(tri.a)
    let p = Vec3::new(
      tri.a.x + ab.x * (r1 * (1.0 - r2)) + ac.x * (r1 * r2),
      tri.a.y + ab.y * (r1 * (1.0 - r2)) + ac.y * (r1 * r2),
      tri.a.z + ab.z * (r1 * (1.0 - r2)) + ac.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)
    match ray_cast_excluding(mesh, p, dir, idx) {
      Some(hit) => {
        hits = hits + 1
        total.add(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.value() / hits.to_double(),
  }
}

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

///|
/// 确定性面索引采样。从表面向内查询,排除来源三角形。
/// 首命中距离受采样、法线和射线阈值影响,不保证全局最薄处。
pub fn check_thickness(mesh : Mesh, samples : Int32) -> ThicknessReport {
  thickness_samples(mesh, samples, false, 0)
}

///|
/// Area-weighted deterministic surface sampling with a repeatable seed.
/// Statistics describe ray first hits, not a certified global minimum.
pub fn check_thickness_area_weighted(
  mesh : Mesh,
  samples : Int32,
  seed~ : Int32 = 0,
) -> ThicknessReport {
  thickness_samples(mesh, samples, true, seed)
}