// 有向包围盒(OBB)。
//
// 对应 trimesh 的 `mesh.bounding_box_oriented`。
//
// 轴对齐包围盒(AABB)好算,但对斜着摆的模型特别不准:一根斜放的长棍,
// AABB 会是一个巨大的立方体,而 OBB 能贴着棍子包上去。
//
// 做法是主成分分析(PCA):
//
//   1. 求所有顶点的重心和协方差矩阵
//   2. 协方差矩阵的特征向量就是「顶点分布最开的三个方向」
//   3. 把顶点投影到这三个方向上,取各自的极值
//
// PCA 不保证最小体积;重复特征值下轴向不唯一。
// 通过一次 Jacobi 分解取得完整正交基,再包围输入顶点。

///|
/// 有向包围盒。
pub(all) struct Obb {
  /// 盒子的中心
  center : Vec3
  /// 三个轴,已单位化。按对应边的长度从大到小排
  axes : Array[Vec3]
  /// 沿三个轴的半长,和 `axes` 一一对应
  half : Array[Double]
}

///|
/// 三条边的长度。
pub fn Obb::size(self : Obb) -> Vec3 {
  Vec3::new(self.half[0] * 2.0, self.half[1] * 2.0, self.half[2] * 2.0)
}

///|
/// 盒子的体积。真实体积的**上界**——模型装在里面,一般填不满。
pub fn Obb::volume(self : Obb) -> Double {
  self.half[0] * 8.0 * self.half[1] * self.half[2]
}

///|
/// 求有向包围盒。空网格返回 `None`。
pub fn Mesh::oriented_bounding_box(self : Mesh) -> Obb? {
  let pts = unique_positions(self)
  if pts.length() == 0 {
    return None
  }

  // 重心
  let mut cx = 0.0
  let mut cy = 0.0
  let mut cz = 0.0
  for p in pts {
    cx = cx + p.x
    cy = cy + p.y
    cz = cz + p.z
  }
  let n = pts.length().to_double()
  let centroid = Vec3::new(cx / n, cy / n, cz / n)

  // 协方差矩阵。对称,所以 9 个数里只有 6 个独立
  let mut xx = 0.0
  let mut yy = 0.0
  let mut zz = 0.0
  let mut xy = 0.0
  let mut yz = 0.0
  let mut zx = 0.0
  for p in pts {
    let dx = p.x - centroid.x
    let dy = p.y - centroid.y
    let dz = p.z - centroid.z
    xx = xx + dx * dx
    yy = yy + dy * dy
    zz = zz + dz * dz
    xy = xy + dx * dy
    yz = yz + dy * dz
    zx = zx + dz * dx
  }
  let cov : FixedArray[Double] = FixedArray::make(9, 0.0)
  cov[0] = xx / n
  cov[1] = xy / n
  cov[2] = zx / n
  cov[3] = xy / n
  cov[4] = yy / n
  cov[5] = yz / n
  cov[6] = zx / n
  cov[7] = yz / n
  cov[8] = zz / n

  // 一次分解返回完整正交基;重复特征值不能分别取零空间向量。
  let axes = symmetric_eigendecomposition(cov).vectors

  // 把顶点投影到三个轴上,各取极值。
  // 投影的基准取重心——选哪里都行,取重心数值上稳一些
  let lo : Array[Double] = [0.0, 0.0, 0.0]
  let hi : Array[Double] = [0.0, 0.0, 0.0]
  for k = 0; k < 3; k = k + 1 {
    lo[k] = 1.0e300
    hi[k] = -1.0e300
  }
  for p in pts {
    for k = 0; k < 3; k = k + 1 {
      let d = p.sub(centroid).dot(axes[k])
      if d < lo[k] {
        lo[k] = d
      }
      if d > hi[k] {
        hi[k] = d
      }
    }
  }

  // 盒子中心 = 重心 + 各轴上的中点偏移
  let mut center = centroid
  let half : Array[Double] = []
  for k = 0; k < 3; k = k + 1 {
    let mid = (lo[k] + hi[k]) / 2.0
    center = Vec3::new(
      center.x + axes[k].x * mid,
      center.y + axes[k].y * mid,
      center.z + axes[k].z * mid,
    )
    half.push((hi[k] - lo[k]) / 2.0)
  }

  // 按边长从大到小排。主轴按特征值排是「分布最开的方向优先」,
  // 但用户看的是「盒子有多长多宽多高」,按长度排更直观
  let order = sort_axes_by_length(half)
  let sorted_axes : Array[Vec3] = []
  let sorted_half : Array[Double] = []
  for k in order {
    sorted_axes.push(axes[k])
    sorted_half.push(half[k])
  }
  Some({ center, axes: sorted_axes, half: sorted_half, })
}

///|
/// 三个轴按半长从大到小排序,返回下标顺序。
///
/// 只有三个元素,直接手写比较比调排序省事
fn sort_axes_by_length(half : Array[Double]) -> Array[Int] {
  let idx : Array[Int] = [0, 1, 2]
  // 冒泡两轮,从大到小
  for _pass = 0; _pass < 2; _pass = _pass + 1 {
    for i = 0; i + 1 < 3; i = i + 1 {
      if half[idx[i]] < half[idx[i + 1]] {
        let t = idx[i]
        idx[i] = idx[i + 1]
        idx[i + 1] = t
      }
    }
  }
  idx
}

///|
/// 去重后的顶点位置。
///
/// 用完整坐标键量化去重——和校验、流式扫描、PLY 导出是同一套。
/// 「什么算同一个点」整个项目只有一个答案,不然同一条边
/// 在这个环节算共用的、到那个环节又算两个点,报告就会自相矛盾。
fn unique_positions(mesh : Mesh) -> Array[Vec3] {
  let table : Map[PositionKey, Int] = Map([])
  let out : Array[Vec3] = []
  for t in mesh.triangles {
    for v in [t.a, t.b, t.c] {
      let key = position_key(v, default_weld_tolerance())
      match table.get(key) {
        Some(_) => ()
        None => {
          table.set(key, out.length())
          out.push(v)
        }
      }
    }
  }
  out
}