///|
/// Apply DOM transforms in order.
///
/// The transform mutates and returns the current root. Selector transforms process the
/// children of the supplied root, matching the Python constructor-time pipeline
/// where the root is normally a document or document fragment container.
pub fn apply_transforms(
  node : @dom.Node,
  transforms : Array[TransformSpec],
) -> @dom.Node raise @core.HtmlError {
  let limits = selector_limits_from_transforms(transforms)
  if transform_list_has_enabled_stage(transforms) {
    return apply_staged_transforms(node, transforms, limits)
  }
  apply_stage_items(node, transforms, limits)
}

///|
fn selector_limits_from_transforms(
  transforms : Array[TransformSpec],
) -> @sel.SelectorLimits {
  selector_limits_from_transform_list(transforms).unwrap_or(
    default_selector_limits(),
  )
}

///|
fn selector_limits_from_transform_list(
  transforms : Array[TransformSpec],
) -> @sel.SelectorLimits? {
  let mut limits : @sel.SelectorLimits? = None
  for transform in transforms {
    if transform.enabled {
      match transform.kind {
        SanitizeTree =>
          match transform.sanitize_policy {
            Some(policy) => limits = Some(policy.selector_limits())
            None => limits = Some(default_selector_limits())
          }
        StageGroup =>
          match
            selector_limits_from_transform_list(transform.stage_transforms) {
            Some(stage_limits) => limits = Some(stage_limits)
            None => ()
          }
        _ => ()
      }
    }
  }
  limits
}

///|
fn transform_list_has_enabled_stage(transforms : Array[TransformSpec]) -> Bool {
  for transform in transforms {
    if transform.enabled {
      match transform.kind {
        StageGroup => return true
        _ => ()
      }
    }
  }
  false
}

///|
fn apply_staged_transforms(
  node : @dom.Node,
  transforms : Array[TransformSpec],
  limits : @sel.SelectorLimits,
) -> @dom.Node raise @core.HtmlError {
  let mut current = node
  let mut pending : Array[TransformSpec] = []
  let mut stage_index = 0
  for transform in transforms {
    match transform.kind {
      StageGroup =>
        if transform.enabled {
          if !pending.is_empty() {
            stage_index += 1
            current = apply_stage_items(current, pending, limits)
            pending = []
          }
          stage_index += 1
          current = apply_explicit_stage(
            current, transform, stage_index, limits,
          )
        }
      _ => pending.push(transform)
    }
  }
  if !pending.is_empty() {
    current = apply_stage_items(current, pending, limits)
  }
  current
}

///|
fn apply_explicit_stage(
  node : @dom.Node,
  stage : TransformSpec,
  index : Int,
  limits : @sel.SelectorLimits,
) -> @dom.Node raise @core.HtmlError {
  transform_call_hook(node, stage)
  transform_report(stage, "Stage " + index.to_string(), Some(node))
  apply_stage_items(node, stage.stage_transforms, limits)
}

///|
fn apply_stage_items(
  node : @dom.Node,
  transforms : Array[TransformSpec],
  limits : @sel.SelectorLimits,
) -> @dom.Node raise @core.HtmlError {
  let transforms = flatten_enabled_stage_items(transforms)
  let mut current = node
  let mut index = 0
  while index < transforms.length() {
    let transform = transforms[index]
    match transform.kind {
      PruneEmptyElements => {
        let prune_batch : Array[TransformSpec] = []
        while index < transforms.length() &&
              transform_kind_is_prune_empty(transforms[index]) {
          prune_batch.push(transforms[index])
          index += 1
        }
        apply_prune_empty_transforms(current, prune_batch, limits)
      }
      _ => {
        current = apply_transform(current, transform, limits)
        index += 1
      }
    }
  }
  current
}

///|
fn transform_kind_is_prune_empty(transform : TransformSpec) -> Bool {
  match transform.kind {
    PruneEmptyElements => true
    _ => false
  }
}

///|
fn flatten_enabled_stage_items(
  transforms : Array[TransformSpec],
) -> Array[TransformSpec] {
  let out : Array[TransformSpec] = []
  for transform in transforms {
    if transform.enabled {
      match transform.kind {
        StageGroup => {
          let nested = flatten_enabled_stage_items(transform.stage_transforms)
          for item in nested {
            out.push(item)
          }
        }
        _ => out.push(transform)
      }
    }
  }
  out
}

///|
fn apply_transform(
  node : @dom.Node,
  transform : TransformSpec,
  limits : @sel.SelectorLimits,
) -> @dom.Node raise @core.HtmlError {
  match transform.kind {
    EditDocumentRoot => {
      apply_edit_document_transform(node, transform)
      node
    }
    DropElements
    | UnwrapElements
    | EscapeElements
    | EmptyElements
    | SetAttributes
    | DropAttributes
    | AllowlistAttributes
    | MergeAttributeTokens
    | EditElement
    | EditAttributes
    | DropUrlAttributes
    | AllowStyleAttributes => {
      apply_selector_transform(node, transform, limits)
      node
    }
    DecideNode => {
      apply_decide_transform(node, transform, limits)
      node
    }
    SanitizeTree =>
      @san.sanitize_dom_with_observer(
        node,
        transform.sanitize_policy,
        sanitize_transform_observer(transform),
      )
    DropCommentNodes => {
      apply_drop_kind_transform(node, Comment, transform)
      node
    }
    DropDoctypeNodes => {
      apply_drop_kind_transform(node, Doctype, transform)
      node
    }
    DropForeignNamespaceNodes => {
      apply_drop_foreign_namespaces_transform(node, transform)
      node
    }
    CollapseTextWhitespace => {
      apply_collapse_whitespace_transform(
        node,
        transform_skip_tag_set(transform.skip_tags),
        transform,
      )
      node
    }
    PruneEmptyElements => {
      apply_prune_empty_transform(node, transform, limits)
      node
    }
    LinkifyTextNodes => {
      let config = transform.linkify_config.unwrap_or(
        @link.LinkifyConfig::new(),
      )
      apply_linkify_transform(
        node,
        config,
        transform_skip_tag_set(transform.linkify_skip_tags),
        transform,
      )
    }
    StageGroup => apply_stage_items(node, transform.stage_transforms, limits)
  }
}

///|
fn transform_skip_tag_set(tags : Array[String]) -> Set[String] {
  let out : Set[String] = Set::default()
  for tag in tags {
    let normalized = @syn.lower_ascii(tag.trim())
    if normalized != "" {
      out.add(normalized)
    }
  }
  out
}