///|
pub fn make_series(name~ : String, points~ : Array[SeriesPoint]) -> FieldSeries {
  { name, points }
}

///|
pub fn series_from_profile(
  profile : ArrayView[ProfileSample],
  name? : String = "profile",
) -> FieldSeries {
  let points : Array[SeriesPoint] = []
  for sample in profile {
    points.push({
      coordinate: sample.distance_m,
      value: sample.value,
      flag: Good,
    })
  }
  { name, points }
}

///|
pub fn series_values(
  series : FieldSeries,
  policy? : MissingPolicy = SkipMissing,
) -> Array[Double] {
  let values : Array[Double] = []
  for point in series.points {
    if is_finite(point.value) && quality_is_usable(point.flag) {
      values.push(point.value)
    } else {
      match policy {
        SkipMissing => ()
        PreserveMissing => values.push(0.0 / 0.0)
        ReplaceWith(value~) => values.push(value)
      }
    }
  }
  values
}

///|
pub fn series_summary(series : FieldSeries) -> Summary {
  summary(series_values(series))
}

///|
pub fn series_is_sorted(series : FieldSeries) -> Bool {
  for i in 1.. FieldSeries {
  let points = series.points.copy()
  points.sort_by(fn(a, b) { a.coordinate.compare(b.coordinate) })
  { ..series, points, }
}

///|
pub fn series_at(series : FieldSeries, coordinate : Double) -> Double? {
  for point in series.points {
    if point.coordinate == coordinate {
      return Some(point.value)
    }
  }
  None
}

///|
pub fn series_interpolate(series : FieldSeries, coordinate : Double) -> Double? {
  let ordered = if series_is_sorted(series) {
    series
  } else {
    series_sort(series)
  }
  if ordered.points.length() == 0 {
    None
  } else {
    for i in 1..= left.coordinate &&
        coordinate <= right.coordinate &&
        is_finite(left.value) &&
        is_finite(right.value) {
        let width = right.coordinate - left.coordinate
        return Some(
          if width == 0.0 {
            left.value
          } else {
            lerp(
              left.value,
              right.value,
              (coordinate - left.coordinate) / width,
            )
          },
        )
      }
    }
    None
  }
}

///|
pub fn series_resample(
  series : FieldSeries,
  coordinates : ArrayView[Double],
) -> FieldSeries {
  let points : Array[SeriesPoint] = []
  for coordinate in coordinates {
    match series_interpolate(series, coordinate) {
      Some(value) => points.push({ coordinate, value, flag: Good })
      None => points.push({ coordinate, value: 0.0 / 0.0, flag: Missing })
    }
  }
  { name: series.name, points }
}

///|
pub fn series_fill_linear(series : FieldSeries) -> FieldSeries {
  let points = series.points.copy()
  for i in 0.. points[i] = { ..points[i], value, flag: Suspect }
        None => ()
      }
    }
  }
  { ..series, points, }
}

///|
pub fn series_differences(series : FieldSeries) -> FieldSeries {
  let points : Array[SeriesPoint] = []
  if series.points.length() > 0 {
    points.push({ ..series.points[0], value: 0.0 })
    for i in 1.. Double {
  let mut total = 0.0
  for i in 1.. FieldSeries {
  let points : Array[SeriesPoint] = []
  let mut total = 0.0
  for i in 0.. 0 {
      let a = series.points[i - 1]
      let b = series.points[i]
      if is_finite(a.value) && is_finite(b.value) {
        total += (b.coordinate - a.coordinate) * (a.value + b.value) / 2.0
      }
    }
    points.push({ ..series.points[i], value: total })
  }
  { name: "integral({series.name})", points }
}

///|
pub fn series_moving_average(series : FieldSeries, radius : Int) -> FieldSeries {
  let values = moving_average(
    series_values(series, policy=PreserveMissing),
    radius,
  )
  let points : Array[SeriesPoint] = []
  for i in 0.. OutlierReport {
  outlier_report(series_values(series), threshold~)
}

///|
pub fn series_peaks(
  series : FieldSeries,
  prominence~ : Double,
) -> Array[SeriesPoint] {
  let peaks : Array[SeriesPoint] = []
  for i in 1..<(series.points.length() - 1) {
    let point = series.points[i]
    if is_finite(point.value) &&
      point.value > series.points[i - 1].value &&
      point.value >= series.points[i + 1].value &&
      point.value -
      Double::max(series.points[i - 1].value, series.points[i + 1].value) >=
      prominence {
      peaks.push(point)
    }
  }
  peaks
}

///|
pub fn series_troughs(
  series : FieldSeries,
  prominence~ : Double,
) -> Array[SeriesPoint] {
  let troughs : Array[SeriesPoint] = []
  for i in 1..<(series.points.length() - 1) {
    let point = series.points[i]
    if is_finite(point.value) &&
      point.value < series.points[i - 1].value &&
      point.value <= series.points[i + 1].value &&
      Double::min(series.points[i - 1].value, series.points[i + 1].value) -
      point.value >=
      prominence {
      troughs.push(point)
    }
  }
  troughs
}

///|
pub fn series_autocorrelation(series : FieldSeries, lag : Int) -> Double {
  let values = series_values(series)
  if lag < 0 || lag >= values.length() {
    0.0
  } else {
    let center = summary(values).mean
    let mut numerator = 0.0
    let mut denominator = 0.0
    for i in 0.. Double {
  let x = series_values(a)
  let y = series_values(b)
  if lag < 0 || lag >= x.length() || lag >= y.length() {
    0.0
  } else {
    covariance_summary(x[lag:], y[:y.length() - lag]).correlation
  }
}

///|
pub fn series_clip(series : FieldSeries, range : NumericRange) -> FieldSeries {
  let points : Array[SeriesPoint] = []
  for point in series.points {
    if range_contains(range, point.coordinate) {
      points.push(point)
    }
  }
  { ..series, points, }
}

///|
pub fn series_shift(series : FieldSeries, offset : Double) -> FieldSeries {
  let points : Array[SeriesPoint] = []
  for point in series.points {
    points.push({ ..point, coordinate: point.coordinate + offset })
  }
  { ..series, points, }
}

///|
pub fn series_scale(series : FieldSeries, factor : Double) -> FieldSeries {
  let points : Array[SeriesPoint] = []
  for point in series.points {
    points.push({ ..point, value: point.value * factor })
  }
  { ..series, points, }
}