///|
/// An occurrence of an IUPAC motif in a nucleotide sequence.
pub struct MotifHit {
  motif : String
  start : Int
  end : Int
  strand : String
  matched : String
} derive(Debug, Eq)

///|
/// Summary of a forward and reverse-complement motif scan.
pub struct MotifScanReport {
  motif : String
  sequence_length : Int
  forward_hits : Int
  reverse_hits : Int
  hits : Array[MotifHit]
} derive(Debug, Eq)

///|
/// Match one IUPAC motif at a zero-based sequence offset.
pub fn iupac_motif_matches_at(
  sequence : String,
  motif : String,
  offset : Int,
) -> Bool {
  let subject = sequence.to_upper()
  let pattern = motif.to_upper()
  if offset < 0 || pattern == "" || offset + pattern.length() > subject.length() {
    return false
  }
  for index in 0.. Bool {
  let wanted = symbol.to_string().to_upper()
  let actual = base.to_string().to_upper()
  iupac_expansion(symbol).any(fn(item) { item.to_string() == actual }) ||
  (wanted == actual && is_iupac_base(base))
}

///|
/// Scan overlapping occurrences on one strand using IUPAC semantics.
pub fn scan_iupac_motif(sequence : String, motif : String) -> Array[MotifHit] {
  let hits : Array[MotifHit] = []
  let subject = sequence.to_upper()
  let pattern = motif.to_upper()
  if pattern == "" || pattern.length() > subject.length() {
    return hits
  }
  for offset in 0..<=(subject.length() - pattern.length()) {
    if iupac_motif_matches_at(subject, pattern, offset) {
      hits.push({
        motif: pattern,
        start: offset + 1,
        end: offset + pattern.length(),
        strand: "+",
        matched: subject.unsafe_substring(
          start=offset,
          end=offset + pattern.length(),
        ),
      })
    }
  }
  hits
}

///|
/// Scan both strands and retain orientation in every hit.
pub fn scan_iupac_both_strands(
  sequence : String,
  motif : String,
) -> MotifScanReport {
  let forward = scan_iupac_motif(sequence, motif)
  let reverse_motif = reverse_complement(motif)
  let reverse_hits = scan_iupac_motif(sequence, reverse_motif)
  let oriented_reverse = reverse_hits.map(fn(hit) {
    { motif, start: hit.start, end: hit.end, strand: "-", matched: hit.matched }
  })
  let all = forward.copy()
  for hit in oriented_reverse {
    all.push(hit)
  }
  all.sort_by(fn(left, right) {
    if left.start == right.start {
      if left.strand == right.strand {
        0
      } else if left.strand == "+" {
        -1
      } else {
        1
      }
    } else {
      left.start - right.start
    }
  })
  {
    motif: motif.to_upper(),
    sequence_length: sequence.length(),
    forward_hits: forward.length(),
    reverse_hits: reverse_hits.length(),
    hits: all,
  }
}

///|
/// Return only one-based coordinates for a strand-specific scan.
pub fn iupac_motif_positions(sequence : String, motif : String) -> Array[Int] {
  scan_iupac_motif(sequence, motif).map(fn(hit) { hit.start })
}

///|
/// Count overlapping IUPAC motif occurrences.
pub fn iupac_motif_count(sequence : String, motif : String) -> Int {
  scan_iupac_motif(sequence, motif).length()
}

///|
/// Build a consensus IUPAC symbol for a set of observed canonical bases.
pub fn iupac_consensus(observed : Array[Char]) -> Char {
  let bases = observed.map(fn(ch) { ch.to_string().to_upper() })
  let has_a = bases.contains("A")
  let has_c = bases.contains("C")
  let has_g = bases.contains("G")
  let has_t = bases.contains("T") || bases.contains("U")
  match (has_a, has_c, has_g, has_t) {
    (true, false, false, false) => 'A'
    (false, true, false, false) => 'C'
    (false, false, true, false) => 'G'
    (false, false, false, true) => 'T'
    (true, false, true, false) => 'R'
    (false, true, false, true) => 'Y'
    (false, true, true, false) => 'S'
    (true, false, false, true) => 'W'
    (false, false, true, true) => 'K'
    (true, true, false, false) => 'M'
    (false, true, true, true) => 'B'
    (true, false, true, true) => 'D'
    (true, true, false, true) => 'H'
    (true, true, true, false) => 'V'
    (true, true, true, true) => 'N'
    _ => 'N'
  }
}

///|
/// Derive a position-wise IUPAC consensus from aligned sequences.
pub fn aligned_iupac_consensus(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 output = StringBuilder()
  for position in 0.. String {
  "{\"motif\":" +
  json_string(self.motif) +
  ",\"start\":" +
  self.start.to_string() +
  ",\"end\":" +
  self.end.to_string() +
  ",\"strand\":" +
  json_string(self.strand) +
  ",\"matched\":" +
  json_string(self.matched) +
  "}"
}

///|
pub fn MotifScanReport::to_json_string(self : MotifScanReport) -> String {
  "{\"motif\":" +
  json_string(self.motif) +
  ",\"sequence_length\":" +
  self.sequence_length.to_string() +
  ",\"forward_hits\":" +
  self.forward_hits.to_string() +
  ",\"reverse_hits\":" +
  self.reverse_hits.to_string() +
  ",\"hits\":[" +
  self.hits.map(fn(item) { item.to_json_string() }).join(",") +
  "]}"
}