///|
/// Summary of a template/catalog merge.
pub(all) struct MergeStats {
  matched : Int
  added : Int
  obsoleted : Int
} derive(Debug, Eq)

///|
/// Result of merging a POT-style template with an existing translation.
pub(all) struct MergeResult {
  document : PoFile
  stats : MergeStats
} derive(Debug, Eq)

///|
fn add_unique_string(values : Array[String], value : String) -> Unit {
  if value != "" && !values.contains(value) {
    values.push(value)
  }
}

///|
fn add_unique_comment(comments : Array[PoComment], comment : PoComment) -> Unit {
  if !comments.contains(comment) {
    comments.push(comment)
  }
}

///|
fn collect_comment_flags(entry : PoEntry, flags : Array[String]) -> Unit {
  for comment in entry.comments {
    if comment.kind == Flag {
      for raw_flag in comment.text.split(",") {
        add_unique_string(flags, raw_flag.trim().to_owned())
      }
    }
  }
}

///|
fn merged_comments(
  template : PoEntry,
  existing : PoEntry,
  source_changed : Bool,
) -> Array[PoComment] {
  let comments : Array[PoComment] = []
  for comment in existing.comments {
    if comment.kind == Translator {
      add_unique_comment(comments, comment)
    }
  }
  for comment in template.comments {
    if comment.kind == Translator ||
      comment.kind == Extracted ||
      comment.kind == Reference {
      add_unique_comment(comments, comment)
    }
  }
  let flags : Array[String] = []
  collect_comment_flags(template, flags)
  collect_comment_flags(existing, flags)
  if source_changed {
    add_unique_string(flags, "fuzzy")
  }
  if !flags.is_empty() {
    comments.push(PoComment::new(Flag, flags.join(", ")))
  }
  for comment in existing.comments {
    if comment.kind == Previous {
      add_unique_comment(comments, comment)
    }
  }
  comments
}

///|
fn merge_translation_values(
  template : PoEntry,
  existing : PoEntry,
) -> Array[String] {
  match template.msgid_plural {
    Some(_) =>
      match existing.msgid_plural {
        Some(_) => existing.translations.copy()
        None => template.translations.copy()
      }
    None => [existing.translations.get(0).unwrap_or("")]
  }
}

///|
fn merged_active_entry(template : PoEntry, existing : PoEntry) -> PoEntry {
  let source_changed = template.msgid_plural != existing.msgid_plural
  {
    comments: merged_comments(template, existing, source_changed),
    context: template.context,
    msgid: template.msgid,
    msgid_plural: template.msgid_plural,
    translations: merge_translation_values(template, existing),
    obsolete: template.obsolete,
  }
}

///|
fn merged_header(template : PoEntry, existing : PoEntry?) -> PoEntry {
  match existing {
    Some(previous) =>
      {
        comments: merged_comments(template, previous, false),
        context: None,
        msgid: "",
        msgid_plural: None,
        translations: previous.translations.copy(),
        obsolete: false,
      }
    None => template
  }
}

///|
fn index_existing_entries(
  document : PoFile,
) -> Map[String, PoEntry] raise GettextError {
  let entries : Map[String, PoEntry] = Map([])
  for entry in document.entries {
    if entry.is_header() {
      continue
    }
    let key = catalog_key(entry.msgid, entry.context)
    if entries.contains(key) {
      raise Validation(
        message="duplicate catalog key during merge: \{entry.msgid}",
      )
    }
    entries[key] = entry
  }
  entries
}

///|
fn ensure_template_keys_unique(document : PoFile) -> Unit raise GettextError {
  let keys : Map[String, Unit] = Map([])
  let mut headers = 0
  for entry in document.entries {
    if entry.is_header() {
      headers += 1
      if headers > 1 {
        raise Validation(message="template contains multiple metadata headers")
      }
      continue
    }
    let key = catalog_key(entry.msgid, entry.context)
    if keys.contains(key) {
      raise Validation(
        message="duplicate template key during merge: \{entry.msgid}",
      )
    }
    keys[key] = ()
  }
}

///|
fn copy_as_obsolete(entry : PoEntry) -> PoEntry {
  {
    comments: entry.comments.copy(),
    context: entry.context,
    msgid: entry.msgid,
    msgid_plural: entry.msgid_plural,
    translations: entry.translations.copy(),
    obsolete: true,
  }
}

///|
/// Merge a POT-style template with an existing translated PO document.
///
/// Source comments and references come from the template; translator and
/// previous-value comments come from the existing catalog. Matching
/// translations are retained. A changed plural source is marked `fuzzy`.
/// Existing entries absent from the template are appended as obsolete unless
/// `keep_obsolete=false`.
pub fn merge_template(
  template : PoFile,
  existing : PoFile,
  keep_obsolete? : Bool = true,
) -> MergeResult raise GettextError {
  ensure_template_keys_unique(template)
  let existing_entries = index_existing_entries(existing)
  let existing_header = existing.header()
  let consumed : Map[String, Unit] = Map([])
  let output : Array[PoEntry] = []
  let mut matched = 0
  let mut added = 0
  let mut obsoleted = 0

  if template.header() is None {
    match existing_header {
      Some(header) => output.push(header)
      None => ()
    }
  }

  for template_entry in template.entries {
    if template_entry.is_header() {
      output.push(merged_header(template_entry, existing_header))
      continue
    }
    let key = catalog_key(template_entry.msgid, template_entry.context)
    match existing_entries.get(key) {
      Some(previous) => {
        output.push(merged_active_entry(template_entry, previous))
        consumed[key] = ()
        matched += 1
      }
      None => {
        output.push(template_entry)
        added += 1
      }
    }
  }

  if keep_obsolete {
    for entry in existing.entries {
      if entry.is_header() {
        continue
      }
      let key = catalog_key(entry.msgid, entry.context)
      if !consumed.contains(key) &&
        !template.entries.any(candidate => {
          !candidate.is_header() &&
          catalog_key(candidate.msgid, candidate.context) == key
        }) {
        output.push(copy_as_obsolete(entry))
        obsoleted += 1
      }
    }
  }

  { document: PoFile::new(output), stats: { matched, added, obsoleted } }
}