///|
pub(all) struct SpriteSheetStats {
  frame_count : Int
  tag_count : Int
  slice_count : Int
  layer_count : Int
  atlas_area : Int
  used_area : Int
  utilization_milli : Int
  trimmed_frames : Int
  rotated_frames : Int
  total_duration_ms : Int
  shortest_duration_ms : Int
  longest_duration_ms : Int
  average_duration_ms : Int
} derive(Eq, @debug.Debug)

///|
pub fn SpriteSheetStats::empty() -> SpriteSheetStats {
  {
    frame_count: 0,
    tag_count: 0,
    slice_count: 0,
    layer_count: 0,
    atlas_area: 0,
    used_area: 0,
    utilization_milli: 0,
    trimmed_frames: 0,
    rotated_frames: 0,
    total_duration_ms: 0,
    shortest_duration_ms: 0,
    longest_duration_ms: 0,
    average_duration_ms: 0,
  }
}

///|
pub fn SpriteSheetStats::utilization_percent(self : SpriteSheetStats) -> Double {
  self.utilization_milli.to_double() / 10.0
}

///|
pub fn analyze_sprite_sheet(sheet : SpriteSheet) -> SpriteSheetStats {
  if sheet.frames.length() == 0 {
    SpriteSheetStats::empty()
  } else {
    let used = for frame in sheet.frames; total = 0 {
      continue total + frame.rect.area()
    } nobreak {
      total
    }
    let trimmed = for frame in sheet.frames; total = 0 {
      if frame.trimmed {
        continue total + 1
      } else {
        continue total
      }
    } nobreak {
      total
    }
    let rotated = for frame in sheet.frames; total = 0 {
      if frame.rotated {
        continue total + 1
      } else {
        continue total
      }
    } nobreak {
      total
    }
    let shortest = for
      frame in sheet.frames
      value = sheet.frames[0].duration_ms {
      if frame.duration_ms < value {
        continue frame.duration_ms
      } else {
        continue value
      }
    } nobreak {
      value
    }
    let longest = for frame in sheet.frames; value = 0 {
      if frame.duration_ms > value {
        continue frame.duration_ms
      } else {
        continue value
      }
    } nobreak {
      value
    }
    let atlas_area = sheet.meta.size.area()
    let utilization = if atlas_area <= 0 { 0 } else { used * 1000 / atlas_area }
    {
      frame_count: sheet.frames.length(),
      tag_count: sheet.meta.frame_tags.length(),
      slice_count: sheet.meta.slices.length(),
      layer_count: sheet.meta.layers.length(),
      atlas_area,
      used_area: used,
      utilization_milli: utilization,
      trimmed_frames: trimmed,
      rotated_frames: rotated,
      total_duration_ms: sheet.duration_ms(),
      shortest_duration_ms: shortest,
      longest_duration_ms: longest,
      average_duration_ms: sheet.duration_ms() / sheet.frames.length(),
    }
  }
}

///|
pub(all) struct FrameTimelineEntry {
  frame_index : Int
  frame_name : String
  start_ms : Int
  end_ms : Int
  duration_ms : Int
} derive(Eq, @debug.Debug)

///|
pub fn FrameTimelineEntry::new(
  frame_index : Int,
  frame_name : String,
  start_ms : Int,
  duration_ms : Int,
) -> FrameTimelineEntry {
  {
    frame_index,
    frame_name,
    start_ms,
    end_ms: start_ms + duration_ms,
    duration_ms,
  }
}

///|
pub fn FrameTimelineEntry::contains(
  self : FrameTimelineEntry,
  ms : Int,
) -> Bool {
  ms >= self.start_ms && ms < self.end_ms
}

///|
pub fn build_sheet_timeline(sheet : SpriteSheet) -> Array[FrameTimelineEntry] {
  let entries : Array[FrameTimelineEntry] = []
  let mut start = 0
  for frame in sheet.frames {
    entries.push(
      FrameTimelineEntry::new(frame.index, frame.name, start, frame.duration_ms),
    )
    start = start + frame.duration_ms
  }
  entries
}

///|
pub fn build_tag_timeline(
  sheet : SpriteSheet,
  tag_name : String,
) -> Array[FrameTimelineEntry] {
  match sheet.find_tag(tag_name) {
    None => []
    Some(tag) => {
      let entries : Array[FrameTimelineEntry] = []
      let mut start = 0
      if tag.direction == DirectionReverse ||
        tag.direction == DirectionPingPongReverse {
        let mut i = tag.to_index
        while i >= tag.from_index {
          match sheet.frame_at(i) {
            Some(frame) => {
              entries.push(
                FrameTimelineEntry::new(
                  frame.index,
                  frame.name,
                  start,
                  frame.duration_ms,
                ),
              )
              start = start + frame.duration_ms
            }
            None => ()
          }
          i = i - 1
        }
      } else {
        for i in tag.from_index..<=tag.to_index {
          match sheet.frame_at(i) {
            Some(frame) => {
              entries.push(
                FrameTimelineEntry::new(
                  frame.index,
                  frame.name,
                  start,
                  frame.duration_ms,
                ),
              )
              start = start + frame.duration_ms
            }
            None => ()
          }
        }
      }
      entries
    }
  }
}

///|
pub fn timeline_total_duration(entries : Array[FrameTimelineEntry]) -> Int {
  if entries.length() == 0 {
    0
  } else {
    entries[entries.length() - 1].end_ms
  }
}

///|
pub fn timeline_frame_at(
  entries : Array[FrameTimelineEntry],
  elapsed_ms : Int,
  looped? : Bool = true,
) -> FrameTimelineEntry? {
  if entries.length() == 0 || elapsed_ms < 0 {
    None
  } else {
    let total = timeline_total_duration(entries)
    let ms = if looped && total > 0 {
      elapsed_ms % total
    } else if elapsed_ms >= total {
      total - 1
    } else {
      elapsed_ms
    }
    for entry in entries {
      if entry.contains(ms) {
        return Some(entry)
      }
    }
    Some(entries[entries.length() - 1])
  }
}

///|
pub(all) struct DurationBucket {
  name : String
  min_ms : Int
  max_ms : Int
  count : Int
} derive(Eq, @debug.Debug)

///|
pub fn DurationBucket::new(
  name : String,
  min_ms : Int,
  max_ms : Int,
  count? : Int = 0,
) -> DurationBucket {
  { name, min_ms, max_ms, count }
}

///|
pub fn build_duration_buckets(sheet : SpriteSheet) -> Array[DurationBucket] {
  let fast = DurationBucket::new("fast", 0, 80)
  let normal = DurationBucket::new("normal", 81, 140)
  let slow = DurationBucket::new("slow", 141, 1000000)
  let buckets = [fast, normal, slow]
  for frame in sheet.frames {
    if frame.duration_ms <= 80 {
      buckets[0] = { ..buckets[0], count: buckets[0].count + 1 }
    } else if frame.duration_ms <= 140 {
      buckets[1] = { ..buckets[1], count: buckets[1].count + 1 }
    } else {
      buckets[2] = { ..buckets[2], count: buckets[2].count + 1 }
    }
  }
  buckets
}

///|
pub(all) struct AssetPolicy {
  min_frames : Int
  max_frames : Int
  require_tags : Bool
  require_box_slices : Bool
  allow_rotated_frames : Bool
  max_frame_duration_ms : Int
  max_atlas_waste_milli : Int
} derive(Eq, @debug.Debug)

///|
pub fn AssetPolicy::game_sprite_default() -> AssetPolicy {
  {
    min_frames: 1,
    max_frames: 512,
    require_tags: true,
    require_box_slices: true,
    allow_rotated_frames: false,
    max_frame_duration_ms: 1000,
    max_atlas_waste_milli: 850,
  }
}

///|
pub fn AssetPolicy::relaxed() -> AssetPolicy {
  {
    min_frames: 1,
    max_frames: 4096,
    require_tags: false,
    require_box_slices: false,
    allow_rotated_frames: true,
    max_frame_duration_ms: 10000,
    max_atlas_waste_milli: 1000,
  }
}

///|
pub fn check_asset_policy(
  sheet : SpriteSheet,
  policy : AssetPolicy,
) -> ValidationReport {
  let issues : Array[ValidationIssue] = []
  let stats = analyze_sprite_sheet(sheet)
  if stats.frame_count < policy.min_frames {
    issues.push(
      ValidationIssue::error(
        "frame count is below policy minimum",
        path="$.frames",
        code="policy-min-frames",
      ),
    )
  }
  if stats.frame_count > policy.max_frames {
    issues.push(
      ValidationIssue::error(
        "frame count exceeds policy maximum",
        path="$.frames",
        code="policy-max-frames",
      ),
    )
  }
  if policy.require_tags && sheet.meta.frame_tags.length() == 0 {
    issues.push(
      ValidationIssue::error(
        "policy requires at least one animation tag",
        path="$.meta.frameTags",
        code="policy-require-tags",
      ),
    )
  }
  if policy.require_box_slices &&
    build_frame_box_table(sheet).boxes.length() == 0 {
    issues.push(
      ValidationIssue::error(
        "policy requires hitbox/hurtbox/collision slices",
        path="$.meta.slices",
        code="policy-require-boxes",
      ),
    )
  }
  if !policy.allow_rotated_frames && stats.rotated_frames > 0 {
    issues.push(
      ValidationIssue::error(
        "policy disallows rotated frames",
        path="$.frames",
        code="policy-rotated-frames",
      ),
    )
  }
  if stats.longest_duration_ms > policy.max_frame_duration_ms {
    issues.push(
      ValidationIssue::warning(
        "some frame durations exceed policy maximum",
        path="$.frames",
        code="policy-long-duration",
      ),
    )
  }
  let waste = 1000 - stats.utilization_milli
  if stats.atlas_area > 0 && waste > policy.max_atlas_waste_milli {
    issues.push(
      ValidationIssue::warning(
        "atlas utilization is low",
        path="$.meta.size",
        code="policy-atlas-waste",
      ),
    )
  }
  ValidationReport::new(issues)
}

///|
pub fn analytics_summary_text(sheet : SpriteSheet) -> String {
  let stats = analyze_sprite_sheet(sheet)
  let buckets = build_duration_buckets(sheet)
  let lines : Array[String] = []
  lines.push("frames=\{stats.frame_count}")
  lines.push("tags=\{stats.tag_count}")
  lines.push("slices=\{stats.slice_count}")
  lines.push("duration=\{stats.total_duration_ms}ms")
  lines.push("atlas-used=\{stats.used_area}/\{stats.atlas_area}")
  lines.push("atlas-utilization=\{stats.utilization_percent()}%")
  for bucket in buckets {
    lines.push("duration-\{bucket.name}=\{bucket.count}")
  }
  join_strings(lines, "\n")
}