///|
pub struct AssetIndexEntry {
  ordinal : Int
  asset : Asset
  tag_count : Int
  shard : Int
} derive(Debug, Eq)

///|
pub struct AssetIndex {
  manifest_name : String
  entries : Array[AssetIndexEntry]
  shard_count : Int
} derive(Debug, Eq)

///|
pub fn AssetIndex::from_manifest(
  manifest : DatasetManifest,
  shard_count? : Int = 1,
) -> AssetIndex {
  let count = max_i(1, shard_count)
  let entries : Array[AssetIndexEntry] = []
  for i, asset in manifest.assets {
    entries.push({
      ordinal: i,
      asset,
      tag_count: asset.tags.length(),
      shard: i % count,
    })
  }
  { manifest_name: manifest.name, entries, shard_count: count }
}

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

///|
pub fn AssetIndex::shard(self : AssetIndex, shard_id : Int) -> Array[Asset] {
  let out : Array[Asset] = []
  let id = clamp(shard_id, low=0, high=self.shard_count - 1)
  for entry in self.entries {
    if entry.shard == id {
      out.push(entry.asset)
    }
  }
  out
}

///|
pub fn AssetIndex::find_prefix(
  self : AssetIndex,
  prefix : String,
) -> Array[Asset] {
  let out : Array[Asset] = []
  for entry in self.entries {
    if starts_with(entry.asset.name, prefix) {
      out.push(entry.asset)
    }
  }
  out
}

///|
pub fn AssetIndex::with_tag(self : AssetIndex, tag : String) -> Array[Asset] {
  let out : Array[Asset] = []
  for entry in self.entries {
    for candidate in entry.asset.tags {
      if candidate == tag {
        out.push(entry.asset)
      }
    }
  }
  out
}

///|
pub fn AssetIndex::page(
  self : AssetIndex,
  offset : Int,
  limit : Int,
) -> Array[Asset] {
  let out : Array[Asset] = []
  let start = clamp(offset, low=0, high=self.entries.length())
  let end = min_i(self.entries.length(), start + max_i(0, limit))
  for i in start.. Array[String] {
  let out : Array[String] = []
  for entry in self.entries {
    for tag in entry.asset.tags {
      if !contains_string(out, tag) {
        out.push(tag)
      }
    }
  }
  out
}

///|
pub fn AssetIndex::checksum(self : AssetIndex) -> UInt {
  let mut checksum : UInt = 2166136261
  for entry in self.entries {
    checksum = (checksum ^ entry.asset.name.length().reinterpret_as_uint()) *
      16777619
    checksum = (checksum ^ entry.asset.seed) * 16777619
    checksum = (checksum ^ entry.shard.reinterpret_as_uint()) * 16777619
  }
  checksum
}

///|
fn starts_with(text : String, prefix : String) -> Bool {
  if prefix.length() > text.length() {
    false
  } else {
    let mut same = true
    for i in 0.. Bool {
  for value in values {
    if value == target {
      return true
    }
  }
  false
}

///|
pub fn assets_to_jsonl(assets : Array[Asset]) -> String {
  let sb = StringBuilder()
  for asset in assets {
    sb.write_string(asset.to_json())
    sb.write_char('\n')
  }
  sb.to_string()
}

///|
pub fn manifest_shards(
  manifest : DatasetManifest,
  shard_count : Int,
) -> Array[DatasetManifest] {
  let count = max_i(1, shard_count)
  let out : Array[DatasetManifest] = []
  for shard in 0.. DatasetManifest {
  let out = DatasetManifest::new(name~)
  for manifest in manifests {
    for asset in manifest.assets {
      out.add(asset)
    }
  }
  out
}

///|
pub fn validate_index(index : AssetIndex) -> ValidationReport {
  let report = ValidationReport::new()
  if index.shard_count <= 0 {
    report.error("asset index must contain at least one shard")
  }
  for entry in index.entries {
    if entry.shard < 0 || entry.shard >= index.shard_count {
      report.error("asset index shard id is out of range")
    }
    if entry.tag_count != entry.asset.tags.length() {
      report.error("asset index tag count is stale")
    }
  }
  report
}

///|
pub fn DatasetManifest::index(
  self : DatasetManifest,
  shard_count : Int,
) -> AssetIndex {
  AssetIndex::from_manifest(self, shard_count~)
}