///|
/// Post-processing stages for combining easing curves with editorial ranges.
pub enum CurveStage2 {
  StageNormalize
  StageClamp(Double, Double)
  StageRemap(Double, Double)
  StageBias(Double)
  StageGain(Double)
  StageQuantize(Int)
  StageMirror
} derive(Debug)

///|
pub fn curve_stages2() -> Array[CurveStage2] {
  [
    StageNormalize,
    StageClamp(0.0, 1.0),
    StageRemap(0.0, 1.0),
    StageBias(0.0),
    StageGain(1.0),
    StageQuantize(8),
    StageMirror,
  ]
}

///|
fn apply_curve_stage(stage : CurveStage2, value : Double) -> Double {
  match stage {
    StageNormalize => clamp01(value)
    StageClamp(minimum, maximum) =>
      clamp(value, minimum.min(maximum), maximum.max(minimum))
    StageRemap(minimum, maximum) => minimum + value * (maximum - minimum)
    StageBias(bias) => value + bias
    StageGain(gain) => value * gain
    StageQuantize(steps) =>
      if steps <= 0 {
        value
      } else {
        @math.floor(value * steps.to_double() + 0.5) / steps.to_double()
      }
    StageMirror => 1.0 - value
  }
}

///|
pub struct CurvePipeline2 {
  curve : Curve
  stages : Array[CurveStage2]
} derive(Debug)

///|
pub fn curve_pipeline2(curve : Curve) -> CurvePipeline2 {
  { curve, stages: [] }
}

///|
pub fn CurvePipeline2::curve(self : CurvePipeline2) -> Curve {
  self.curve
}

///|
pub fn CurvePipeline2::stages(self : CurvePipeline2) -> Array[CurveStage2] {
  self.stages.copy()
}

///|
pub fn CurvePipeline2::add(
  self : CurvePipeline2,
  stage : CurveStage2,
) -> CurvePipeline2 {
  let stages = self.stages.copy()
  stages.push(stage)
  { curve: self.curve, stages }
}

///|
pub fn CurvePipeline2::then(
  self : CurvePipeline2,
  other : CurvePipeline2,
) -> CurvePipeline2 {
  let stages = self.stages.copy()
  for stage in other.stages {
    stages.push(stage)
  }
  { curve: self.curve, stages }
}

///|
pub fn CurvePipeline2::apply(self : CurvePipeline2, time : Double) -> Double {
  let mut value = self.curve.apply(clamp01(time))
  for stage in self.stages {
    value = apply_curve_stage(stage, value)
  }
  value
}

///|
pub fn CurvePipeline2::sample_many(
  self : CurvePipeline2,
  count : Int,
) -> Array[Double] {
  let values : Array[Double] = []
  let actual = count.max(1)
  for index in 0.. CurveLookupTable raise MotionError {
  let table = CurveLookupTable::new(self.curve, samples.max(2))
  if self.stages.length() == 0 {
    return table
  }
  let values : Array[Double] = []
  for index in 0.. 0 {
      integral = integral + (values[index - 1] + values[index]) * step * 0.5
    }
  }
  { values, derivatives, minimum, maximum, integral }
}

///|
pub fn CurvePipeline2::range(
  self : CurvePipeline2,
  samples : Int,
) -> (Double, Double) {
  let values = self.sample_many(samples)
  if values.length() == 0 {
    return (0.0, 0.0)
  }
  let mut minimum = values[0]
  let mut maximum = values[0]
  for value in values {
    minimum = minimum.min(value)
    maximum = maximum.max(value)
  }
  (minimum, maximum)
}

///|
pub fn compose_curve_pipeline(
  first : CurvePipeline2,
  second : CurvePipeline2,
) -> CurvePipeline2 {
  first.then(second)
}

///|
pub fn curve_pipeline_error(
  pipeline : CurvePipeline2,
  table : CurveLookupTable,
  samples : Int,
) -> Double {
  let count = samples.max(2)
  let mut maximum = 0.0
  for index in 0..<=count {
    let time = index.to_double() / count.to_double()
    maximum = maximum.max(
      (pipeline.apply(time) - table.sample_smooth(time)).abs(),
    )
  }
  maximum
}

///|
pub struct CurveRemap2 {
  input_min : Double
  input_max : Double
  output_min : Double
  output_max : Double
  extrapolation : Extrapolation
} derive(Debug)

///|
pub fn curve_remap2(
  input_min : Double,
  input_max : Double,
  output_min : Double,
  output_max : Double,
) -> CurveRemap2 {
  { input_min, input_max, output_min, output_max, extrapolation: Clamp }
}

///|
pub fn CurveRemap2::with_extrapolation(
  self : CurveRemap2,
  mode : Extrapolation,
) -> CurveRemap2 {
  {
    input_min: self.input_min,
    input_max: self.input_max,
    output_min: self.output_min,
    output_max: self.output_max,
    extrapolation: mode,
  }
}

///|
pub fn CurveRemap2::apply(self : CurveRemap2, value : Double) -> Double {
  let denominator = self.input_max - self.input_min
  if denominator == 0.0 {
    return self.output_min
  }
  let ratio = (value - self.input_min) / denominator
  let mapped = match self.extrapolation {
    Clamp => clamp01(ratio)
    Continue => ratio
    Repeat => ratio - @math.floor(ratio)
    Mirror => {
      let cycle = @math.floor(ratio)
      let fraction = ratio - cycle
      if cycle.to_int() % 2 == 0 {
        fraction
      } else {
        1.0 - fraction
      }
    }
  }
  self.output_min + mapped * (self.output_max - self.output_min)
}

///|
pub fn remap_curve_values(
  values : Array[Double],
  remap : CurveRemap2,
) -> Array[Double] {
  let result : Array[Double] = []
  for value in values {
    result.push(remap.apply(value))
  }
  result
}