///|
/// Robust baseline estimate for a force plate recording.
pub struct BaselineCalibration {
  offset_fx : Double
  offset_fy : Double
  offset_fz : Double
  noise_fx : Double
  noise_fy : Double
  noise_fz : Double
  baseline_samples : Int
  stable : Bool
} derive(Debug, Eq)

///|
fn finite_prefix(values : Array[Double], count : Int) -> Array[Double] {
  let range = clamp_range(values.length(), 0, count)
  let result : Array[Double] = []
  for i = range.start; i < range.end; i = i + 1 {
    if !values[i].is_nan() && !values[i].is_inf() {
      result.push(values[i])
    }
  }
  result
}

///|
/// Estimates tare offsets with medians and noise with scaled MAD.
pub fn estimate_baseline_calibration(
  trial : ForceTrial,
  baseline_samples : Int,
  stability_threshold_n : Double,
) -> BaselineCalibration {
  let fx = finite_prefix(trial.get_fx(), baseline_samples)
  let fy = finite_prefix(trial.get_fy(), baseline_samples)
  let fz = finite_prefix(trial.get_fz(), baseline_samples)
  let offset_fx = median(fx)
  let offset_fy = median(fy)
  let offset_fz = median(fz)
  let noise_fx = mad(fx) * 1.4826
  let noise_fy = mad(fy) * 1.4826
  let noise_fz = mad(fz) * 1.4826
  let threshold = if stability_threshold_n < 0.0 {
    0.0
  } else {
    stability_threshold_n
  }
  let stable = (fx.length() > 0 || fy.length() > 0 || fz.length() > 0) &&
    noise_fx <= threshold &&
    noise_fy <= threshold &&
    noise_fz <= threshold
  {
    offset_fx,
    offset_fy,
    offset_fz,
    noise_fx,
    noise_fy,
    noise_fz,
    baseline_samples: if fx.length() < fy.length() {
      if fx.length() < fz.length() {
        fx.length()
      } else {
        fz.length()
      }
    } else if fy.length() < fz.length() {
      fy.length()
    } else {
      fz.length()
    },
    stable,
  }
}

///|
/// Applies robust baseline offsets while preserving the trial timebase.
pub fn apply_baseline_tare(
  trial : ForceTrial,
  baseline : BaselineCalibration,
) -> ForceTrial {
  trial.apply_calibration(
    CalibrationParams::new(
      1.0,
      1.0,
      1.0,
      baseline.offset_fx,
      baseline.offset_fy,
      baseline.offset_fz,
      0.0,
    ),
  )
}

///|
/// Raw-signal diagnostics for sensor saturation and finite data coverage.
pub struct CalibrationDiagnostics {
  sample_count : Int
  finite_samples : Int
  saturated_samples : Int
  maximum_abs_force_n : Double
  saturation_limit_n : Double
} derive(Debug, Eq)

///|
pub fn inspect_calibration(
  trial : ForceTrial,
  saturation_limit_n : Double,
) -> CalibrationDiagnostics {
  let limit = if saturation_limit_n <= 0.0 || saturation_limit_n.is_nan() {
    0.0
  } else {
    saturation_limit_n
  }
  let mut finite = 0
  let mut saturated = 0
  let mut maximum = 0.0
  for sample in trial.samples {
    let force = sample.force
    let valid = !force.x.is_nan() &&
      !force.x.is_inf() &&
      !force.y.is_nan() &&
      !force.y.is_inf() &&
      !force.z.is_nan() &&
      !force.z.is_inf()
    if valid {
      finite = finite + 1
      let ax = force.x.abs()
      let ay = force.y.abs()
      let az = force.z.abs()
      if ax > maximum {
        maximum = ax
      }
      if ay > maximum {
        maximum = ay
      }
      if az > maximum {
        maximum = az
      }
      if limit > 0.0 && (ax >= limit || ay >= limit || az >= limit) {
        saturated = saturated + 1
      }
    }
  }
  {
    sample_count: trial.length(),
    finite_samples: finite,
    saturated_samples: saturated,
    maximum_abs_force_n: maximum,
    saturation_limit_n: limit,
  }
}