///|
pub fn is_finite(value : Double) -> Bool {
  value == value && value.abs() < 1.0e308
}

///|
pub fn valid_latitude(latitude_deg : Double) -> Bool {
  is_finite(latitude_deg) && latitude_deg >= -90.0 && latitude_deg <= 90.0
}

///|
pub fn valid_longitude(longitude_deg : Double) -> Bool {
  is_finite(longitude_deg) && longitude_deg >= -540.0 && longitude_deg <= 540.0
}

///|
pub fn valid_point(point : GeoPoint) -> Bool {
  valid_latitude(point.lat) &&
  valid_longitude(point.lon) &&
  is_finite(point.elevation_m)
}

///|
pub fn valid_range(range : NumericRange) -> Bool {
  is_finite(range.minimum) &&
  is_finite(range.maximum) &&
  range.minimum <= range.maximum
}

///|
pub fn range_contains(range : NumericRange, value : Double) -> Bool {
  valid_range(range) &&
  is_finite(value) &&
  value >= range.minimum &&
  value <= range.maximum
}

///|
pub fn make_range(minimum~ : Double, maximum~ : Double) -> NumericRange {
  { minimum, maximum }
}

///|
pub fn clamp_to_range(value : Double, range : NumericRange) -> Double {
  if value < range.minimum {
    range.minimum
  } else if value > range.maximum {
    range.maximum
  } else {
    value
  }
}

///|
pub fn valid_grid(grid : Grid) -> Bool {
  grid.is_valid() &&
  is_finite(grid.origin_lat) &&
  is_finite(grid.origin_lon) &&
  is_finite(grid.lat_step) &&
  is_finite(grid.lon_step) &&
  grid.values.all(fn(value) { is_finite(value) })
}

///|
pub fn valid_term(term : HarmonicTerm) -> Bool {
  term.degree >= 0 &&
  term.order >= 0 &&
  term.order <= term.degree &&
  is_finite(term.cosine) &&
  is_finite(term.sine)
}

///|
pub fn valid_model(model : SphericalHarmonicModel) -> Bool {
  is_finite(model.reference_radius_m) &&
  model.reference_radius_m > 0.0 &&
  is_finite(model.scale) &&
  model.terms.all(valid_term)
}

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

///|
pub fn mark_missing(sample : FieldSample) -> QualitySample {
  { sample, flag: Missing, uncertainty: 0.0, source: "unknown" }
}

///|
pub fn mark_outlier(
  sample : FieldSample,
  uncertainty~ : Double,
) -> QualitySample {
  { sample, flag: Outlier, uncertainty, source: "quality-control" }
}

///|
pub fn quality_is_usable(flag : QualityFlag) -> Bool {
  flag == Good || flag == Suspect
}

///|
pub fn quality_name(flag : QualityFlag) -> String {
  match flag {
    Good => "good"
    Suspect => "suspect"
    Missing => "missing"
    Invalid => "invalid"
    Outlier => "outlier"
  }
}

///|
pub fn missing_policy_names() -> Array[String] {
  let policies : Array[MissingPolicy] = [
    SkipMissing,
    PreserveMissing,
    ReplaceWith(value=0.0),
  ]
  let names : Array[String] = []
  for policy in policies {
    match policy {
      SkipMissing => names.push("skip")
      PreserveMissing => names.push("preserve")
      ReplaceWith(_) => names.push("replace")
    }
  }
  names
}