///|
/// Per-axis affine transform for sensor gains and offsets.
pub struct AxisTransform {
  gain_x : Double
  gain_y : Double
  gain_z : Double
  offset_x : Double
  offset_y : Double
  offset_z : Double
} derive(Debug, Eq)

///|
pub fn AxisTransform::new(
  gain_x : Double,
  gain_y : Double,
  gain_z : Double,
  offset_x : Double,
  offset_y : Double,
  offset_z : Double,
) -> AxisTransform {
  { gain_x, gain_y, gain_z, offset_x, offset_y, offset_z }
}

///|
pub fn AxisTransform::identity() -> AxisTransform {
  AxisTransform::new(1.0, 1.0, 1.0, 0.0, 0.0, 0.0)
}

///|
pub fn transform_force_sample(
  sample : ForceSample,
  transform : AxisTransform,
) -> ForceSample {
  ForceSample::new(
    sample.timestamp,
    Vec3::new(
      (sample.force.x - transform.offset_x) * transform.gain_x,
      (sample.force.y - transform.offset_y) * transform.gain_y,
      (sample.force.z - transform.offset_z) * transform.gain_z,
    ),
    sample.moment,
    sample.cop,
  )
}

///|
/// Applies an affine transform while preserving moments, COP, and timestamps.
pub fn transform_trial_axes(
  trial : ForceTrial,
  transform : AxisTransform,
) -> ForceTrial {
  let samples : Array[ForceSample] = Array::new(capacity=trial.samples.length())
  for sample in trial.samples {
    samples.push(transform_force_sample(sample, transform))
  }
  ForceTrial::new(trial.metadata, samples)
}

///|
pub fn compute_force_magnitude_series(trial : ForceTrial) -> Array[Double] {
  let result : Array[Double] = Array::new(capacity=trial.samples.length())
  for sample in trial.samples {
    result.push(sample.force.magnitude())
  }
  result
}

///|
/// Projects each force vector onto a normalized axis; a zero axis gives zero.
pub fn project_force_series(trial : ForceTrial, axis : Vec3) -> Array[Double] {
  let magnitude = axis.magnitude()
  let result : Array[Double] = Array::new(capacity=trial.samples.length())
  if magnitude <= 0.000001 || magnitude.is_nan() || magnitude.is_inf() {
    for _sample in trial.samples {
      result.push(0.0)
    }
    return result
  }
  let normalized = axis.scale(1.0 / magnitude)
  for sample in trial.samples {
    result.push(sample.force.dot(normalized))
  }
  result
}

///|
pub struct ForceMagnitudeSummary {
  count : Int
  mean_magnitude_n : Double
  rms_magnitude_n : Double
  peak_magnitude_n : Double
  p95_magnitude_n : Double
} derive(Debug, Eq)

///|
pub fn summarize_force_magnitude(
  values : Array[Double],
) -> ForceMagnitudeSummary {
  let summary = summarize_signal(values)
  {
    count: summary.count,
    mean_magnitude_n: summary.mean,
    rms_magnitude_n: summary.rms,
    peak_magnitude_n: summary.maximum,
    p95_magnitude_n: quantile(values, 0.95),
  }
}