///|
fn attribute_value_by_name(node : @dom.Node, attr_name : StringView) -> String? {
  let wanted = @syn.lower_ascii(attr_name)
  for name, value in node.attrs {
    if @syn.lower_ascii(name) == wanted {
      return Some(
        match value {
          Some(text) => text
          None => ""
        },
      )
    }
  }
  None
}

///|
priv enum AttributeSelectorOperator {
  AttrExact
  AttrWord
  AttrHyphen
  AttrPrefix
  AttrSuffix
  AttrContains
}

///|
fn attribute_name_with_operator(
  name : StringView,
  operator : AttributeSelectorOperator,
) -> (StringView, AttributeSelectorOperator)? {
  let end = name.length() - 1
  guard name.get_view(start=0, end~) is Some(name) else { return None }
  let name = name.trim()
  if !selector_identifier_is_valid(name) {
    None
  } else {
    Some((name, operator))
  }
}

///|
fn attribute_name_and_operator(
  name : StringView,
) -> (StringView, AttributeSelectorOperator)? {
  let name = name.trim()
  if name.has_suffix("~") {
    attribute_name_with_operator(name, AttrWord)
  } else if name.has_suffix("|") {
    attribute_name_with_operator(name, AttrHyphen)
  } else if name.has_suffix("^") {
    attribute_name_with_operator(name, AttrPrefix)
  } else if name.has_suffix("$") {
    attribute_name_with_operator(name, AttrSuffix)
  } else if name.has_suffix("*") {
    attribute_name_with_operator(name, AttrContains)
  } else if selector_identifier_is_valid(name) {
    Some((name, AttrExact))
  } else {
    None
  }
}

///|
fn quoted_attribute_selector_value(value : StringView) -> String? {
  let value = value.trim()
  if value.length() < 2 {
    return Some(value.to_owned())
  }
  let end = value.length() - 1
  match (value.get_char(0), value.get_char(end)) {
    (Some('"'), Some('"')) | (Some('\''), Some('\'')) =>
      Some(unescape_quoted_attribute_selector_value(value[1:end]))
    _ =>
      if string_view_has_html_whitespace(value) {
        None
      } else {
        Some(value.to_owned())
      }
  }
}

///|
fn unescape_quoted_attribute_selector_value(value : StringView) -> String {
  let out = StringBuilder::new(size_hint=value.length())
  let mut escaped = false
  for ch in value {
    if escaped {
      out.write_char(ch)
      escaped = false
    } else if ch == '\\' {
      escaped = true
    } else {
      out.write_char(ch)
    }
  }
  out.to_string()
}

///|
fn attribute_hyphen_matches(actual : StringView, expected : StringView) -> Bool {
  if actual == expected {
    return true
  }
  if actual.length() <= expected.length() || !actual.has_prefix(expected) {
    return false
  }
  match actual.get_char(expected.length()) {
    Some('-') => true
    _ => false
  }
}

///|
fn attribute_value_matches(
  actual : StringView,
  operator : AttributeSelectorOperator,
  expected : StringView,
) -> Bool {
  match operator {
    AttrExact => actual == expected
    AttrWord => class_contains(actual, expected)
    AttrHyphen => attribute_hyphen_matches(actual, expected)
    AttrPrefix => !expected.is_empty() && actual.has_prefix(expected)
    AttrSuffix => !expected.is_empty() && actual.has_suffix(expected)
    AttrContains => string_view_contains(actual, expected)
  }
}

///|
fn attribute_selector_matches(node : @dom.Node, selector : StringView) -> Bool {
  let selector = selector.trim()
  if selector.is_empty() {
    return false
  }
  match selector.split_once("=") {
    Some((name, expected)) => {
      guard attribute_name_and_operator(name) is Some((name, operator)) else {
        return false
      }
      guard quoted_attribute_selector_value(expected) is Some(expected) else {
        return false
      }
      match attribute_value_by_name(node, name) {
        Some(actual) => attribute_value_matches(actual, operator, expected)
        None => false
      }
    }
    None =>
      selector_identifier_is_valid(selector) &&
      attribute_value_by_name(node, selector) is Some(_)
  }
}

///|
fn attribute_selector_is_valid(selector : StringView) -> Bool {
  let selector = selector.trim()
  if selector.is_empty() {
    return false
  }
  match selector.split_once("=") {
    Some((name, expected)) =>
      attribute_name_and_operator(name) is Some(_) &&
      quoted_attribute_selector_value(expected) is Some(_)
    None => selector_identifier_is_valid(selector)
  }
}