///|
fn pdf_bookmark_name_spec_scan_to_at(
  bytes : BytesView,
  start : Int,
) -> (Int, Int) {
  let mut end = start
  while end < bytes.length() && bytes[end].to_int() != 64 {
    end = end + 1
  }
  let next = if end < bytes.length() { end + 1 } else { end }
  (end - start, next)
}

///|
fn pdf_bookmark_name_spec_ascii_length(text : String) -> Int {
  @ascii.encode(text).length()
}

///|
fn pdf_bookmark_name_spec_copy_ascii(
  output : Array[Byte],
  position : Int,
  text : String,
) -> Int {
  let mut position = position
  for byte in @ascii.encode(text) {
    output[position] = byte
    position += 1
  }
  position
}

///|
fn pdf_bookmark_name_spec_process_at_fields_length(
  bytes : BytesView,
  bookmark_name : String,
  sequence : Int,
  filename : String,
  start_page : Int,
  end_page : Int,
) -> Int raise @core.PdfError {
  let mut output_length = 0
  let mut index = 0
  while index < bytes.length() {
    if bytes[index].to_int() == 64 && index + 1 < bytes.length() {
      match bytes[index + 1].to_int() {
        70 => {
          output_length += pdf_bookmark_name_spec_ascii_length(filename)
          index = index + 2
        }
        78 | 83 | 69 as field => {
          let (width, next) = pdf_bookmark_name_spec_scan_to_at(
            bytes,
            index + 2,
          )
          let value = match field {
            78 => sequence
            83 => start_page
            _ => end_page
          }
          output_length += pdf_bookmark_name_spec_ascii_length(
            pdf_bookmark_name_spec_padded_number(width, value),
          )
          index = next
        }
        66 => {
          output_length += pdf_bookmark_name_spec_ascii_length(bookmark_name)
          index = index + 2
        }
        98 => {
          let mut digits_end = index + 2
          while digits_end < bytes.length() {
            let digit = bytes[digits_end].to_int()
            if digit < 48 || digit > 57 {
              break
            }
            digits_end = digits_end + 1
          }
          if digits_end == index + 2 {
            raise SoftError("Bad @b spec")
          }
          let length = pdf_bookmark_name_spec_decimal(
            bytes,
            index + 2,
            digits_end,
          )
          output_length += pdf_bookmark_name_spec_ascii_length(
            pdf_bookmark_name_spec_utf8_prefix(bookmark_name, length),
          )
          index = if digits_end < bytes.length() {
            digits_end + 1
          } else {
            digits_end
          }
        }
        _ => {
          output_length += 1
          index = index + 1
        }
      }
    } else {
      output_length += 1
      index = index + 1
    }
  }
  output_length
}

///|
fn PdfDocument::pdf_bookmark_name_spec_process_at_fields(
  self : PdfDocument,
  bookmarks : ArrayView[@bookmark.PdfBookmark],
  split_level : Int,
  spec : String,
  sequence : Int,
  filename : String,
  start_page : Int,
  end_page : Int,
) -> String raise @core.PdfError {
  let bytes = @utf8.encode(spec)
  let bookmark_name = self.pdf_bookmark_name_for_page(
    bookmarks, split_level, start_page,
  )
  let output = Array::make(
    pdf_bookmark_name_spec_process_at_fields_length(
      bytes, bookmark_name, sequence, filename, start_page, end_page,
    ),
    b'\x00',
  )
  let mut position = 0
  let mut index = 0
  while index < bytes.length() {
    if bytes[index].to_int() == 64 && index + 1 < bytes.length() {
      match bytes[index + 1].to_int() {
        70 => {
          position = pdf_bookmark_name_spec_copy_ascii(
            output, position, filename,
          )
          index = index + 2
        }
        78 | 83 | 69 as field => {
          let (width, next) = pdf_bookmark_name_spec_scan_to_at(
            bytes,
            index + 2,
          )
          let value = match field {
            78 => sequence
            83 => start_page
            _ => end_page
          }
          position = pdf_bookmark_name_spec_copy_ascii(
            output,
            position,
            pdf_bookmark_name_spec_padded_number(width, value),
          )
          index = next
        }
        66 => {
          position = pdf_bookmark_name_spec_copy_ascii(
            output, position, bookmark_name,
          )
          index = index + 2
        }
        98 => {
          let mut digits_end = index + 2
          while digits_end < bytes.length() {
            let digit = bytes[digits_end].to_int()
            if digit < 48 || digit > 57 {
              break
            }
            digits_end = digits_end + 1
          }
          if digits_end == index + 2 {
            raise SoftError("Bad @b spec")
          }
          let length = pdf_bookmark_name_spec_decimal(
            bytes,
            index + 2,
            digits_end,
          )
          position = pdf_bookmark_name_spec_copy_ascii(
            output,
            position,
            pdf_bookmark_name_spec_utf8_prefix(bookmark_name, length),
          )
          index = if digits_end < bytes.length() {
            digits_end + 1
          } else {
            digits_end
          }
        }
        _ => {
          output[position] = bytes[index]
          position += 1
          index = index + 1
        }
      }
    } else {
      output[position] = bytes[index]
      position += 1
      index = index + 1
    }
  }
  @utf8.decode(Bytes::from_array(output)) catch {
    _ => raise InvalidUTF8
  }
}