///|
/// Detailed CDS translation result with source coordinates.
pub struct CdsTranslationResult {
  feature_index : Int
  location : String
  nucleotide : String
  protein : String
  expected : String?
  matches_expected : Bool
  start_codon : String
  terminal_codon : String
  codon_count : Int
} derive(Debug, Eq)

///|
pub fn translate_feature(
  record : Record,
  feature_index : Int,
  table? : CodonTable = CodonTable::Standard,
) -> CdsTranslationResult? {
  match record.features.get(feature_index) {
    None => None
    Some(feature) if feature.key != "CDS" => None
    Some(feature) => {
      let nucleotide = record.sequence_for(feature)
      let protein = translate_frame(nucleotide, 1, table)
      let expected = feature.qualifier("translation")
      {
        feature_index,
        location: feature.location.raw,
        nucleotide,
        protein,
        expected,
        matches_expected: match expected {
          Some(value) => value == protein
          None => true
        },
        start_codon: if nucleotide.length() >= 3 {
          nucleotide.unsafe_substring(start=0, end=3)
        } else {
          ""
        },
        terminal_codon: if nucleotide.length() >= 3 {
          nucleotide.unsafe_substring(
            start=nucleotide.length() - 3,
            end=nucleotide.length(),
          )
        } else {
          ""
        },
        codon_count: nucleotide.length() / 3,
      }
      |> Some
    }
  }
}

///|
pub fn CdsTranslationResult::to_json_string(
  self : CdsTranslationResult,
) -> String {
  let expected = match self.expected {
    Some(value) => json_string(value)
    None => "null"
  }
  "{\"feature_index\":" +
  self.feature_index.to_string() +
  ",\"location\":" +
  json_string(self.location) +
  ",\"nucleotide\":" +
  json_string(self.nucleotide) +
  ",\"protein\":" +
  json_string(self.protein) +
  ",\"expected\":" +
  expected +
  ",\"matches_expected\":" +
  self.matches_expected.to_string() +
  ",\"start_codon\":" +
  json_string(self.start_codon) +
  ",\"terminal_codon\":" +
  json_string(self.terminal_codon) +
  ",\"codon_count\":" +
  self.codon_count.to_string() +
  "}"
}

///|
pub fn cds_translation_results(record : Record) -> Array[CdsTranslationResult] {
  let result : Array[CdsTranslationResult] = []
  for index, feature in record.features {
    if feature.key == "CDS" {
      match translate_feature(record, index) {
        Some(translation) => result.push(translation)
        None => ()
      }
    }
  }
  result
}

///|
pub fn cds_translation_json(record : Record) -> String {
  array_json(
    cds_translation_results(record).map(fn(item) { item.to_json_string() }),
  )
}

///|
pub fn cds_translation_mismatches(
  record : Record,
) -> Array[CdsTranslationResult] {
  cds_translation_results(record).filter(fn(item) { !item.matches_expected })
}

///|
pub fn cds_translation_matches(record : Record) -> Array[CdsTranslationResult] {
  cds_translation_results(record).filter(fn(item) { item.matches_expected })
}

///|
pub fn cds_translation_error_count(record : Record) -> Int {
  cds_translation_mismatches(record).length()
}

///|
pub fn cds_translation_ok(record : Record) -> Bool {
  cds_translation_error_count(record) == 0
}

///|
pub fn cds_protein_lengths(record : Record) -> Array[Int] {
  cds_translation_results(record).map(fn(item) { item.protein.length() })
}

///|
pub fn cds_nucleotide_lengths(record : Record) -> Array[Int] {
  cds_translation_results(record).map(fn(item) { item.nucleotide.length() })
}

///|
pub fn cds_start_codons(record : Record) -> Array[String] {
  cds_translation_results(record).map(fn(item) { item.start_codon })
}

///|
pub fn cds_terminal_codons(record : Record) -> Array[String] {
  cds_translation_results(record).map(fn(item) { item.terminal_codon })
}

///|
pub fn cds_with_start_codon(record : Record, codon : String) -> Array[Feature] {
  let wanted = codon.to_upper()
  record.features.filter(fn(feature) {
    feature.key == "CDS" &&
    record.sequence_for(feature).to_upper().has_prefix(wanted)
  })
}

///|
pub fn cds_with_stop_codon(record : Record, codon : String) -> Array[Feature] {
  let wanted = codon.to_upper()
  record.features.filter(fn(feature) {
    if feature.key != "CDS" {
      false
    } else {
      let sequence = record.sequence_for(feature).to_upper()
      sequence.length() >= 3 && sequence.has_suffix(wanted)
    }
  })
}

///|
pub fn cds_complete(record : Record, feature : Feature) -> Bool {
  let sequence = record.sequence_for(feature).to_upper()
  sequence.length() >= 6 &&
  sequence.has_prefix("ATG") &&
  (
    sequence.has_suffix("TAA") ||
    sequence.has_suffix("TAG") ||
    sequence.has_suffix("TGA")
  )
}

///|
pub fn cds_partial(record : Record, feature : Feature) -> Bool {
  feature.key == "CDS" && !cds_complete(record, feature)
}

///|
pub fn cds_frame_length(record : Record, feature : Feature) -> Int {
  record.sequence_for(feature).length() % 3
}

///|
pub fn cds_in_frame(record : Record, feature : Feature) -> Bool {
  cds_frame_length(record, feature) == 0
}

///|
pub fn cds_translation_summary(record : Record) -> String {
  let results = cds_translation_results(record)
  let out = StringBuilder()
  for item in results {
    out.write_string("CDS[" + item.feature_index.to_string() + "] ")
    out.write_string(item.location + " ")
    out.write_string(item.protein + " ")
    out.write_string(if item.matches_expected { "ok" } else { "mismatch" })
    out.write_char('\n')
  }
  out.to_string()
}

///|
pub fn codon_frequency_by_amino(
  sequence : String,
  table : CodonTable,
) -> Array[(String, Int)] {
  let result : Array[(String, Int)] = []
  for item in codon_usage(sequence, table) {
    match result.search_by(fn(pair) { pair.0 == item.amino }) {
      Some(index) => {
        let current = result[index]
        result[index] = (current.0, current.1 + item.count)
      }
      None => result.push((item.amino, item.count))
    }
  }
  result
}

///|
pub fn synonymous_codon_count(sequence : String, table : CodonTable) -> Int {
  codon_usage(sequence, table).fold(init=0, fn(total, item) {
    total + (if item.count > 1 { item.count - 1 } else { 0 })
  })
}

///|
pub fn stop_codon_count(sequence : String, table : CodonTable) -> Int {
  count_base(translate_frame(sequence, 1, table), '*')
}

///|
pub fn ambiguous_codon_count(sequence : String, table : CodonTable) -> Int {
  count_base(translate_frame(sequence, 1, table), 'X')
}

///|
pub fn translation_has_internal_stop(protein : String) -> Bool {
  if protein.length() <= 1 {
    false
  } else {
    protein.unsafe_substring(start=0, end=protein.length() - 1).contains("*")
  }
}

///|
pub fn translation_is_terminated(protein : String) -> Bool {
  protein.has_suffix("*")
}

///|
pub fn translation_without_stop(protein : String) -> String {
  if protein.has_suffix("*") {
    protein.unsafe_substring(start=0, end=protein.length() - 1)
  } else {
    protein
  }
}

///|
pub fn protein_molecular_weight_approximation(protein : String) -> Double {
  protein.length().to_double() * 110.0
}

///|
pub fn protein_has_unknowns(protein : String) -> Bool {
  protein.contains("X")
}

///|
pub fn protein_unknown_count(protein : String) -> Int {
  count_base(protein, 'X')
}

///|
pub fn protein_stop_count(protein : String) -> Int {
  count_base(protein, '*')
}

///|
pub fn protein_is_empty(protein : String) -> Bool {
  protein == ""
}

///|
pub fn protein_length(protein : String) -> Int {
  protein.length()
}