///|
pub fn quality_sample(
  sample : FieldSample,
  flag : QualityFlag,
  uncertainty : Double,
  source : String,
) -> QualitySample {
  { sample, flag, uncertainty, source }
}

///|
pub fn quality_summary(samples : ArrayView[QualitySample]) -> QualitySummary {
  let mut usable = 0
  let mut missing = 0
  let mut invalid = 0
  let mut suspect = 0
  let mut outlier = 0
  for sample in samples {
    match sample.flag {
      Good => usable += 1
      Missing => missing += 1
      Invalid => invalid += 1
      Suspect => suspect += 1
      Outlier => outlier += 1
    }
  }
  { total: samples.length(), usable, missing, invalid, suspect, outlier }
}

///|
pub fn quality_from_samples(
  samples : ArrayView[FieldSample],
  source : String,
) -> Array[QualitySample] {
  let output : Array[QualitySample] = []
  for sample in samples {
    let flag = if !is_finite(sample.value) {
      Missing
    } else if !valid_point(sample.point) {
      Invalid
    } else {
      Good
    }
    output.push({ sample, flag, uncertainty: 0.0, source })
  }
  output
}

///|
pub fn quality_reflag_missing(
  samples : ArrayView[QualitySample],
) -> Array[QualitySample] {
  let output : Array[QualitySample] = []
  for sample in samples {
    if !is_finite(sample.sample.value) {
      output.push({ ..sample, flag: Missing })
    } else {
      output.push(sample)
    }
  }
  output
}

///|
pub fn quality_reflag_range(
  samples : ArrayView[QualitySample],
  range : NumericRange,
) -> Array[QualitySample] {
  let output : Array[QualitySample] = []
  for sample in samples {
    let flag = if !is_finite(sample.sample.value) {
      Missing
    } else if range_contains(range, sample.sample.value) {
      sample.flag
    } else {
      Invalid
    }
    output.push({ ..sample, flag, })
  }
  output
}

///|
pub fn quality_reflag_outliers(
  samples : ArrayView[QualitySample],
  threshold : Double,
) -> Array[QualitySample] {
  let values : Array[Double] = []
  for sample in samples {
    values.push(sample.sample.value)
  }
  let report = outlier_report(values, threshold~)
  let outlier_indices : Map[Int, Bool] = Map([])
  for index in report.indices {
    outlier_indices[index] = true
  }
  let output : Array[QualitySample] = []
  for i in 0.. Array[QualitySample] {
  samples.map(fn(sample) { { ..sample, uncertainty: uncertainty.abs() } })
}

///|
pub fn quality_usable_samples(
  samples : ArrayView[QualitySample],
) -> Array[QualitySample] {
  samples.filter(fn(sample) { quality_is_usable(sample.flag) })
}

///|
pub fn quality_values(
  samples : ArrayView[QualitySample],
  policy : MissingPolicy,
) -> Array[Double] {
  let output : Array[Double] = []
  for sample in samples {
    if quality_is_usable(sample.flag) && is_finite(sample.sample.value) {
      output.push(sample.sample.value)
    } else {
      match policy {
        SkipMissing => ()
        PreserveMissing => output.push(0.0 / 0.0)
        ReplaceWith(value~) => output.push(value)
      }
    }
  }
  output
}

///|
pub fn quality_weighted_mean(samples : ArrayView[QualitySample]) -> Double {
  let mut weighted = 0.0
  let mut total_weight = 0.0
  for sample in samples {
    if quality_is_usable(sample.flag) && is_finite(sample.sample.value) {
      let weight = if sample.uncertainty <= 0.0 {
        1.0
      } else {
        1.0 / (sample.uncertainty * sample.uncertainty)
      }
      weighted += sample.sample.value * weight
      total_weight += weight
    }
  }
  if total_weight == 0.0 {
    0.0 / 0.0
  } else {
    weighted / total_weight
  }
}

///|
pub fn quality_weighted_variance(samples : ArrayView[QualitySample]) -> Double {
  let mean = quality_weighted_mean(samples)
  let mut weighted = 0.0
  let mut total_weight = 0.0
  for sample in samples {
    if quality_is_usable(sample.flag) && is_finite(sample.sample.value) {
      let weight = if sample.uncertainty <= 0.0 {
        1.0
      } else {
        1.0 / (sample.uncertainty * sample.uncertainty)
      }
      weighted += weight *
        (sample.sample.value - mean) *
        (sample.sample.value - mean)
      total_weight += weight
    }
  }
  if total_weight == 0.0 {
    0.0 / 0.0
  } else {
    weighted / total_weight
  }
}

///|
pub fn quality_flag_counts(samples : ArrayView[QualitySample]) -> Array[Int] {
  let summary = quality_summary(samples)
  [
    summary.usable,
    summary.suspect,
    summary.missing,
    summary.invalid,
    summary.outlier,
  ]
}

///|
pub fn quality_completeness(samples : ArrayView[QualitySample]) -> Double {
  if samples.length() == 0 {
    0.0
  } else {
    Double::from_int(quality_summary(samples).usable) /
    Double::from_int(samples.length())
  }
}

///|
pub fn quality_merge(
  left : ArrayView[QualitySample],
  right : ArrayView[QualitySample],
) -> Array[QualitySample] {
  let output = left.to_owned()
  for sample in right {
    output.push(sample)
  }
  output
}

///|
pub fn quality_deduplicate(
  samples : ArrayView[QualitySample],
  tolerance_m : Double,
) -> Array[QualitySample] {
  let output : Array[QualitySample] = []
  for sample in samples {
    let mut found = false
    for existing in output {
      if point_approximately_equal(
          sample.sample.point,
          existing.sample.point,
          tolerance_m,
        ) {
        found = true
      }
    }
    if !found {
      output.push(sample)
    }
  }
  output
}

///|
pub fn quality_sort_by_coordinate(
  samples : ArrayView[QualitySample],
  origin : GeoPoint,
) -> Array[QualitySample] {
  let output = samples.to_owned()
  output.sort_by(fn(a, b) {
    haversine_distance_m(origin, a.sample.point).compare(
      haversine_distance_m(origin, b.sample.point),
    )
  })
  output
}

///|
pub fn quality_apply_offset(
  samples : ArrayView[QualitySample],
  offset : Double,
) -> Array[QualitySample] {
  samples.map(fn(sample) {
    {
      ..sample,
      sample: { ..sample.sample, value: sample.sample.value + offset },
    }
  })
}

///|
pub fn quality_to_samples(
  samples : ArrayView[QualitySample],
) -> Array[FieldSample] {
  samples.map(fn(sample) { sample.sample })
}