///|
pub(all) enum AnimationCompressionMode {
  KeepAll
  RemoveLinear
  Quantize16
  Quantize8
} derive(Debug, ToJson, Eq)

///|
pub(all) struct AnimationCompressionPolicy {
  mode : AnimationCompressionMode
  tolerance : Double
  minimum_keys : Int
  preserve_endpoints : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct AnimationCompressionStats {
  source_keys : Int
  output_keys : Int
  removed_keys : Int
  source_components : Int
  output_components : Int
  maximum_error : Double
  average_error : Double
  compression_ratio : Double
  valid : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct AnimationCompressedTrack {
  times : Array[Double]
  values : Array[Double]
  component_count : Int
  stats : AnimationCompressionStats
  source_indices : Array[Int]
} derive(Debug, ToJson, Eq)

///|
pub fn animation_compression_mode_name(
  mode : AnimationCompressionMode,
) -> String {
  match mode {
    KeepAll => "keep-all"
    RemoveLinear => "remove-linear"
    Quantize16 => "quantize-16"
    Quantize8 => "quantize-8"
  }
}

///|
pub fn default_animation_compression_policy() -> AnimationCompressionPolicy {
  {
    mode: RemoveLinear,
    tolerance: 0.0001,
    minimum_keys: 2,
    preserve_endpoints: true,
  }
}

///|
pub fn animation_quantize(
  value : Double,
  mode : AnimationCompressionMode,
) -> Double {
  let steps = match mode {
    Quantize16 => 65535.0
    Quantize8 => 255.0
    _ => 0.0
  }
  if steps == 0.0 {
    value
  } else {
    Double::round(value * steps) / steps
  }
}

///|
pub fn animation_value_at(
  values : Array[Double],
  key : Int,
  component : Int,
  width : Int,
) -> Double {
  match values.get(key * width + component) {
    Some(value) => value
    None => 0.0
  }
}

///|
pub fn animation_linear_value(
  times : Array[Double],
  values : Array[Double],
  left : Int,
  time : Double,
  width : Int,
  component : Int,
) -> Double {
  let right = left + 1
  let left_time = match times.get(left) {
    Some(value) => value
    None => 0.0
  }
  let right_time = match times.get(right) {
    Some(value) => value
    None => left_time
  }
  let factor = if right_time <= left_time {
    0.0
  } else {
    (time - left_time) / (right_time - left_time)
  }
  let a = animation_value_at(values, left, component, width)
  let b = animation_value_at(values, right, component, width)
  a + (b - a) * factor
}

///|
pub fn animation_key_error(
  times : Array[Double],
  values : Array[Double],
  key : Int,
  width : Int,
) -> Double {
  if key <= 0 || key + 1 >= times.length() {
    return 0.0
  }
  let time = match times.get(key) {
    Some(value) => value
    None => 0.0
  }
  let mut squared = 0.0
  for component in 0.. Array[Bool] {
  let keep = Array::make(times.length(), true)
  let minimum = if minimum_keys < 2 { 2 } else { minimum_keys }
  if times.length() <= minimum {
    return keep
  }
  let limit = times.length() - 1
  for key in 1..= minimum {
        break
      }
    }
  }
  keep
}

///|
pub fn animation_compress_track(
  track : AnimationTrack,
  policy : AnimationCompressionPolicy,
) -> AnimationCompressedTrack {
  let width = if track.component_count < 1 { 1 } else { track.component_count }
  let keep = match policy.mode {
    RemoveLinear =>
      animation_reducible_keys(
        track.times,
        track.values,
        width,
        policy.tolerance,
        policy.minimum_keys,
      )
    _ => Array::make(track.times.length(), true)
  }
  if policy.preserve_endpoints && keep.length() > 0 {
    keep[0] = true
    keep[keep.length() - 1] = true
  }
  let times : Array[Double] = []
  let values : Array[Double] = []
  let source_indices : Array[Int] = []
  let mut maximum_error = 0.0
  let mut error_sum = 0.0
  for key, keep_key in keep {
    if keep_key {
      match track.times.get(key) {
        Some(value) => times.push(value)
        None => ()
      }
      for component in 0.. maximum_error {
        maximum_error = error
      }
      error_sum = error_sum + error
    }
  }
  let source_keys = track.times.length()
  let output_keys = times.length()
  let source_components = track.values.length()
  let output_components = values.length()
  {
    times,
    values,
    component_count: width,
    stats: {
      source_keys,
      output_keys,
      removed_keys: source_keys - output_keys,
      source_components,
      output_components,
      maximum_error,
      average_error: if source_keys == output_keys {
        0.0
      } else {
        error_sum / Double::from_int(source_keys - output_keys)
      },
      compression_ratio: if source_components == 0 {
        1.0
      } else {
        Double::from_int(output_components) /
        Double::from_int(source_components)
      },
      valid: output_keys >= policy.minimum_keys,
    },
    source_indices,
  }
}

///|
pub fn animation_compress_tracks(
  tracks : Array[AnimationTrack],
  policy : AnimationCompressionPolicy,
) -> Array[AnimationCompressedTrack] {
  tracks.map(fn(track) { animation_compress_track(track, policy) })
}

///|
pub fn animation_compression_total(
  results : Array[AnimationCompressedTrack],
) -> AnimationCompressionStats {
  let mut source_keys = 0
  let mut output_keys = 0
  let mut source_components = 0
  let mut output_components = 0
  let mut maximum_error = 0.0
  let mut error_sum = 0.0
  for result in results {
    source_keys = source_keys + result.stats.source_keys
    output_keys = output_keys + result.stats.output_keys
    source_components = source_components + result.stats.source_components
    output_components = output_components + result.stats.output_components
    if result.stats.maximum_error > maximum_error {
      maximum_error = result.stats.maximum_error
    }
    error_sum = error_sum + result.stats.average_error
  }
  let removed = source_keys - output_keys
  {
    source_keys,
    output_keys,
    removed_keys: removed,
    source_components,
    output_components,
    maximum_error,
    average_error: if results.length() == 0 {
      0.0
    } else {
      error_sum / Double::from_int(results.length())
    },
    compression_ratio: if source_components == 0 {
      1.0
    } else {
      Double::from_int(output_components) / Double::from_int(source_components)
    },
    valid: results.all(fn(result) { result.stats.valid }),
  }
}

///|
pub fn animation_compressed_value(
  track : AnimationCompressedTrack,
  key : Int,
  component : Int,
) -> Double {
  animation_value_at(track.values, key, component, track.component_count)
}

///|
pub fn animation_compressed_duration(
  track : AnimationCompressedTrack,
) -> Double {
  match (track.times.get(0), track.times.get(track.times.length() - 1)) {
    (Some(first), Some(last)) => last - first
    _ => 0.0
  }
}

///|
pub fn animation_compressed_monotonic(track : AnimationCompressedTrack) -> Bool {
  animation_times_are_monotonic(track.times)
}

///|
pub fn animation_compressed_expand(
  track : AnimationCompressedTrack,
  count : Int,
) -> Array[Double] {
  let result : Array[Double] = []
  if count <= 0 || track.times.length() == 0 {
    return result
  }
  for sample in 0.. Array[String] {
  [
    "source_keys=\{stats.source_keys}",
    "output_keys=\{stats.output_keys}",
    "removed_keys=\{stats.removed_keys}",
    "source_components=\{stats.source_components}",
    "output_components=\{stats.output_components}",
    "maximum_error=\{stats.maximum_error}",
    "average_error=\{stats.average_error}",
    "compression_ratio=\{stats.compression_ratio}",
    "valid=\{stats.valid}",
  ]
}