///|
/// A deterministic comparison between two parsed records.
pub struct RecordDiff {
  accession_changed : Bool
  definition_changed : Bool
  sequence_changed : Bool
  length_delta : Int
  feature_count_left : Int
  feature_count_right : Int
  added_features : Int
  removed_features : Int
  changed_features : Int
  qualifier_changes : Int
  identical : Bool
} derive(Debug, Eq)

///|
/// Compare records using normalized sequence and feature signatures.
pub fn compare_records(left : Record, right : Record) -> RecordDiff {
  let left_signatures = feature_signatures(left)
  let right_signatures = feature_signatures(right)
  let added = count_not_in(right_signatures, left_signatures)
  let removed = count_not_in(left_signatures, right_signatures)
  let common = Int::min(left_signatures.length(), right_signatures.length()) -
    added
  let changed = Int::max(
    0,
    common - (left_signatures.length() - removed - added),
  )
  let qualifier_changes = qualifier_difference(left, right)
  let sequence_changed = normalize_sequence(left.origin) !=
    normalize_sequence(right.origin)
  let accession_changed = left.accession != right.accession
  let definition_changed = left.definition != right.definition
  let identical = !accession_changed &&
    !definition_changed &&
    !sequence_changed &&
    added == 0 &&
    removed == 0 &&
    qualifier_changes == 0
  {
    accession_changed,
    definition_changed,
    sequence_changed,
    length_delta: right.origin.length() - left.origin.length(),
    feature_count_left: left.features.length(),
    feature_count_right: right.features.length(),
    added_features: added,
    removed_features: removed,
    changed_features: Int::max(0, changed),
    qualifier_changes,
    identical,
  }
}

///|
/// Compare a record against itself after canonical sequence normalization.
pub fn records_equivalent(left : Record, right : Record) -> Bool {
  compare_records(left, right).identical
}

///|
/// Build a signature that includes key, location, and all qualifier values.
pub fn feature_signatures(record : Record) -> Array[String] {
  record.features.map(fn(feature) {
    let qualifiers = feature.qualifiers.map(fn(item) {
      let value = match item.value {
        Some(text) => text
        None => ""
      }
      item.name + "=" + value
    })
    feature.key + "|" + feature.location.raw + "|" + qualifiers.join(";")
  })
}

///|
/// Count signatures that occur in the left array but not the right array.
pub fn count_not_in(left : Array[String], right : Array[String]) -> Int {
  left.fold(init=0, fn(total, item) {
    total + (if right.contains(item) { 0 } else { 1 })
  })
}

///|
/// Count qualifier values that changed for features occupying the same slot.
pub fn qualifier_difference(left : Record, right : Record) -> Int {
  let limit = Int::min(left.features.length(), right.features.length())
  let mut changes = 0
  for index in 0.. String {
  feature.qualifiers
  .map(fn(item) {
    let value = match item.value {
      Some(value) => value
      None => ""
    }
    item.name + ":" + value
  })
  .join("|")
}

///|
/// Produce an actionable human-readable comparison.
pub fn RecordDiff::to_text(self : RecordDiff) -> String {
  if self.identical {
    return "records are equivalent"
  }
  let out = StringBuilder()
  if self.accession_changed {
    out.write_string("accession changed\n")
  }
  if self.definition_changed {
    out.write_string("definition changed\n")
  }
  if self.sequence_changed {
    out.write_string("sequence changed\n")
  }
  if self.length_delta != 0 {
    out.write_string("length delta: " + self.length_delta.to_string() + "\n")
  }
  if self.added_features != 0 {
    out.write_string(
      "added features: " + self.added_features.to_string() + "\n",
    )
  }
  if self.removed_features != 0 {
    out.write_string(
      "removed features: " + self.removed_features.to_string() + "\n",
    )
  }
  if self.changed_features != 0 {
    out.write_string(
      "changed feature signatures: " + self.changed_features.to_string() + "\n",
    )
  }
  if self.qualifier_changes != 0 {
    out.write_string(
      "changed qualifier sets: " + self.qualifier_changes.to_string() + "\n",
    )
  }
  out.to_string()
}

///|
pub fn RecordDiff::to_json_string(self : RecordDiff) -> String {
  "{\"accession_changed\":" +
  self.accession_changed.to_string() +
  ",\"definition_changed\":" +
  self.definition_changed.to_string() +
  ",\"sequence_changed\":" +
  self.sequence_changed.to_string() +
  ",\"length_delta\":" +
  self.length_delta.to_string() +
  ",\"feature_count_left\":" +
  self.feature_count_left.to_string() +
  ",\"feature_count_right\":" +
  self.feature_count_right.to_string() +
  ",\"added_features\":" +
  self.added_features.to_string() +
  ",\"removed_features\":" +
  self.removed_features.to_string() +
  ",\"changed_features\":" +
  self.changed_features.to_string() +
  ",\"qualifier_changes\":" +
  self.qualifier_changes.to_string() +
  ",\"identical\":" +
  self.identical.to_string() +
  "}"
}