///|
/// Animation timeline inspection and deterministic sampling.
///
/// Import pipelines need more than a channel count: they need to know whether
/// key times are ordered, how long a clip lasts, whether a target can be
/// sampled safely, and what happens at the ends of a playback interval.

///|
pub(all) enum AnimationPlaybackMode {
  Clamp
  Repeat
  Mirror
} derive(Debug, ToJson, Eq)

///|
pub(all) enum AnimationTrackStatus {
  Valid
  Empty
  InvalidReference
  InvalidTimes
  UnsupportedShape
  DecodeFailed
} derive(Debug, ToJson, Eq)

///|
pub(all) struct AnimationTrack {
  animation_index : Int
  channel_index : Int
  sampler_index : Int
  node : Int?
  path : AnimationPath
  interpolation : Interpolation
  input_accessor : Int
  output_accessor : Int
  times : Array[Double]
  values : Array[Double]
  component_count : Int
  status : AnimationTrackStatus
  errors : Array[String]
} derive(Debug, ToJson, Eq)

///|
pub(all) struct AnimationClipReport {
  animation_index : Int
  name : String?
  channel_count : Int
  sampler_count : Int
  track_count : Int
  duration : Double
  start_time : Double
  end_time : Double
  loopable : Bool
  invalid_track_count : Int
  target_node_count : Int
  animated_path_count : Int
  tracks : Array[AnimationTrack]
} derive(Debug, ToJson, Eq)

///|
pub(all) struct AnimationSample {
  time : Double
  track_index : Int
  node : Int?
  path : AnimationPath
  values : Array[Double]
  normalized_time : Double
  clamped : Bool
  valid : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct AnimationPoseValue {
  node : Int
  translation : Vec3?
  rotation : Quat?
  scale : Vec3?
  weights : Array[Double]
} derive(Debug, ToJson, Eq)

///|
pub(all) struct AnimationPose {
  animation_index : Int
  time : Double
  duration : Double
  values : Array[AnimationPoseValue]
  valid : Bool
  errors : Array[String]
} derive(Debug, ToJson, Eq)

///|
pub(all) struct AnimationBudget {
  max_clips : Int
  max_channels : Int
  max_duration : Double
  max_keyframes : Int
  require_monotonic_times : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct AnimationBudgetResult {
  passed : Bool
  violations : Array[String]
  clip_overage : Int
  channel_overage : Int
  duration_overage : Double
  keyframe_overage : Int
} derive(Debug, ToJson, Eq)

///|
pub fn default_animation_budget() -> AnimationBudget {
  {
    max_clips: 256,
    max_channels: 10000,
    max_duration: 3600.0,
    max_keyframes: 2_000_000,
    require_monotonic_times: true,
  }
}

///|
pub fn animation_playback_time(
  time : Double,
  duration : Double,
  mode : AnimationPlaybackMode,
) -> (Double, Bool) {
  if duration <= 0.0 {
    (0.0, true)
  } else {
    match mode {
      Clamp =>
        if time < 0.0 {
          (0.0, true)
        } else if time > duration {
          (duration, true)
        } else {
          (time, false)
        }
      Repeat => {
        let cycle = time % duration
        let normalized = if cycle < 0.0 { cycle + duration } else { cycle }
        (normalized, time < 0.0 || time > duration)
      }
      Mirror => {
        let period = duration * 2.0
        let cycle = time % period
        let normalized = if cycle < 0.0 { cycle + period } else { cycle }
        if normalized <= duration {
          (normalized, time < 0.0 || time > duration)
        } else {
          (period - normalized, time < 0.0 || time > duration)
        }
      }
    }
  }
}

///|
pub fn animation_path_name(path : AnimationPath) -> String {
  match path {
    Translation => "translation"
    Rotation => "rotation"
    Scale => "scale"
    Weights => "weights"
  }
}

///|
pub fn interpolation_name(interpolation : Interpolation) -> String {
  match interpolation {
    Linear => "LINEAR"
    Step => "STEP"
    CubicSpline => "CUBICSPLINE"
  }
}

///|
pub fn animation_value_width(path : AnimationPath) -> Int {
  match path {
    Translation | Scale => 3
    Rotation => 4
    Weights => 1
  }
}

///|
fn empty_track(
  animation_index : Int,
  channel_index : Int,
  sampler_index : Int,
  node : Int?,
  path : AnimationPath,
  input_accessor : Int,
  output_accessor : Int,
  status : AnimationTrackStatus,
  error : String,
) -> AnimationTrack {
  {
    animation_index,
    channel_index,
    sampler_index,
    node,
    path,
    interpolation: Linear,
    input_accessor,
    output_accessor,
    times: [],
    values: [],
    component_count: animation_value_width(path),
    status,
    errors: [error],
  }
}

///|
pub fn animation_track(
  doc : GltfDocument,
  asset : BinaryAsset,
  animation_index : Int,
  channel_index : Int,
) -> AnimationTrack {
  match doc.animations.get(animation_index) {
    None =>
      empty_track(
        animation_index,
        channel_index,
        -1,
        None,
        Translation,
        -1,
        -1,
        InvalidReference,
        "animation index is outside the document",
      )
    Some(animation) =>
      match animation.channels.get(channel_index) {
        None =>
          empty_track(
            animation_index,
            channel_index,
            -1,
            None,
            Translation,
            -1,
            -1,
            InvalidReference,
            "channel index is outside the animation",
          )
        Some(channel) =>
          match animation.samplers.get(channel.sampler) {
            None =>
              empty_track(
                animation_index,
                channel_index,
                channel.sampler,
                channel.target.node,
                channel.target.path,
                -1,
                -1,
                InvalidReference,
                "sampler index is outside the animation",
              )
            Some(sampler) => {
              let times = decode_accessor_scalars(doc, asset, sampler.input)
              let decoded = decode_accessor(doc, asset, sampler.output)
              let expected = animation_value_width(channel.target.path)
              let mut status = Valid
              let errors : Array[String] = []
              if times.length() == 0 {
                status = Empty
                errors.push("animation input accessor is empty")
              }
              if !decoded.valid {
                status = DecodeFailed
                for error in decoded.errors {
                  errors.push(error)
                }
              }
              if decoded.component_count != expected &&
                channel.target.path != Weights {
                status = UnsupportedShape
                errors.push(
                  "animation output component count does not match the target path",
                )
              }
              if times.length() != decoded.count &&
                channel.target.path != Weights {
                status = UnsupportedShape
                errors.push("input and output key counts do not match")
              }
              if !animation_times_are_monotonic(times) {
                status = InvalidTimes
                errors.push("animation input times are not monotonic")
              }
              {
                animation_index,
                channel_index,
                sampler_index: channel.sampler,
                node: channel.target.node,
                path: channel.target.path,
                interpolation: sampler.interpolation,
                input_accessor: sampler.input,
                output_accessor: sampler.output,
                times,
                values: decoded.values,
                component_count: decoded.component_count,
                status,
                errors,
              }
            }
          }
      }
  }
}

///|
pub fn animation_tracks(
  doc : GltfDocument,
  asset : BinaryAsset,
  animation_index : Int,
) -> Array[AnimationTrack] {
  match doc.animations.get(animation_index) {
    None => []
    Some(animation) => {
      let tracks : Array[AnimationTrack] = []
      for channel in 0.. Array[AnimationTrack] {
  let tracks : Array[AnimationTrack] = []
  for animation in 0.. Bool {
  let mut result = true
  let mut previous : Double? = None
  for value in times {
    match previous {
      Some(old) => if value < old { result = false }
      None => ()
    }
    previous = Some(value)
  }
  result
}

///|
pub fn animation_times_are_strict(times : Array[Double]) -> Bool {
  let mut result = true
  let mut previous : Double? = None
  for value in times {
    match previous {
      Some(old) => if value <= old { result = false }
      None => ()
    }
    previous = Some(value)
  }
  result
}

///|
pub fn animation_time_start(times : Array[Double]) -> Double {
  match times.get(0) {
    Some(value) => value
    None => 0.0
  }
}

///|
pub fn animation_time_end(times : Array[Double]) -> Double {
  match times.get(times.length() - 1) {
    Some(value) => value
    None => 0.0
  }
}

///|
pub fn animation_duration_from_times(times : Array[Double]) -> Double {
  let start = animation_time_start(times)
  let end = animation_time_end(times)
  if end > start {
    end - start
  } else {
    0.0
  }
}

///|
pub fn animation_track_duration(track : AnimationTrack) -> Double {
  animation_duration_from_times(track.times)
}

///|
pub fn animation_track_key_count(track : AnimationTrack) -> Int {
  track.times.length()
}

///|
pub fn animation_track_is_valid(track : AnimationTrack) -> Bool {
  track.status == Valid && track.times.length() > 0
}

///|
pub fn animation_track_is_rotation(track : AnimationTrack) -> Bool {
  track.path == Rotation
}

///|
pub fn animation_track_is_transform(track : AnimationTrack) -> Bool {
  track.path == Translation || track.path == Rotation || track.path == Scale
}

///|
pub fn animation_track_value(
  track : AnimationTrack,
  key : Int,
) -> Array[Double] {
  if key < 0 || key >= track.times.length() || track.component_count <= 0 {
    []
  } else {
    let start = key * track.component_count
    let result : Array[Double] = []
    for component in 0.. result.push(value)
        None => ()
      }
    }
    result
  }
}

///|
pub fn animation_track_find_interval(
  track : AnimationTrack,
  time : Double,
) -> (Int, Int, Double) {
  if track.times.length() == 0 {
    (0, 0, 0.0)
  } else if time <= track.times[0] {
    (0, 0, 0.0)
  } else if time >= track.times[track.times.length() - 1] {
    let last = track.times.length() - 1
    (last, last, 0.0)
  } else {
    let mut left = 0
    let mut right = track.times.length() - 1
    while left + 1 < right {
      let middle = (left + right) / 2
      if track.times[middle] <= time {
        left = middle
      } else {
        right = middle
      }
    }
    let span = track.times[right] - track.times[left]
    let factor = if span <= 0.0 {
      0.0
    } else {
      (time - track.times[left]) / span
    }
    (left, right, factor)
  }
}

///|
pub fn interpolate_scalar(
  left : Double,
  right : Double,
  factor : Double,
) -> Double {
  left + (right - left) * factor
}

///|
pub fn interpolate_array(
  left : Array[Double],
  right : Array[Double],
  factor : Double,
) -> Array[Double] {
  let count = if left.length() < right.length() {
    left.length()
  } else {
    right.length()
  }
  let result : Array[Double] = []
  for index in 0.. Array[Double] {
  animation_track_value(track, left)
}

///|
pub fn interpolate_linear(
  track : AnimationTrack,
  left : Int,
  right : Int,
  factor : Double,
) -> Array[Double] {
  if left == right {
    animation_track_value(track, left)
  } else {
    interpolate_array(
      animation_track_value(track, left),
      animation_track_value(track, right),
      factor,
    )
  }
}

///|
pub fn cubic_hermite(
  p0 : Double,
  m0 : Double,
  p1 : Double,
  m1 : Double,
  factor : Double,
  duration : Double,
) -> Double {
  let t = factor
  let t2 = t * t
  let t3 = t2 * t
  let h00 = 2.0 * t3 - 3.0 * t2 + 1.0
  let h10 = t3 - 2.0 * t2 + t
  let h01 = -2.0 * t3 + 3.0 * t2
  let h11 = t3 - t2
  h00 * p0 + h10 * m0 * duration + h01 * p1 + h11 * m1 * duration
}

///|
pub fn interpolate_cubic(
  track : AnimationTrack,
  left : Int,
  right : Int,
  factor : Double,
) -> Array[Double] {
  if track.component_count <= 0 ||
    track.values.length() < track.times.length() * track.component_count * 3 {
    interpolate_linear(track, left, right, factor)
  } else {
    let width = track.component_count
    let left_base = left * width * 3
    let right_base = right * width * 3
    let duration = if right == left {
      0.0
    } else {
      track.times[right] - track.times[left]
    }
    let result : Array[Double] = []
    for component in 0.. AnimationSample {
  let duration = animation_track_duration(track)
  let (sample_time, clamped) = animation_playback_time(time, duration, mode)
  if !animation_track_is_valid(track) {
    {
      time: sample_time,
      track_index: track.channel_index,
      node: track.node,
      path: track.path,
      values: [],
      normalized_time: 0.0,
      clamped,
      valid: false,
    }
  } else {
    let (left, right, factor) = animation_track_find_interval(
      track, sample_time,
    )
    let values = match track.interpolation {
      Step => interpolate_step(track, left)
      Linear => interpolate_linear(track, left, right, factor)
      CubicSpline => interpolate_cubic(track, left, right, factor)
    }
    let normalized_time = if duration <= 0.0 {
      0.0
    } else {
      (sample_time - animation_time_start(track.times)) / duration
    }
    {
      time: sample_time,
      track_index: track.channel_index,
      node: track.node,
      path: track.path,
      values,
      normalized_time,
      clamped,
      valid: true,
    }
  }
}

///|
pub fn sample_animation_track(
  doc : GltfDocument,
  asset : BinaryAsset,
  animation_index : Int,
  channel_index : Int,
  time : Double,
) -> AnimationSample {
  let track = animation_track(doc, asset, animation_index, channel_index)
  sample_animation_track_at(track, time, Clamp)
}

///|
fn values_to_vec3(values : Array[Double]) -> Vec3? {
  if values.length() < 3 {
    None
  } else {
    Some({ x: values[0], y: values[1], z: values[2] })
  }
}

///|
fn values_to_quat(values : Array[Double]) -> Quat? {
  if values.length() < 4 {
    None
  } else {
    Some({ x: values[0], y: values[1], z: values[2], w: values[3] })
  }
}

///|
pub fn normalize_quaternion(value : Quat) -> Quat {
  let length = Double::sqrt(
    value.x * value.x +
    value.y * value.y +
    value.z * value.z +
    value.w * value.w,
  )
  if length <= 0.0 {
    { x: 0.0, y: 0.0, z: 0.0, w: 1.0 }
  } else {
    {
      x: value.x / length,
      y: value.y / length,
      z: value.z / length,
      w: value.w / length,
    }
  }
}

///|
pub fn quaternion_dot(a : Quat, b : Quat) -> Double {
  a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w
}

///|
pub fn quaternion_lerp(a : Quat, b : Quat, factor : Double) -> Quat {
  normalize_quaternion({
    x: interpolate_scalar(a.x, b.x, factor),
    y: interpolate_scalar(a.y, b.y, factor),
    z: interpolate_scalar(a.z, b.z, factor),
    w: interpolate_scalar(a.w, b.w, factor),
  })
}

///|
pub fn animation_pose(
  doc : GltfDocument,
  asset : BinaryAsset,
  animation_index : Int,
  time : Double,
  mode : AnimationPlaybackMode,
) -> AnimationPose {
  let tracks = animation_tracks(doc, asset, animation_index)
  let values : Map[Int, AnimationPoseValue] = Map([])
  let errors : Array[String] = []
  let duration = tracks.fold(init=0.0, (maximum, track) => {
    let value = animation_track_duration(track)
    if value > maximum {
      value
    } else {
      maximum
    }
  })
  let mut valid = true
  for track in tracks {
    let sample = sample_animation_track_at(track, time, mode)
    match sample.node {
      None => {
        valid = false
        errors.push("animation track has no target node")
      }
      Some(node) => {
        let old = match values.get(node) {
          Some(value) => value
          None =>
            {
              node,
              translation: None,
              rotation: None,
              scale: None,
              weights: [],
            }
        }
        let next = match track.path {
          Translation => { ..old, translation: values_to_vec3(sample.values) }
          Rotation => { ..old, rotation: values_to_quat(sample.values) }
          Scale => { ..old, scale: values_to_vec3(sample.values) }
          Weights => { ..old, weights: sample.values }
        }
        values[node] = next
      }
    }
    if !sample.valid {
      valid = false
    }
  }
  let pose_values : Array[AnimationPoseValue] = []
  for _, value in values {
    pose_values.push(value)
  }
  { animation_index, time, duration, values: pose_values, valid, errors }
}

///|
pub fn animation_clip_report(
  doc : GltfDocument,
  asset : BinaryAsset,
  animation_index : Int,
) -> AnimationClipReport {
  let tracks = animation_tracks(doc, asset, animation_index)
  let mut start = 0.0
  let mut end = 0.0
  let mut first = true
  let mut invalid = 0
  let targets : Map[Int, Bool] = Map([])
  let paths : Map[String, Bool] = Map([])
  for track in tracks {
    if !animation_track_is_valid(track) {
      invalid += 1
    }
    if track.times.length() > 0 {
      let one_start = animation_time_start(track.times)
      let one_end = animation_time_end(track.times)
      if first || one_start < start {
        start = one_start
      }
      if first || one_end > end {
        end = one_end
      }
      first = false
    }
    match track.node {
      Some(node) => targets[node] = true
      None => ()
    }
    paths[animation_path_name(track.path)] = true
  }
  let name = match doc.animations.get(animation_index) {
    Some(animation) => animation.name
    None => None
  }
  {
    animation_index,
    name,
    channel_count: match doc.animations.get(animation_index) {
      Some(animation) => animation.channels.length()
      None => 0
    },
    sampler_count: match doc.animations.get(animation_index) {
      Some(animation) => animation.samplers.length()
      None => 0
    },
    track_count: tracks.length(),
    duration: if end > start {
      end - start
    } else {
      0.0
    },
    start_time: start,
    end_time: end,
    loopable: tracks.length() > 0 && invalid == 0,
    invalid_track_count: invalid,
    target_node_count: targets.length(),
    animated_path_count: paths.length(),
    tracks,
  }
}

///|
pub fn all_animation_clip_reports(
  doc : GltfDocument,
  asset : BinaryAsset,
) -> Array[AnimationClipReport] {
  let reports : Array[AnimationClipReport] = []
  for index in 0.. Int {
  reports.fold(init=0, (sum, report) => {
    report.tracks.fold(init=sum, (inner, track) => inner + track.times.length())
  })
}

///|
pub fn longest_animation_clip(reports : Array[AnimationClipReport]) -> Int? {
  let mut best : Int? = None
  let mut duration = -1.0
  for report in reports {
    if report.duration > duration {
      duration = report.duration
      best = Some(report.animation_index)
    }
  }
  best
}

///|
pub fn animation_budget_result(
  doc : GltfDocument,
  reports : Array[AnimationClipReport],
  budget : AnimationBudget,
) -> AnimationBudgetResult {
  ignore(doc)
  animation_budget_result_from_reports(reports, budget)
}

///|
/// Evaluate animation limits from already decoded reports.
///
/// Keeping this operation report-based makes it safe for callers that have
/// decoded animations from a GLB buffer and prevents a second decode from an
/// unrelated or empty binary asset.
pub fn animation_budget_result_from_reports(
  reports : Array[AnimationClipReport],
  budget : AnimationBudget,
) -> AnimationBudgetResult {
  let clip_overage = if reports.length() > budget.max_clips {
    reports.length() - budget.max_clips
  } else {
    0
  }
  let channels = reports.fold(init=0, (sum, report) => {
    sum + report.channel_count
  })
  let channel_overage = if channels > budget.max_channels {
    channels - budget.max_channels
  } else {
    0
  }
  let duration = reports.fold(init=0.0, (maximum, report) => {
    if report.duration > maximum {
      report.duration
    } else {
      maximum
    }
  })
  let duration_overage = if duration > budget.max_duration {
    duration - budget.max_duration
  } else {
    0.0
  }
  let keyframes = total_animation_keyframes(reports)
  let keyframe_overage = if keyframes > budget.max_keyframes {
    keyframes - budget.max_keyframes
  } else {
    0
  }
  let violations : Array[String] = []
  if clip_overage > 0 {
    violations.push("animation clip budget exceeded by \{clip_overage}")
  }
  if channel_overage > 0 {
    violations.push("animation channel budget exceeded by \{channel_overage}")
  }
  if duration_overage > 0.0 {
    violations.push("animation duration exceeds the configured budget")
  }
  if keyframe_overage > 0 {
    violations.push("animation keyframe budget exceeded by \{keyframe_overage}")
  }
  if budget.require_monotonic_times {
    for report in reports {
      for track in report.tracks {
        if !animation_times_are_monotonic(track.times) {
          violations.push("animation input times are not monotonic")
        }
      }
    }
  }
  {
    passed: violations.length() == 0,
    violations,
    clip_overage,
    channel_overage,
    duration_overage,
    keyframe_overage,
  }
}

///|
pub fn animation_report_summary_lines(
  reports : Array[AnimationClipReport],
) -> Array[String] {
  [
    "clips=\{reports.length()}, keyframes=\{total_animation_keyframes(reports)}",
    "longest=\{Repr(longest_animation_clip(reports))}",
    "durations=\{reports.map(report => "\{report.duration}").join(",")}",
  ]
}