///|
/// Exposed single-strand polarity; unknown natural termini are not inferred.
pub(all) enum EndKind {
  Blunt
  FivePrime
  ThreePrime
} derive(Eq, Debug, ToJson)

///|
/// Exposed sequence is always written 5-prime to 3-prime.
pub(all) struct End {
  kind : EndKind
  sequence : String
} derive(Eq, Debug, ToJson)

///|
/// Sequence-only complementarity, NOT a prediction of laboratory ligation.
pub(all) enum JoinStatus {
  NoJoin
  PossibleJoin
  CertainJoin
} derive(Eq, Debug, ToJson)

///|
fn validate_site(dna : Dna, site : Site) -> Unit raise RestrictError {
  let n = dna.length()
  if !site.cuttable || site.stagger < -192 || site.stagger > 192 {
    raise InvalidInput("SITE_GEOMETRY")
  }
  if dna.topology == Linear {
    if site.top < 0 ||
      site.top > n ||
      site.bottom < 0 ||
      site.bottom > n ||
      site.bottom - site.top != site.stagger {
      raise InvalidInput("SITE_GEOMETRY")
    }
  } else if site.top < 0 ||
    site.top >= n ||
    site.bottom < 0 ||
    site.bottom >= n ||
    site.stagger.abs() >= n ||
    wrap(site.top + site.stagger, n) != site.bottom {
    raise InvalidInput("SITE_GEOMETRY")
  }
}

///|
/// Return ends on the piece before/after a reference cut, respectively.
pub fn cut_ends(dna : Dna, site : Site) -> (End, End) raise RestrictError {
  validate_site(dna, site)
  if site.stagger == 0 {
    return ({ kind: Blunt, sequence: "" }, { kind: Blunt, sequence: "" })
  }
  let begin = if site.stagger > 0 { site.top } else { site.top + site.stagger }
  let top = circular_bases(dna, begin, site.stagger.abs())
  let complement = reverse_complement(Dna::new(top)).sequence()
  if site.stagger > 0 {
    (
      { kind: FivePrime, sequence: complement },
      { kind: FivePrime, sequence: top },
    )
  } else {
    (
      { kind: ThreePrime, sequence: top },
      { kind: ThreePrime, sequence: complement },
    )
  }
}

///|
/// Conservative Cartesian base-set complementarity; ambiguity is never certain.
pub fn compatible_ends(a : End, b : End) -> JoinStatus raise RestrictError {
  for end in [a, b] {
    if end.kind == Blunt {
      if end.sequence != "" {
        raise InvalidInput("BLUNT_SEQUENCE")
      }
    } else {
      if end.sequence.length() > 192 {
        raise LimitExceeded("END_LENGTH")
      }
      let _ = Dna::new(end.sequence)
    }
  }
  if a.kind == Blunt || b.kind == Blunt {
    return if a.kind == b.kind { CertainJoin } else { NoJoin }
  }
  let left = Dna::new(a.sequence)
  let right = reverse_complement(Dna::new(b.sequence))
  if left.length() > 192 || right.length() > 192 {
    raise LimitExceeded("END_LENGTH")
  }
  if a.kind != b.kind || left.length() != right.length() {
    return NoJoin
  }
  let lm = left.bases.to_array().map(base_mask)
  let rm = right.bases.to_array().map(base_mask)
  let mut certain = true
  for i in 0..