///|
/// Remove formatting characters and normalize a sequence to uppercase.
pub fn normalize_sequence(text : String) -> String {
  let out = StringBuilder()
  for ch in text.to_upper() {
    if is_iupac_base(ch) {
      out.write_char(ch)
    }
  }
  out.to_string()
}

///|
/// Hamming distance, or -1 when strings have different lengths.
pub fn hamming_distance(left : String, right : String) -> Int {
  if left.length() != right.length() {
    return -1
  }
  let mut distance = 0
  for i in 0.. Int {
  let mut g = 0
  let mut c = 0
  for ch in sequence.to_upper() {
    if ch == 'G' {
      g += 1
    }
    if ch == 'C' {
      c += 1
    }
  }
  g - c
}

///|
/// Check whether a sequence equals its reverse complement.
pub fn sequence_palindrome(sequence : String) -> Bool {
  let normalized = normalize_sequence(sequence)
  normalized == reverse_complement(normalized)
}

///|
/// Extract an inclusive one-based subsequence.
pub fn sequence_subsequence(
  sequence : String,
  start : Int,
  end : Int,
) -> String {
  if start < 1 || end < start || end > sequence.length() {
    return ""
  }
  sequence.unsafe_substring(start=start - 1, end~)
}

///|
/// Return the length of the longest run of the same base.
pub fn longest_homopolymer(sequence : String) -> Int {
  let mut best = 0
  let mut current = 0
  let mut previous : Char? = None
  for ch in sequence.to_upper() {
    match previous {
      Some(old) if old == ch => current += 1
      _ => current = 1
    }
    previous = Some(ch)
    if current > best {
      best = current
    }
  }
  best
}

///|
/// Build a majority consensus; ties and absent positions become N.
pub fn consensus_sequence(sequences : Array[String]) -> String {
  if sequences.length() == 0 {
    return ""
  }
  let width = sequences
    .map(fn(item) { item.length() })
    .fold(init=0, fn(a, b) { Int::max(a, b) })
  let out = StringBuilder()
  for position in 0.. counts[0] += 1
          'C' | 'c' => counts[1] += 1
          'G' | 'g' => counts[2] += 1
          'T' | 't' | 'U' | 'u' => counts[3] += 1
          _ => ()
        }
      }
    }
    let mut best = 0
    let mut winner = 'N'
    let mut tied = false
    let letters = ['A', 'C', 'G', 'T']
    for i in 0..<4 {
      if counts[i] > best {
        best = counts[i]
        winner = letters[i]
        tied = false
      } else if counts[i] > 0 && counts[i] == best {
        tied = true
      }
    }
    out.write_char(if tied { 'N' } else { winner })
  }
  out.to_string()
}

///|
pub fn is_valid_dna(sequence : String) -> Bool {
  for ch in sequence.to_upper() {
    if !is_iupac_base(ch) {
      return false
    }
  }
  true
}

///|
pub fn rna_sequence(sequence : String) -> String {
  normalize_sequence(sequence).replace_all(old="T", new="U")
}

///|
pub fn dna_sequence(sequence : String) -> String {
  normalize_sequence(sequence).replace_all(old="U", new="T")
}

///|
pub fn mask_ambiguous(sequence : String) -> String {
  let out = StringBuilder()
  for ch in sequence.to_upper() {
    match ch {
      'A' | 'C' | 'G' | 'T' => out.write_char(ch)
      _ if is_iupac_base(ch) => out.write_char('N')
      _ => ()
    }
  }
  out.to_string()
}

///|
/// Return all positions where a sequence changes base.
pub fn change_points(sequence : String) -> Array[Int] {
  let positions : Array[Int] = []
  if sequence.length() < 2 {
    return positions
  }
  for i in 1.. Int {
  let wanted = base.to_string().to_upper()
  sequence
  .to_upper()
  .fold(init=0, fn(total, ch) {
    total + (if ch.to_string() == wanted { 1 } else { 0 })
  })
}

///|
pub fn base_frequencies(sequence : String) -> Array[(Char, Int)] {
  [
    ('A', count_base(sequence, 'A')),
    ('C', count_base(sequence, 'C')),
    ('G', count_base(sequence, 'G')),
    ('T', count_base(sequence, 'T')),
    ('N', count_base(sequence, 'N')),
  ]
}

///|
pub fn has_motif(sequence : String, motif : String) -> Bool {
  find_motif_positions(sequence, motif).length() > 0
}

///|
pub fn motif_count(sequence : String, motif : String) -> Int {
  find_motif_positions(sequence, motif).length()
}

///|
pub fn is_palindromic_motif(motif : String) -> Bool {
  let normalized = normalize_sequence(motif)
  normalized == reverse_complement(normalized)
}

///|
pub fn trim_sequence(sequence : String, left : Int, right : Int) -> String {
  if left < 0 || right < 0 || left + right >= sequence.length() {
    return ""
  }
  sequence.unsafe_substring(start=left, end=sequence.length() - right)
}

///|
pub fn pad_sequence(sequence : String, length : Int, base : Char) -> String {
  let out = StringBuilder()
  out.write_string(sequence)
  let mut index = sequence.length()
  while index < length {
    out.write_char(base)
    index += 1
  }
  out.to_string()
}

///|
pub fn sequence_chunks(sequence : String, size : Int) -> Array[String] {
  let chunks : Array[String] = []
  if size <= 0 {
    return chunks
  }
  let mut start = 0
  while start < sequence.length() {
    let end = Int::min(start + size, sequence.length())
    chunks.push(sequence.unsafe_substring(start~, end~))
    start = end
  }
  chunks
}

///|
pub fn join_sequences(sequences : Array[String]) -> String {
  sequences.join("")
}

///|
pub fn remove_gaps(sequence : String) -> String {
  sequence.replace_all(old="-", new="")
}

///|
pub fn gap_count(sequence : String) -> Int {
  count_base(sequence, '-')
}

///|
pub fn nucleotide_length(sequence : String) -> Int {
  normalize_sequence(sequence).length()
}

///|
pub fn sequence_identity(left : String, right : String) -> Double {
  let distance = hamming_distance(left, right)
  if distance < 0 || left.length() == 0 {
    0.0
  } else {
    (left.length() - distance).to_double() * 100.0 / left.length().to_double()
  }
}