///|
fn pdf_metadata_add_unique(values : Array[String], value : String) -> Unit {
  if value != "" && !values.contains(value) {
    values.push(value)
  }
}

///|
fn pdf_metadata_namespace_prefixes(
  data : BytesView,
  xml_uri : String,
  fallback : String,
) -> Array[String] {
  let prefixes : Array[String] = []
  pdf_metadata_add_unique(prefixes, fallback)
  let namespace_bytes = @ascii.encode(xml_uri)
  let xmlns : ReadOnlyArray[Int] = [120, 109, 108, 110, 115, 58]
  let mut search_start = 0
  while search_start < data.length() {
    let namespace_start = pdf_metadata_find_bytes_from(
      data, search_start, namespace_bytes,
    )
    if namespace_start < 0 {
      search_start = data.length()
    } else {
      let mut attribute_start = namespace_start
      while attribute_start > 0 &&
            !pdf_metadata_is_xml_separator(data[attribute_start - 1]) {
        attribute_start -= 1
      }
      if @core.pdf_view_matches_ascii_at(data, attribute_start, xmlns) {
        let prefix_start = attribute_start + xmlns.length()
        let mut prefix_end = prefix_start
        while prefix_end < namespace_start &&
              data[prefix_end].to_int() != 61 &&
              data[prefix_end].to_int() != 34 &&
              data[prefix_end].to_int() != 39 {
          prefix_end += 1
        }
        match pdf_metadata_decode_xml_text(data[prefix_start:prefix_end]) {
          Some(prefix) => pdf_metadata_add_unique(prefixes, prefix)
          None => ()
        }
      }
      search_start = namespace_start + namespace_bytes.length()
    }
  }
  prefixes
}

///|
fn pdf_metadata_extract_quoted_xml_value(
  data : BytesView,
  start : Int,
  quote : Int,
) -> String? {
  let end = pdf_metadata_find_byte(data, start, quote)
  if end < 0 {
    None
  } else {
    pdf_metadata_decode_xml_text(data[start:end])
  }
}

///|
fn pdf_metadata_extract_xmp_attribute(
  data : BytesView,
  prefix : String,
  name : String,
) -> String? {
  let double_pattern = @ascii.encode(prefix + ":" + name + "=\"")
  let double_start = pdf_metadata_find_bytes_from(data, 0, double_pattern)
  if double_start >= 0 {
    pdf_metadata_extract_quoted_xml_value(
      data,
      double_start + double_pattern.length(),
      34,
    )
  } else {
    let single_pattern = @ascii.encode(prefix + ":" + name + "='")
    let single_start = pdf_metadata_find_bytes_from(data, 0, single_pattern)
    if single_start >= 0 {
      pdf_metadata_extract_quoted_xml_value(
        data,
        single_start + single_pattern.length(),
        39,
      )
    } else {
      None
    }
  }
}

///|
fn pdf_metadata_combine_list_items(values : ArrayView[String]) -> String {
  let output = StringBuilder()
  for value in values {
    if !output.is_empty() {
      output.write_string(", ")
    }
    output.write_string(value)
  }
  output.to_string()
}

///|
fn pdf_metadata_extract_rdf_list_items_for_prefix(
  value : BytesView,
  prefix : String,
) -> Array[String] {
  let items : Array[String] = []
  let open_pattern = @ascii.encode("<" + prefix + ":li")
  let close_pattern = @ascii.encode("")
  let mut search_start = 0
  while search_start < value.length() {
    let item_start = pdf_metadata_find_bytes_from(
      value, search_start, open_pattern,
    )
    if item_start < 0 {
      search_start = value.length()
    } else {
      let name_end = item_start + open_pattern.length()
      if name_end >= value.length() ||
        !pdf_metadata_xml_tag_boundary(value[name_end]) {
        search_start = name_end
      } else {
        let item_tag_end = pdf_metadata_find_byte(value, name_end, 62)
        if item_tag_end < 0 {
          search_start = value.length()
        } else if value[item_tag_end - 1].to_int() == 47 {
          search_start = item_tag_end + 1
        } else {
          let item_value_start = item_tag_end + 1
          let item_value_end = pdf_metadata_find_bytes_from(
            value, item_value_start, close_pattern,
          )
          if item_value_end < 0 {
            search_start = value.length()
          } else {
            match
              pdf_metadata_decode_xml_text(
                value[item_value_start:item_value_end],
              ) {
              Some(item) => items.push(item)
              None => ()
            }
            search_start = item_value_end + close_pattern.length()
          }
        }
      }
    }
  }
  items
}

///|
fn pdf_metadata_extract_rdf_list_items(
  data : BytesView,
  value : BytesView,
) -> String? {
  let items : Array[String] = []
  let prefixes = pdf_metadata_namespace_prefixes(
    data, @metadata.pdf_metadata_rdf_namespace, "rdf",
  )
  for prefix in prefixes {
    for item in pdf_metadata_extract_rdf_list_items_for_prefix(value, prefix) {
      items.push(item)
    }
  }
  if items.length() == 0 {
    None
  } else {
    Some(pdf_metadata_combine_list_items(items))
  }
}

///|
fn pdf_metadata_extract_xmp_element(
  data : BytesView,
  prefix : String,
  name : String,
) -> String? {
  let open_pattern = @ascii.encode("<" + prefix + ":" + name)
  let tag_start = pdf_metadata_find_bytes_from(data, 0, open_pattern)
  if tag_start < 0 {
    None
  } else {
    let tag_end = pdf_metadata_find_byte(
      data,
      tag_start + open_pattern.length(),
      62,
    )
    if tag_end < 0 || data[tag_end - 1].to_int() == 47 {
      None
    } else {
      let close_pattern = @ascii.encode("")
      let value_start = tag_end + 1
      let value_end = pdf_metadata_find_bytes_from(
        data, value_start, close_pattern,
      )
      if value_end < 0 {
        None
      } else {
        let value = data[value_start:value_end]
        match pdf_metadata_extract_rdf_list_items(data, value) {
          Some(items) => Some(items)
          None => pdf_metadata_decode_xml_text(value)
        }
      }
    }
  }
}

///|
fn pdf_metadata_get_xmp_data(
  data : BytesView,
  xml_uri : String,
  fallback_prefix : String,
  name : String,
) -> String? {
  let prefixes = pdf_metadata_namespace_prefixes(data, xml_uri, fallback_prefix)
  for prefix in prefixes {
    match pdf_metadata_extract_xmp_attribute(data, prefix, name) {
      Some(value) => break Some(value)
      None =>
        match pdf_metadata_extract_xmp_element(data, prefix, name) {
          Some(value) => break Some(value)
          None => ()
        }
    }
  } nobreak {
    None
  }
}

///|
/// Return the first XMP metadata value matching an XML namespace URI and local
/// name. This is the tree-free counterpart of cpdfmetadata's `get_data_for`,
/// covering namespace aliases, attributes, simple element text, and RDF lists.
pub fn pdf_metadata_get_data_for_bytes(
  xml_uri : String,
  name : String,
  metadata : BytesView,
) -> String? {
  pdf_metadata_get_xmp_data(
    metadata,
    xml_uri,
    pdf_metadata_namespace_fallback_prefix(xml_uri),
    name,
  )
}