///|
/// A sample enriched with first and second derivative estimates.
pub(all) struct CurveAnalysisPoint {
  time : Double
  value : Double
  velocity : Double
  acceleration : Double
  normalized_value : Double
} derive(Debug)

///|
pub fn CurveAnalysisPoint::time(self : CurveAnalysisPoint) -> Double {
  self.time
}

///|
pub fn CurveAnalysisPoint::value(self : CurveAnalysisPoint) -> Double {
  self.value
}

///|
pub fn CurveAnalysisPoint::velocity(self : CurveAnalysisPoint) -> Double {
  self.velocity
}

///|
pub fn CurveAnalysisPoint::acceleration(self : CurveAnalysisPoint) -> Double {
  self.acceleration
}

///|
pub fn CurveAnalysisPoint::normalized_value(
  self : CurveAnalysisPoint,
) -> Double {
  self.normalized_value
}

///|
pub(all) enum CurveMonotonicity {
  Increasing
  Decreasing
  Constant
  Mixed
} derive(Eq, Debug)

///|
pub fn analyze_samples(
  samples : Array[SamplePoint],
) -> Array[CurveAnalysisPoint] {
  let result : Array[CurveAnalysisPoint] = []
  if samples.length() == 0 {
    return result
  }
  let mut minimum = samples[0].value
  let mut maximum = samples[0].value
  for sample in samples {
    minimum = minimum.min(sample.value)
    maximum = maximum.max(sample.value)
  }
  let span = maximum - minimum
  for index in 0.. CurveMonotonicity {
  if samples.length() < 2 {
    return Constant
  }
  let epsilon = tolerance.max(0.0)
  let mut increasing = false
  let mut decreasing = false
  for index in 1.. epsilon {
      increasing = true
    } else if difference < -epsilon {
      decreasing = true
    }
  }
  if increasing && decreasing {
    Mixed
  } else if increasing {
    Increasing
  } else if decreasing {
    Decreasing
  } else {
    Constant
  }
}

///|
pub fn curve_minimum(samples : Array[SamplePoint]) -> SamplePoint? {
  if samples.length() == 0 {
    return None
  }
  let mut result = samples[0]
  for sample in samples {
    if sample.value < result.value {
      result = sample
    }
  }
  Some(result)
}

///|
pub fn curve_maximum(samples : Array[SamplePoint]) -> SamplePoint? {
  if samples.length() == 0 {
    return None
  }
  let mut result = samples[0]
  for sample in samples {
    if sample.value > result.value {
      result = sample
    }
  }
  Some(result)
}

///|
/// Estimate the signed area under a curve using the trapezoidal rule.
pub fn integrate_analyzed_curve(samples : Array[SamplePoint]) -> Double {
  integrate_samples(samples)
}

///|
/// Estimate total variation, which is useful for quantizing a motion signal.
pub fn total_variation(samples : Array[SamplePoint]) -> Double {
  if samples.length() < 2 {
    return 0.0
  }
  let mut total = 0.0
  for index in 1.. Double? {
  if samples.length() == 0 {
    return None
  }
  for index in 0.. Double,
  start : Double,
  end : Double,
  count : Int,
) -> Array[SamplePoint] raise MotionError {
  sample_function(func, start, end, count)
}

///|
/// Evaluate a table with linear interpolation and clamped boundaries.
pub fn lookup_value(samples : Array[SamplePoint], time : Double) -> Double {
  interpolate_samples(
    samples.map(fn(sample) {
      { time: sample.time, value: sample.value, depth: 0 }
    }),
    time,
  )
}

///|
/// Compose a source curve with an output range.
pub fn map_curve_range(
  samples : Array[SamplePoint],
  output_min : Double,
  output_max : Double,
) -> Array[SamplePoint] {
  let minimum = curve_minimum(samples)
  let maximum = curve_maximum(samples)
  match (minimum, maximum) {
    (Some(low), Some(high)) => {
      let span = high.value - low.value
      samples.map(fn(sample) {
        let ratio = if span == 0.0 {
          0.0
        } else {
          (sample.value - low.value) / span
        }
        {
          time: sample.time,
          value: output_min + (output_max - output_min) * ratio,
          frame: sample.frame,
        }
      })
    }
    _ => []
  }
}

///|
/// Reverse the time direction while preserving the original sample order in
/// the returned array, making it convenient for playback direction toggles.
pub fn reverse_samples(samples : Array[SamplePoint]) -> Array[SamplePoint] {
  if samples.length() == 0 {
    return []
  }
  let first = samples[0].time
  let last = samples[samples.length() - 1].time
  let result : Array[SamplePoint] = []
  for index in 0.. Array[SamplePoint] {
  if samples.length() == 0 {
    return []
  }
  let source_start = samples[0].time
  let source_end = samples[samples.length() - 1].time
  let source_span = source_end - source_start
  samples.map(fn(sample) {
    let ratio = if source_span == 0.0 {
      0.0
    } else {
      (sample.time - source_start) / source_span
    }
    {
      time: start + (end - start) * ratio,
      value: sample.value,
      frame: sample.frame,
    }
  })
}

///|
/// Compute a percentile value from a copied and sorted scalar array.
pub fn percentile(values : Array[Double], ratio : Double) -> Double {
  if values.length() == 0 {
    return 0.0
  }
  let sorted = values.copy()
  sorted.sort_by(fn(left, right) { left.compare(right) })
  let index = (clamp01(ratio) * (sorted.length() - 1).to_double()).to_int()
  sorted[index]
}

///|
pub fn median(values : Array[Double]) -> Double {
  percentile(values, 0.5)
}

///|
pub fn mean(values : Array[Double]) -> Double {
  if values.length() == 0 {
    return 0.0
  }
  let mut total = 0.0
  for value in values {
    total = total + value
  }
  total / values.length().to_double()
}

///|
pub fn variance(values : Array[Double]) -> Double {
  if values.length() == 0 {
    return 0.0
  }
  let average = mean(values)
  let mut total = 0.0
  for value in values {
    let difference = value - average
    total = total + difference * difference
  }
  total / values.length().to_double()
}

///|
pub fn standard_deviation(values : Array[Double]) -> Double {
  variance(values).sqrt()
}