///|
/// Generated content, pseudo-element, and CSS counter helpers.

///|
priv struct CounterEntry {
  value : Int
  owner : String
  owner_is_style_scope : Bool
}

///|
priv struct CounterDirective {
  name : String
  amount : Int
}

///|
priv enum ContentPart {
  Text(String)
  Counter(String)
  Counters(String, String)
}

///|
priv enum PseudoKind {
  Before
  After
}

///|
fn is_ascii_whitespace(c : Char) -> Bool {
  c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\u000C'
}

///|
fn is_ascii_alpha_num_hyphen(c : Char) -> Bool {
  let cp = c.to_int()
  (cp >= 'a'.to_int() && cp <= 'z'.to_int()) ||
  (cp >= 'A'.to_int() && cp <= 'Z'.to_int()) ||
  (cp >= '0'.to_int() && cp <= '9'.to_int()) ||
  c == '-' ||
  c == '_'
}

///|
fn is_ascii_hex_digit(c : Char) -> Bool {
  let cp = c.to_int()
  (cp >= '0'.to_int() && cp <= '9'.to_int()) ||
  (cp >= 'a'.to_int() && cp <= 'f'.to_int()) ||
  (cp >= 'A'.to_int() && cp <= 'F'.to_int())
}

///|
fn ascii_hex_digit_to_int(c : Char) -> Int {
  let cp = c.to_int()
  if cp >= '0'.to_int() && cp <= '9'.to_int() {
    cp - '0'.to_int()
  } else if cp >= 'a'.to_int() && cp <= 'f'.to_int() {
    cp - 'a'.to_int() + 10
  } else {
    cp - 'A'.to_int() + 10
  }
}

///|
fn split_ascii_whitespace_tokens(value : String) -> Array[String] {
  let tokens : Array[String] = []
  let current = StringBuilder::new()
  for c in value.iter() {
    if is_ascii_whitespace(c) {
      if current.to_string().length() > 0 {
        tokens.push(current.to_string())
        current.reset()
      }
    } else {
      current.write_char(c)
    }
  }
  if current.to_string().length() > 0 {
    tokens.push(current.to_string())
  }
  tokens
}

///|
fn first_token_or_empty(tokens : Array[String]) -> String {
  if tokens.is_empty() {
    ""
  } else {
    tokens[0]
  }
}

///|
fn find_char_index(value : String, target : Char) -> Int? {
  let mut i = 0
  while i < value.length() {
    if value[i].to_int().unsafe_to_char() == target {
      return Some(i)
    }
    i += 1
  }
  None
}

///|
fn parse_counter_directives(
  value : String,
  default_amount : Int,
) -> Array[CounterDirective] {
  let lowered = value.trim().to_lower().to_owned()
  if lowered.is_empty() || lowered == "none" {
    return []
  }
  let tokens = split_ascii_whitespace_tokens(value)
  let directives : Array[CounterDirective] = []
  let mut i = 0
  while i < tokens.length() {
    let name = tokens[i].trim().to_owned()
    i += 1
    if name.is_empty() {
      continue
    }
    let mut amount = default_amount
    if i < tokens.length() {
      let parsed = @string.parse_int(tokens[i]) catch { _ => default_amount }
      // Heuristic: only treat token as number when parsing changed it from default
      // or when token is explicit zero/negative.
      if parsed != default_amount ||
        tokens[i] == "0" ||
        tokens[i].has_prefix("-") ||
        tokens[i].has_prefix("+") {
        amount = parsed
        i += 1
      }
    }
    directives.push({ name, amount })
  }
  directives
}

///|
fn copy_counter_state(
  state : Map[String, CounterEntry],
) -> Map[String, CounterEntry] {
  let copied : Map[String, CounterEntry] = {}
  state.each(fn(name, entry) { copied[name] = entry })
  copied
}

///|
fn filter_counter_state_for_style_containment(
  state : Map[String, CounterEntry],
  owner_id : String,
) -> Map[String, CounterEntry] {
  let filtered : Map[String, CounterEntry] = {}
  state.each(fn(name, entry) {
    if entry.owner == owner_id {
      filtered[name] = entry
    }
  })
  filtered
}

///|
fn apply_counter_reset_directives(
  state : Map[String, CounterEntry],
  directives : Array[CounterDirective],
  owner_id : String,
  owner_is_style_scope : Bool,
) -> Unit {
  for directive in directives {
    state[directive.name] = {
      value: directive.amount,
      owner: owner_id,
      owner_is_style_scope,
    }
  }
}

///|
fn apply_counter_increment_directives(
  state : Map[String, CounterEntry],
  directives : Array[CounterDirective],
  owner_id : String,
  owner_is_style_scope : Bool,
) -> Unit {
  for directive in directives {
    let amount = directive.amount
    match state.get(directive.name) {
      Some(entry) =>
        if entry.owner_is_style_scope && entry.owner != owner_id {
          state[directive.name] = {
            value: amount,
            owner: owner_id,
            owner_is_style_scope,
          }
        } else {
          state[directive.name] = { ..entry, value: entry.value + amount }
        }
      None =>
        state[directive.name] = {
          value: amount,
          owner: owner_id,
          owner_is_style_scope,
        }
    }
  }
}

///|
fn counter_value(state : Map[String, CounterEntry], name : String) -> Int {
  match state.get(name) {
    Some(entry) => entry.value
    None => 0
  }
}

///|
fn parse_css_quoted_string(value : String, start : Int) -> (String, Int)? {
  if start >= value.length() {
    return None
  }
  let quote = value[start].to_int().unsafe_to_char()
  if quote != '"' && quote != '\'' {
    return None
  }
  let buf = StringBuilder::new()
  let mut i = start + 1
  while i < value.length() {
    let c = value[i].to_int().unsafe_to_char()
    if c == quote {
      return Some((buf.to_string(), i - start + 1))
    }
    if c == '\\' {
      if i + 1 >= value.length() {
        i += 1
        continue
      }
      let next = value[i + 1].to_int().unsafe_to_char()
      if is_ascii_hex_digit(next) {
        let mut code = 0
        let mut hex_i = i + 1
        let mut count = 0
        while hex_i < value.length() && count < 6 {
          let hc = value[hex_i].to_int().unsafe_to_char()
          if !is_ascii_hex_digit(hc) {
            break
          }
          code = code * 16 + ascii_hex_digit_to_int(hc)
          hex_i += 1
          count += 1
        }
        if code > 0 {
          buf.write_char(code.unsafe_to_char())
        }
        if hex_i < value.length() {
          let ws = value[hex_i].to_int().unsafe_to_char()
          if is_ascii_whitespace(ws) {
            hex_i += 1
          }
        }
        i = hex_i
        continue
      }
      if next == '\n' || next == '\r' {
        i += 2
        continue
      }
      buf.write_char(next)
      i += 2
      continue
    }
    buf.write_char(c)
    i += 1
  }
  Some((buf.to_string(), value.length() - start))
}

///|
fn escape_css_string_content(value : String) -> String {
  let buf = StringBuilder::new()
  for c in value.iter() {
    if c == '\\' || c == '"' {
      buf.write_char('\\')
    }
    buf.write_char(c)
  }
  buf.to_string()
}

///|
fn resolve_pseudo_attr_functions(
  value : String,
  element_attributes : Map[String, String],
) -> String {
  if !value.contains("attr(") {
    return value
  }
  let buf = StringBuilder::new()
  let mut i = 0
  while i < value.length() {
    let remaining = value.length() - i
    if remaining >= 5 {
      let chunk = value.unsafe_substring(start=i, end=i + 5)
      if chunk == "attr(" {
        let mut depth = 1
        let arg_start = i + 5
        let mut j = arg_start
        while j < value.length() {
          let c = value[j].to_int().unsafe_to_char()
          if c == '(' {
            depth += 1
          } else if c == ')' {
            depth -= 1
            if depth == 0 {
              break
            }
          }
          j += 1
        }
        if j < value.length() && depth == 0 {
          let raw_args = value.unsafe_substring(start=arg_start, end=j)
          let attr_name = first_token_or_empty(
            split_ascii_whitespace_tokens(raw_args),
          )
          let attr_value = match element_attributes.get(attr_name) {
            Some(found) => found
            None => ""
          }
          buf.write_char('"')
          buf.write_string(escape_css_string_content(attr_value))
          buf.write_char('"')
          i = j + 1
          continue
        }
      }
    }
    buf.write_char(value[i].to_int().unsafe_to_char())
    i += 1
  }
  buf.to_string()
}

///|
fn find_top_level_comma(value : String) -> Int? {
  let mut depth = 0
  let mut i = 0
  while i < value.length() {
    let c = value[i].to_int().unsafe_to_char()
    if c == '(' {
      depth += 1
    } else if c == ')' {
      if depth > 0 {
        depth -= 1
      }
    } else if c == ',' && depth == 0 {
      return Some(i)
    }
    i += 1
  }
  None
}

///|
fn resolve_pseudo_content_value(
  value : String,
  css_vars : Map[String, String],
  element_attributes : Map[String, String],
) -> String {
  let mut resolved = value
  let mut loop_count = 0
  while loop_count < 16 && resolved.contains("var(") {
    let buf = StringBuilder::new()
    let mut i = 0
    let mut changed = false
    while i < resolved.length() {
      let remaining = resolved.length() - i
      if remaining >= 4 {
        let chunk = resolved.unsafe_substring(start=i, end=i + 4)
        if chunk == "var(" {
          let mut depth = 1
          let arg_start = i + 4
          let mut j = arg_start
          while j < resolved.length() {
            let c = resolved[j].to_int().unsafe_to_char()
            if c == '(' {
              depth += 1
            } else if c == ')' {
              depth -= 1
              if depth == 0 {
                break
              }
            }
            j += 1
          }
          if j < resolved.length() && depth == 0 {
            let raw_args = resolved.unsafe_substring(start=arg_start, end=j)
            let replacement = match find_top_level_comma(raw_args) {
              Some(comma_index) => {
                let name = raw_args.unsafe_substring(start=0, end=comma_index)
                let fallback = raw_args.unsafe_substring(
                  start=comma_index + 1,
                  end=raw_args.length(),
                )
                match css_vars.get(name.trim().to_owned()) {
                  Some(found) =>
                    resolve_pseudo_content_value(
                      found, css_vars, element_attributes,
                    )
                  None =>
                    resolve_pseudo_content_value(
                      fallback, css_vars, element_attributes,
                    )
                }
              }
              None => {
                let name = raw_args.trim().to_owned()
                match css_vars.get(name) {
                  Some(found) =>
                    resolve_pseudo_content_value(
                      found, css_vars, element_attributes,
                    )
                  None => ""
                }
              }
            }
            buf.write_string(replacement)
            i = j + 1
            changed = true
            continue
          }
        }
      }
      buf.write_char(resolved[i].to_int().unsafe_to_char())
      i += 1
    }
    resolved = if changed { buf.to_string() } else { resolved }
    if !changed {
      break
    }
    loop_count += 1
  }
  resolve_pseudo_attr_functions(resolved, element_attributes)
}

///|
fn parse_content_parts(value : String) -> Array[ContentPart]? {
  let trimmed = value.trim().to_owned()
  if trimmed.is_empty() || trimmed == "none" || trimmed == "normal" {
    return None
  }
  let parts : Array[ContentPart] = []
  let mut i = 0
  while i < trimmed.length() {
    let c = trimmed[i].to_int().unsafe_to_char()
    if is_ascii_whitespace(c) {
      i += 1
      continue
    }
    if c == '"' || c == '\'' {
      match parse_css_quoted_string(trimmed, i) {
        Some((text, consumed)) => {
          parts.push(Text(text))
          i += consumed
          continue
        }
        None => ()
      }
    }
    if is_ascii_alpha_num_hyphen(c) {
      let mut end = i + 1
      while end < trimmed.length() {
        let ec = trimmed[end].to_int().unsafe_to_char()
        if !is_ascii_alpha_num_hyphen(ec) {
          break
        }
        end += 1
      }
      let ident = trimmed.unsafe_substring(start=i, end~)
      let ident_lower = ident.to_lower()
      let mut j = end
      while j < trimmed.length() {
        let jc = trimmed[j].to_int().unsafe_to_char()
        if !is_ascii_whitespace(jc) {
          break
        }
        j += 1
      }
      if j < trimmed.length() && trimmed[j].to_int().unsafe_to_char() == '(' {
        let mut depth = 1
        let arg_start = j + 1
        let mut k = j + 1
        while k < trimmed.length() {
          let kc = trimmed[k].to_int().unsafe_to_char()
          if kc == '(' {
            depth += 1
          } else if kc == ')' {
            depth -= 1
            if depth == 0 {
              break
            }
          }
          k += 1
        }
        if k < trimmed.length() && depth == 0 {
          let args = trimmed.unsafe_substring(start=arg_start, end=k)
          if ident_lower == "counter" {
            let name = first_token_or_empty(split_ascii_whitespace_tokens(args))
            let name = name.trim().to_owned()
            if !name.is_empty() {
              parts.push(Counter(name))
            }
          } else if ident_lower == "counters" {
            let comma_index = find_char_index(args, ',')
            match comma_index {
              Some(ci) => {
                let name = args.unsafe_substring(start=0, end=ci)
                let raw_sep = args.unsafe_substring(
                  start=ci + 1,
                  end=args.length(),
                )
                let sep = match
                  parse_css_quoted_string(raw_sep.trim().to_owned(), 0) {
                  Some((s, _)) => s
                  None => raw_sep.trim().to_owned()
                }
                let counter_name = name.trim().to_owned()
                if !counter_name.is_empty() {
                  parts.push(Counters(counter_name, sep))
                }
              }
              None => {
                let name = args.trim().to_owned()
                if !name.is_empty() {
                  parts.push(Counter(name))
                }
              }
            }
          }
          i = k + 1
          continue
        }
      }
    }
    i += 1
  }
  if parts.is_empty() {
    None
  } else {
    Some(parts)
  }
}

///|
fn evaluate_content_parts(
  parts : Array[ContentPart],
  counters : Map[String, CounterEntry],
) -> String {
  let buf = StringBuilder::new()
  for part in parts {
    match part {
      Text(text) => buf.write_string(text)
      Counter(name) =>
        buf.write_string(counter_value(counters, name).to_string())
      Counters(name, _sep) =>
        // For current WPT subset we keep only the active innermost value.
        buf.write_string(counter_value(counters, name).to_string())
    }
  }
  buf.to_string()
}

///|
fn parse_relative_left_offset(
  value : String,
  host_style : @style.Style,
) -> Double {
  let trimmed = value.trim().to_lower().to_owned()
  if trimmed.is_empty() {
    return 0.0
  }
  let base_font_size = if host_style.font_size > 0.0 {
    host_style.font_size
  } else {
    16.0
  }
  if trimmed.has_suffix("ch") {
    let num = @string.parse_double(remove_suffix(trimmed, 2).to_string()) catch {
      _ => 0.0
    }
    num * base_font_size * 0.5
  } else if trimmed.has_suffix("em") {
    let num = @string.parse_double(remove_suffix(trimmed, 2).to_string()) catch {
      _ => 0.0
    }
    num * base_font_size
  } else if trimmed.has_suffix("px") {
    @string.parse_double(remove_suffix(trimmed, 2).to_string()) catch {
      _ => 0.0
    }
  } else {
    @string.parse_double(trimmed) catch {
      _ => 0.0
    }
  }
}

///|
fn selector_text_without_pseudo(
  selector_text : String,
  pseudo_kind : PseudoKind,
) -> String? {
  let trimmed = selector_text.trim().to_owned()
  let (double_suffix, single_suffix) = match pseudo_kind {
    Before => ("::before", ":before")
    After => ("::after", ":after")
  }
  if trimmed.has_suffix(double_suffix) {
    let len = trimmed.length()
    let base_len = len - double_suffix.length()
    let base = trimmed.unsafe_substring(start=0, end=base_len)
    let cleaned = base.trim().to_owned()
    if cleaned.is_empty() {
      None
    } else {
      Some(cleaned)
    }
  } else if trimmed.has_suffix(single_suffix) {
    let len = trimmed.length()
    let base_len = len - single_suffix.length()
    let base = trimmed.unsafe_substring(start=0, end=base_len)
    let cleaned = base.trim().to_owned()
    if cleaned.is_empty() {
      None
    } else {
      Some(cleaned)
    }
  } else {
    None
  }
}

///|
fn selector_text_fallback_matches(
  selector_text : String,
  selector_elem : @css.Element,
) -> Bool {
  let lowered = selector_text.to_lower()
  let tag_lower = selector_elem.tag_name.to_lower()
  if lowered.contains(":is(") {
    if lowered.contains(tag_lower) {
      return true
    }
  }
  match selector_elem.id {
    Some(id) => if lowered.contains("#" + id.to_lower()) { return true }
    None => ()
  }
  for cls in selector_elem.classes {
    if lowered.contains("." + cls.to_lower()) {
      return true
    }
  }
  lowered.contains(tag_lower)
}

///|
priv struct AfterPseudoSpec {
  content : String
  position_relative : Bool
  out_of_flow : Bool
  left_offset : Double
  generated_style : @style.Style
}

///|
fn apply_generated_pseudo_host_style_offsets(
  style : @style.Style,
  before_pseudo : AfterPseudoSpec?,
  after_pseudo : AfterPseudoSpec?,
) -> @style.Style {
  match after_pseudo {
    Some(spec) if spec.position_relative && spec.left_offset != 0.0 =>
      {
        ..style,
        position: @types.Relative,
        inset: { ..style.inset, left: @types.Length(spec.left_offset) },
      }
    _ =>
      match before_pseudo {
        Some(spec) if spec.position_relative && spec.left_offset != 0.0 =>
          {
            ..style,
            position: @types.Relative,
            inset: { ..style.inset, left: @types.Length(spec.left_offset) },
          }
        _ => style
      }
  }
}

///|
fn resolve_element_counter_pseudos(
  incoming_counters : Map[String, CounterEntry],
  selector_elem : @css.Element,
  style : @style.Style,
  stylesheets : Array[@css.Stylesheet],
  ctx : RenderContext,
  owner_id : String,
  below_viewport : Bool,
  element_css_vars : Map[String, String],
  element_attributes : Map[String, String],
) -> (
  Map[String, CounterEntry],
  AfterPseudoSpec?,
  AfterPseudoSpec?,
  @style.Style,
) {
  let own_counters = if stylesheets.length() == 0 {
    incoming_counters
  } else {
    compute_element_own_counters(
      incoming_counters, selector_elem, style, stylesheets, ctx, owner_id,
    )
  }
  let before_pseudo = if below_viewport || stylesheets.length() == 0 {
    None
  } else {
    resolve_pseudo_spec_fast(
      selector_elem,
      style,
      ctx,
      Before,
      own_counters,
      element_css_vars,
      element_attributes,
    )
  }
  let after_pseudo = if below_viewport || stylesheets.length() == 0 {
    None
  } else {
    resolve_pseudo_spec_fast(
      selector_elem,
      style,
      ctx,
      After,
      own_counters,
      element_css_vars,
      element_attributes,
    )
  }
  let style = apply_generated_pseudo_host_style_offsets(
    style, before_pseudo, after_pseudo,
  )
  (own_counters, before_pseudo, after_pseudo, style)
}

///|
fn create_generated_pseudo_node(
  spec : AfterPseudoSpec,
  pseudo_kind : PseudoKind,
) -> @node.Node {
  let pseudo_id = match pseudo_kind {
    Before => "#pseudo::before"
    After => "#pseudo::after"
  }
  let pseudo_is_contents = spec.generated_style.display == @types.Contents
  let inline_plain_generated_text = spec.generated_style.display ==
    @types.Inline &&
    !spec.position_relative &&
    !spec.out_of_flow &&
    spec.left_offset.abs() <= 0.01
  if spec.content.is_empty() {
    @node.Node::leaf(pseudo_id, spec.generated_style)
  } else if pseudo_is_contents {
    // display: contents pseudo-elements should expose generated text as child content.
    let text_child = create_text_node(spec.content, spec.generated_style)
    @node.Node::new(pseudo_id, spec.generated_style, [text_child])
  } else if inline_plain_generated_text {
    // Static inline generated content behaves like regular inline text runs.
    create_text_node(spec.content, spec.generated_style)
  } else {
    let font_size = spec.generated_style.font_size
    let text_line_height = spec.generated_style.line_height
    let white_space = spec.generated_style.white_space
    let writing_mode = spec.generated_style.writing_mode
    @node.Node::with_measure(
      pseudo_id,
      spec.generated_style,
      create_text_measure_with_provider(
        spec.content,
        font_size,
        text_line_height,
        white_space,
        writing_mode,
      ),
      text=spec.content,
    )
  }
}

///|
fn pseudo_base_style_from_host(host_style : @style.Style) -> @style.Style {
  {
    ..@style.Style::default(),
    display: @types.Inline,
    visibility: host_style.visibility,
    pointer_events: host_style.pointer_events,
    color: host_style.color,
    font_size: host_style.font_size,
    font_weight: host_style.font_weight,
    font_family: host_style.font_family,
    line_height: host_style.line_height,
    text_align: host_style.text_align,
    white_space: host_style.white_space,
    writing_mode: host_style.writing_mode,
    direction: host_style.direction,
    text_decoration_underline: host_style.text_decoration_underline,
    text_decoration_line_through: host_style.text_decoration_line_through,
    text_decoration_overline: host_style.text_decoration_overline,
    letter_spacing: host_style.letter_spacing,
    word_spacing: host_style.word_spacing,
  }
}

///|
fn resolve_pseudo_spec(
  selector_elem : @css.Element,
  host_style : @style.Style,
  stylesheets : Array[@css.Stylesheet],
  ctx : RenderContext,
  pseudo_kind : PseudoKind,
  counters : Map[String, CounterEntry],
  css_vars : Map[String, String],
  element_attributes : Map[String, String],
) -> AfterPseudoSpec? {
  let media_env = @css.MediaEnvironment::with_color_scheme(
    ctx.viewport_width,
    ctx.viewport_height,
    ctx.color_scheme,
  )
  let mut content_parts : Array[ContentPart]? = None
  let mut content_specified = false
  let mut content_disabled = false
  let mut position_relative = false
  let mut out_of_flow = false
  let mut left_offset = 0.0
  let pseudo_css_vars = clone_string_map(css_vars)
  // ::before/::after inherit text properties, not host box dimensions.
  let mut generated_style = pseudo_base_style_from_host(host_style)

  // Quick suffix check keyword to skip rules that can't match this pseudo
  let pseudo_keyword = match pseudo_kind {
    Before => "before"
    After => "after"
  }
  for stylesheet in stylesheets {
    for rule in stylesheet.rules {
      // Fast path: skip rules whose selector text doesn't contain the pseudo keyword
      if !rule.selector_text.contains(pseudo_keyword) {
        continue
      }
      let media_matches = match rule.media_query {
        Some(mq) => mq.evaluate(media_env)
        None => true
      }
      if !media_matches {
        continue
      }
      match selector_text_without_pseudo(rule.selector_text, pseudo_kind) {
        Some(base_selector_text) => {
          let matches_selector = match @css.parse_selector(base_selector_text) {
            Some(base_selector) =>
              @css.matches_selector(selector_elem, base_selector)
            None =>
              selector_text_fallback_matches(base_selector_text, selector_elem)
          }
          if matches_selector {
            for decl in rule.declarations {
              match decl.value {
                @css.PropertyValue::Value(v) => {
                  let prop = decl.property.to_lower()
                  if prop.has_prefix("--") {
                    pseudo_css_vars[prop] = v
                  } else if prop == "content" {
                    content_specified = true
                    let resolved_value = resolve_pseudo_content_value(
                      v, pseudo_css_vars, element_attributes,
                    )
                    let normalized = resolved_value.trim().to_lower().to_owned()
                    if normalized == "none" || normalized == "normal" {
                      content_parts = None
                      content_disabled = true
                    } else {
                      content_parts = parse_content_parts(resolved_value)
                      content_disabled = false
                    }
                  } else if prop == "position" {
                    let pos = v.trim().to_lower()
                    if pos == "relative" {
                      position_relative = true
                    } else if pos == "absolute" || pos == "fixed" {
                      out_of_flow = true
                    }
                  } else if prop == "left" {
                    left_offset = parse_relative_left_offset(v, host_style)
                  }
                  if prop != "content" && !prop.has_prefix("--") {
                    generated_style = apply_css_property_with_viewport(
                      generated_style,
                      prop,
                      v,
                      ctx.viewport_width,
                      ctx.viewport_height,
                      pseudo_css_vars,
                      Some(host_style),
                    )
                  }
                }
                _ => ()
              }
            }
          }
        }
        None => ()
      }
    }
  }

  if content_disabled || !content_specified {
    return None
  }
  let text = match content_parts {
    Some(parts) => evaluate_content_parts(parts, counters)
    None => ""
  }
  Some({
    content: text,
    position_relative,
    out_of_flow,
    left_offset,
    generated_style,
  })
}

///|
fn resolve_pseudo_spec_fast(
  selector_elem : @css.Element,
  host_style : @style.Style,
  ctx : RenderContext,
  pseudo_kind : PseudoKind,
  counters : Map[String, CounterEntry],
  css_vars : Map[String, String],
  element_attributes : Map[String, String],
) -> AfterPseudoSpec? {
  let media_env = @css.MediaEnvironment::with_color_scheme(
    ctx.viewport_width,
    ctx.viewport_height,
    ctx.color_scheme,
  )
  let mut content_parts : Array[ContentPart]? = None
  let mut content_specified = false
  let mut content_disabled = false
  let mut position_relative = false
  let mut out_of_flow = false
  let mut left_offset = 0.0
  let pseudo_css_vars = clone_string_map(css_vars)
  let mut generated_style = pseudo_base_style_from_host(host_style)
  let index = match pseudo_kind {
    Before => active_before_index.val
    After => active_after_index.val
  }
  if index.rules.is_empty() {
    return None
  }
  // Use index to get candidates (much faster than scanning all rules)
  let candidates = index.get_candidates(selector_elem)
  for rule_idx in candidates {
    let rule = index.rules[rule_idx]
    let media_matches = match rule.media_query {
      Some(mq) => mq.evaluate(media_env)
      None => true
    }
    if !media_matches {
      continue
    }
    let matches_selector = match rule.base_selector {
      Some(base_selector) => @css.matches_selector(selector_elem, base_selector)
      None =>
        selector_text_fallback_matches(rule.base_selector_text, selector_elem)
    }
    if matches_selector {
      for decl in rule.declarations {
        match decl.value {
          @css.PropertyValue::Value(v) => {
            let prop = decl.property.to_lower()
            if prop.has_prefix("--") {
              pseudo_css_vars[prop] = v
            } else if prop == "content" {
              content_specified = true
              let resolved_value = resolve_pseudo_content_value(
                v, pseudo_css_vars, element_attributes,
              )
              let normalized = resolved_value.trim().to_lower().to_owned()
              if normalized == "none" || normalized == "normal" {
                content_parts = None
                content_disabled = true
              } else {
                content_parts = parse_content_parts(resolved_value)
                content_disabled = false
              }
            } else if prop == "position" {
              let pos = v.trim().to_lower()
              if pos == "relative" {
                position_relative = true
              } else if pos == "absolute" || pos == "fixed" {
                out_of_flow = true
              }
            } else if prop == "left" {
              left_offset = parse_relative_left_offset(v, host_style)
            }
            if prop != "content" && !prop.has_prefix("--") {
              generated_style = apply_css_property_with_viewport(
                generated_style,
                prop,
                v,
                ctx.viewport_width,
                ctx.viewport_height,
                pseudo_css_vars,
                Some(host_style),
              )
            }
          }
          _ => ()
        }
      }
    }
  }
  if content_disabled || !content_specified {
    return None
  }
  let text = match content_parts {
    Some(parts) => evaluate_content_parts(parts, counters)
    None => ""
  }
  Some({
    content: text,
    position_relative,
    out_of_flow,
    left_offset,
    generated_style,
  })
}

///|
fn get_counter_directives(
  selector_elem : @css.Element,
  stylesheets : Array[@css.Stylesheet],
  ctx : RenderContext,
) -> (Array[CounterDirective], Array[CounterDirective]) {
  let media_env = @css.MediaEnvironment::with_color_scheme(
    ctx.viewport_width,
    ctx.viewport_height,
    ctx.color_scheme,
  )
  let cascaded = @css.cascade_element_with_media(
    selector_elem,
    stylesheets,
    [],
    Some(media_env),
  )
  let reset_raw = match cascaded.get_value("counter-reset") {
    Some(v) => v
    None => ""
  }
  let increment_raw = match cascaded.get_value("counter-increment") {
    Some(v) => v
    None => ""
  }
  (
    parse_counter_directives(reset_raw, 0),
    parse_counter_directives(increment_raw, 1),
  )
}

///|
fn selector_has_generated_pseudo_content(
  selector_elem : @css.Element,
  stylesheets : Array[@css.Stylesheet],
  ctx : RenderContext,
) -> Bool {
  // `stylesheets` is retained for signature compatibility; the fast variant
  // resolves against the precomputed before/after pseudo-rule indexes instead
  // of re-scanning every rule's selector text per element (which on text-heavy
  // pages dominated the render — O(elements x rules) substring searches; #316).
  ignore(stylesheets)
  let empty_counters : Map[String, CounterEntry] = {}
  let empty_css_vars : Map[String, String] = {}
  let empty_attributes : Map[String, String] = {}
  let default_style = @style.Style::default()
  let has_before = resolve_pseudo_spec_fast(
      selector_elem,
      default_style,
      ctx,
      Before,
      empty_counters,
      empty_css_vars,
      empty_attributes,
    )
    is Some(_)
  let has_after = resolve_pseudo_spec_fast(
      selector_elem,
      default_style,
      ctx,
      After,
      empty_counters,
      empty_css_vars,
      empty_attributes,
    )
    is Some(_)
  has_before || has_after
}

///|
fn establishes_style_containment_scope(style : @style.Style) -> Bool {
  style.contain.style && style.display != @types.Contents
}

///|
fn compute_element_own_counters(
  incoming_counters : Map[String, CounterEntry],
  selector_elem : @css.Element,
  style : @style.Style,
  stylesheets : Array[@css.Stylesheet],
  ctx : RenderContext,
  owner_id : String,
) -> Map[String, CounterEntry] {
  let own_counters = copy_counter_state(incoming_counters)
  let is_style_scope = establishes_style_containment_scope(style)
  let (counter_resets, counter_increments) = get_counter_directives(
    selector_elem, stylesheets, ctx,
  )
  apply_counter_reset_directives(
    own_counters, counter_resets, owner_id, is_style_scope,
  )
  apply_counter_increment_directives(
    own_counters, counter_increments, owner_id, is_style_scope,
  )
  own_counters
}