///|
/// Physical parameters for a damped spring response.
pub struct SpringSpec {
  stiffness : Double
  damping : Double
  mass : Double
  initial_velocity : Double
} derive(Debug)

///|
pub fn spring_spec(
  stiffness : Double,
  damping : Double,
  mass? : Double = 1.0,
  initial_velocity? : Double = 0.0,
) -> SpringSpec raise MotionError {
  if stiffness <= 0.0 {
    raise MotionError::InvalidDuration(stiffness)
  }
  if damping < 0.0 {
    raise MotionError::InvalidDelay(damping)
  }
  if mass <= 0.0 {
    raise MotionError::InvalidDuration(mass)
  }
  ensure_finite(initial_velocity)
  { stiffness, damping, mass, initial_velocity }
}

///|
pub fn SpringSpec::natural_frequency(self : SpringSpec) -> Double {
  (self.stiffness / self.mass).sqrt()
}

///|
pub fn SpringSpec::damping_ratio(self : SpringSpec) -> Double {
  self.damping / (2.0 * (self.stiffness * self.mass).sqrt())
}

///|
pub fn SpringSpec::settle_time(
  self : SpringSpec,
  tolerance? : Double = 0.001,
) -> Double {
  let safe_tolerance = clamp(tolerance, 0.0000001, 0.5)
  let decay = self.damping / (2.0 * self.mass)
  if decay <= 0.0 {
    10.0 / self.natural_frequency()
  } else {
    -@math.ln(safe_tolerance) / decay
  }
}

///|
/// Spring position for a unit step from start to end.
pub fn SpringSpec::value_at(
  self : SpringSpec,
  start : Double,
  end : Double,
  time : Double,
) -> Double {
  if time <= 0.0 {
    start
  } else {
    let omega = self.natural_frequency()
    let ratio = self.damping_ratio()
    let displacement = start - end
    if ratio < 1.0 {
      let damped = omega * (1.0 - ratio * ratio).sqrt()
      let decay = @math.exp(-ratio * omega * time)
      let coefficient = (self.initial_velocity + ratio * omega * displacement) /
        damped
      end +
      decay *
      (
        displacement * @math.cos(damped * time) +
        coefficient * @math.sin(damped * time)
      )
    } else if ratio == 1.0 {
      let decay = @math.exp(-omega * time)
      end +
      decay *
      (displacement + (self.initial_velocity + omega * displacement) * time)
    } else {
      let root = (ratio * ratio - 1.0).sqrt()
      let first = -omega * (ratio - root)
      let second = -omega * (ratio + root)
      let coefficient_second = (self.initial_velocity - first * displacement) /
        (second - first)
      let coefficient_first = displacement - coefficient_second
      end +
      coefficient_first * @math.exp(first * time) +
      coefficient_second * @math.exp(second * time)
    }
  }
}

///|
pub fn SpringSpec::velocity_at(
  self : SpringSpec,
  start : Double,
  end : Double,
  time : Double,
) -> Double {
  derivative_of(
    fn(value) { self.value_at(start, end, value) },
    time,
    step=0.00001,
  )
}

///|
pub fn SpringSpec::sample(
  self : SpringSpec,
  start : Double,
  end : Double,
  time : Double,
) -> MotionSample {
  let value = self.value_at(start, end, time)
  let velocity = self.velocity_at(start, end, time)
  let acceleration = derivative_of(
    fn(value_at_time) { self.velocity_at(start, end, value_at_time) },
    time,
    step=0.00001,
  )
  { time, value, velocity, acceleration }
}

///|
pub fn SpringSpec::sample_many(
  self : SpringSpec,
  start : Double,
  end : Double,
  duration : Double,
  count : Int,
) -> Array[MotionSample] raise MotionError {
  if duration <= 0.0 {
    raise MotionError::InvalidDuration(duration)
  }
  if count < 2 {
    raise MotionError::InvalidSampleCount(count)
  }
  let result : Array[MotionSample] = []
  for i in 0.. Bool {
  let value = self.value_at(start, end, time)
  (value - end).abs() <= tolerance &&
  self.velocity_at(start, end, time).abs() <= tolerance
}