///|
/// Find the first occurrence of an opening tag `` and return the
/// tag text without the leading `<` and without the closing `>`.
///
/// Example: `` -> `a b="c"`.
fn xml_open_tag_end_from(xml : StringView, start : Int) -> Int? {
  let mut index = start
  let mut quote : UInt16? = None
  while index < xml.length() {
    let unit = xml[index]
    match quote {
      Some(delimiter) => if unit == delimiter { quote = None }
      None =>
        if unit == ('"' : UInt16) || unit == ('\'' : UInt16) {
          quote = Some(unit)
        } else if unit == ('>' : UInt16) {
          return Some(index)
        }
    }
    index = index + 1
  }
  None
}

///|
fn is_xml_open_tag_boundary(unit : UInt16) -> Bool {
  is_xml_attr_space_unit(unit) ||
  unit == ('/' : UInt16) ||
  unit == ('>' : UInt16)
}

///|
fn find_xml_open_tag_start_from(
  xml : StringView,
  tag_name : StringView,
  start_at : Int,
) -> Int? {
  let mut start = start_at.max(0)
  let name_length = tag_name.length()
  while start + 1 + name_length <= xml.length() {
    if xml[start] == ('<' : UInt16) {
      let mut matches = true
      for offset in 0.. Int? {
  find_xml_open_tag_start_from(xml, tag_name, 0)
}

///|
/// Finds an XML end tag and accepts the XML-permitted whitespace between its
/// QName and `>`, for example `` and ``.
fn find_xml_close_tag_from(
  xml : StringView,
  tag_name : StringView,
  start_at : Int,
) -> (Int, Int)? {
  let mut start = start_at.max(0)
  let name_length = tag_name.length()
  while start + 2 + name_length <= xml.length() {
    if xml[start] == ('<' : UInt16) && xml[start + 1] == ('/' : UInt16) {
      let mut matches = true
      for offset in 0..' : UInt16) {
          return Some((start, end))
        }
      }
    }
    start = start + 1
  }
  None
}

///|
fn tag_attributes_in(xml : StringView, tag_name : StringView) -> String? {
  let xml_str = xml.to_owned()
  let start = match find_xml_open_tag_start(xml_str, tag_name) {
    Some(pos) => pos
    None => return None
  }
  let end = match xml_open_tag_end_from(xml_str, start + 1) {
    Some(pos) => pos
    None => return None
  }
  let slice = xml_str[start + 1:end]
  Some(slice.to_owned())
}

///|
/// Extract the body text from the first `...` pair.
fn extract_tag_body_from(xml : StringView, tag_name : StringView) -> String? {
  let xml_str = xml.to_owned()
  let start = match find_xml_open_tag_start(xml_str, tag_name) {
    Some(pos) => pos
    None => return None
  }
  let open_end = match xml_open_tag_end_from(xml_str, start + 1) {
    Some(pos) => pos
    None => return None
  }
  let body_start = open_end + 1
  let end = match find_xml_close_tag_from(xml_str, tag_name, body_start) {
    Some((pos, _)) => pos
    None => return None
  }
  let body = xml_str[body_start:end]
  Some(body.to_owned())
}

///|
/// Extract the body text from the first `...` pair.
///
/// This differs from `extract_tag_body_from` only in that it errors (instead of
/// returning `None`) when the tag is found but is malformed (missing close `>`
/// or missing close tag).
fn extract_tag_body(
  xml : StringView,
  tag_name : StringView,
) -> String? raise XlsxError {
  let xml_str = xml.to_owned()
  let start = match find_xml_open_tag_start(xml_str, tag_name) {
    Some(pos) => pos
    None => return None
  }
  let open_end = match xml_open_tag_end_from(xml_str, start + 1) {
    Some(pos) => pos
    None => raise InvalidXml(msg="tag open not closed")
  }
  if xml_str[start + 1:open_end].trim().has_suffix("/") {
    return Some("")
  }
  let body_start = open_end + 1
  let end = match find_xml_close_tag_from(xml_str, tag_name, body_start) {
    Some((pos, _)) => pos
    None => raise InvalidXml(msg="tag close missing")
  }
  let body = xml_str[body_start:end]
  Some(body.to_owned())
}

///|
fn is_xml_attr_space_unit(ch : UInt16) -> Bool {
  ch == (' ' : UInt16) ||
  ch == ('\t' : UInt16) ||
  ch == ('\n' : UInt16) ||
  ch == ('\r' : UInt16)
}

///|
/// Replace `attr_name="..."` inside the first open tag in `xml`.
fn replace_attr_value_in_open_tag(
  xml : StringView,
  attr_name : StringView,
  value : String,
) -> String? {
  let text = xml.to_owned()
  let tag_start = match text.find("<") {
    Some(pos) => pos
    None => return None
  }
  let mut pos = tag_start + 1
  while pos < text.length() &&
        !is_xml_attr_space_unit(text[pos]) &&
        text[pos] != '>' {
    pos = pos + 1
  }
  let mut value_start = -1
  let mut value_end = -1
  while pos < text.length() {
    while pos < text.length() && is_xml_attr_space_unit(text[pos]) {
      pos = pos + 1
    }
    if pos >= text.length() || text[pos] == '>' || text[pos] == '/' {
      break
    }
    let name_start = pos
    while pos < text.length() &&
          !is_xml_attr_space_unit(text[pos]) &&
          text[pos] != '=' &&
          text[pos] != '>' &&
          text[pos] != '/' {
      pos = pos + 1
    }
    let name_end = pos
    while pos < text.length() && is_xml_attr_space_unit(text[pos]) {
      pos = pos + 1
    }
    if pos >= text.length() || text[pos] != '=' {
      return None
    }
    pos = pos + 1
    while pos < text.length() && is_xml_attr_space_unit(text[pos]) {
      pos = pos + 1
    }
    if pos >= text.length() || (text[pos] != '"' && text[pos] != '\'') {
      return None
    }
    let quote = text[pos]
    pos = pos + 1
    let current_value_start = pos
    while pos < text.length() && text[pos] != quote {
      pos = pos + 1
    }
    if pos >= text.length() {
      return None
    }
    if text[name_start:name_end] == attr_name {
      value_start = current_value_start
      value_end = pos
      break
    }
    pos = pos + 1
  }
  if value_start < 0 {
    return None
  }
  let sb = StringBuilder::new()
  sb.write_view(text[:value_start])
  sb.write_view(escape_xml_attr(value))
  sb.write_view(text[value_end:])
  Some(sb.to_string())
}

///|
test "ooxml_utils: tag_attributes_in finds first open tag" {
  let xml = ""
  debug_inspect(tag_attributes_in(xml, "a"), content="Some(\"a b=\\\"c\\\"/\")")
}

///|
test "ooxml_utils: opening-tag scanners ignore delimiters inside either quote" {
  let xml = " y\" sqref='A1'>ok"
  debug_inspect(
    tag_attributes_in(xml, "a"),
    content="Some(\"a prompt='1 > 0' title=\\\"x > y\\\" sqref='A1'\")",
  )
  debug_inspect(extract_tag_body_from(xml, "a"), content="Some(\"ok\")")
  debug_inspect(extract_tag_body(xml, "a"), content="Some(\"ok\")")
}

///|
test "ooxml_utils: tag_attributes_in returns None when missing" {
  let xml = ""
  debug_inspect(tag_attributes_in(xml, "a"), content="None")
}

///|
test "ooxml_utils: opening-tag lookup requires an exact name boundary" {
  let xml = ""
  debug_inspect(tag_attributes_in(xml, "a"), content="Some(\"a sqref='A1'/\")")
}

///|
test "ooxml_utils: extract_tag_body_from extracts body" {
  let xml = "helloworld"
  debug_inspect(extract_tag_body_from(xml, "a"), content="Some(\"hello\")")
}

///|
test "ooxml_utils: extract_tag_body accepts an empty self-closing container" {
  debug_inspect(
    extract_tag_body("", "items"),
    content="Some(\"\")",
  )
}

///|
test "ooxml_utils: extract_tag_body_from returns None when missing" {
  let xml = ""
  debug_inspect(extract_tag_body_from(xml, "a"), content="None")
}

///|
test "ooxml_utils: replace_attr_value_in_open_tag rewrites attribute and escapes" {
  let xml = "x"
  let updated = replace_attr_value_in_open_tag(xml, "sqref", "B2 & \"<\"")
  debug_inspect(
    updated,
    content="Some(\"x\")",
  )
}

///|
test "ooxml_utils: replace_attr_value_in_open_tag returns None when attribute missing" {
  let xml = "nope"
  debug_inspect(
    replace_attr_value_in_open_tag(xml, "sqref", "B2"),
    content="None",
  )
}

///|
test "ooxml_utils: replace_attr_value_in_open_tag accepts spaces around equals" {
  let xml = "x"
  debug_inspect(
    replace_attr_value_in_open_tag(xml, "sqref", "B2"),
    content="Some(\"x\")",
  )
}

///|
test "ooxml_utils: replace_attr_value_in_open_tag ignores prefixed names" {
  let xml = "x"
  debug_inspect(
    replace_attr_value_in_open_tag(xml, "sqref", "B2"),
    content="Some(\"x\")",
  )
}

///|
test "ooxml_utils: replace_attr_value_in_open_tag supports mixed quotes" {
  let xml = "x"
  debug_inspect(
    replace_attr_value_in_open_tag(xml, "sqref", "B2"),
    content="Some(\"x\")",
  )
}