///|
pub(all) struct BvhFrameWindow {
  start_frame : Int
  end_frame : Int
  frame_count : Int
  duration_seconds : Double
} derive(Eq, @debug.Debug)

///|
pub fn BvhFrameWindow::new(
  start_frame : Int,
  end_frame : Int,
  frame_time : Double,
) -> BvhFrameWindow {
  let count = if end_frame > start_frame { end_frame - start_frame } else { 0 }
  {
    start_frame,
    end_frame,
    frame_count: count,
    duration_seconds: count.to_double() * frame_time,
  }
}

///|
pub fn clamp_frame_index(document : BvhDocument, frame_index : Int) -> Int {
  if document.motion.frames.length() == 0 {
    0
  } else if frame_index < 0 {
    0
  } else if frame_index >= document.motion.frames.length() {
    document.motion.frames.length() - 1
  } else {
    frame_index
  }
}

///|
pub fn frame_at(document : BvhDocument, frame_index : Int) -> Array[Double] {
  if document.motion.frames.length() == 0 {
    []
  } else {
    document.motion.frames[clamp_frame_index(document, frame_index)].copy()
  }
}

///|
pub fn nearest_frame_index_at_time(
  document : BvhDocument,
  seconds : Double,
) -> Int {
  if document.motion.frame_time <= 0.0 || document.motion.frames.length() == 0 {
    0
  } else {
    let raw = (seconds / document.motion.frame_time).round().to_int()
    clamp_frame_index(document, raw)
  }
}

///|
pub fn frame_at_time(document : BvhDocument, seconds : Double) -> Array[Double] {
  frame_at(document, nearest_frame_index_at_time(document, seconds))
}

///|
pub fn frame_window(
  document : BvhDocument,
  start_frame : Int,
  end_frame : Int,
) -> BvhFrameWindow {
  let start = clamp_frame_index(document, start_frame)
  let raw_end = if end_frame < start {
    start
  } else if end_frame > document.motion.frames.length() {
    document.motion.frames.length()
  } else {
    end_frame
  }
  BvhFrameWindow::new(start, raw_end, document.motion.frame_time)
}

///|
pub fn slice_motion(
  motion : BvhMotion,
  start_frame : Int,
  end_frame : Int,
) -> BvhMotion {
  if motion.frames.length() == 0 {
    BvhMotion::empty()
  } else {
    let start = if start_frame < 0 { 0 } else { start_frame }
    let end = if end_frame < start {
      start
    } else if end_frame > motion.frames.length() {
      motion.frames.length()
    } else {
      end_frame
    }
    let frames : Array[Array[Double]] = []
    for i in start.. BvhDocument {
  {
    root: document.root,
    motion: slice_motion(document.motion, start_frame, end_frame),
    source_length: document.source_length,
    token_count: document.token_count,
  }
}

///|
pub fn first_frame(document : BvhDocument) -> Array[Double] {
  frame_at(document, 0)
}

///|
pub fn last_frame(document : BvhDocument) -> Array[Double] {
  frame_at(document, document.motion.frames.length() - 1)
}

///|
pub fn loop_closure_error(document : BvhDocument) -> Double {
  if document.motion.frames.length() <= 1 {
    0.0
  } else {
    root_position_at(document, document.motion.frames.length() - 1)
    .sub(root_position_at(document, 0))
    .magnitude()
  }
}

///|
pub(all) struct BvhSpeedSample {
  frame_index : Int
  seconds : Double
  distance_from_previous : Double
  speed_units_per_second : Double
} derive(Eq, @debug.Debug)

///|
pub fn BvhSpeedSample::new(
  frame_index : Int,
  seconds : Double,
  distance_from_previous : Double,
  speed_units_per_second : Double,
) -> BvhSpeedSample {
  { frame_index, seconds, distance_from_previous, speed_units_per_second }
}

///|
pub fn root_speed_samples(document : BvhDocument) -> Array[BvhSpeedSample] {
  let out : Array[BvhSpeedSample] = []
  if document.motion.frames.length() <= 1 || document.motion.frame_time <= 0.0 {
    return out
  }
  for i in 1.. Double {
  let samples = root_speed_samples(document)
  if samples.length() == 0 {
    0.0
  } else {
    let total = for sample in samples; sum = 0.0 {
      continue sum + sample.speed_units_per_second
    } nobreak {
      sum
    }
    total / samples.length().to_double()
  }
}

///|
pub fn max_root_speed(document : BvhDocument) -> Double {
  let samples = root_speed_samples(document)
  if samples.length() == 0 {
    0.0
  } else {
    for sample in samples; speed = samples[0].speed_units_per_second {
      continue max_double(speed, sample.speed_units_per_second)
    } nobreak {
      speed
    }
  }
}

///|
pub(all) struct BvhRootTeleport {
  frame_index : Int
  distance : Double
  threshold : Double
} derive(Eq, @debug.Debug)

///|
pub fn BvhRootTeleport::new(
  frame_index : Int,
  distance : Double,
  threshold : Double,
) -> BvhRootTeleport {
  { frame_index, distance, threshold }
}

///|
pub fn detect_root_teleports(
  document : BvhDocument,
  threshold : Double,
) -> Array[BvhRootTeleport] {
  let out : Array[BvhRootTeleport] = []
  if threshold <= 0.0 {
    return out
  }
  for sample in root_speed_samples(document) {
    if sample.distance_from_previous > threshold {
      out.push(
        BvhRootTeleport::new(
          sample.frame_index,
          sample.distance_from_previous,
          threshold,
        ),
      )
    }
  }
  out
}

///|
fn copy_frame_with_root_offset(
  frame : Array[Double],
  x_index : Int,
  y_index : Int,
  z_index : Int,
  first : BvhVec3,
) -> Array[Double] {
  let out = frame.copy()
  if x_index >= 0 && x_index < out.length() {
    out[x_index] = out[x_index] - first.x
  }
  if y_index >= 0 && y_index < out.length() {
    out[y_index] = out[y_index] - first.y
  }
  if z_index >= 0 && z_index < out.length() {
    out[z_index] = out[z_index] - first.z
  }
  out
}

///|
pub fn normalize_root_origin(document : BvhDocument) -> BvhDocument {
  if document.motion.frames.length() == 0 {
    document
  } else {
    let first = root_position_at(document, 0)
    let x_index = channel_index_for_path(
      document,
      document.root.path,
      ChannelXPosition,
    )
    let y_index = channel_index_for_path(
      document,
      document.root.path,
      ChannelYPosition,
    )
    let z_index = channel_index_for_path(
      document,
      document.root.path,
      ChannelZPosition,
    )
    let frames : Array[Array[Double]] = []
    for frame in document.motion.frames {
      frames.push(
        copy_frame_with_root_offset(frame, x_index, y_index, z_index, first),
      )
    }
    {
      root: document.root,
      motion: {
        frame_count: document.motion.frame_count,
        frame_time: document.motion.frame_time,
        channel_count: document.motion.channel_count,
        frames,
      },
      source_length: document.source_length,
      token_count: document.token_count,
    }
  }
}

///|
pub(all) struct BvhChannelMean {
  index : Int
  name : String
  mean : Double
  sample_count : Int
} derive(Eq, @debug.Debug)

///|
pub fn BvhChannelMean::new(
  index : Int,
  name : String,
  mean : Double,
  sample_count : Int,
) -> BvhChannelMean {
  { index, name, mean, sample_count }
}

///|
pub fn motion_channel_means(document : BvhDocument) -> Array[BvhChannelMean] {
  let out : Array[BvhChannelMean] = []
  let names = channels_in_motion_order(document)
  if document.motion.channel_count == 0 || document.motion.frames.length() == 0 {
    return out
  }
  for index in 0.. String {
  let rows : Array[String] = [
    "frame,seconds,distance_from_previous,speed_units_per_second",
  ]
  for sample in samples {
    rows.push(
      "\{sample.frame_index},\{sample.seconds},\{sample.distance_from_previous},\{sample.speed_units_per_second}",
    )
  }
  rows.join("\n")
}