///|
fn is_first_element_child(node : @dom.Node) -> Bool {
  match node.parent {
    Some(parent) => {
      for child in parent.children {
        if child.kind == Element {
          return physical_equal(child, node)
        }
      }
      false
    }
    None => false
  }
}

///|
fn is_last_element_child(node : @dom.Node) -> Bool {
  match node.parent {
    Some(parent) => {
      let mut last : @dom.Node? = None
      for child in parent.children {
        if child.kind == Element {
          last = Some(child)
        }
      }
      match last {
        Some(child) => physical_equal(child, node)
        None => false
      }
    }
    None => false
  }
}

///|
fn is_only_element_child(node : @dom.Node) -> Bool {
  is_first_element_child(node) && is_last_element_child(node)
}

///|
fn is_empty_for_selector(node : @dom.Node) -> Bool {
  for child in node.children {
    match child.kind {
      Element => return false
      Text if !child.data[:].trim().is_empty() => return false
      _ => ()
    }
  }
  true
}

///|
fn is_root_for_selector(node : @dom.Node) -> Bool {
  match node.parent {
    Some(parent) => parent.kind == Document || parent.kind == Fragment
    None => false
  }
}

///|
fn is_first_of_type(node : @dom.Node) -> Bool {
  match node.parent {
    Some(parent) => {
      for child in parent.children {
        if child.kind == Element && child.name == node.name {
          return physical_equal(child, node)
        }
      }
      false
    }
    None => false
  }
}

///|
fn is_last_of_type(node : @dom.Node) -> Bool {
  match node.parent {
    Some(parent) => {
      let mut last : @dom.Node? = None
      for child in parent.children {
        if child.kind == Element && child.name == node.name {
          last = Some(child)
        }
      }
      match last {
        Some(child) => physical_equal(child, node)
        None => false
      }
    }
    None => false
  }
}

///|
fn is_only_of_type(node : @dom.Node) -> Bool {
  is_first_of_type(node) && is_last_of_type(node)
}

///|
fn element_child_index(node : @dom.Node) -> Int? {
  match node.parent {
    Some(parent) => {
      let mut index = 0
      for child in parent.children {
        if child.kind == Element {
          index += 1
          if physical_equal(child, node) {
            return Some(index)
          }
        }
      }
      None
    }
    None => None
  }
}

///|
fn element_type_index(node : @dom.Node) -> Int? {
  match node.parent {
    Some(parent) => {
      let mut index = 0
      for child in parent.children {
        if child.kind == Element && child.name == node.name {
          index += 1
          if physical_equal(child, node) {
            return Some(index)
          }
        }
      }
      None
    }
    None => None
  }
}

///|
fn normalize_nth_expression(expr : StringView) -> String {
  let out = StringBuilder(size_hint=expr.length())
  for ch in expr {
    if !ch.is_ascii_whitespace() {
      if ch.is_ascii_uppercase() {
        out.write_char(ch.to_ascii_lowercase())
      } else {
        out.write_char(ch)
      }
    }
  }
  out.to_string()
}

///|
fn parse_signed_int_view(expr : StringView) -> Int? {
  if expr.is_empty() {
    return None
  }
  let mut sign = 1
  let mut pos = 0
  match expr.get_char(pos) {
    Some('+') => pos += 1
    Some('-') => {
      sign = -1
      pos += 1
    }
    _ => ()
  }
  if pos >= expr.length() {
    return None
  }
  // A number past the range of `Int` is rejected rather than allowed to wrap
  // around to a small one, so it matches nothing, as in browsers. The bound
  // is one larger for negative numbers, which reach -2147483648.
  let limit = if sign < 0 { 2147483648L } else { 2147483647L }
  let mut value = 0L
  while pos < expr.length() {
    match expr.get_char(pos) {
      Some(ch) if ch.is_digit(10) => {
        let digit = (ch.to_int() - ('0' : Int)).to_int64()
        if value > (limit - digit) / 10L {
          return None
        }
        value = value * 10L + digit
        pos += ch.utf16_len()
      }
      _ => return None
    }
  }
  Some((sign.to_int64() * value).to_int())
}

///|
fn parse_nth_expression(expr : StringView) -> (Int, Int)? {
  let normalized = normalize_nth_expression(expr)
  let expr = normalized[:]
  if expr.is_empty() {
    return None
  }
  if expr == "odd" {
    return Some((2, 1))
  }
  if expr == "even" {
    return Some((2, 0))
  }
  match expr.split_once("n") {
    Some((a_part, b_part)) => {
      if string_view_contains(b_part, "n") {
        return None
      }
      let a = if a_part.is_empty() || a_part == "+" {
        Some(1)
      } else if a_part == "-" {
        Some(-1)
      } else {
        parse_signed_int_view(a_part)
      }
      guard a is Some(a) else { return None }
      let b = if b_part.is_empty() {
        Some(0)
      } else {
        parse_signed_int_view(b_part)
      }
      guard b is Some(b) else { return None }
      Some((a, b))
    }
    None => parse_signed_int_view(expr).map(b => (0, b))
  }
}

///|
fn nth_formula_matches(index : Int, a : Int, b : Int) -> Bool {
  if a == 0 {
    index == b
  } else {
    // In `Int64`: `index - b` overflows `Int` when `b` is far below zero.
    let diff = index.to_int64() - b.to_int64()
    let a = a.to_int64()
    if a > 0L {
      diff >= 0L && diff % a == 0L
    } else {
      diff <= 0L && diff % a == 0L
    }
  }
}

///|
fn is_nth_child(node : @dom.Node, expr : StringView) -> Bool {
  guard parse_nth_expression(expr) is Some((a, b)) else { return false }
  match element_child_index(node) {
    Some(index) => nth_formula_matches(index, a, b)
    None => false
  }
}

///|
fn is_nth_of_type(node : @dom.Node, expr : StringView) -> Bool {
  guard parse_nth_expression(expr) is Some((a, b)) else { return false }
  match element_type_index(node) {
    Some(index) => nth_formula_matches(index, a, b)
    None => false
  }
}