///|
/// An annotation interval follows reference top-strand coordinates.
pub(all) struct Annotation {
  id : String
  start : Int
  length : Int
} derive(Eq, Debug, ToJson)

///|
/// One contiguous annotation piece in fragment-local coordinates.
pub(all) struct AnnotationPiece {
  id : String
  fragment_index : Int
  local_start : Int
  length : Int
  source_start : Int
} derive(Eq, Debug, ToJson)

///|
/// Split intervals on fragment and circular-origin boundaries. No biological labels inferred.
pub fn annotate_fragments(
  dna : Dna,
  enzymes : Array[Enzyme],
  report : Digest,
  annotations : Array[Annotation],
) -> Array[AnnotationPiece] raise RestrictError {
  verify_digest(dna, enzymes, report)
  if annotations.length() > 128 || annotations.length() * dna.length() > 4000000 {
    raise LimitExceeded("ANNOTATION_WORK")
  }
  let names : Map[String, Bool] = Map([])
  let result : Array[AnnotationPiece] = []
  for a in annotations {
    if a.id.is_empty() || a.id.length() > 128 || names.contains(a.id) {
      raise InvalidInput("ANNOTATION_ID")
    }
    names[a.id] = true
    if a.start < 0 ||
      a.start >= dna.length() ||
      a.length < 1 ||
      a.length > dna.length() {
      raise InvalidInput("ANNOTATION_RANGE")
    }
    if dna.topology == Linear && a.start + a.length > dna.length() {
      raise InvalidInput("ANNOTATION_RANGE")
    }
    for fi in 0..= a.start && position < a.start + a.length
          })
        let breaks_origin = j > 0 && position == 0
        if run >= 0 && (!inside || breaks_origin) {
          result.push({
            id: a.id,
            fragment_index: fi,
            local_start: run,
            length: j - run,
            source_start: (f.start + run) % dna.length(),
          })
          run = -1
        }
        if inside && run < 0 {
          run = j
        }
      }
    }
  }
  result
}