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

///|
fn decide_transform_all_nodes(transform : TransformSpec) -> Bool {
  transform.selector[:].trim() == "*"
}

///|
fn decide_action_label(action : DecideAction) -> String {
  match action {
    Keep => "keep"
    Drop => "drop"
    Unwrap => "unwrap"
    Empty => "empty"
    Escape => "escape"
  }
}

///|
fn decide_node_label(node : @dom.Node) -> String {
  if node.name.has_prefix("#") || node.name == "!doctype" {
    node.name
  } else {
    @syn.lower_ascii(node.name)
  }
}

///|
fn decide_transform_matches_node(
  node : @dom.Node,
  transform : TransformSpec,
  all_nodes : Bool,
  limits : @sel.SelectorLimits,
) -> Bool raise @core.HtmlError {
  if all_nodes {
    true
  } else {
    node.kind == Element && transform_matches_element(node, transform, limits)
  }
}

///|
fn decide_transform_action(
  node : @dom.Node,
  transform : TransformSpec,
) -> DecideAction {
  match transform.decide_callback {
    Some(callback) => {
      let action = (callback.callback)(node)
      if action != Keep {
        transform_call_hook(node, transform)
        let action_label = decide_action_label(action)
        let node_label = decide_node_label(node)
        transform_report(
          transform,
          "Decide -> \{action_label} '\{node_label}' (matched selector '\{transform.selector}')",
          Some(node),
        )
      }
      action
    }
    None => Keep
  }
}

///|
fn decide_escape_child_at(parent : @dom.Node, index : Int32) -> Unit {
  match parent.children[index].kind {
    Element => ignore(escape_transform_child_at(parent, index))
    Document | Fragment => ignore(transform_unwrap_child_at(parent, index))
    Text | Comment | Doctype => transform_remove_child_at(parent, index)
  }
}

///|
/// Walk the children of `root` (template contents first, then the regular
/// children) applying the decide callback. Uses an explicit frame stack
/// instead of recursion so deeply nested trees cannot overflow the call stack;
/// each frame mirrors one activation of the former recursive walk.
fn decide_transform_children(
  root : @dom.Node,
  transform : TransformSpec,
  limits : @sel.SelectorLimits,
) -> Unit raise @core.HtmlError {
  let all_nodes = decide_transform_all_nodes(transform)
  let stack : Array[WalkFrame] = [WalkFrame::new(root)]
  while stack.last() is Some(frame) {
    if frame.schedule_template_first(stack) {
      continue
    }
    let parent = frame.node
    if frame.index >= parent.children.length() {
      ignore(stack.pop())
      continue
    }
    let index = frame.index
    let child = parent.children[index]
    if decide_transform_matches_node(child, transform, all_nodes, limits) {
      match decide_transform_action(child, transform) {
        Keep => {
          frame.index = index + 1
          match child.kind {
            Document | Fragment | Element => stack.push(WalkFrame::new(child))
            _ => ()
          }
        }
        Empty => {
          ignore(empty_transform_node(child))
          frame.index = index + 1
        }
        Drop => transform_remove_child_at(parent, index)
        Unwrap => ignore(transform_unwrap_child_at(parent, index))
        Escape => decide_escape_child_at(parent, index)
      }
    } else {
      frame.index = index + 1
      match child.kind {
        Document | Fragment | Element => stack.push(WalkFrame::new(child))
        _ => ()
      }
    }
  }
}