///|
fn apply_selector_transform(
  root : @dom.Node,
  transform : TransformSpec,
  limits : @sel.SelectorLimits,
) -> Unit raise @core.HtmlError {
  match root.kind {
    Document | Fragment | Element => transform_children(root, transform, limits)
    _ => ()
  }
}

///|
/// Continuation of an unwrap inside `transform_children`: walk the subtrees of
/// the `count` children hoisted into `parent` at `start`, one per step.
priv struct AfterUnwrapFrame {
  parent : @dom.Node
  start : Int32
  count : Int32
  mut offset : Int32
}

///|
/// Explicit-stack frame for `transform_children`.
priv enum SelectorWalkFrame {
  Walk(WalkFrame)
  AfterUnwrap(AfterUnwrapFrame)
}

///|
/// Apply a selector transform to the elements below `root`, visiting template
/// contents before regular children. Uses an explicit frame stack instead of
/// recursion so deeply nested trees (including long chains of unwrapped
/// elements) cannot overflow the call stack; each frame mirrors one activation
/// of the former recursive walk.
fn transform_children(
  root : @dom.Node,
  transform : TransformSpec,
  limits : @sel.SelectorLimits,
) -> Unit raise @core.HtmlError {
  let stack : Array[SelectorWalkFrame] = [Walk(WalkFrame::new(root))]
  while stack.last() is Some(top) {
    match top {
      AfterUnwrap(frame) => {
        let parent = frame.parent
        let offset = frame.offset
        if offset < frame.count &&
          frame.start + offset < parent.children.length() {
          let child = parent.children[frame.start + offset]
          frame.offset = offset + 1
          match child.kind {
            Document | Fragment | Element =>
              stack.push(Walk(WalkFrame::new(child)))
            _ => ()
          }
        } else {
          ignore(stack.pop())
        }
      }
      Walk(frame) => {
        if !frame.entered {
          frame.entered = true
          if frame.node.template_contents is Some(contents) {
            stack.push(Walk(WalkFrame::new(contents)))
            continue
          }
        }
        let parent = frame.node
        let index = frame.index
        if index >= parent.children.length() {
          ignore(stack.pop())
          continue
        }
        let child = parent.children[index]
        if child.kind == Element &&
          transform_matches_element(child, transform, limits) {
          match transform.kind {
            DropElements => {
              transform_report_node_action(child, transform, "Dropped")
              transform_remove_child_at(parent, index)
            }
            UnwrapElements => {
              transform_report_node_action(child, transform, "Unwrapped")
              let inserted = transform_unwrap_child_at(parent, index)
              if inserted > 0 {
                frame.index = index + inserted
                stack.push(
                  AfterUnwrap({
                    parent,
                    start: index,
                    count: inserted,
                    offset: 0,
                  }),
                )
              }
            }
            EscapeElements => {
              transform_report_node_action(child, transform, "Escaped")
              ignore(escape_transform_child_at(parent, index))
            }
            EmptyElements => {
              if empty_transform_node(child) {
                transform_report_node_action(child, transform, "Emptied")
              }
              frame.index = index + 1
            }
            SetAttributes => {
              if apply_set_attrs_transform(child, transform) {
                transform_report_node_action(
                  child, transform, "Set attributes on",
                )
              }
              frame.index = index + 1
              stack.push(Walk(WalkFrame::new(child)))
            }
            DropAttributes => {
              ignore(apply_drop_attrs_transform(child, transform))
              frame.index = index + 1
              stack.push(Walk(WalkFrame::new(child)))
            }
            AllowlistAttributes => {
              ignore(apply_allowlist_attrs_transform(child, transform))
              frame.index = index + 1
              stack.push(Walk(WalkFrame::new(child)))
            }
            MergeAttributeTokens => {
              if apply_merge_attrs_transform(child, transform) {
                transform_call_hook(child, transform)
                transform_report(
                  transform,
                  "Merged tokens into attribute '\{transform.merge_attr}' on <\{transform.merge_tag}>",
                  Some(child),
                )
              }
              frame.index = index + 1
              stack.push(Walk(WalkFrame::new(child)))
            }
            EditElement => {
              apply_edit_transform(child, transform)
              if index < parent.children.length() &&
                physical_equal(parent.children[index], child) {
                frame.index = index + 1
                stack.push(Walk(WalkFrame::new(child)))
              }
            }
            EditAttributes => {
              if apply_edit_attrs_transform(child, transform) {
                transform_report_node_action(
                  child, transform, "Edited attributes on",
                )
              }
              if index < parent.children.length() &&
                physical_equal(parent.children[index], child) {
                frame.index = index + 1
                stack.push(Walk(WalkFrame::new(child)))
              }
            }
            DropUrlAttributes => {
              ignore(apply_drop_url_attrs_transform(child, transform))
              frame.index = index + 1
              stack.push(Walk(WalkFrame::new(child)))
            }
            AllowStyleAttributes => {
              ignore(apply_allow_style_attrs_transform(child, transform))
              frame.index = index + 1
              stack.push(Walk(WalkFrame::new(child)))
            }
            _ => frame.index = index + 1
          }
        } else {
          frame.index = index + 1
          match child.kind {
            Document | Fragment | Element =>
              stack.push(Walk(WalkFrame::new(child)))
            _ => ()
          }
        }
      }
    }
  }
}

///|
fn transform_matches_element(
  node : @dom.Node,
  transform : TransformSpec,
  limits : @sel.SelectorLimits,
) -> Bool raise @core.HtmlError {
  match transform.kind {
    MergeAttributeTokens => @syn.lower_ascii(node.name) == transform.merge_tag
    _ => @sel.matches_with_limits(node, transform.selector, limits)
  }
}