///|
/// Rewrite compiled candidate nodes so that they read as nested content of the
/// rule the `@apply` lives in.
///
/// The candidate selector becomes `&`, and an at-rule that wraps a single rule
/// is turned inside out so the nesting matches what upstream produces for
/// variants such as `hover`, which pair a selector with a media query.
fn relative_candidate_nodes(
  nodes : ArrayView[CssNode],
  selector : String,
) -> Array[CssNode] {
  let output : Array[CssNode] = []
  for
    node in strip_self_rules(
      replace_candidate_selector_nodes(nodes, selector, "&"),
    ) {
    output.push(hoist_single_rule(node))
  }
  output
}

///|
/// Drop `&` rules, which only exist because compiled candidates always start
/// from a class selector.
fn strip_self_rules(nodes : ArrayView[CssNode]) -> Array[CssNode] {
  let output : Array[CssNode] = []
  for node in nodes {
    match node {
      Rule(selector="&", nodes=children, ..) =>
        for child in strip_self_rules(children) {
          output.push(child)
        }
      Rule(selector~, nodes=children, span~) =>
        output.push(Rule(selector~, nodes=strip_self_rules(children), span~))
      AtRule(name~, params~, nodes=Some(children), span~) =>
        output.push(
          AtRule(name~, params~, nodes=Some(strip_self_rules(children)), span~),
        )
      Context(values~, nodes=children, span~) =>
        output.push(Context(values~, nodes=strip_self_rules(children), span~))
      _ => output.push(node)
    }
  }
  output
}

///|
fn hoist_single_rule(node : CssNode) -> CssNode {
  match node {
    AtRule(name~, params~, nodes=Some(children), span~) => {
      let hoisted = children.map(hoist_single_rule)
      match hoisted {
        [Rule(selector=inner_selector, nodes=inner_nodes, span=inner_span)] =>
          Rule(
            selector=inner_selector,
            nodes=[AtRule(name~, params~, nodes=Some(inner_nodes), span~)],
            span=inner_span,
          )
        _ => AtRule(name~, params~, nodes=Some(hoisted), span~)
      }
    }
    Rule(selector~, nodes=children, span~) =>
      Rule(selector~, nodes=children.map(hoist_single_rule), span~)
    _ => node
  }
}

///|
/// Compile one `@apply` candidate into nodes relative to the surrounding rule.
fn nodes_for_apply(
  theme : Map[String, String],
  custom_utilities : Map[String, Array[CssNode]],
  functional_utilities : Map[String, Array[CssNode]],
  custom_variants : Map[String, CustomVariantTemplate],
  raw : String,
  span : SourceSpan,
) -> (Array[CssNode], RenderedCandidate) raise CompileError {
  guard render_candidate(
      theme, custom_utilities, functional_utilities, custom_variants, raw,
    )
    is Some(rendered) else {
    raise InvalidApplyCandidate(raw)
  }
  let relative = relative_candidate_nodes(
    rendered.nodes,
    ".\{escape_class_name(raw)}",
  )
  let output : Array[CssNode] = []
  for node in relative {
    match node {
      // The candidate rule itself disappears; its declarations belong to the
      // rule that used `@apply`.
      Rule(selector="&", nodes=children, ..) =>
        for child in children {
          output.push(child)
        }
      _ => output.push(node)
    }
  }
  ignore(span)
  (output, rendered)
}

///|
/// Expand all candidates of one `@apply` directive, in build order.
fn nodes_for_apply_directive(
  theme : Map[String, String],
  custom_utilities : Map[String, Array[CssNode]],
  functional_utilities : Map[String, Array[CssNode]],
  custom_variants : Map[String, CustomVariantTemplate],
  params : String,
  span : SourceSpan,
) -> Array[CssNode] raise CompileError {
  let compiled : Array[(Array[CssNode], RenderedCandidate)] = []
  for candidate in params.split(" ") {
    let raw = trim(candidate.to_owned())
    if raw == "" {
      continue
    }
    compiled.push(
      nodes_for_apply(
        theme, custom_utilities, functional_utilities, custom_variants, raw, span,
      ),
    )
  }
  compiled.sort_by(fn(a, b) {
    let (_, left) = a
    let (_, right) = b
    compare_rendered_candidates(left, right)
  })
  let output : Array[CssNode] = []
  for entry in compiled {
    let (nodes, _) = entry
    for node in nodes {
      output.push(node)
    }
  }
  output
}

///|
fn substitute_apply(
  nodes : ArrayView[CssNode],
  theme : Map[String, String],
  custom_utilities : Map[String, Array[CssNode]],
  functional_utilities : Map[String, Array[CssNode]],
  custom_variants : Map[String, CustomVariantTemplate],
) -> (Array[CssNode], Bool) raise CompileError {
  let output : Array[CssNode] = []
  let mut changed = false
  for node in nodes {
    match node {
      AtRule(name="@apply", params~, nodes=None, span~) => {
        changed = true
        for
          applied in nodes_for_apply_directive(
            theme, custom_utilities, functional_utilities, custom_variants, params,
            span,
          ) {
          output.push(applied)
        }
      }
      Rule(selector~, nodes~, span~) => {
        let (children, child_changed) = substitute_apply(
          nodes, theme, custom_utilities, functional_utilities, custom_variants,
        )
        changed = changed || child_changed
        output.push(Rule(selector~, nodes=children, span~))
      }
      AtRule(name~, params~, nodes=Some(nodes), span~) => {
        let (children, child_changed) = substitute_apply(
          nodes, theme, custom_utilities, functional_utilities, custom_variants,
        )
        changed = changed || child_changed
        output.push(AtRule(name~, params~, nodes=Some(children), span~))
      }
      Context(values~, nodes=children, span~) => {
        let (inner, inner_changed) = substitute_apply(
          children, theme, custom_utilities, functional_utilities, custom_variants,
        )
        changed = changed || inner_changed
        output.push(Context(values~, nodes=inner, span~))
      }
      _ => output.push(node)
    }
  }
  (output, changed)
}