///|
/// A time-binned station trend measurement.
pub struct StationTrendPoint {
  start_time : Double
  end_time : Double
  sample_count : Int
  mean : Double
  rms : Double
  peak : Double
  completeness : Double
  quality_score : Double
}

///|
/// A trend report with linear drift coefficients.
pub struct StationTrendReport {
  station : String
  channel : String
  points : Array[StationTrendPoint]
  mean_drift_per_second : Double
  rms_drift_per_second : Double
  peak_drift_per_second : Double
  declining : Bool
}

///|
/// Calculate trends for fixed physical windows.
pub fn station_trend(
  trace : Trace,
  window_seconds : Double,
  expected_rate : Double,
) -> StationTrendReport {
  let points : Array[StationTrendPoint] = Array::new()
  if window_seconds <= 0.0 || trace.sampling_rate <= 0.0 {
    return {
      station: trace.station,
      channel: trace.channel,
      points,
      mean_drift_per_second: 0.0,
      rms_drift_per_second: 0.0,
      peak_drift_per_second: 0.0,
      declining: false,
    }
  }
  let window = (window_seconds * trace.sampling_rate).to_int().max(1)
  let mut start = 0
  while start < trace.npts() {
    let end = (start + window).min(trace.npts())
    let data = slice_samples(trace.data, start, end)
    let expected = (window_seconds * expected_rate.max(trace.sampling_rate))
      .to_int()
      .max(1)
    let report = station_quality_report(
      trace_slice(trace, start, end),
      expected,
      1.0e300,
      1.0e-12,
      data,
    )
    points.push({
      start_time: trace.start_time + start.to_double() / trace.sampling_rate,
      end_time: trace.start_time +
      (end - 1).max(start).to_double() / trace.sampling_rate,
      sample_count: data.length(),
      mean: mean_samples(data),
      rms: rms_samples(data),
      peak: absolute_peak(data),
      completeness: report.completeness,
      quality_score: report.score,
    })
    start = end
  }
  let times = points.map(point => (point.start_time + point.end_time) / 2.0)
  let means = points.map(point => point.mean)
  let rmss = points.map(point => point.rms)
  let peaks = points.map(point => point.peak)
  let mean_slope = linear_slope(times, means)
  let rms_slope = linear_slope(times, rmss)
  let peak_slope = linear_slope(times, peaks)
  {
    station: trace.station,
    channel: trace.channel,
    points,
    mean_drift_per_second: mean_slope,
    rms_drift_per_second: rms_slope,
    peak_drift_per_second: peak_slope,
    declining: rms_slope < 0.0,
  }
}

///|
/// Calculate the least-squares slope of y with respect to x.
pub fn linear_slope(x : Array[Double], y : Array[Double]) -> Double {
  let count = x.length().min(y.length())
  if count < 2 {
    0.0
  } else {
    let mean_x = mean_samples(slice_samples(x, 0, count))
    let mean_y = mean_samples(slice_samples(y, 0, count))
    let mut numerator = 0.0
    let mut denominator = 0.0
    for index = 0; index < count; index = index + 1 {
      let dx = x[index] - mean_x
      numerator += dx * (y[index] - mean_y)
      denominator += dx * dx
    }
    if denominator <= 0.0 {
      0.0
    } else {
      numerator / denominator
    }
  }
}

///|
/// Return points whose score is below an operational threshold.
pub fn degraded_trend_points(
  report : StationTrendReport,
  threshold : Double,
) -> Array[StationTrendPoint] {
  report.points.filter(point => point.quality_score < threshold)
}

///|
/// Export station trend points for a dashboard.
pub fn station_trend_to_csv(report : StationTrendReport) -> String {
  let mut output = "station,channel,start,end,samples,mean,rms,peak,completeness,quality\n"
  for point in report.points {
    output = output +
      report.station +
      "," +
      report.channel +
      "," +
      point.start_time.to_string() +
      "," +
      point.end_time.to_string() +
      "," +
      point.sample_count.to_string() +
      "," +
      point.mean.to_string() +
      "," +
      point.rms.to_string() +
      "," +
      point.peak.to_string() +
      "," +
      point.completeness.to_string() +
      "," +
      point.quality_score.to_string() +
      "\n"
  }
  output
}

///|
/// Format station trend slopes.
pub fn station_trend_text(report : StationTrendReport) -> String {
  "station=" +
  report.station +
  " channel=" +
  report.channel +
  " points=" +
  report.points.length().to_string() +
  " mean_slope=" +
  report.mean_drift_per_second.to_string() +
  " rms_slope=" +
  report.rms_drift_per_second.to_string() +
  " peak_slope=" +
  report.peak_drift_per_second.to_string() +
  " declining=" +
  report.declining.to_string()
}