///|
/// Quality and composition findings for a sequence supplied by a caller.
pub struct SequenceQualityReport {
  sequence_length : Int
  canonical_bases : Int
  ambiguous_bases : Int
  invalid_bases : Int
  gc_percent : Double
  longest_run : Int
  run_count : Int
  has_internal_gap : Bool
  valid : Bool
} derive(Debug, Eq)

///|
/// A normalized interval used by trimming and masking workflows.
pub struct SequenceInterval {
  start : Int
  end : Int
  sequence : String
} derive(Debug, Eq)

///|
/// Analyze canonical, ambiguous, gap, and invalid symbols without silently
/// discarding input. Coordinates in all reports are one-based and inclusive.
pub fn sequence_quality_report(sequence : String) -> SequenceQualityReport {
  let upper = sequence.to_upper()
  let mut canonical = 0
  let mut ambiguous = 0
  let mut invalid = 0
  let mut gc = 0
  let mut longest = 0
  let mut current = 0
  let mut previous : Char? = None
  let mut runs = 0
  let mut internal_gap = false
  for index in 0.. {
        canonical += 1
        if ch == 'C' || ch == 'G' {
          gc += 1
        }
      }
      'R' | 'Y' | 'S' | 'W' | 'K' | 'M' | 'B' | 'D' | 'H' | 'V' | 'N' =>
        ambiguous += 1
      '-' | '.' =>
        if index > 0 && index + 1 < upper.length() {
          internal_gap = true
        }
      _ => invalid += 1
    }
    match previous {
      Some(old) if old == ch => current += 1
      _ => {
        current = 1
        runs += 1
      }
    }
    previous = Some(ch)
    if current > longest {
      longest = current
    }
  }
  let length = upper.length()
  let gc_percent = if canonical == 0 {
    0.0
  } else {
    gc.to_double() * 100.0 / canonical.to_double()
  }
  {
    sequence_length: length,
    canonical_bases: canonical,
    ambiguous_bases: ambiguous,
    invalid_bases: invalid,
    gc_percent,
    longest_run: longest,
    run_count: runs,
    has_internal_gap: internal_gap,
    valid: invalid == 0 && !internal_gap,
  }
}

///|
/// Return positions of symbols that are not valid IUPAC nucleotides.
pub fn invalid_base_positions(sequence : String) -> Array[Int] {
  let positions : Array[Int] = []
  for index in 0.. Array[Int] {
  let positions : Array[Int] = []
  for index in 0.. ()
      _ if is_iupac_base(ch) => positions.push(index + 1)
      _ => ()
    }
  }
  positions
}

///|
/// Find every homopolymer run of at least `minimum_length`.
pub fn homopolymer_runs(
  sequence : String,
  minimum_length : Int,
) -> Array[SequenceRun] {
  let result : Array[SequenceRun] = []
  let upper = sequence.to_upper()
  if upper.length() == 0 || minimum_length <= 0 {
    return result
  }
  let mut start = 0
  let mut index = 1
  while index <= upper.length() {
    let different = if index == upper.length() {
      true
    } else {
      upper.get_char(index).unwrap() != upper.get_char(start).unwrap()
    }
    if different {
      let length = index - start
      if length >= minimum_length {
        result.push({
          base: upper.get_char(start).unwrap(),
          start: start + 1,
          end: index,
          length,
        })
      }
      start = index
    }
    index += 1
  }
  result
}

///|
/// Produce non-overlapping windows containing GC skew `(G-C)/(G+C)`.
pub fn gc_skew_windows(
  sequence : String,
  window_size : Int,
  step : Int,
) -> Array[(Int, Int, Double)] {
  let result : Array[(Int, Int, Double)] = []
  if window_size <= 0 || step <= 0 || window_size > sequence.length() {
    return result
  }
  let upper = sequence.to_upper()
  let mut start = 0
  while start + window_size <= upper.length() {
    let end = start + window_size
    let mut g = 0
    let mut c = 0
    for index in start.. Array[SequenceInterval] {
  let result : Array[SequenceInterval] = []
  let upper = sequence.to_upper()
  let mut start = -1
  for index in 0..<=upper.length() {
    let gap = if index == upper.length() {
      true
    } else {
      let ch = upper.get_char(index).unwrap()
      ch == '-' || ch == '.'
    }
    if !gap && start < 0 {
      start = index
    } else if gap && start >= 0 {
      result.push({
        start: start + 1,
        end: index,
        sequence: upper.unsafe_substring(start~, end=index),
      })
      start = -1
    }
  }
  result
}

///|
/// Mask ambiguity symbols to `N` while retaining gaps and their coordinates.
pub fn mask_ambiguities(sequence : String) -> String {
  let out = StringBuilder()
  for ch in sequence.to_upper() {
    match ch {
      'A' | 'C' | 'G' | 'T' | 'U' | '-' | '.' => out.write_char(ch)
      _ if is_iupac_base(ch) => out.write_char('N')
      _ => out.write_char('N')
    }
  }
  out.to_string()
}

///|
/// Validate that a sequence has no invalid symbols and no internal gaps.
pub fn sequence_is_clean(sequence : String) -> Bool {
  sequence_quality_report(sequence).valid
}

///|
/// Render a compact machine-readable quality report.
pub fn SequenceQualityReport::to_json_string(
  self : SequenceQualityReport,
) -> String {
  "{\"sequence_length\":" +
  self.sequence_length.to_string() +
  ",\"canonical_bases\":" +
  self.canonical_bases.to_string() +
  ",\"ambiguous_bases\":" +
  self.ambiguous_bases.to_string() +
  ",\"invalid_bases\":" +
  self.invalid_bases.to_string() +
  ",\"gc_percent\":" +
  self.gc_percent.to_string() +
  ",\"longest_run\":" +
  self.longest_run.to_string() +
  ",\"run_count\":" +
  self.run_count.to_string() +
  ",\"has_internal_gap\":" +
  self.has_internal_gap.to_string() +
  ",\"valid\":" +
  self.valid.to_string() +
  "}"
}

///|
pub fn SequenceRun::to_text(self : SequenceRun) -> String {
  self.base.to_string() +
  " " +
  self.start.to_string() +
  ".." +
  self.end.to_string() +
  " (" +
  self.length.to_string() +
  ")"
}