///|
/// Aggregate coding-sequence statistics for reporting and QC.
pub struct CodingSequenceReport {
  nucleotide_length : Int
  complete_codons : Int
  trailing_bases : Int
  start_codons : Int
  stop_codons : Int
  ambiguous_codons : Int
  gc_first : Double
  gc_second : Double
  gc_third : Double
  usage : Array[CodonUsage]
} derive(Debug, Eq)

///|
/// Calculate codon-position GC and translation diagnostics.
pub fn coding_sequence_report(
  sequence : String,
  table : CodonTable,
) -> CodingSequenceReport {
  let dna = normalize_sequence(sequence).replace_all(old="U", new="T")
  let codons = dna.length() / 3
  let usage = codon_usage(dna, table)
  let mut starts = 0
  let mut stops = 0
  let mut ambiguous = 0
  let gc = [0, 0, 0]
  let seen = [0, 0, 0]
  for index in 0.. Double {
  if denominator == 0 {
    0.0
  } else {
    numerator.to_double() * 100.0 / denominator.to_double()
  }
}

///|
/// Return codons sorted by descending usage, preserving first-seen ties.
pub fn codon_usage_ranked(
  sequence : String,
  table : CodonTable,
) -> Array[CodonUsage] {
  let result = codon_usage(sequence, table)
  result.sort_by(fn(left, right) { right.count - left.count })
  result
}

///|
/// Calculate the proportion of each codon among complete codons.
pub fn codon_usage_frequencies(
  sequence : String,
  table : CodonTable,
) -> Array[(String, Double)] {
  let usage = codon_usage(sequence, table)
  let total = codon_usage_total(usage)
  if total == 0 {
    return []
  }
  usage.map(fn(item) {
    (item.codon, item.count.to_double() * 100.0 / total.to_double())
  })
}

///|
/// Calculate the effective number of distinct codons in a sequence.
pub fn distinct_codon_count(sequence : String, table : CodonTable) -> Int {
  codon_usage(sequence, table).length()
}

///|
/// Emit a stable tabular codon report for downstream scripts.
pub fn CodingSequenceReport::to_tsv(self : CodingSequenceReport) -> String {
  let out = StringBuilder()
  out.write_string("codon\tamino\tcount\n")
  for item in self.usage {
    out.write_string(item.codon)
    out.write_char('\t')
    out.write_string(item.amino)
    out.write_char('\t')
    out.write_string(item.count.to_string())
    out.write_char('\n')
  }
  out.to_string()
}

///|
pub fn CodingSequenceReport::to_json_string(
  self : CodingSequenceReport,
) -> String {
  let usage_json = self.usage.map(fn(item) { item.to_json_string() }).join(",")
  "{\"nucleotide_length\":" +
  self.nucleotide_length.to_string() +
  ",\"complete_codons\":" +
  self.complete_codons.to_string() +
  ",\"trailing_bases\":" +
  self.trailing_bases.to_string() +
  ",\"start_codons\":" +
  self.start_codons.to_string() +
  ",\"stop_codons\":" +
  self.stop_codons.to_string() +
  ",\"ambiguous_codons\":" +
  self.ambiguous_codons.to_string() +
  ",\"gc_first\":" +
  self.gc_first.to_string() +
  ",\"gc_second\":" +
  self.gc_second.to_string() +
  ",\"gc_third\":" +
  self.gc_third.to_string() +
  ",\"usage\":[" +
  usage_json +
  "]}"
}

///|
/// Calculate codon usage for every CDS feature in one record.
pub fn record_coding_reports(
  record : Record,
  table : CodonTable,
) -> Array[CodingSequenceReport] {
  record.features
  .filter(fn(feature) { feature.key == "CDS" })
  .map(fn(feature) {
    coding_sequence_report(record.sequence_for(feature), table)
  })
}

///|
/// Sum complete codons across all CDS features.
pub fn record_complete_codon_count(record : Record) -> Int {
  record_coding_reports(record, standard_codon_table()).fold(init=0, fn(
    total,
    item,
  ) {
    total + item.complete_codons
  })
}

///|
/// Return the fraction of a coding sequence represented by a stop codon.
pub fn stop_codon_fraction(sequence : String, table : CodonTable) -> Double {
  let report = coding_sequence_report(sequence, table)
  if report.complete_codons == 0 {
    0.0
  } else {
    report.stop_codons.to_double() / report.complete_codons.to_double()
  }
}