///|
pub struct FrameMetrics {
  filename : String
  frame_area : Int
  source_area : Int
  duration : Int
  trimmed : Bool
  rotated : Bool
  collision_boxes : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct SheetMetrics {
  image : String
  frame_count : Int
  tag_count : Int
  slice_count : Int
  total_frame_area : Int
  total_source_area : Int
  total_duration : Int
  trimmed_frames : Int
  rotated_frames : Int
  collision_boxes : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct AtlasMetrics {
  width : Int
  height : Int
  frame_count : Int
  packed_area : Int
  occupancy : Double
  padding_gaps : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct AnimationMetrics {
  name : String
  frame_count : Int
  total_duration : Int
  unique_frame_count : Int
  looped : Bool
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn frame_metrics(frame : SourceFrame) -> FrameMetrics {
  {
    filename: frame.filename,
    frame_area: frame.frame.area(),
    source_area: frame.source_size.area(),
    duration: frame.duration,
    trimmed: frame.trimmed,
    rotated: frame.rotated,
    collision_boxes: frame.boxes.length(),
  }
}

///|
pub fn sheet_metrics(sheet : SpriteSheet) -> SheetMetrics {
  let mut frame_area = 0
  let mut source_area = 0
  let mut duration = 0
  let mut trimmed = 0
  let mut rotated = 0
  let mut boxes = 0
  for frame in sheet.frames {
    frame_area += frame.frame.area()
    source_area += frame.source_size.area()
    duration += frame.duration
    if frame.trimmed {
      trimmed += 1
    }
    if frame.rotated {
      rotated += 1
    }
    boxes += frame.boxes.length()
  }
  {
    image: sheet.image,
    frame_count: sheet.frames.length(),
    tag_count: sheet.tags.length(),
    slice_count: sheet.slices.length(),
    total_frame_area: frame_area,
    total_source_area: source_area,
    total_duration: duration,
    trimmed_frames: trimmed,
    rotated_frames: rotated,
    collision_boxes: boxes,
  }
}

///|
pub fn atlas_metrics(atlas : AtlasPlan) -> AtlasMetrics {
  {
    width: atlas.size.w,
    height: atlas.size.h,
    frame_count: atlas.frames.length(),
    packed_area: atlas.packed_area(),
    occupancy: atlas.occupancy,
    padding_gaps: atlas.padding_gaps(),
  }
}

///|
pub fn animation_metrics(clip : AnimationClip) -> AnimationMetrics {
  let names : Array[String] = []
  for name in clip.frames {
    if !names.contains(name) {
      names.push(name)
    }
  }
  {
    name: clip.name,
    frame_count: clip.frames.length(),
    total_duration: clip.total_duration(),
    unique_frame_count: names.length(),
    looped: clip.loop_mode == Loop,
  }
}

///|
pub fn all_frame_metrics(sheet : SpriteSheet) -> Array[FrameMetrics] {
  sheet.frames.map(frame_metrics)
}

///|
pub fn all_animation_metrics(
  machine : AnimationMachine,
) -> Array[AnimationMetrics] {
  machine.clips.map(animation_metrics)
}

///|
pub fn average_frame_duration(sheet : SpriteSheet) -> Double {
  if sheet.frames.length() == 0 {
    0.0
  } else {
    total_duration(sheet).to_double() / sheet.frames.length().to_double()
  }
}

///|
pub fn frame_area_ratio(frame : SourceFrame) -> Double {
  let source = frame.source_size.area()
  if source <= 0 {
    0.0
  } else {
    frame.frame.area().to_double() / source.to_double()
  }
}

///|
pub fn sheet_trim_ratio(sheet : SpriteSheet) -> Double {
  if sheet.frames.length() == 0 {
    0.0
  } else {
    sheet_metrics(sheet).trimmed_frames.to_double() /
    sheet.frames.length().to_double()
  }
}

///|
pub fn sheet_source_occupancy(sheet : SpriteSheet) -> Double {
  let area = sheet.size.area()
  if area <= 0 {
    0.0
  } else {
    sheet_metrics(sheet).total_frame_area.to_double() / area.to_double()
  }
}

///|
pub fn machine_duration(machine : AnimationMachine) -> Int {
  machine.clips.fold(init=0, (total, clip) => total + clip.total_duration())
}

///|
pub fn machine_unique_frame_names(machine : AnimationMachine) -> Array[String] {
  let names : Array[String] = []
  for clip in machine.clips {
    for frame in clip.frames {
      if !names.contains(frame) {
        names.push(frame)
      }
    }
  }
  names
}

///|
pub fn sheet_frame_names(sheet : SpriteSheet) -> Array[String] {
  sheet.frames.map(fn(frame) { frame.filename })
}

///|
pub fn sheet_tag_names(sheet : SpriteSheet) -> Array[String] {
  sheet.tags.map(fn(tag) { tag.name })
}

///|
pub fn sheet_slice_names(sheet : SpriteSheet) -> Array[String] {
  sheet.slices.map(fn(slice) { slice.name })
}

///|
pub fn sheet_find_frames(
  sheet : SpriteSheet,
  prefix : String,
) -> Array[SourceFrame] {
  sheet.frames.filter(fn(frame) { frame.filename.has_prefix(prefix) })
}

///|
pub fn sheet_frames_with_boxes(sheet : SpriteSheet) -> Array[SourceFrame] {
  sheet.frames.filter(fn(frame) { frame.boxes.length() > 0 })
}

///|
pub fn sheet_frames_with_duration(
  sheet : SpriteSheet,
  minimum : Int,
) -> Array[SourceFrame] {
  sheet.frames.filter(fn(frame) { frame.duration >= minimum })
}

///|
pub fn sheet_largest_frame(sheet : SpriteSheet) -> SourceFrame? {
  match sheet.frames.get(0) {
    None => None
    Some(first) => {
      let mut result = first
      for frame in sheet.frames[1:] {
        if frame.source_size.area() > result.source_size.area() {
          result = frame
        }
      }
      Some(result)
    }
  }
}

///|
pub fn sheet_longest_frame(sheet : SpriteSheet) -> SourceFrame? {
  match sheet.frames.get(0) {
    None => None
    Some(first) => {
      let mut result = first
      for frame in sheet.frames[1:] {
        if frame.duration > result.duration {
          result = frame
        }
      }
      Some(result)
    }
  }
}

///|
pub fn sheet_frame_index(sheet : SpriteSheet, filename : String) -> Int? {
  sheet.frames.search_by(fn(frame) { frame.filename == filename })
}

///|
pub fn sheet_tag_index(sheet : SpriteSheet, name : String) -> Int? {
  sheet.tags.search_by(fn(tag) { tag.name == name })
}

///|
pub fn sheet_slice_index(sheet : SpriteSheet, name : String) -> Int? {
  sheet.slices.search_by(fn(slice) { slice.name == name })
}

///|
pub fn atlas_frame_names(atlas : AtlasPlan) -> Array[String] {
  atlas.frames.map(fn(frame) { frame.filename })
}

///|
pub fn atlas_source_bounds(atlas : AtlasPlan) -> Rect? {
  rect_bounds(atlas.frames.map(fn(frame) { frame.packed }))
}

///|
pub fn atlas_has_overlap(atlas : AtlasPlan) -> Bool {
  rects_overlap_any(atlas.frames.map(fn(frame) { frame.packed }))
}

///|
pub fn atlas_is_deterministic(left : AtlasPlan, right : AtlasPlan) -> Bool {
  left.to_json_string(indent=0) == right.to_json_string(indent=0)
}

///|
pub fn animation_contains(clip : AnimationClip, filename : String) -> Bool {
  clip.frames.contains(filename)
}

///|
pub fn machine_contains_clip(machine : AnimationMachine, name : String) -> Bool {
  machine.clip_named(name) is Some(_)
}

///|
pub fn machine_transition_count(machine : AnimationMachine) -> Int {
  machine.transitions.length()
}

///|
pub fn diagnostics_error_count(sheet : SpriteSheet) -> Int {
  summarize_diagnostics(validate_sheet(sheet)).errors
}

///|
pub fn diagnostics_warning_count(sheet : SpriteSheet) -> Int {
  summarize_diagnostics(validate_sheet(sheet)).warnings
}

///|
pub fn sheet_is_valid(sheet : SpriteSheet) -> Bool {
  diagnostics_error_count(sheet) == 0
}

///|
pub fn sheet_summary(sheet : SpriteSheet) -> String {
  let metrics = sheet_metrics(sheet)
  "\{metrics.image}: \{metrics.frame_count} frames, \{metrics.tag_count} tags, \{metrics.slice_count} slices"
}