///|
pub struct CatalogInput {
  key : String
  sheet : SpriteSheet
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct CatalogEntry {
  key : String
  image : String
  frame_count : Int
  animation_count : Int
  slice_count : Int
  duration : Int
  valid : Bool
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct AssetCatalog {
  entries : Array[CatalogEntry]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn catalog_entry(key : String, sheet : SpriteSheet) -> CatalogEntry {
  let metrics = sheet_metrics(sheet)
  let machine = build_machine(sheet)
  {
    key,
    image: metrics.image,
    frame_count: metrics.frame_count,
    animation_count: machine.clips.length(),
    slice_count: metrics.slice_count,
    duration: metrics.total_duration,
    valid: sheet_is_valid(sheet),
  }
}

///|
pub fn catalog_from_sheets(inputs : Array[CatalogInput]) -> AssetCatalog {
  { entries: inputs.map(fn(input) { catalog_entry(input.key, input.sheet) }) }
}

///|
pub fn AssetCatalog::empty() -> AssetCatalog {
  { entries: [] }
}

///|
pub fn AssetCatalog::add(
  self : AssetCatalog,
  key : String,
  sheet : SpriteSheet,
) -> AssetCatalog {
  let entries = self.entries.copy()
  entries.push(catalog_entry(key, sheet))
  { entries, }
}

///|
pub fn AssetCatalog::find(self : AssetCatalog, key : String) -> CatalogEntry? {
  match self.entries.search_by(fn(entry) { entry.key == key }) {
    Some(index) => Some(self.entries[index])
    None => None
  }
}

///|
pub fn AssetCatalog::remove(self : AssetCatalog, key : String) -> AssetCatalog {
  { entries: self.entries.filter(fn(entry) { entry.key != key }) }
}

///|
pub fn AssetCatalog::total_frames(self : AssetCatalog) -> Int {
  self.entries.fold(init=0, (total, entry) => total + entry.frame_count)
}

///|
pub fn AssetCatalog::total_animations(self : AssetCatalog) -> Int {
  self.entries.fold(init=0, (total, entry) => total + entry.animation_count)
}

///|
pub fn AssetCatalog::total_duration(self : AssetCatalog) -> Int {
  self.entries.fold(init=0, (total, entry) => total + entry.duration)
}

///|
pub fn AssetCatalog::valid_count(self : AssetCatalog) -> Int {
  self.entries.fold(init=0, (total, entry) => {
    if entry.valid {
      total + 1
    } else {
      total
    }
  })
}

///|
pub fn AssetCatalog::invalid_count(self : AssetCatalog) -> Int {
  self.entries.length() - self.valid_count()
}

///|
pub fn AssetCatalog::duplicate_keys(self : AssetCatalog) -> Array[String] {
  let duplicates : Array[String] = []
  for i in 0.. Array[String] {
  self.entries.map(fn(entry) { entry.key })
}

///|
pub fn AssetCatalog::images(self : AssetCatalog) -> Array[String] {
  self.entries.map(fn(entry) { entry.image })
}

///|
pub fn AssetCatalog::filter_valid(self : AssetCatalog) -> AssetCatalog {
  { entries: self.entries.filter(fn(entry) { entry.valid }) }
}

///|
pub fn AssetCatalog::filter_invalid(self : AssetCatalog) -> AssetCatalog {
  { entries: self.entries.filter(fn(entry) { !entry.valid }) }
}

///|
pub fn AssetCatalog::filter_prefix(
  self : AssetCatalog,
  prefix : String,
) -> AssetCatalog {
  { entries: self.entries.filter(fn(entry) { entry.key.has_prefix(prefix) }) }
}

///|
pub fn AssetCatalog::sort_by_frames(self : AssetCatalog) -> AssetCatalog {
  let entries = self.entries.copy()
  entries.sort_by(fn(left, right) {
    right.frame_count.compare(left.frame_count)
  })
  { entries, }
}

///|
pub fn AssetCatalog::sort_by_key(self : AssetCatalog) -> AssetCatalog {
  let entries = self.entries.copy()
  entries.sort_by(fn(left, right) { left.key.compare(right.key) })
  { entries, }
}

///|
pub fn AssetCatalog::merge(
  self : AssetCatalog,
  other : AssetCatalog,
) -> AssetCatalog {
  let entries = self.entries.copy()
  for entry in other.entries {
    entries.push(entry)
  }
  { entries, }
}

///|
pub fn AssetCatalog::to_json_string(
  self : AssetCatalog,
  indent? : Int = 2,
) -> String {
  self.to_json().stringify(indent~)
}

///|
pub fn AssetCatalog::summary(self : AssetCatalog) -> String {
  "\{self.entries.length()} assets, \{self.total_frames()} frames, \{self.total_animations()} animations, \{self.valid_count()} valid"
}

///|
pub fn AssetCatalog::has_key(self : AssetCatalog, key : String) -> Bool {
  self.find(key) is Some(_)
}

///|
pub fn AssetCatalog::has_invalid(self : AssetCatalog) -> Bool {
  self.invalid_count() > 0
}

///|
pub fn AssetCatalog::is_unique(self : AssetCatalog) -> Bool {
  self.duplicate_keys().length() == 0
}

///|
pub fn AssetCatalog::entry_count(self : AssetCatalog) -> Int {
  self.entries.length()
}

///|
pub fn AssetCatalog::average_frames(self : AssetCatalog) -> Double {
  if self.entries.length() == 0 {
    0.0
  } else {
    self.total_frames().to_double() / self.entries.length().to_double()
  }
}

///|
pub fn AssetCatalog::largest_entry(self : AssetCatalog) -> CatalogEntry? {
  match self.entries.get(0) {
    None => None
    Some(first) => {
      let mut result = first
      for entry in self.entries[1:] {
        if entry.frame_count > result.frame_count {
          result = entry
        }
      }
      Some(result)
    }
  }
}

///|
pub fn catalog_json(inputs : Array[CatalogInput], indent? : Int = 2) -> String {
  catalog_from_sheets(inputs).to_json_string(indent~)
}

///|
pub fn catalog_validate(catalog : AssetCatalog) -> Bool {
  catalog.is_unique() && !catalog.has_invalid()
}