///|
fn decode_catalog_entries(
  source : String,
) -> Map[String, String] raise MessageError {
  let json = @json.parse(source) catch {
    error => raise InvalidCatalog(error.to_string())
  }
  @json.from_json(json) catch {
    _ =>
      raise InvalidCatalog(
        "Catalog must be a JSON object whose values are strings.",
      )
  }
}

///|
fn copy_entries(entries : Map[String, String]) -> Map[String, String] {
  let output : Map[String, String] = Map([])
  for key, value in entries {
    output[key] = value
  }
  output
}

///|
/// Load a catalog without rejecting invalid message templates.
///
/// This is intended for validators and editors that need to report every
/// malformed entry in one pass. Applications should normally use `from_json`.
pub fn Catalog::from_json_lenient(
  locale : String,
  source : String,
) -> Catalog raise MessageError {
  { locale, entries: decode_catalog_entries(source) }
}

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

///|
pub fn Catalog::is_empty(self : Catalog) -> Bool {
  self.entries.length() == 0
}

///|
pub fn Catalog::contains(self : Catalog, key : String) -> Bool {
  self.entries.contains(key)
}

///|
pub fn Catalog::keys(self : Catalog) -> Array[String] {
  let keys : Array[String] = []
  for key, _ in self.entries {
    keys.push(key)
  }
  keys.sort()
  keys
}

///|
pub fn Catalog::values(self : Catalog) -> Array[String] {
  let values : Array[String] = []
  for key in self.keys() {
    values.push(self.entries[key])
  }
  values
}

///|
pub fn Catalog::entries(self : Catalog) -> Map[String, String] {
  copy_entries(self.entries)
}

///|
pub fn Catalog::parsed(self : Catalog, key : String) -> Message? {
  match self.entries.get(key) {
    Some(template) =>
      try parse_message(template) catch {
        _ => None
      } noraise {
        message => Some(message)
      }
    None => None
  }
}

///|
pub fn Catalog::valid_keys(self : Catalog) -> Array[String] {
  let keys : Array[String] = []
  for key in self.keys() {
    if diagnose_message(self.entries[key]) is None {
      keys.push(key)
    }
  }
  keys
}

///|
pub fn Catalog::invalid_keys(self : Catalog) -> Array[String] {
  let keys : Array[String] = []
  for key in self.keys() {
    if diagnose_message(self.entries[key]) is Some(_) {
      keys.push(key)
    }
  }
  keys
}

///|
pub fn Catalog::entry_analysis(
  self : Catalog,
  key : String,
) -> CatalogEntryAnalysis? {
  match self.entries.get(key) {
    None => None
    Some(template) =>
      match diagnose_message(template) {
        Some(diagnostic) =>
          Some({
            key,
            template,
            valid: false,
            diagnostic: Some(diagnostic),
            message: None,
          })
        None => {
          let message = try! parse_message(template)
          Some({
            key,
            template,
            valid: true,
            diagnostic: None,
            message: Some(message.analyze()),
          })
        }
      }
  }
}

///|
pub fn Catalog::analyses(self : Catalog) -> Array[CatalogEntryAnalysis] {
  let output : Array[CatalogEntryAnalysis] = []
  for key in self.keys() {
    match self.entry_analysis(key) {
      Some(value) => output.push(value)
      None => ()
    }
  }
  output
}

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

///|
pub fn CatalogEntryAnalysis::template(self : CatalogEntryAnalysis) -> String {
  self.template
}

///|
pub fn CatalogEntryAnalysis::valid(self : CatalogEntryAnalysis) -> Bool {
  self.valid
}

///|
pub fn CatalogEntryAnalysis::diagnostic(
  self : CatalogEntryAnalysis,
) -> MessageDiagnostic? {
  self.diagnostic
}

///|
pub fn CatalogEntryAnalysis::message(
  self : CatalogEntryAnalysis,
) -> MessageAnalysis? {
  self.message
}

///|
fn count_catalog_arguments(catalog : Catalog) -> Int {
  let arguments : Map[String, Unit] = Map([])
  for analysis in catalog.analyses() {
    match analysis.message {
      Some(message) =>
        for argument in message.arguments {
          arguments[argument.name] = ()
        }
      None => ()
    }
  }
  arguments.length()
}

///|
fn count_catalog_choices(catalog : Catalog) -> Int {
  let mut count = 0
  for analysis in catalog.analyses() {
    match analysis.message {
      Some(message) => count += message.choice_node_count
      None => ()
    }
  }
  count
}

///|
/// Compute aggregate catalog statistics.
pub fn Catalog::stats(self : Catalog) -> CatalogStats {
  let invalid = self.invalid_keys().length()
  {
    locale: self.locale,
    key_count: self.entries.length(),
    template_count: self.entries.length(),
    valid_template_count: self.entries.length() - invalid,
    invalid_template_count: invalid,
    argument_count: count_catalog_arguments(self),
    choice_count: count_catalog_choices(self),
  }
}

///|
pub fn CatalogStats::locale(self : CatalogStats) -> String {
  self.locale
}

///|
pub fn CatalogStats::key_count(self : CatalogStats) -> Int {
  self.key_count
}

///|
pub fn CatalogStats::template_count(self : CatalogStats) -> Int {
  self.template_count
}

///|
pub fn CatalogStats::valid_template_count(self : CatalogStats) -> Int {
  self.valid_template_count
}

///|
pub fn CatalogStats::invalid_template_count(self : CatalogStats) -> Int {
  self.invalid_template_count
}

///|
pub fn CatalogStats::argument_count(self : CatalogStats) -> Int {
  self.argument_count
}

///|
pub fn CatalogStats::choice_count(self : CatalogStats) -> Int {
  self.choice_count
}

///|
pub fn CatalogStats::summary(self : CatalogStats) -> String {
  "\{self.locale}: \{self.key_count} keys, \{self.valid_template_count} valid, \{self.invalid_template_count} invalid, \{self.argument_count} arguments, \{self.choice_count} choices"
}

///|
/// Compare the key sets of two catalogs.
pub fn diff_catalogs(reference : Catalog, translation : Catalog) -> CatalogDiff {
  let shared_keys : Array[String] = []
  let missing_keys : Array[String] = []
  let extra_keys : Array[String] = []
  for key in reference.keys() {
    if translation.contains(key) {
      shared_keys.push(key)
    } else {
      missing_keys.push(key)
    }
  }
  for key in translation.keys() {
    if !reference.contains(key) {
      extra_keys.push(key)
    }
  }
  {
    reference_locale: reference.locale,
    translation_locale: translation.locale,
    shared_keys,
    missing_keys,
    extra_keys,
  }
}

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

///|
pub fn CatalogDiff::translation_locale(self : CatalogDiff) -> String {
  self.translation_locale
}

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

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

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

///|
pub fn CatalogDiff::is_equal(self : CatalogDiff) -> Bool {
  self.missing_keys.length() == 0 && self.extra_keys.length() == 0
}

///|
pub fn CatalogDiff::coverage(self : CatalogDiff) -> CatalogCoverage {
  let reference_keys = self.shared_keys.length() + self.missing_keys.length()
  let translated_keys = self.shared_keys.length()
  let coverage_percent = if reference_keys == 0 {
    100
  } else {
    translated_keys * 100 / reference_keys
  }
  {
    reference_keys,
    translated_keys,
    missing_keys: self.missing_keys.length(),
    extra_keys: self.extra_keys.length(),
    coverage_percent,
  }
}

///|
pub fn CatalogCoverage::reference_keys(self : CatalogCoverage) -> Int {
  self.reference_keys
}

///|
pub fn CatalogCoverage::translated_keys(self : CatalogCoverage) -> Int {
  self.translated_keys
}

///|
pub fn CatalogCoverage::missing_keys(self : CatalogCoverage) -> Int {
  self.missing_keys
}

///|
pub fn CatalogCoverage::extra_keys(self : CatalogCoverage) -> Int {
  self.extra_keys
}

///|
pub fn CatalogCoverage::coverage_percent(self : CatalogCoverage) -> Int {
  self.coverage_percent
}

///|
pub fn CatalogCoverage::summary(self : CatalogCoverage) -> String {
  "\{self.coverage_percent}% (\{self.translated_keys}/\{self.reference_keys}), \{self.missing_keys} missing, \{self.extra_keys} extra"
}

///|
pub fn CatalogBuilder::new(locale : String) -> CatalogBuilder {
  { locale, entries: Map([]) }
}

///|
pub fn CatalogBuilder::from_catalog(catalog : Catalog) -> CatalogBuilder {
  { locale: catalog.locale, entries: copy_entries(catalog.entries) }
}

///|
pub fn CatalogBuilder::locale(self : CatalogBuilder) -> String {
  self.locale
}

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

///|
pub fn CatalogBuilder::contains(self : CatalogBuilder, key : String) -> Bool {
  self.entries.contains(key)
}

///|
pub fn CatalogBuilder::set(
  self : CatalogBuilder,
  key : String,
  template : String,
) -> CatalogBuilder {
  self.entries[key] = template
  self
}

///|
pub fn CatalogBuilder::remove(
  self : CatalogBuilder,
  key : String,
) -> CatalogBuilder {
  ignore(self.entries.remove(key))
  self
}

///|
pub fn CatalogBuilder::clear(self : CatalogBuilder) -> CatalogBuilder {
  self.entries.clear()
  self
}

///|
pub fn CatalogBuilder::build(
  self : CatalogBuilder,
  validate? : Bool = true,
) -> Catalog raise MessageError {
  let catalog : Catalog = {
    locale: self.locale,
    entries: copy_entries(self.entries),
  }
  if validate {
    for key, template in catalog.entries {
      match diagnose_message(template) {
        Some(diagnostic) =>
          raise InvalidCatalog(
            "\{catalog.locale}/\{key}: \{diagnostic.display()}",
          )
        None => ()
      }
    }
  }
  catalog
}

///|
pub fn Catalog::with_entry(
  self : Catalog,
  key : String,
  template : String,
) -> Catalog raise MessageError {
  CatalogBuilder::from_catalog(self).set(key, template).build()
}

///|
pub fn Catalog::without_entry(self : Catalog, key : String) -> Catalog {
  try! CatalogBuilder::from_catalog(self).remove(key).build(validate=false)
}

///|
pub fn Catalog::subset(self : Catalog, prefix : String) -> Catalog {
  let builder = CatalogBuilder::new(self.locale)
  for key, template in self.entries {
    if key.has_prefix(prefix) {
      ignore(builder.set(key, template))
    }
  }
  try! builder.build(validate=false)
}

///|
pub fn Catalog::merge(
  self : Catalog,
  other : Catalog,
  policy? : CatalogMergePolicy = ReplaceExisting,
) -> Catalog raise MessageError {
  let builder = CatalogBuilder::from_catalog(self)
  for key, template in other.entries {
    if builder.contains(key) {
      match policy {
        KeepExisting => ()
        ReplaceExisting => ignore(builder.set(key, template))
        RejectDuplicate =>
          raise InvalidCatalog("Duplicate catalog key '\{key}'.")
      }
    } else {
      ignore(builder.set(key, template))
    }
  }
  builder.build()
}

///|
fn write_json_string(output : StringBuilder, value : String) -> Unit {
  output.write_char('"')
  for ch in value {
    match ch {
      '"' => output.write_string("\\\"")
      '\\' => output.write_string("\\\\")
      '\n' => output.write_string("\\n")
      '\r' => output.write_string("\\r")
      '\t' => output.write_string("\\t")
      _ => output.write_char(ch)
    }
  }
  output.write_char('"')
}

///|
/// Serialize a catalog deterministically with keys in lexical order.
pub fn Catalog::to_json(self : Catalog, pretty? : Bool = false) -> String {
  let output = StringBuilder::new()
  let keys = self.keys()
  output.write_char('{')
  if pretty && keys.length() > 0 {
    output.write_char('\n')
  }
  for index, key in keys {
    if index > 0 {
      output.write_char(',')
      if pretty {
        output.write_char('\n')
      }
    }
    if pretty {
      output.write_string("  ")
    }
    write_json_string(output, key)
    output.write_char(':')
    if pretty {
      output.write_char(' ')
    }
    write_json_string(output, self.entries[key])
  }
  if pretty && keys.length() > 0 {
    output.write_char('\n')
  }
  output.write_char('}')
  output.to_string()
}