///|
pub fn analyze_bvh(document : BvhDocument) -> BvhStats {
  if !document.is_complete() {
    BvhStats::empty()
  } else {
    {
      joint_count: document.joint_count(),
      end_site_count: document.end_site_count(),
      max_depth: max_joint_depth(document.root),
      channel_count: document.motion.channel_count,
      frame_count: document.motion.frame_count,
      frame_time: document.motion.frame_time,
      duration_seconds: document.duration_seconds(),
      root_motion_distance: root_motion_distance(document),
    }
  }
}

///|
pub(all) struct BvhChannelRange {
  path : String
  channel : String
  index : Int
  min : Double
  max : Double
  span : Double
} derive(Eq, @debug.Debug)

///|
pub fn BvhChannelRange::new(
  path : String,
  channel : String,
  index : Int,
  min : Double,
  max : Double,
) -> BvhChannelRange {
  { path, channel, index, min, max, span: max - min }
}

///|
fn collect_channel_ranges_for_joint(
  document : BvhDocument,
  joint : BvhJoint,
  out : Array[BvhChannelRange],
) -> Unit {
  for channel_offset in 0.. 0 &&
      index < document.motion.frames[0].length() {
      document.motion.frames[0][index]
    } else {
      0.0
    }
    let mut min_value = first
    let mut max_value = first
    for frame in document.motion.frames {
      if index >= 0 && index < frame.length() {
        min_value = min_double(min_value, frame[index])
        max_value = max_double(max_value, frame[index])
      }
    }
    out.push(
      BvhChannelRange::new(
        joint.path,
        joint.channels[channel_offset].to_string(),
        index,
        min_value,
        max_value,
      ),
    )
  }
  for child in joint.children {
    collect_channel_ranges_for_joint(document, child, out)
  }
}

///|
pub fn channel_ranges(document : BvhDocument) -> Array[BvhChannelRange] {
  let out : Array[BvhChannelRange] = []
  if document.root.name.length() > 0 {
    collect_channel_ranges_for_joint(document, document.root, out)
  }
  out
}

///|
pub fn widest_channel_range(document : BvhDocument) -> BvhChannelRange? {
  let ranges = channel_ranges(document)
  if ranges.length() == 0 {
    None
  } else {
    let mut best = ranges[0]
    for i in 1.. abs_double(best.span) {
        best = ranges[i]
      }
    }
    Some(best)
  }
}

///|
pub fn detect_static_channels(
  document : BvhDocument,
  epsilon? : Double = 0.00001,
) -> Array[BvhChannelRange] {
  let out : Array[BvhChannelRange] = []
  for range in channel_ranges(document) {
    if abs_double(range.span) <= epsilon {
      out.push(range)
    }
  }
  out
}

///|
pub(all) struct BvhPoseSample {
  frame_index : Int
  root_position : BvhVec3
  root_delta_from_first : BvhVec3
} derive(Eq, @debug.Debug)

///|
pub fn BvhPoseSample::new(
  frame_index : Int,
  root_position : BvhVec3,
  root_delta_from_first : BvhVec3,
) -> BvhPoseSample {
  { frame_index, root_position, root_delta_from_first }
}

///|
pub fn sample_root_motion(
  document : BvhDocument,
  max_samples? : Int = 8,
) -> Array[BvhPoseSample] {
  if document.motion.frames.length() == 0 || max_samples <= 0 {
    []
  } else {
    let out : Array[BvhPoseSample] = []
    let first = root_position_at(document, 0)
    let step = max_int(1, document.motion.frames.length() / max_samples)
    let mut index = 0
    while index < document.motion.frames.length() {
      let position = root_position_at(document, index)
      out.push(BvhPoseSample::new(index, position, position.sub(first)))
      index += step
    }
    if out.length() == 0 ||
      out[out.length() - 1].frame_index != document.motion.frames.length() - 1 {
      let last_index = document.motion.frames.length() - 1
      let position = root_position_at(document, last_index)
      out.push(BvhPoseSample::new(last_index, position, position.sub(first)))
    }
    out
  }
}

///|
pub fn bvh_summary_line(document : BvhDocument) -> String {
  let stats = analyze_bvh(document)
  "joints=\{stats.joint_count}, end_sites=\{stats.end_site_count}, channels=\{stats.channel_count}, frames=\{stats.frame_count}, duration=\{stats.duration_seconds}s"
}