///|
/// Exact top-strand base-count bin; multiplicity is never discarded.
pub(all) struct SizeBand {
  length : Int
  multiplicity : Int
} derive(Eq, Debug, ToJson)

///|
/// Group fragments by exact length, not gel mobility or molecular mass.
pub fn size_bands(
  fragments : Array[Fragment],
) -> Array[SizeBand] raise RestrictError {
  if fragments.is_empty() || fragments.length() > 10001 {
    raise InvalidInput("BANDS_FRAGMENTS")
  }
  let sizes : Array[Int] = []
  let mut total = 0
  for f in fragments {
    if f.length < 1 || f.length > 100000 || f.sequence.length() != f.length {
      raise InvalidInput("BANDS_LENGTH")
    }
    total = total + f.length
    if total > 100000 {
      raise LimitExceeded("BANDS_TOTAL_BASES")
    }
    sizes.push(f.length)
  }
  sizes.sort()
  let bands : Array[SizeBand] = []
  for length in sizes {
    let last = bands.length() - 1
    if last >= 0 && bands[last].length == length {
      bands[last] = { length, multiplicity: bands[last].multiplicity + 1 }
    } else {
      bands.push({ length, multiplicity: 1 })
    }
  }
  bands
}

///|
/// A deterministic length-count string for comparison, independent of fragment order.
pub fn band_signature(
  fragments : Array[Fragment],
) -> String raise RestrictError {
  let out = StringBuilder::new()
  for band in size_bands(fragments) {
    out.write_string(
      band.length.to_string() + "x" + band.multiplicity.to_string() + ";",
    )
  }
  out.to_string()
}