///|
fn pdf_pagespec_is_digit(value : Int) -> Bool {
  value >= 48 && value <= 57
}

///|
fn pdf_pagespec_is_separator(value : Int) -> Bool {
  value == 45 || value == 44 || pdf_is_whitespace_byte(value)
}

///|
fn pdf_pagespec_parse_nonnegative_int(text : String) -> Int? {
  if text.length() == 0 {
    None
  } else {
    let mut valid = true
    for char in text {
      let value = char.to_int()
      if !pdf_pagespec_is_digit(value) {
        valid = false
      }
    }
    if !valid {
      None
    } else {
      let parsed : Result[Int, Error] = try? @strconv.from_str(text)
      match parsed {
        Ok(value) if value >= 0 => Some(value)
        _ => None
      }
    }
  }
}

///|
fn pdf_pagespec_token_of_view(
  view : BytesView,
  context : PdfPageSpecContext,
) -> PdfPageSpecToken raise PdfError {
  let text = @ascii.decode_lossy(view)
  match pdf_pagespec_parse_nonnegative_int(text) {
    Some(value) => PageSpecNumber(value)
    None =>
      if text.length() == 0 {
        context.bad()
        PageSpecWord("")
      } else {
        PageSpecWord(text)
      }
  }
}

///|
fn pdf_pagespec_tokenize(
  text : String,
  context : PdfPageSpecContext,
) -> Array[PdfPageSpecToken] raise PdfError {
  let bytes = @utf8.encode(text)
  let tokens : Array[PdfPageSpecToken] = Array::new(capacity=bytes.length())
  let mut index = 0
  while index < bytes.length() {
    let value = bytes[index].to_int()
    if pdf_is_whitespace_byte(value) {
      index += 1
    } else if value == 45 {
      tokens.push(PageSpecDash)
      index += 1
    } else if value == 44 {
      tokens.push(PageSpecComma)
      index += 1
    } else {
      let start = index
      while index < bytes.length() &&
            !pdf_pagespec_is_separator(bytes[index].to_int()) {
        index += 1
      }
      tokens.push(pdf_pagespec_token_of_view(bytes[start:index], context))
    }
  }
  tokens
}