///|
fn pdf_text_hex_digit_value(byte : Byte) -> Int? {
  match byte {
    b'0'..=b'9' => Some(byte.to_int() - '0')
    b'A'..=b'F' => Some(byte.to_int() - 'A' + 10)
    b'a'..=b'f' => Some(byte.to_int() - 'a' + 10)
    _ => None
  }
}

///|
fn pdf_text_parse_hex_bytes(
  line : BytesView,
  start : Int,
  stop : Int,
) -> @core.PdfBytes? {
  guard stop >= start else { return None }
  // a zero-length range is valid even when the indices sit outside the
  // view (the previous index-based loops never dereferenced them)
  guard stop > start else { return Some(b"") }
  let nibbles : Array[Int] = []
  for byte in line[start:stop] {
    if @core.pdf_is_whitespace_byte(byte.to_int()) {
      continue
    }
    match pdf_text_hex_digit_value(byte) {
      Some(nibble) => nibbles.push(nibble)
      None => return None
    }
  }
  let output = Array::make((nibbles.length() + 1) / 2, b'\x00')
  for index, nibble in nibbles {
    if index % 2 == 0 {
      output[index / 2] = (nibble * 16).to_byte()
    } else {
      output[index / 2] = (output[index / 2].to_int() + nibble).to_byte()
    }
  }
  Some(Bytes::from_array(output))
}

///|
fn pdf_text_skip_cmap_whitespace(line : BytesView, start : Int) -> Int {
  for index = start; index < line.length() &&
     @core.pdf_is_whitespace_byte(line[index].to_int()); {
    continue index + 1
  } nobreak {
    index
  }
}

///|
fn pdf_text_line_contains_ascii(line : BytesView, word : String) -> Bool {
  pdf_text_index_of_ascii(line, word) != None
}

///|
fn pdf_text_index_of_ascii(line : BytesView, word : String) -> Int? {
  let needle = @ascii.encode(word)
  guard needle.length() > 0 else { return None }
  line.find(needle)
}

///|
fn pdf_text_index_of_ascii_from(
  line : BytesView,
  word : String,
  start : Int,
) -> Int? {
  guard start < line.length() else { return None }
  pdf_text_index_of_ascii(line[start:], word).map(index => start + index)
}

///|
fn pdf_text_after_ascii(line : BytesView, word : String) -> BytesView? {
  let needle = @ascii.encode(word)
  guard needle.length() > 0 else { return None }
  line.find(needle).map(index => line[index + needle.length():])
}

///|
fn pdf_text_view_equals_ascii(view : BytesView, text : String) -> Bool {
  view == @ascii.encode(text)
}

///|
fn pdf_text_cmap_end_marker(line : BytesView, start : Int) -> (Int, Int)? {
  let markers = [
    "endbfchar", "endbfrange", "endcidchar", "endcidrange", "endnotdefchar", "endnotdefrange",
    "endcodespacerange",
  ]
  let mut best : (Int, Int)? = None
  for marker in markers {
    match pdf_text_index_of_ascii_from(line, marker, start) {
      Some(index) =>
        match best {
          Some((best_index, _)) if best_index <= index => ()
          _ => best = Some((index, marker.length()))
        }
      None => ()
    }
  }
  best
}

///|
fn pdf_text_cmap_section_line_views(line : BytesView) -> Array[BytesView] {
  let views : Array[BytesView] = Array(capacity=line.length())
  for start = 0; start < line.length(); {
    match pdf_text_cmap_end_marker(line, start) {
      Some((index, length)) => {
        let stop = index + length
        views.push(line[start:stop])
        continue stop
      }
      None => {
        views.push(line[start:])
        break
      }
    }
  }
  views
}

///|
fn pdf_text_cmap_comment_start(line : BytesView) -> Int {
  for index = 0, in_literal = false, escaped = false, depth = 0; index <
     line.length(); {
    match (in_literal, line[index]) {
      (true, _) if escaped => continue index + 1, true, false, depth
      (true, b'\\') => continue index + 1, true, true, depth
      (true, b'(') => continue index + 1, true, false, depth + 1
      (true, b')') =>
        if depth <= 1 {
          continue index + 1, false, false, 0
        } else {
          continue index + 1, true, false, depth - 1
        }
      (true, _) => continue index + 1, true, false, depth
      (false, b'%') => break index
      (false, b'(') => continue index + 1, true, false, 1
      (false, _) => continue index + 1, false, false, depth
    }
  } nobreak {
    line.length()
  }
}

///|
fn pdf_text_cmap_line_without_comment(line : BytesView) -> @core.PdfBytes {
  line[0:pdf_text_cmap_comment_start(line)].to_owned()
}

///|
fn pdf_text_parse_cmap_hex_groups(line : BytesView) -> Array[@core.PdfBytes]? {
  let groups : Array[@core.PdfBytes] = Array(capacity=line.length() / 2)
  for index = 0; index < line.length(); {
    let start = pdf_text_skip_cmap_whitespace(line, index)
    guard start < line.length() && line[start] is b'<' else { break }
    match pdf_text_parse_cmap_hex_group_at(line, start) {
      Some((bytes, next)) => {
        groups.push(bytes)
        continue next
      }
      None => return None
    }
  }
  if groups.length() == 0 {
    None
  } else {
    Some(groups)
  }
}

///|
fn pdf_text_parse_cmap_hex_group_at(
  line : BytesView,
  start : Int,
) -> (@core.PdfBytes, Int)? {
  let index = pdf_text_skip_cmap_whitespace(line, start)
  guard index < line.length() && line[index] is b'<' else { return None }
  let body_start = index + 1
  guard pdf_text_index_of_byte(line, '>', body_start) is Some(stop) else {
    return None
  }
  pdf_text_parse_hex_bytes(line, body_start, stop).map(bytes => {
    (bytes, stop + 1)
  })
}

///|
fn pdf_text_bytes_to_int(bytes : BytesView) -> Int? {
  guard bytes.length() > 0 else { return None }
  let mut value = 0
  for byte in bytes {
    value = (value << 8) | byte.to_int()
  }
  Some(value)
}

///|
fn pdf_text_parse_cmap_ascii_int_at(
  line : BytesView,
  start : Int,
) -> (Int, Int)? {
  let index = pdf_text_skip_cmap_whitespace(line, start)
  guard index < line.length() else { return None }
  let digit_start = if line[index] is (b'+' | b'-') { index + 1 } else { index }
  let stop = for scan = digit_start; scan < line.length() &&
                line[scan] is (b'0'..=b'9'); {
    continue scan + 1
  } nobreak {
    scan
  }
  guard stop > digit_start else { return None }
  try @core.pdf_parse_ascii_int_view(line[index:stop]) catch {
    _ => None
  } noraise {
    value => Some((value, stop))
  }
}

///|
fn pdf_text_index_of_byte(line : BytesView, target : Int, start : Int) -> Int? {
  for index in start.. Bool {
  for byte in data {
    if @core.pdf_is_whitespace_byte(byte.to_int()) {
      break true
    }
  } nobreak {
    false
  }
}

///|
fn pdf_text_cmap_without_whitespace(data : BytesView) -> @core.PdfBytes {
  let output : Array[Byte] = Array(capacity=data.length())
  for byte in data {
    if !@core.pdf_is_whitespace_byte(byte.to_int()) {
      output.push(byte)
    }
  }
  Bytes::from_array(output)
}

///|
let pdf_text_cmap_split_begin_marker : @core.PdfBytes = b"begin"

///|
let pdf_text_cmap_split_end_marker : @core.PdfBytes = b"end"

///|
let pdf_text_cmap_split_suffixes : ReadOnlyArray[@core.PdfBytes] = [
  b"bfchar", b"bfrange", b"cidchar", b"cidrange", b"notdefchar", b"notdefrange",
  b"codespacerange",
]

///|
fn pdf_text_cmap_has_split_marker_at(
  data : BytesView,
  start : Int,
  prefix : BytesView,
) -> Bool {
  guard start >= 0 && start <= data.length() else { return false }
  guard data[start:].chop_prefix(prefix) is Some(rest) else { return false }
  let suffix_start = pdf_text_skip_cmap_whitespace(rest, 0)
  guard suffix_start > 0 else { return false }
  for suffix in pdf_text_cmap_split_suffixes {
    if rest[suffix_start:].has_prefix(suffix) {
      return true
    }
  }
  false
}

///|
fn pdf_text_cmap_has_split_marker(data : BytesView) -> Bool {
  for index in 0..