///|
/// Quality gates for large multi-view track tables.
pub(all) struct TrackQualityBatchSummary {
  tracks : Int
  observations : Int
  accepted : Int
  rejected : Int
  mean_score : Double
  best_score : Double
} derive(Debug, Eq)

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

///|
pub fn track_quality_batch_scores(
  observations : ArrayView[TrackObservation],
  minimum_views : Int,
  minimum_span : Double,
) -> Array[Double] {
  let result : Array[Double] = []
  for id in track_quality_batch_ids(observations) {
    result.push(
      track_quality_score2(
        track_observations_for(observations, id),
        minimum_views,
        minimum_span,
      ),
    )
  }
  result
}

///|
pub fn track_quality_batch_summary(
  observations : ArrayView[TrackObservation],
  minimum_views : Int,
  minimum_span : Double,
  threshold : Double,
) -> TrackQualityBatchSummary {
  let scores = track_quality_batch_scores(
    observations, minimum_views, minimum_span,
  )
  let mut accepted = 0
  let mut total = 0.0
  let mut best = 0.0
  for score in scores {
    if score >= threshold {
      accepted += 1
    }
    total += score
    best = best.max(score)
  }
  {
    tracks: scores.length(),
    observations: observations.length(),
    accepted,
    rejected: scores.length() - accepted,
    mean_score: if scores.length() == 0 {
      0.0
    } else {
      total / scores.length().to_double()
    },
    best_score: best,
  }
}

///|
pub fn track_quality_batch_accepted_ids(
  observations : ArrayView[TrackObservation],
  minimum_views : Int,
  minimum_span : Double,
  threshold : Double,
) -> Array[Int] {
  let result : Array[Int] = []
  for id in track_quality_batch_ids(observations) {
    if track_quality_score2(
        track_observations_for(observations, id),
        minimum_views,
        minimum_span,
      ) >=
      threshold {
      result.push(id)
    }
  }
  result
}

///|
pub fn track_quality_batch_rejected_ids(
  observations : ArrayView[TrackObservation],
  minimum_views : Int,
  minimum_span : Double,
  threshold : Double,
) -> Array[Int] {
  let result : Array[Int] = []
  for id in track_quality_batch_ids(observations) {
    if track_quality_score2(
        track_observations_for(observations, id),
        minimum_views,
        minimum_span,
      ) <
      threshold {
      result.push(id)
    }
  }
  result
}

///|
pub fn track_quality_batch_mean_span(
  observations : ArrayView[TrackObservation],
) -> Double {
  let ids = track_quality_batch_ids(observations)
  if ids.length() == 0 {
    0.0
  } else {
    let mut total = 0.0
    for id in ids {
      total += track_pixel_span(track_observations_for(observations, id))
    }
    total / ids.length().to_double()
  }
}

///|
pub fn track_quality_batch_max_span(
  observations : ArrayView[TrackObservation],
) -> Double {
  let mut result = 0.0
  for id in track_quality_batch_ids(observations) {
    result = result.max(
      track_pixel_span(track_observations_for(observations, id)),
    )
  }
  result
}

///|
pub fn track_quality_batch_mean_views(
  observations : ArrayView[TrackObservation],
) -> Double {
  let ids = track_quality_batch_ids(observations)
  if ids.length() == 0 {
    0.0
  } else {
    let mut total = 0
    for id in ids {
      total += track_view_ids(track_observations_for(observations, id)).length()
    }
    total.to_double() / ids.length().to_double()
  }
}

///|
pub fn track_quality_batch_view_support(
  observations : ArrayView[TrackObservation],
  minimum : Int,
) -> Double {
  let ids = track_quality_batch_ids(observations)
  if ids.length() == 0 {
    0.0
  } else {
    let mut accepted = 0
    for id in ids {
      if track_view_ids(track_observations_for(observations, id)).length() >=
        minimum {
        accepted += 1
      }
    }
    accepted.to_double() / ids.length().to_double()
  }
}

///|
pub fn track_quality_batch_motion_support(
  observations : ArrayView[TrackObservation],
  maximum : Double,
) -> Double {
  let ids = track_quality_batch_ids(observations)
  if ids.length() == 0 {
    0.0
  } else {
    let mut accepted = 0
    for id in ids {
      if track_max_motion(track_observations_for(observations, id)) <= maximum {
        accepted += 1
      }
    }
    accepted.to_double() / ids.length().to_double()
  }
}

///|
pub fn track_quality_batch_filter(
  observations : ArrayView[TrackObservation],
  minimum_views : Int,
  minimum_span : Double,
  threshold : Double,
) -> Array[TrackObservation] {
  let accepted = track_quality_batch_accepted_ids(
    observations, minimum_views, minimum_span, threshold,
  )
  let result : Array[TrackObservation] = []
  for observation in observations {
    if accepted.contains(observation.point_id) {
      result.push(observation)
    }
  }
  result
}

///|
pub fn track_quality_batch_reject(
  observations : ArrayView[TrackObservation],
  minimum_views : Int,
  minimum_span : Double,
  threshold : Double,
) -> Array[TrackObservation] {
  let rejected = track_quality_batch_rejected_ids(
    observations, minimum_views, minimum_span, threshold,
  )
  let result : Array[TrackObservation] = []
  for observation in observations {
    if rejected.contains(observation.point_id) {
      result.push(observation)
    }
  }
  result
}

///|
pub fn track_quality_batch_balance(
  observations : ArrayView[TrackObservation],
) -> Double {
  let counts = track_window_view_counts(observations)
  if counts.length() == 0 {
    0.0
  } else {
    let mut minimum = counts[0]
    let mut maximum = counts[0]
    for count in counts {
      minimum = minimum.min(count)
      maximum = maximum.max(count)
    }
    if maximum <= 0 {
      0.0
    } else {
      minimum.to_double() / maximum.to_double()
    }
  }
}

///|
pub fn track_quality_batch_duplicate_ratio(
  observations : ArrayView[TrackObservation],
) -> Double {
  if observations.length() == 0 {
    0.0
  } else {
    track_analytics(observations).duplicate_observations.to_double() /
    observations.length().to_double()
  }
}

///|
pub fn track_quality_batch_temporal_density(
  observations : ArrayView[TrackObservation],
) -> Double {
  let summary = track_window_summary(observations)
  if summary.temporal_span <= 0 {
    1.0
  } else {
    summary.views.to_double() / (summary.temporal_span + 1).to_double()
  }
}

///|
pub fn track_quality_batch_score(
  observations : ArrayView[TrackObservation],
  minimum_views : Int,
  minimum_span : Double,
) -> Double {
  let summary = track_quality_batch_summary(
    observations, minimum_views, minimum_span, 0.5,
  )
  summary.mean_score *
  track_quality_batch_balance(observations) *
  (1.0 - track_quality_batch_duplicate_ratio(observations))
}

///|
pub fn track_quality_batch_is_usable(
  observations : ArrayView[TrackObservation],
  minimum_views : Int,
  minimum_span : Double,
  threshold : Double,
) -> Bool {
  track_quality_batch_score(observations, minimum_views, minimum_span) >=
  threshold
}

///|
pub fn track_quality_batch_csv(summary : TrackQualityBatchSummary) -> String {
  summary.tracks.to_string() +
  "," +
  summary.observations.to_string() +
  "," +
  summary.accepted.to_string() +
  "," +
  summary.rejected.to_string() +
  "," +
  summary.mean_score.to_string() +
  "," +
  summary.best_score.to_string()
}

///|
pub fn track_quality_batch_merge(
  left : TrackQualityBatchSummary,
  right : TrackQualityBatchSummary,
) -> TrackQualityBatchSummary {
  let tracks = left.tracks + right.tracks
  let observations = left.observations + right.observations
  {
    tracks,
    observations,
    accepted: left.accepted + right.accepted,
    rejected: left.rejected + right.rejected,
    mean_score: if tracks == 0 {
      0.0
    } else {
      (
        left.mean_score * left.tracks.to_double() +
        right.mean_score * right.tracks.to_double()
      ) /
      tracks.to_double()
    },
    best_score: left.best_score.max(right.best_score),
  }
}

///|
pub fn track_quality_batch_window(
  observations : ArrayView[TrackObservation],
  first_view : Int,
  last_view : Int,
  minimum_views : Int,
  minimum_span : Double,
) -> Double {
  track_quality_batch_score(
    track_window_limit_views(observations, first_view, last_view),
    minimum_views,
    minimum_span,
  )
}

///|
pub fn track_quality_batch_stable_windows(
  observations : ArrayView[TrackObservation],
  window : Int,
  minimum_views : Int,
  minimum_span : Double,
  threshold : Double,
) -> Array[Int] {
  let views = track_view_ids(observations)
  let result : Array[Int] = []
  for view in views {
    if track_quality_batch_window(
        observations,
        view,
        view + window.max(1) - 1,
        minimum_views,
        minimum_span,
      ) >=
      threshold {
      result.push(view)
    }
  }
  result
}