///|
/// Additional point-cloud transforms for streaming reconstruction jobs.
pub fn point_cloud_reflect_x(points : ArrayView[Point3]) -> Array[Point3] {
  let result : Array[Point3] = []
  for point in points {
    result.push(Point3::new(x=-point.x, y=point.y, z=point.z))
  }
  result
}

///|
pub fn point_cloud_reflect_y(points : ArrayView[Point3]) -> Array[Point3] {
  let result : Array[Point3] = []
  for point in points {
    result.push(Point3::new(x=point.x, y=-point.y, z=point.z))
  }
  result
}

///|
pub fn point_cloud_reflect_z(points : ArrayView[Point3]) -> Array[Point3] {
  let result : Array[Point3] = []
  for point in points {
    result.push(Point3::new(x=point.x, y=point.y, z=-point.z))
  }
  result
}

///|
pub fn point_cloud_axis_scale(
  points : ArrayView[Point3],
  sx : Double,
  sy : Double,
  sz : Double,
) -> Array[Point3] {
  let result : Array[Point3] = []
  for point in points {
    result.push(Point3::new(x=point.x * sx, y=point.y * sy, z=point.z * sz))
  }
  result
}

///|
pub fn point_cloud_axis_offset(
  points : ArrayView[Point3],
  ox : Double,
  oy : Double,
  oz : Double,
) -> Array[Point3] {
  point_cloud_translate(points, Vec3::new(x=ox, y=oy, z=oz))
}

///|
pub fn point_cloud_abs(points : ArrayView[Point3]) -> Array[Point3] {
  let result : Array[Point3] = []
  for point in points {
    result.push(Point3::new(x=point.x.abs(), y=point.y.abs(), z=point.z.abs()))
  }
  result
}

///|
pub fn point_cloud_sign(points : ArrayView[Point3]) -> Array[Point3] {
  let result : Array[Point3] = []
  for point in points {
    result.push(
      Point3::new(
        x=if point.x < 0.0 { -1.0 } else { 1.0 },
        y=if point.y < 0.0 { -1.0 } else { 1.0 },
        z=if point.z < 0.0 { -1.0 } else { 1.0 },
      ),
    )
  }
  result
}

///|
pub fn point_cloud_axis_minimum(
  points : ArrayView[Point3],
  axis : Int,
) -> Double {
  let (minimum, _) = point_cloud_bounds(points)
  if axis == 0 {
    minimum.x
  } else if axis == 1 {
    minimum.y
  } else {
    minimum.z
  }
}

///|
pub fn point_cloud_axis_maximum(
  points : ArrayView[Point3],
  axis : Int,
) -> Double {
  let (_, maximum) = point_cloud_bounds(points)
  if axis == 0 {
    maximum.x
  } else if axis == 1 {
    maximum.y
  } else {
    maximum.z
  }
}

///|
pub fn point_cloud_axis_mean(points : ArrayView[Point3], axis : Int) -> Double {
  mean(point_cloud_axis_values(points, axis)) catch {
    _ => 0.0
  }
}

///|
pub fn point_cloud_axis_median(
  points : ArrayView[Point3],
  axis : Int,
) -> Double {
  median(point_cloud_axis_values(points, axis)) catch {
    _ => 0.0
  }
}

///|
pub fn point_cloud_axis_quantile(
  points : ArrayView[Point3],
  axis : Int,
  quantile : Double,
) -> Double {
  quantile_linear(point_cloud_axis_values(points, axis), quantile) catch {
    _ => 0.0
  }
}

///|
pub fn point_cloud_axis_center(
  points : ArrayView[Point3],
  axis : Int,
) -> Double {
  (
    point_cloud_axis_minimum(points, axis) +
    point_cloud_axis_maximum(points, axis)
  ) *
  0.5
}

///|
pub fn point_cloud_axis_normalized(
  points : ArrayView[Point3],
  axis : Int,
) -> Array[Double] {
  let center = point_cloud_axis_center(points, axis)
  let scale = point_cloud_axis_range(points, axis)
  let result : Array[Double] = []
  for value in point_cloud_axis_values(points, axis) {
    result.push(if scale <= 0.0 { 0.0 } else { (value - center) / scale })
  }
  result
}

///|
pub fn point_cloud_radius_histogram(
  points : ArrayView[Point3],
  center : Point3,
  bins : Int,
) -> Array[Int] {
  let result = Array::make(bins.max(0), 0)
  let radius = point_cloud_radius(points, center)
  if radius <= 0.0 {
    return result
  }
  for point in points {
    let index = (distance3(point, center) / radius * bins.to_double())
      .to_int()
      .min(result.length() - 1)
      .max(0)
    if result.length() > 0 {
      result[index] += 1
    }
  }
  result
}

///|
pub fn point_cloud_distance_histogram(
  points : ArrayView[Point3],
  bins : Int,
  maximum : Double,
) -> Array[Int] {
  let result = Array::make(bins.max(0), 0)
  if maximum <= 0.0 {
    return result
  }
  for value in point_cloud_pairwise_distances(points) {
    let index = (value / maximum * bins.to_double())
      .to_int()
      .min(result.length() - 1)
      .max(0)
    if result.length() > 0 {
      result[index] += 1
    }
  }
  result
}

///|
pub fn point_cloud_histogram_total(histogram : ArrayView[Int]) -> Int {
  let mut result = 0
  for value in histogram {
    result += value
  }
  result
}

///|
pub fn point_cloud_histogram_peak(histogram : ArrayView[Int]) -> Int {
  let mut result = 0
  for i in 0.. histogram[result] {
      result = i
    }
  }
  result
}

///|
pub fn point_cloud_histogram_probability(
  histogram : ArrayView[Int],
) -> Array[Double] {
  let total = point_cloud_histogram_total(histogram).to_double()
  let result : Array[Double] = []
  for value in histogram {
    result.push(if total <= 0.0 { 0.0 } else { value.to_double() / total })
  }
  result
}

///|
pub fn point_cloud_project_xy(points : ArrayView[Point3]) -> Array[Point2] {
  let result : Array[Point2] = []
  for point in points {
    result.push(Point2::new(x=point.x, y=point.y))
  }
  result
}

///|
pub fn point_cloud_project_xz(points : ArrayView[Point3]) -> Array[Point2] {
  let result : Array[Point2] = []
  for point in points {
    result.push(Point2::new(x=point.x, y=point.z))
  }
  result
}

///|
pub fn point_cloud_project_yz(points : ArrayView[Point3]) -> Array[Point2] {
  let result : Array[Point2] = []
  for point in points {
    result.push(Point2::new(x=point.y, y=point.z))
  }
  result
}

///|
pub fn point_cloud_lift_xy(
  points : ArrayView[Point2],
  depth : Double,
) -> Array[Point3] {
  let result : Array[Point3] = []
  for point in points {
    result.push(Point3::new(x=point.x, y=point.y, z=depth))
  }
  result
}

///|
pub fn point_cloud_lift_xz(
  points : ArrayView[Point2],
  depth : Double,
) -> Array[Point3] {
  let result : Array[Point3] = []
  for point in points {
    result.push(Point3::new(x=point.x, y=depth, z=point.y))
  }
  result
}

///|
pub fn point_cloud_lift_yz(
  points : ArrayView[Point2],
  depth : Double,
) -> Array[Point3] {
  let result : Array[Point3] = []
  for point in points {
    result.push(Point3::new(x=depth, y=point.x, z=point.y))
  }
  result
}

///|
pub fn point_cloud_centroid_delta(
  left : ArrayView[Point3],
  right : ArrayView[Point3],
) -> Vec3 {
  point_cloud_centroid(right).minus(point_cloud_centroid(left))
}

///|
pub fn point_cloud_centroid_distance(
  left : ArrayView[Point3],
  right : ArrayView[Point3],
) -> Double {
  point_cloud_centroid_delta(left, right).norm()
}

///|
pub fn point_cloud_chamfer_one_way(
  left : ArrayView[Point3],
  right : ArrayView[Point3],
) -> Double {
  if left.length() == 0 {
    0.0
  } else {
    let mut total = 0.0
    for point in left {
      total += point_cloud_nearest_distance(point, right)
    }
    total / left.length().to_double()
  }
}

///|
pub fn point_cloud_chamfer(
  left : ArrayView[Point3],
  right : ArrayView[Point3],
) -> Double {
  (
    point_cloud_chamfer_one_way(left, right) +
    point_cloud_chamfer_one_way(right, left)
  ) *
  0.5
}

///|
pub fn point_cloud_hausdorff_one_way(
  left : ArrayView[Point3],
  right : ArrayView[Point3],
) -> Double {
  let mut result = 0.0
  for point in left {
    result = result.max(point_cloud_nearest_distance(point, right))
  }
  result
}

///|
pub fn point_cloud_hausdorff(
  left : ArrayView[Point3],
  right : ArrayView[Point3],
) -> Double {
  point_cloud_hausdorff_one_way(left, right).max(
    point_cloud_hausdorff_one_way(right, left),
  )
}

///|
pub fn point_cloud_overlap(
  left : ArrayView[Point3],
  right : ArrayView[Point3],
  tolerance : Double,
) -> Double {
  if left.length() == 0 {
    0.0
  } else {
    let mut count = 0
    for point in left {
      if point_cloud_nearest_distance(point, right) <= tolerance.max(0.0) {
        count += 1
      }
    }
    count.to_double() / left.length().to_double()
  }
}

///|
pub fn point_cloud_symmetric_overlap(
  left : ArrayView[Point3],
  right : ArrayView[Point3],
  tolerance : Double,
) -> Double {
  (
    point_cloud_overlap(left, right, tolerance) +
    point_cloud_overlap(right, left, tolerance)
  ) *
  0.5
}

///|
pub fn point_cloud_match_score(
  left : ArrayView[Point3],
  right : ArrayView[Point3],
  tolerance : Double,
) -> Double {
  let overlap = point_cloud_symmetric_overlap(left, right, tolerance)
  let distance = point_cloud_chamfer(left, right)
  overlap / (1.0 + distance)
}

///|
pub fn point_cloud_subtract_reference(
  points : ArrayView[Point3],
  reference : ArrayView[Point3],
  tolerance : Double,
) -> Array[Point3] {
  let result : Array[Point3] = []
  for point in points {
    if point_cloud_nearest_distance(point, reference) > tolerance.max(0.0) {
      result.push(point)
    }
  }
  result
}

///|
pub fn point_cloud_intersection(
  left : ArrayView[Point3],
  right : ArrayView[Point3],
  tolerance : Double,
) -> Array[Point3] {
  let result : Array[Point3] = []
  for point in left {
    if point_cloud_nearest_distance(point, right) <= tolerance.max(0.0) {
      result.push(point)
    }
  }
  result
}

///|
pub fn point_cloud_union(
  left : ArrayView[Point3],
  right : ArrayView[Point3],
  tolerance : Double,
) -> Array[Point3] {
  point_cloud_remove_duplicates(point_cloud_merge(left, right), tolerance)
}

///|
pub fn point_cloud_sample_uniform(
  points : ArrayView[Point3],
  count : Int,
) -> Array[Point3] {
  if count <= 0 {
    []
  } else {
    point_cloud_stride(points, (points.length() / count.max(1)).max(1))
  }
}

///|
pub fn point_cloud_sample_endpoints(
  points : ArrayView[Point3],
  count : Int,
) -> Array[Point3] {
  if count <= 1 {
    point_cloud_take(points, count)
  } else {
    point_cloud_resample_line(points[0], points[points.length() - 1], count)
  }
}

///|
pub fn point_cloud_safe_mean_distance(points : ArrayView[Point3]) -> Double {
  point_cloud_mean_distance(points, point_cloud_centroid(points))
}

///|
pub fn point_cloud_safe_radius(points : ArrayView[Point3]) -> Double {
  point_cloud_radius(points, point_cloud_centroid(points))
}

///|
pub fn point_cloud_is_degenerate(
  points : ArrayView[Point3],
  tolerance : Double,
) -> Bool {
  point_cloud_safe_radius(points) <= tolerance.max(0.0) || points.length() < 2
}

///|
pub fn point_cloud_is_planar_xy(
  points : ArrayView[Point3],
  tolerance : Double,
) -> Bool {
  point_cloud_axis_range(points, 2) <= tolerance.max(0.0)
}

///|
pub fn point_cloud_is_planar_xz(
  points : ArrayView[Point3],
  tolerance : Double,
) -> Bool {
  point_cloud_axis_range(points, 1) <= tolerance.max(0.0)
}

///|
pub fn point_cloud_is_planar_yz(
  points : ArrayView[Point3],
  tolerance : Double,
) -> Bool {
  point_cloud_axis_range(points, 0) <= tolerance.max(0.0)
}

///|
pub fn point_cloud_quality_with_reference(
  points : ArrayView[Point3],
  reference : ArrayView[Point3],
  tolerance : Double,
) -> Double {
  point_cloud_match_score(points, reference, tolerance)
}