///|
/// Summary of feature tracks suitable for multi-view reconstruction.
pub(all) struct ReconstructionTrackReport {
  total_tracks : Int
  triangulatable_tracks : Int
  total_observations : Int
  mean_views : Double
  maximum_views : Int
} derive(Debug, Eq)

///|
pub fn reconstruction_track_report(
  observations : ArrayView[TrackObservation],
  minimum_views~ : Int,
) -> ReconstructionTrackReport {
  let ids : Array[Int] = []
  for observation in observations {
    if !ids.contains(observation.point_id) {
      ids.push(observation.point_id)
    }
  }
  let mut triangulatable = 0
  let mut total_views = 0
  let mut maximum = 0
  for id in ids {
    let views : Array[Int] = []
    for observation in observations {
      if observation.point_id == id && !views.contains(observation.view_id) {
        views.push(observation.view_id)
      }
    }
    total_views += views.length()
    maximum = maximum.max(views.length())
    if views.length() >= minimum_views {
      triangulatable += 1
    }
  }
  {
    total_tracks: ids.length(),
    triangulatable_tracks: triangulatable,
    total_observations: observations.length(),
    mean_views: if ids.length() == 0 {
      0.0
    } else {
      total_views.to_double() / ids.length().to_double()
    },
    maximum_views: maximum,
  }
}

///|
pub fn track_observations_for(
  observations : ArrayView[TrackObservation],
  point_id : Int,
) -> Array[TrackObservation] {
  let result : Array[TrackObservation] = []
  for observation in observations {
    if observation.point_id == point_id {
      result.push(observation)
    }
  }
  result
}

///|
pub fn track_view_ids(observations : ArrayView[TrackObservation]) -> Array[Int] {
  let result : Array[Int] = []
  for observation in observations {
    if !result.contains(observation.view_id) {
      result.push(observation.view_id)
    }
  }
  result
}

///|
pub fn track_baseline_support(
  observations : ArrayView[TrackObservation],
) -> Int {
  track_view_ids(observations).length()
}

///|
pub fn select_tracks_by_support(
  observations : ArrayView[TrackObservation],
  minimum_views : Int,
) -> Array[Int] {
  let ids : Array[Int] = []
  for observation in observations {
    if !ids.contains(observation.point_id) &&
      track_view_ids(track_observations_for(observations, observation.point_id)).length() >=
      minimum_views {
      ids.push(observation.point_id)
    }
  }
  ids
}

///|
pub fn select_tracks_by_pixel_span(
  observations : ArrayView[TrackObservation],
  minimum_span : Double,
) -> Array[Int] {
  let ids : Array[Int] = []
  for observation in observations {
    if !ids.contains(observation.point_id) {
      let group = track_observations_for(observations, observation.point_id)
      let mut min_x = observation.pixel.x
      let mut max_x = observation.pixel.x
      let mut min_y = observation.pixel.y
      let mut max_y = observation.pixel.y
      for item in group {
        min_x = min_x.min(item.pixel.x)
        max_x = max_x.max(item.pixel.x)
        min_y = min_y.min(item.pixel.y)
        max_y = max_y.max(item.pixel.y)
      }
      if @core.Vec2::new(x=max_x - min_x, y=max_y - min_y).norm() >=
        minimum_span {
        ids.push(observation.point_id)
      }
    }
  }
  ids
}

///|
pub fn deduplicate_observations(
  observations : ArrayView[TrackObservation],
) -> Array[TrackObservation] {
  let result : Array[TrackObservation] = []
  let keys : Array[(Int, Int)] = []
  for observation in observations {
    let key = (observation.point_id, observation.view_id)
    if !keys.contains(key) {
      keys.push(key)
      result.push(observation)
    }
  }
  result
}

///|
pub fn observation_centroid(
  observations : ArrayView[TrackObservation],
) -> @core.Point2 raise @core.GeometryError {
  let points : Array[@core.Point2] = []
  for observation in observations {
    points.push(observation.pixel)
  }
  @core.polygon_centroid(points)
}

///|
pub fn observation_pixel_rms(
  observations : ArrayView[TrackObservation],
) -> Double raise @core.GeometryError {
  let center = observation_centroid(observations)
  if observations.length() == 0 {
    raise @core.GeometryError::NotEnoughPoints(
      "observation RMS needs observations",
    )
  }
  let mut sum = 0.0
  for observation in observations {
    let distance = observation.pixel.minus(center).norm()
    sum += distance * distance
  }
  (sum / observations.length().to_double()).sqrt()
}

///|
pub fn track_consistency_score(
  observations : ArrayView[TrackObservation],
  minimum_views : Int,
  minimum_span : Double,
) -> Double {
  let support = if minimum_views <= 0 {
    1.0
  } else {
    (track_view_ids(observations).length().to_double() /
    minimum_views.to_double()).min(1.0)
  }
  let span = observation_pixel_rms(observations) catch { _ => 0.0 }
  let spread = if minimum_span <= 0.0 {
    1.0
  } else {
    (span / minimum_span).min(1.0)
  }
  support * spread
}

///|
pub(all) struct TriangulationGate {
  minimum_views : Int
  minimum_baseline : Double
  maximum_reprojection : Double
} derive(Debug, Eq)

///|
pub fn TriangulationGate::new(
  minimum_views~ : Int,
  minimum_baseline~ : Double,
  maximum_reprojection~ : Double,
) -> TriangulationGate {
  {
    minimum_views: minimum_views.max(2),
    minimum_baseline: minimum_baseline.max(0.0),
    maximum_reprojection: maximum_reprojection.max(0.0),
  }
}

///|
pub fn triangulation_gate_accepts(
  gate : TriangulationGate,
  view_count : Int,
  baseline : Double,
  reprojection : Double,
) -> Bool {
  view_count >= gate.minimum_views &&
  baseline >= gate.minimum_baseline &&
  reprojection <= gate.maximum_reprojection
}

///|
pub fn triangulation_depth_is_valid(
  depth : Double,
  minimum : Double,
  maximum : Double,
) -> Bool {
  depth == depth && depth >= minimum && depth <= maximum
}

///|
pub fn triangulation_depth_mask(
  depths : ArrayView[Double],
  minimum : Double,
  maximum : Double,
) -> Array[Bool] {
  let result : Array[Bool] = []
  for depth in depths {
    result.push(triangulation_depth_is_valid(depth, minimum, maximum))
  }
  result
}

///|
pub fn triangulation_depth_indices(
  depths : ArrayView[Double],
  minimum : Double,
  maximum : Double,
) -> Array[Int] {
  let result : Array[Int] = []
  for i in 0..