///|
/// A compact diagnostic for a batch of camera rays.
pub(all) struct RayBundleReport {
  count : Int
  mean_direction_norm : Double
  min_direction_z : Double
  max_direction_z : Double
} derive(Debug, Eq)

///|
/// Create a ray through every point of a regular image lattice.
pub fn image_ray_grid(
  size : ImageSize,
  intrinsics : CameraIntrinsics,
  columns : Int,
  rows : Int,
) -> Array[@core.Ray3] {
  let result : Array[@core.Ray3] = []
  if columns <= 0 || rows <= 0 {
    return result
  }
  for row in 0.. @core.Vec3::new(x=0.0, y=0.0, z=1.0)
      }
      result.push({ origin: @core.Point3::new(x=0.0, y=0.0, z=0.0), direction })
    }
  }
  result
}

///|
/// Convert valid pixels to bearing rays, ignoring points outside the image.
pub fn sample_pixels(
  size : ImageSize,
  intrinsics : CameraIntrinsics,
  pixels : ArrayView[@core.Point2],
) -> Array[@core.Ray3] {
  let result : Array[@core.Ray3] = []
  for pixel in pixels {
    if pixel.x >= 0.0 &&
      pixel.y >= 0.0 &&
      pixel.x < size.width.to_double() &&
      pixel.y < size.height.to_double() {
      let n = pixel_to_normalized(pixel, intrinsics)
      let direction = @core.Vec3::new(x=n.x, y=n.y, z=1.0).normalize() catch {
        _ => @core.Vec3::new(x=0.0, y=0.0, z=1.0)
      }
      result.push({ origin: @core.Point3::new(x=0.0, y=0.0, z=0.0), direction })
    }
  }
  result
}

///|
/// Summarize ray normalization and forward-facing coverage.
pub fn ray_bundle_report(rays : ArrayView[@core.Ray3]) -> RayBundleReport {
  if rays.length() == 0 {
    return {
      count: 0,
      mean_direction_norm: 0.0,
      min_direction_z: 0.0,
      max_direction_z: 0.0,
    }
  }
  let mut sum = 0.0
  let mut min_z = rays[0].direction.z
  let mut max_z = rays[0].direction.z
  for ray in rays {
    sum += ray.direction.norm()
    if ray.direction.z < min_z {
      min_z = ray.direction.z
    }
    if ray.direction.z > max_z {
      max_z = ray.direction.z
    }
  }
  {
    count: rays.length(),
    mean_direction_norm: sum / rays.length().to_double(),
    min_direction_z: min_z,
    max_direction_z: max_z,
  }
}

///|
/// Return only forward-facing rays for a camera-centered reconstruction.
pub fn forward_rays(
  rays : ArrayView[@core.Ray3],
  minimum_z? : Double = 0.000001,
) -> Array[@core.Ray3] {
  let result : Array[@core.Ray3] = []
  for ray in rays {
    if ray.direction.z >= minimum_z {
      result.push(ray)
    }
  }
  result
}