///|
pub(all) struct FrameNameParts {
  original : String
  stem : String
  suffix_number : Int
  has_number : Bool
} derive(Eq, @debug.Debug)

///|
pub fn FrameNameParts::new(
  original : String,
  stem : String,
  suffix_number? : Int = -1,
  has_number? : Bool = false,
) -> FrameNameParts {
  { original, stem, suffix_number, has_number }
}

///|
fn is_ascii_digit_code(code : Int) -> Bool {
  code >= 48 && code <= 57
}

///|
fn parse_decimal_suffix(text : String) -> Int? {
  if text.length() == 0 {
    None
  } else {
    let mut value = 0
    for i in 0.. FrameNameParts {
  match name.rev_find("_") {
    None => FrameNameParts::new(name, name)
    Some(pos) => {
      let stem = name[:pos].to_owned()
      let suffix = name[pos + 1:].to_owned()
      match parse_decimal_suffix(suffix) {
        Some(value) =>
          FrameNameParts::new(name, stem, suffix_number=value, has_number=true)
        None => FrameNameParts::new(name, name)
      }
    }
  }
}

///|
pub(all) struct FrameNameGroup {
  stem : String
  count : Int
  first_frame_index : Int
  last_frame_index : Int
  numeric_count : Int
  min_number : Int
  max_number : Int
} derive(Eq, @debug.Debug)

///|
pub fn FrameNameGroup::new(stem : String, frame : FrameInfo) -> FrameNameGroup {
  let parts = split_frame_name(frame.name)
  {
    stem,
    count: 1,
    first_frame_index: frame.index,
    last_frame_index: frame.index,
    numeric_count: if parts.has_number {
      1
    } else {
      0
    },
    min_number: if parts.has_number {
      parts.suffix_number
    } else {
      -1
    },
    max_number: if parts.has_number {
      parts.suffix_number
    } else {
      -1
    },
  }
}

///|
pub fn FrameNameGroup::is_numbered(self : FrameNameGroup) -> Bool {
  self.numeric_count == self.count && self.count > 0
}

///|
pub fn FrameNameGroup::has_number_gap(self : FrameNameGroup) -> Bool {
  self.is_numbered() &&
  self.min_number >= 0 &&
  self.max_number - self.min_number + 1 != self.count
}

///|
fn update_group(group : FrameNameGroup, frame : FrameInfo) -> FrameNameGroup {
  let parts = split_frame_name(frame.name)
  let numeric_inc = if parts.has_number { 1 } else { 0 }
  {
    ..group,
    count: group.count + 1,
    last_frame_index: frame.index,
    numeric_count: group.numeric_count + numeric_inc,
    min_number: if parts.has_number &&
      (group.min_number < 0 || parts.suffix_number < group.min_number) {
      parts.suffix_number
    } else {
      group.min_number
    },
    max_number: if parts.has_number && parts.suffix_number > group.max_number {
      parts.suffix_number
    } else {
      group.max_number
    },
  }
}

///|
fn find_group_index(groups : Array[FrameNameGroup], stem : String) -> Int {
  for i, group in groups {
    if group.stem == stem {
      return i
    }
  }
  -1
}

///|
pub fn group_frame_names(sheet : SpriteSheet) -> Array[FrameNameGroup] {
  let groups : Array[FrameNameGroup] = []
  for frame in sheet.frames {
    let parts = split_frame_name(frame.name)
    let index = find_group_index(groups, parts.stem)
    if index < 0 {
      groups.push(FrameNameGroup::new(parts.stem, frame))
    } else {
      groups[index] = update_group(groups[index], frame)
    }
  }
  groups
}

///|
pub(all) struct NamingIssue {
  stem : String
  code : String
  message : String
} derive(Eq, @debug.Debug)

///|
pub fn NamingIssue::new(
  stem : String,
  code : String,
  message : String,
) -> NamingIssue {
  { stem, code, message }
}

///|
pub fn inspect_frame_name_groups(sheet : SpriteSheet) -> Array[NamingIssue] {
  let issues : Array[NamingIssue] = []
  for group in group_frame_names(sheet) {
    if group.has_number_gap() {
      issues.push(
        NamingIssue::new(
          group.stem,
          "number-gap",
          "numbered frame sequence has a gap in suffix values",
        ),
      )
    }
    if group.count == 1 && group.stem.find("_") != None {
      issues.push(
        NamingIssue::new(
          group.stem,
          "single-frame-group",
          "frame name looks grouped but only one frame was found",
        ),
      )
    }
  }
  issues
}

///|
pub fn naming_report_text(sheet : SpriteSheet) -> String {
  let groups = group_frame_names(sheet)
  let issues = inspect_frame_name_groups(sheet)
  let lines : Array[String] = []
  lines.push("name-groups=\{groups.length()}")
  for group in groups {
    lines.push(
      "group " +
      group.stem +
      " count=\{group.count} frames=\{group.first_frame_index}..\{group.last_frame_index}",
    )
  }
  if issues.length() > 0 {
    lines.push("naming-issues=\{issues.length()}")
    for issue in issues {
      lines.push("- " + issue.code + " " + issue.stem + ": " + issue.message)
    }
  } else {
    lines.push("naming-issues=0")
  }
  join_strings(lines, "\n")
}