///|
/// Composition metrics for one coordinate window.
pub struct SequenceProfile {
  start : Int
  end : Int
  length : Int
  gc_percent : Double
  ambiguous : Int
  entropy : Double
  motif_hits : Int
} derive(Debug, Eq)

///|
/// Compute composition and motif metrics for overlapping windows.
pub fn sequence_profile(
  sequence : String,
  window_size : Int,
  step : Int,
  motif : String,
) -> Array[SequenceProfile] {
  let result : Array[SequenceProfile] = []
  let normalized = normalize_sequence(sequence)
  if window_size <= 0 || step <= 0 || window_size > normalized.length() {
    return result
  }
  let mut start = 0
  while start + window_size <= normalized.length() {
    let end = start + window_size
    let piece = normalized.unsafe_substring(start~, end~)
    let composition = composition_report(piece)
    result.push({
      start: start + 1,
      end,
      length: piece.length(),
      gc_percent: composition.gc_percent,
      ambiguous: composition.ambiguous,
      entropy: composition.entropy,
      motif_hits: if motif == "" {
        0
      } else {
        iupac_motif_count(piece, motif)
      },
    })
    start += step
  }
  result
}

///|
/// Return the window with the highest GC percentage, if one exists.
pub fn highest_gc_profile(
  profiles : Array[SequenceProfile],
) -> SequenceProfile? {
  if profiles.length() == 0 {
    return None
  }
  Some(
    profiles.fold(init=profiles[0], fn(best, item) {
      if item.gc_percent > best.gc_percent {
        item
      } else {
        best
      }
    }),
  )
}

///|
/// Return the window with the greatest ambiguity burden.
pub fn most_ambiguous_profile(
  profiles : Array[SequenceProfile],
) -> SequenceProfile? {
  if profiles.length() == 0 {
    return None
  }
  Some(
    profiles.fold(init=profiles[0], fn(best, item) {
      if item.ambiguous > best.ambiguous {
        item
      } else {
        best
      }
    }),
  )
}

///|
/// Export profiles as tab-separated rows for plotting or spreadsheet review.
pub fn sequence_profiles_tsv(profiles : Array[SequenceProfile]) -> String {
  let out = StringBuilder()
  out.write_string(
    "start\tend\tlength\tgc_percent\tambiguous\tentropy\tmotif_hits\n",
  )
  for item in profiles {
    out.write_string(
      item.start.to_string() +
      "\t" +
      item.end.to_string() +
      "\t" +
      item.length.to_string() +
      "\t",
    )
    out.write_string(
      item.gc_percent.to_string() +
      "\t" +
      item.ambiguous.to_string() +
      "\t" +
      item.entropy.to_string() +
      "\t" +
      item.motif_hits.to_string() +
      "\n",
    )
  }
  out.to_string()
}

///|
pub fn SequenceProfile::to_json_string(self : SequenceProfile) -> String {
  "{\"start\":" +
  self.start.to_string() +
  ",\"end\":" +
  self.end.to_string() +
  ",\"length\":" +
  self.length.to_string() +
  ",\"gc_percent\":" +
  self.gc_percent.to_string() +
  ",\"ambiguous\":" +
  self.ambiguous.to_string() +
  ",\"entropy\":" +
  self.entropy.to_string() +
  ",\"motif_hits\":" +
  self.motif_hits.to_string() +
  "}"
}

///|
/// Calculate a length-weighted mean GC percentage across profiles.
pub fn profile_mean_gc(profiles : Array[SequenceProfile]) -> Double {
  let total = profiles.fold(init=0, fn(sum, item) { sum + item.length })
  if total == 0 {
    0.0
  } else {
    profiles.fold(init=0.0, fn(sum, item) {
      sum + item.gc_percent * item.length.to_double()
    }) /
    total.to_double()
  }
}

///|
/// Count motif occurrences represented by a profile series.
pub fn profile_total_motif_hits(profiles : Array[SequenceProfile]) -> Int {
  profiles.fold(init=0, fn(total, item) { total + item.motif_hits })
}

///|
/// Check whether any profile contains ambiguity symbols.
pub fn profile_has_ambiguity(profiles : Array[SequenceProfile]) -> Bool {
  profiles.any(fn(item) { item.ambiguous > 0 })
}

///|
/// Return the number of windows in a profile series.
pub fn profile_window_count(profiles : Array[SequenceProfile]) -> Int {
  profiles.length()
}