///|
/// Parse strict signed decimal offsets without integer overflow or coercion.
fn read_offset(text : String) -> Int raise RestrictError {
  if text.is_empty() || text.length() > 4 {
    raise InvalidInput("OFFSET_DECIMAL")
  }
  let negative = text.has_prefix("-")
  let begin = if negative { 1 } else { 0 }
  if begin == text.length() {
    raise InvalidInput("OFFSET_DECIMAL")
  }
  let mut value = 0
  for i in begin.. 57 {
      raise InvalidInput("OFFSET_DECIMAL")
    }
    value = value * 10 + c - 48
  }
  if negative {
    -value
  } else {
    value
  }
}

///|
/// Tab-separated header name/motif/top/bottom; blank lines and # comments allowed.
pub fn read_enzymes(text : String) -> Array[Enzyme] raise RestrictError {
  if text.length() > 16384 {
    raise LimitExceeded("ENZYME_TEXT")
  }
  let result : Array[Enzyme] = []
  let names : Map[String, Bool] = Map([])
  let mut header = false
  for raw in text.split("\n") {
    let line = raw.trim().to_owned()
    if line.is_empty() || line.has_prefix("#") {
      continue
    }
    if !header {
      if line != "name\tmotif\ttop\tbottom" {
        raise InvalidInput("ENZYME_HEADER")
      }
      header = true
      continue
    }
    let fields = line.split("\t").map(v => v.to_owned()).to_array()
    if fields.length() != 4 {
      raise InvalidInput("ENZYME_COLUMNS")
    }
    if result.length() >= 32 {
      raise LimitExceeded("ENZYME_MAX_32")
    }
    let enzyme = Enzyme::new(
      fields[0],
      fields[1],
      read_offset(fields[2]),
      read_offset(fields[3]),
    )
    if names.contains(enzyme.name) {
      raise InvalidInput("DUPLICATE_ENZYME_NAME")
    }
    names[enzyme.name] = true
    result.push(enzyme)
  }
  if !header || result.is_empty() {
    raise InvalidInput("ENZYME_EMPTY")
  }
  result
}

///|
/// Canonical synthetic/user-defined model interchange. No catalogue is bundled.
pub fn write_enzymes(enzymes : Array[Enzyme]) -> String raise RestrictError {
  let out = StringBuilder::new()
  out.write_string("name\tmotif\ttop\tbottom\n")
  for e in enzymes {
    out.write_string(
      e.name +
      "\t" +
      e.motif +
      "\t" +
      e.top_offset.to_string() +
      "\t" +
      e.bottom_offset.to_string() +
      "\n",
    )
  }
  let text = out.to_string()
  let _ = read_enzymes(text)
  text
}