///|
fn catalog_locale_map(catalogs : Array[Catalog]) -> Map[String, Catalog] {
  let output : Map[String, Catalog] = Map([])
  for catalog in catalogs {
    output[canonicalize_locale_lossy(catalog.locale)] = catalog
  }
  output
}

///|
fn matrix_locales(catalogs : Array[Catalog]) -> Array[String] {
  let locales : Array[String] = []
  for catalog in catalogs {
    locales.push(canonicalize_locale_lossy(catalog.locale))
  }
  locales.sort()
  locales
}

///|
/// Build a key-presence matrix for a reference and multiple translations.
pub fn catalog_matrix(
  reference : Catalog,
  translations : Array[Catalog],
) -> CatalogMatrix {
  let by_locale = catalog_locale_map(translations)
  let locales = matrix_locales(translations)
  let rows : Array[CatalogMatrixRow] = []
  for key in reference.keys() {
    let present_locales : Array[String] = []
    let missing_locales : Array[String] = []
    for locale in locales {
      if by_locale[locale].contains(key) {
        present_locales.push(locale)
      } else {
        missing_locales.push(locale)
      }
    }
    rows.push({ key, present_locales, missing_locales })
  }
  {
    reference_locale: canonicalize_locale_lossy(reference.locale),
    locales,
    rows,
  }
}

///|
pub fn CatalogMatrixRow::key(self : CatalogMatrixRow) -> String {
  self.key
}

///|
pub fn CatalogMatrixRow::present_locales(
  self : CatalogMatrixRow,
) -> Array[String] {
  self.present_locales
}

///|
pub fn CatalogMatrixRow::missing_locales(
  self : CatalogMatrixRow,
) -> Array[String] {
  self.missing_locales
}

///|
pub fn CatalogMatrixRow::complete(self : CatalogMatrixRow) -> Bool {
  self.missing_locales.length() == 0
}

///|
pub fn CatalogMatrixRow::coverage_percent(self : CatalogMatrixRow) -> Int {
  let total = self.present_locales.length() + self.missing_locales.length()
  if total == 0 {
    100
  } else {
    self.present_locales.length() * 100 / total
  }
}

///|
pub fn CatalogMatrix::reference_locale(self : CatalogMatrix) -> String {
  self.reference_locale
}

///|
pub fn CatalogMatrix::locales(self : CatalogMatrix) -> Array[String] {
  self.locales
}

///|
pub fn CatalogMatrix::rows(self : CatalogMatrix) -> Array[CatalogMatrixRow] {
  self.rows
}

///|
pub fn CatalogMatrix::key_count(self : CatalogMatrix) -> Int {
  self.rows.length()
}

///|
pub fn CatalogMatrix::locale_count(self : CatalogMatrix) -> Int {
  self.locales.length()
}

///|
pub fn CatalogMatrix::has_locale(self : CatalogMatrix, locale : String) -> Bool {
  let canonical = canonicalize_locale_lossy(locale)
  for available in self.locales {
    if available == canonical {
      return true
    }
  }
  false
}

///|
pub fn CatalogMatrix::row(
  self : CatalogMatrix,
  key : String,
) -> CatalogMatrixRow? {
  for row in self.rows {
    if row.key == key {
      return Some(row)
    }
  }
  None
}

///|
pub fn CatalogMatrix::incomplete_rows(
  self : CatalogMatrix,
) -> Array[CatalogMatrixRow] {
  let output : Array[CatalogMatrixRow] = []
  for row in self.rows {
    if !row.complete() {
      output.push(row)
    }
  }
  output
}

///|
pub fn CatalogMatrix::complete_rows(
  self : CatalogMatrix,
) -> Array[CatalogMatrixRow] {
  let output : Array[CatalogMatrixRow] = []
  for row in self.rows {
    if row.complete() {
      output.push(row)
    }
  }
  output
}

///|
pub fn CatalogMatrix::missing_keys_for(
  self : CatalogMatrix,
  locale : String,
) -> Array[String] {
  let canonical = canonicalize_locale_lossy(locale)
  let output : Array[String] = []
  for row in self.rows {
    for missing in row.missing_locales {
      if missing == canonical {
        output.push(row.key)
        break
      }
    }
  }
  output
}

///|
pub fn CatalogMatrix::present_keys_for(
  self : CatalogMatrix,
  locale : String,
) -> Array[String] {
  let canonical = canonicalize_locale_lossy(locale)
  let output : Array[String] = []
  for row in self.rows {
    for present in row.present_locales {
      if present == canonical {
        output.push(row.key)
        break
      }
    }
  }
  output
}

///|
pub fn CatalogMatrix::complete(self : CatalogMatrix) -> Bool {
  for row in self.rows {
    if !row.complete() {
      return false
    }
  }
  true
}

///|
pub fn CatalogMatrix::missing_count(self : CatalogMatrix) -> Int {
  let mut count = 0
  for row in self.rows {
    count += row.missing_locales.length()
  }
  count
}

///|
pub fn CatalogMatrix::coverage_for(
  self : CatalogMatrix,
  locale : String,
) -> CatalogCoverage? {
  let canonical = canonicalize_locale_lossy(locale)
  let mut translated = 0
  let mut missing = 0
  let mut known = false
  for available in self.locales {
    if available == canonical {
      known = true
      break
    }
  }
  if !known {
    return None
  }
  for row in self.rows {
    let mut present = false
    for available in row.present_locales {
      if available == canonical {
        present = true
        break
      }
    }
    if present {
      translated += 1
    } else {
      missing += 1
    }
  }
  let total = translated + missing
  Some({
    reference_keys: total,
    translated_keys: translated,
    missing_keys: missing,
    extra_keys: 0,
    coverage_percent: if total == 0 {
      100
    } else {
      translated * 100 / total
    },
  })
}

///|
pub fn CatalogMatrix::to_text(self : CatalogMatrix) -> String {
  let output = StringBuilder::new()
  output.write_string("reference: ")
  output.write_string(self.reference_locale)
  output.write_char('\n')
  output.write_string("locales: ")
  output.write_string(self.locales.join(", "))
  output.write_char('\n')
  for row in self.rows {
    output.write_string(row.key)
    output.write_string(" ")
    output.write_string(row.coverage_percent().to_string())
    output.write_char('%')
    if row.missing_locales.length() > 0 {
      output.write_string(" missing=")
      output.write_string(row.missing_locales.join(","))
    }
    output.write_char('\n')
  }
  output.to_string()
}

///|
fn write_matrix_row_json(
  output : StringBuilder,
  row : CatalogMatrixRow,
) -> Unit {
  output.write_char('{')
  write_json_string(output, "key")
  output.write_char(':')
  write_json_string(output, row.key)
  output.write_char(',')
  write_json_string(output, "coverage_percent")
  output.write_char(':')
  output.write_string(row.coverage_percent().to_string())
  output.write_char(',')
  write_json_string(output, "present_locales")
  output.write_char(':')
  write_string_array_json(output, row.present_locales)
  output.write_char(',')
  write_json_string(output, "missing_locales")
  output.write_char(':')
  write_string_array_json(output, row.missing_locales)
  output.write_char('}')
}

///|
pub fn CatalogMatrix::to_json(self : CatalogMatrix) -> String {
  let output = StringBuilder::new()
  output.write_char('{')
  write_json_string(output, "reference_locale")
  output.write_char(':')
  write_json_string(output, self.reference_locale)
  output.write_char(',')
  write_json_string(output, "locales")
  output.write_char(':')
  write_string_array_json(output, self.locales)
  output.write_char(',')
  write_json_string(output, "complete")
  output.write_char(':')
  output.write_string(if self.complete() { "true" } else { "false" })
  output.write_char(',')
  write_json_string(output, "missing_count")
  output.write_char(':')
  output.write_string(self.missing_count().to_string())
  output.write_char(',')
  write_json_string(output, "rows")
  output.write_string(":[")
  for index, row in self.rows {
    if index > 0 {
      output.write_char(',')
    }
    write_matrix_row_json(output, row)
  }
  output.write_string("]}")
  output.to_string()
}