///|
fn pdf_metadata_xml_entity_digit(byte : Byte, base : Int) -> Int? {
  let value = byte.to_int()
  if value >= 48 && value <= 57 {
    let digit = value - 48
    if digit < base {
      Some(digit)
    } else {
      None
    }
  } else if base == 16 && value >= 65 && value <= 70 {
    Some(value - 55)
  } else if base == 16 && value >= 97 && value <= 102 {
    Some(value - 87)
  } else {
    None
  }
}

///|
fn pdf_metadata_xml_entity_codepoint(entity : BytesView) -> Int? {
  if entity.length() == 3 && pdf_metadata_view_matches_at(entity, 0, b"amp") {
    Some(38)
  } else if entity.length() == 2 &&
    pdf_metadata_view_matches_at(entity, 0, b"lt") {
    Some(60)
  } else if entity.length() == 2 &&
    pdf_metadata_view_matches_at(entity, 0, b"gt") {
    Some(62)
  } else if entity.length() == 4 &&
    pdf_metadata_view_matches_at(entity, 0, b"quot") {
    Some(34)
  } else if entity.length() == 4 &&
    pdf_metadata_view_matches_at(entity, 0, b"apos") {
    Some(39)
  } else if entity.length() >= 2 && entity[0].to_int() == 35 {
    let mut index = 1
    let mut base = 10
    if index < entity.length() &&
      (entity[index].to_int() == 120 || entity[index].to_int() == 88) {
      base = 16
      index += 1
    }
    if index >= entity.length() {
      return None
    }
    let mut codepoint = 0
    while index < entity.length() {
      match pdf_metadata_xml_entity_digit(entity[index], base) {
        Some(digit) => codepoint = codepoint * base + digit
        None => return None
      }
      if codepoint > 0x10FFFF {
        return None
      }
      index += 1
    }
    if codepoint >= 0xD800 && codepoint <= 0xDFFF {
      None
    } else {
      Some(codepoint)
    }
  } else {
    None
  }
}

///|
fn pdf_metadata_write_xml_codepoint(
  output : Array[Byte],
  position : Int,
  codepoint : Int,
) -> Int? {
  let encoded = pdf_utf8_of_codepoints([codepoint]) catch { _ => return None }
  let mut current = position
  for byte in encoded {
    output[current] = byte
    current += 1
  }
  Some(current)
}

///|
fn pdf_metadata_xml_codepoint_is_space(codepoint : Int) -> Bool {
  codepoint == 9 || codepoint == 10 || codepoint == 13 || codepoint == 32
}

///|
fn pdf_metadata_decode_xml_text(value : BytesView) -> String? {
  let trimmed = pdf_metadata_trim_xml_text(value)
  let output = Array::make(trimmed.length(), b'\x00')
  let mut output_position = 0
  let mut pending_space = false
  let mut index = 0
  while index < trimmed.length() {
    if trimmed[index].to_int() == 38 {
      let semicolon = pdf_metadata_find_byte(trimmed, index + 1, 59)
      if semicolon > index {
        match pdf_metadata_xml_entity_codepoint(trimmed[index + 1:semicolon]) {
          Some(codepoint) =>
            if pdf_metadata_xml_codepoint_is_space(codepoint) {
              if output_position > 0 {
                pending_space = true
              }
              index = semicolon + 1
            } else {
              if pending_space {
                output[output_position] = 32
                output_position += 1
                pending_space = false
              }
              match
                pdf_metadata_write_xml_codepoint(
                  output, output_position, codepoint,
                ) {
                Some(position) => {
                  output_position = position
                  index = semicolon + 1
                }
                None => {
                  output[output_position] = trimmed[index]
                  output_position += 1
                  index += 1
                }
              }
            }
          _ => {
            if pending_space {
              output[output_position] = 32
              output_position += 1
              pending_space = false
            }
            output[output_position] = trimmed[index]
            output_position += 1
            index += 1
          }
        }
      } else {
        if pending_space {
          output[output_position] = 32
          output_position += 1
          pending_space = false
        }
        output[output_position] = trimmed[index]
        output_position += 1
        index += 1
      }
    } else if pdf_metadata_is_xml_space(trimmed[index]) {
      if output_position > 0 {
        pending_space = true
      }
      index += 1
    } else {
      if pending_space {
        output[output_position] = 32
        output_position += 1
        pending_space = false
      }
      output[output_position] = trimmed[index]
      output_position += 1
      index += 1
    }
  }
  let text = @utf8.decode(Bytes::from_array(output[:output_position])) catch {
    _ => return None
  }
  Some(text)
}