///|
/// A validation result grouped for human and machine consumers.
pub struct ValidationReport {
  ok : Bool
  error_count : Int
  warning_count : Int
  issues : Array[ValidationIssue]
  accession : String
  sequence_length : Int
  feature_count : Int
} derive(Debug, Eq)

///|
pub fn build_validation_report(record : Record) -> ValidationReport {
  let issues = record.validate()
  let mut errors = 0
  let mut warnings = 0
  for issue in issues {
    if issue.is_error() {
      errors += 1
    } else {
      warnings += 1
    }
  }
  {
    ok: errors == 0,
    error_count: errors,
    warning_count: warnings,
    issues,
    accession: record.accession,
    sequence_length: record.origin.length(),
    feature_count: record.features.length(),
  }
}

///|
pub fn ValidationReport::to_json_string(self : ValidationReport) -> String {
  "{\"ok\":" +
  self.ok.to_string() +
  ",\"error_count\":" +
  self.error_count.to_string() +
  ",\"warning_count\":" +
  self.warning_count.to_string() +
  ",\"accession\":" +
  json_string(self.accession) +
  ",\"sequence_length\":" +
  self.sequence_length.to_string() +
  ",\"feature_count\":" +
  self.feature_count.to_string() +
  ",\"issues\":" +
  array_json(self.issues.map(fn(issue) { issue.to_json_string() })) +
  "}"
}

///|
pub fn ValidationReport::to_text(self : ValidationReport) -> String {
  let out = StringBuilder()
  out.write_string("accession: " + self.accession + "\n")
  out.write_string(
    "sequence length: " + self.sequence_length.to_string() + "\n",
  )
  out.write_string("features: " + self.feature_count.to_string() + "\n")
  out.write_string("errors: " + self.error_count.to_string() + "\n")
  out.write_string("warnings: " + self.warning_count.to_string() + "\n")
  out.write_string(if self.ok { "status: ok\n" } else { "status: failed\n" })
  for issue in self.issues {
    let severity = if issue.is_error() { "ERROR" } else { "WARN" }
    out.write_string(severity + " " + issue.code + ": " + issue.message + "\n")
  }
  out.to_string()
}

///|
pub fn record_report(record : Record) -> String {
  let summary = annotation_summary(record)
  let validation = build_validation_report(record)
  let out = StringBuilder()
  out.write_string("GenBank record report\n")
  out.write_string("accession: " + record.accession + "\n")
  out.write_string("definition: " + record.definition + "\n")
  out.write_string("sequence: " + record.origin.length().to_string() + " bp\n")
  out.write_string("features: " + summary.feature_count.to_string() + "\n")
  out.write_string("CDS: " + summary.cds_count.to_string() + "\n")
  out.write_string("genes: " + summary.gene_count.to_string() + "\n")
  out.write_string("GC: " + summary.gc_percent.to_string() + "%\n")
  out.write_string(
    "validation: " + (if validation.ok { "ok" } else { "failed" }) + "\n",
  )
  out.to_string()
}

///|
pub fn record_report_json(record : Record) -> String {
  let summary = annotation_summary(record)
  let validation = build_validation_report(record)
  "{\"accession\":" +
  json_string(record.accession) +
  ",\"summary\":" +
  summary.to_json_string() +
  ",\"validation\":" +
  validation.to_json_string() +
  "}"
}

///|
pub fn normalize_record(record : Record) -> Record {
  let normalized_features = record.features.map(fn(feature) {
    let normalized_qualifiers = feature.qualifiers.map(fn(qualifier) {
      {
        name: normalize_identifier(qualifier.name),
        value: qualifier.value.map(fn(value) {
          normalize_qualifier_value(value)
        }),
      }
    })
    {
      key: normalize_identifier(feature.key),
      location: feature.location,
      qualifiers: normalized_qualifiers,
    }
  })
  {
    accession: normalize_identifier(record.accession),
    version: normalize_identifier(record.version),
    locus: record.locus,
    definition: normalize_spaces(record.definition),
    keywords: normalize_spaces(record.keywords),
    source: normalize_spaces(record.source),
    features: normalized_features,
    origin: normalize_sequence(record.origin),
  }
}

///|
fn normalize_identifier(text : String) -> String {
  normalize_spaces(text).to_lower()
}

///|
fn normalize_qualifier_value(text : String) -> String {
  normalize_spaces(text).trim().to_owned()
}

///|
pub fn normalize_spaces(text : String) -> String {
  let out = StringBuilder()
  let mut previous_space = false
  for ch in text.trim() {
    let is_space = ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'
    if is_space {
      if !previous_space {
        out.write_char(' ')
      }
    } else {
      out.write_char(ch)
    }
    previous_space = is_space
  }
  out.to_string()
}

///|
pub fn record_is_normalized(record : Record) -> Bool {
  normalize_record(record) == record
}

///|
pub fn record_error_count(record : Record) -> Int {
  build_validation_report(record).error_count
}

///|
pub fn record_warning_count(record : Record) -> Int {
  build_validation_report(record).warning_count
}

///|
pub fn record_is_valid(record : Record) -> Bool {
  build_validation_report(record).ok
}

///|
pub fn record_has_translation_errors(record : Record) -> Bool {
  record.check_cds_translations().any(fn(check) { !check.ok })
}

///|
pub fn record_remote_accession_count(record : Record) -> Int {
  let mut total = 0
  for feature in record.features {
    for segment in feature.location.segments {
      if segment.remote is Some(_) {
        total += 1
      }
    }
  }
  total
}

///|
pub fn record_fuzzy_location_count(record : Record) -> Int {
  let mut total = 0
  for feature in record.features {
    if location_is_fuzzy(feature.location) {
      total += 1
    }
  }
  total
}

///|
pub fn record_between_location_count(record : Record) -> Int {
  let mut total = 0
  for feature in record.features {
    for range in feature.location.ranges {
      if range.between {
        total += 1
      }
    }
  }
  total
}

///|
pub fn record_cds_count(record : Record) -> Int {
  let mut total = 0
  for feature in record.features {
    if feature.key == "CDS" {
      total += 1
    }
  }
  total
}

///|
pub fn record_gene_count(record : Record) -> Int {
  let mut total = 0
  for feature in record.features {
    if feature.key == "gene" {
      total += 1
    }
  }
  total
}

///|
pub fn record_source_count(record : Record) -> Int {
  let mut total = 0
  for feature in record.features {
    if feature.key == "source" {
      total += 1
    }
  }
  total
}