///|
/// A uniformly sampled, allocation-free-at-read-time representation of a
/// timing curve. Baking is useful when a render loop evaluates the same curve
/// for thousands of particles or UI properties.
pub struct CurveLookupTable {
  values : Array[Double]
  derivatives : Array[Double]
  minimum : Double
  maximum : Double
  integral : Double
} derive(Debug)

///|
pub fn CurveLookupTable::new(
  curve : Curve,
  samples : Int,
) -> CurveLookupTable raise MotionError {
  if samples < 2 {
    raise MotionError::InvalidSampleCount(samples)
  }
  let values : Array[Double] = []
  let derivatives : Array[Double] = []
  let step = 1.0 / (samples - 1).to_double()
  let mut minimum = curve.apply(0.0)
  let mut maximum = minimum
  let mut area = 0.0
  for index in 0.. 0 {
      area = area + (values[index - 1] + value) * step * 0.5
    }
  }
  for index in 0.. Array[Double] {
  self.values.copy()
}

///|
pub fn CurveLookupTable::derivatives(self : CurveLookupTable) -> Array[Double] {
  self.derivatives.copy()
}

///|
pub fn CurveLookupTable::samples(self : CurveLookupTable) -> Int {
  self.values.length()
}

///|
pub fn CurveLookupTable::minimum(self : CurveLookupTable) -> Double {
  self.minimum
}

///|
pub fn CurveLookupTable::maximum(self : CurveLookupTable) -> Double {
  self.maximum
}

///|
pub fn CurveLookupTable::integral(self : CurveLookupTable) -> Double {
  self.integral
}

///|
fn CurveLookupTable::index(
  self : CurveLookupTable,
  time : Double,
) -> (Int, Double) {
  let count = self.values.length()
  let position = clamp01(time) * (count - 1).to_double()
  let lower = @math.floor(position).to_int().min(count - 2)
  (lower, position - lower.to_double())
}

///|
pub fn CurveLookupTable::sample(
  self : CurveLookupTable,
  time : Double,
) -> Double {
  let (index, ratio) = self.index(time)
  self.values[index] + (self.values[index + 1] - self.values[index]) * ratio
}

///|
pub fn CurveLookupTable::sample_smooth(
  self : CurveLookupTable,
  time : Double,
) -> Double {
  let (index, ratio) = self.index(time)
  let left = self.values[index]
  let right = self.values[index + 1]
  let left_slope = self.derivatives[index]
  let right_slope = self.derivatives[index + 1]
  let count = (self.values.length() - 1).to_double()
  let step = 1.0 / count
  let h00 = 2.0 * ratio * ratio * ratio - 3.0 * ratio * ratio + 1.0
  let h10 = ratio * ratio * ratio - 2.0 * ratio * ratio + ratio
  let h01 = -2.0 * ratio * ratio * ratio + 3.0 * ratio * ratio
  let h11 = ratio * ratio * ratio - ratio * ratio
  h00 * left + h10 * left_slope * step + h01 * right + h11 * right_slope * step
}

///|
pub fn CurveLookupTable::derivative_at(
  self : CurveLookupTable,
  time : Double,
) -> Double {
  let (index, ratio) = self.index(time)
  self.derivatives[index] +
  (self.derivatives[index + 1] - self.derivatives[index]) * ratio
}

///|
pub fn CurveLookupTable::inverse(
  self : CurveLookupTable,
  value : Double,
  iterations? : Int = 8,
) -> Double {
  let mut low = 0.0
  let mut high = 1.0
  let increasing = self.values[self.values.length() - 1] >= self.values[0]
  let target = clamp(value, self.minimum, self.maximum)
  for _ in 0.. Double {
  let target = clamp01(time)
  let (index, ratio) = self.index(target)
  let step = 1.0 / (self.values.length() - 1).to_double()
  let mut area = 0.0
  for cursor in 0.. CurveLookupTable raise MotionError {
  if samples < 2 {
    raise MotionError::InvalidSampleCount(samples)
  }
  let values : Array[Double] = []
  let derivatives : Array[Double] = []
  let step = 1.0 / (samples - 1).to_double()
  let mut minimum = self.sample(0.0)
  let mut maximum = minimum
  let mut area = 0.0
  for index in 0.. 0 {
      area = area + (values[index - 1] + value) * step * 0.5
    }
  }
  for index in 0.. Double {
  output_min + self.sample(input) * (output_max - output_min)
}

///|
pub fn CurveLookupTable::error(
  self : CurveLookupTable,
  curve : Curve,
  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((self.sample_smooth(time) - curve.apply(time)).abs())
  }
  maximum
}

///|
/// A compact collection of baked curves with shared sample density.
pub struct CurveLutBank {
  tables : Array[CurveLookupTable]
  labels : Array[String]
} derive(Debug)

///|
pub fn curve_lut_bank() -> CurveLutBank {
  { tables: [], labels: [] }
}

///|
pub fn CurveLutBank::length(self : CurveLutBank) -> Int {
  self.tables.length()
}

///|
pub fn CurveLutBank::add(
  self : CurveLutBank,
  label : String,
  curve : Curve,
  samples : Int,
) -> Unit raise MotionError {
  self.labels.push(label)
  self.tables.push(CurveLookupTable::new(curve, samples))
}

///|
pub fn CurveLutBank::label(self : CurveLutBank, index : Int) -> String {
  self.labels[index]
}

///|
pub fn CurveLutBank::table(
  self : CurveLutBank,
  index : Int,
) -> CurveLookupTable {
  self.tables[index]
}

///|
pub fn CurveLutBank::sample(
  self : CurveLutBank,
  index : Int,
  time : Double,
) -> Double {
  self.tables[index].sample(time)
}

///|
pub fn CurveLutBank::samples(
  self : CurveLutBank,
  time : Double,
) -> Array[Double] {
  let result : Array[Double] = []
  for table in self.tables {
    result.push(table.sample(time))
  }
  result
}

///|
pub fn CurveLutBank::max_error(
  self : CurveLutBank,
  curves : Array[Curve],
  samples : Int,
) -> Double {
  let count = curves.length().min(self.tables.length())
  let mut maximum = 0.0
  for index in 0.. CurveLutBuilder {
  { curve, samples: 256, adaptive: true, tolerance: 0.000001 }
}

///|
pub fn CurveLutBuilder::samples(self : CurveLutBuilder) -> Int {
  self.samples
}

///|
pub fn CurveLutBuilder::with_samples(
  self : CurveLutBuilder,
  samples : Int,
) -> CurveLutBuilder {
  {
    curve: self.curve,
    samples: samples.max(2),
    adaptive: self.adaptive,
    tolerance: self.tolerance,
  }
}

///|
pub fn CurveLutBuilder::with_tolerance(
  self : CurveLutBuilder,
  tolerance : Double,
) -> CurveLutBuilder {
  {
    curve: self.curve,
    samples: self.samples,
    adaptive: self.adaptive,
    tolerance: tolerance.max(0.000000000001),
  }
}

///|
pub fn CurveLutBuilder::adaptive(
  self : CurveLutBuilder,
  enabled : Bool,
) -> CurveLutBuilder {
  {
    curve: self.curve,
    samples: self.samples,
    adaptive: enabled,
    tolerance: self.tolerance,
  }
}

///|
pub fn CurveLutBuilder::build(
  self : CurveLutBuilder,
) -> CurveLookupTable raise MotionError {
  if !self.adaptive {
    return CurveLookupTable::new(self.curve, self.samples)
  }
  let mut samples = self.samples
  let mut table = CurveLookupTable::new(self.curve, samples)
  while samples < 8192 && table.error(self.curve, samples * 2) > self.tolerance {
    samples = samples * 2
    table = CurveLookupTable::new(self.curve, samples)
  }
  table
}

///|
pub fn CurveLookupTable::monotonic(self : CurveLookupTable) -> Bool {
  let increasing = self.values[self.values.length() - 1] >= self.values[0]
  for index in 1.. self.values[index - 1] {
      return false
    }
  }
  true
}

///|
pub fn CurveLookupTable::peak_speed(
  self : CurveLookupTable,
) -> (Double, Double) {
  let mut index = 0
  for cursor in 1.. self.derivatives[index].abs() {
      index = cursor
    }
  }
  (
    index.to_double() / (self.values.length() - 1).to_double(),
    self.derivatives[index],
  )
}

///|
pub fn CurveLookupTable::range(self : CurveLookupTable) -> Double {
  self.maximum - self.minimum
}

///|
pub fn CurveLookupTable::normalized(
  self : CurveLookupTable,
) -> CurveLookupTable {
  let span = self.range()
  if span == 0.0 {
    return self
  }
  let values : Array[Double] = []
  for value in self.values {
    values.push((value - self.minimum) / span)
  }
  {
    values,
    derivatives: self.derivatives.copy(),
    minimum: 0.0,
    maximum: 1.0,
    integral: self.integral / span,
  }
}