///|
fn invalid(line : Int32, reason : String) -> Unit raise LockError {
  raise Invalid(line~, reason~)
}

///|
/// Checked digit accumulation, including the full unsigned inode domain.
fn unsigned(
  text : String,
  base : UInt64,
  cap : UInt64,
  line : Int32,
) -> UInt64 raise LockError {
  if text.is_empty() {
    invalid(line, "empty integer")
  }
  let mut value = 0UL
  for c in text {
    let digit = match c {
      '0'..<='9' => (c.to_int() - 48).to_uint64()
      'a'..<='f' => (c.to_int() - 87).to_uint64()
      'A'..<='F' => (c.to_int() - 55).to_uint64()
      _ => {
        invalid(line, "invalid integer digit")
        0UL
      }
    }
    if digit >= base || digit > cap || value > (cap - digit) / base {
      invalid(line, "integer outside supported range")
    }
    value = value * base + digit
  }
  value
}

///|
fn nonnegative(text : String, line : Int32) -> Int64 raise LockError {
  unsigned(text, 10UL, 9223372036854775807UL, line).reinterpret_as_int64()
}

///|
fn parse_file(text : String, line : Int32) -> FileKey? raise LockError {
  if text == ":0" {
    return None
  }
  let parts = text.split(":").map(x => x.to_owned()).collect()
  guard parts is [major, minor, inode] else {
    invalid(line, "expected hex-major:hex-minor:decimal-inode")
    return None
  }
  Some({
    major: unsigned(major, 16UL, 4294967295UL, line).to_uint(),
    minor: unsigned(minor, 16UL, 4294967295UL, line).to_uint(),
    inode: unsigned(inode, 10UL, 18446744073709551615UL, line),
  })
}

///|
pub fn file_key(text : String) -> FileKey raise LockError {
  match parse_file(text, 0) {
    Some(key) => key
    None => {
      invalid(0, "a concrete file identity is required")
      abort("unreachable")
    }
  }
}

///|
pub fn byte_range(start : String, end : String) -> ByteRange raise LockError {
  range_at(start, end, 0)
}

///|
fn range_at(
  start : String,
  end : String,
  line : Int32,
) -> ByteRange raise LockError {
  let first = nonnegative(start, line)
  let last = if end == "EOF" { None } else { Some(nonnegative(end, line)) }
  if last is Some(n) && n < first {
    invalid(line, "range end precedes start")
  }
  { start: first, end: last, }
}