///|
/// Resolve a nested selector against the selector of its parent rule.
///
/// A selector list has to become a single compound before it can be nested,
/// which upstream does with `:is(…)`.
fn nested_selector(child : String, parent : String) -> String {
  let reference = if has_top_level_comma(parent) {
    ":is(\{parent})"
  } else {
    parent
  }
  if child.contains("&") {
    replace_all(child, "&", reference)
  } else {
    "\{reference} \{child}"
  }
}

///|
fn node_is_declaration(node : CssNode) -> Bool {
  match node {
    Declaration(..) => true
    _ => false
  }
}

///|
/// Flatten nested rules the way the upstream optimizer does.
///
/// A rule that declares nothing itself is dissolved into its children: nested
/// selectors merge with the parent selector and nested at-rules move above it.
/// A rule that does declare something keeps its nested children as authored.
fn flatten_css_nesting(nodes : ArrayView[CssNode]) -> Array[CssNode] {
  let output : Array[CssNode] = []
  for node in nodes {
    match node {
      Rule(selector="&", nodes=children, ..) =>
        for flattened in flatten_css_nesting(children) {
          output.push(flattened)
        }
      Rule(selector~, nodes=children, span~) =>
        for flattened in flatten_css_rule(selector, children, span) {
          output.push(flattened)
        }
      AtRule(name~, params~, nodes=Some(children), span~) => {
        let inner = flatten_css_nesting(children)
        if !inner.is_empty() {
          output.push(AtRule(name~, params~, nodes=Some(inner), span~))
        }
      }
      Context(values~, nodes=children, span~) =>
        output.push(
          Context(values~, nodes=flatten_css_nesting(children), span~),
        )
      AtRoot(nodes=children, span~) =>
        output.push(AtRoot(nodes=flatten_css_nesting(children), span~))
      _ => output.push(node)
    }
  }
  output
}

///|
/// Drop declarations that a later identical declaration repeats.
///
/// Upstream keeps the last occurrence, so the earlier copy is the one removed.
fn dedupe_declarations(nodes : ArrayView[CssNode]) -> Array[CssNode] {
  let output : Array[CssNode] = []
  for index, node in nodes {
    match node {
      Declaration(name~, value~, important~, ..) => {
        let mut repeated = false
        for later in (index + 1)..
              if later_name == name &&
                later_value == value &&
                later_important == important {
                repeated = true
                break
              }
            _ => ()
          }
        }
        if !repeated {
          output.push(node)
        }
      }
      _ => output.push(node)
    }
  }
  output
}

///|
/// Merge neighbouring rules and at-rules that describe the same thing.
///
/// Stacked at-rule variants produce one wrapper per candidate, and repeated
/// selectors produce one rule per source block; upstream collapses each run.
fn merge_adjacent_at_rules(nodes : ArrayView[CssNode]) -> Array[CssNode] {
  let output : Array[CssNode] = []
  for node in nodes {
    match node {
      AtRule(name~, params~, nodes=Some(children), span~) => {
        let merged = merge_adjacent_at_rules(children)
        match output.last() {
          Some(
            AtRule(
              name=previous_name,
              params=previous_params,
              nodes=Some(previous_children),
              ..
            )
          ) if previous_name == name && previous_params == params =>
            // The wrapper already in `output` keeps its own span, and its child
            // array is extended in place, so a run of K wrappers costs one walk
            // rather than one per member.
            append_merged(previous_children, merged)
          _ =>
            output.push(
              AtRule(
                name~,
                params~,
                nodes=Some(dedupe_declarations(merged)),
                span~,
              ),
            )
        }
      }
      Rule(selector~, nodes=children, span~) => {
        let merged = merge_adjacent_at_rules(children)
        match output.last() {
          Some(Rule(selector=previous_selector, nodes=previous_children, ..)) if previous_selector ==
            selector => {
            append_merged(previous_children, merged)
            // Deduplication spans the whole run: a declaration repeated by a
            // later member removes the earlier copy.
            rededuplicate(previous_children)
          }
          _ =>
            output.push(
              Rule(selector~, nodes=dedupe_declarations(merged), span~),
            )
        }
      }
      Context(values~, nodes=children, span~) =>
        output.push(
          Context(values~, nodes=merge_adjacent_at_rules(children), span~),
        )
      _ => output.push(node)
    }
  }
  output
}

///|
/// Replace `nodes` with its deduplicated form, in place.
fn rededuplicate(nodes : Array[CssNode]) -> Unit {
  let deduped = dedupe_declarations(nodes[:])
  nodes.clear()
  nodes.append(deduped)
}

///|
/// Append one already-merged sibling list onto another, in place.
///
/// Both sides are internally merged, so concatenating them can only create a
/// single new adjacency — the junction — and merging that junction can only
/// create one more, one level down. The previous implementation instead copied
/// the accumulated children and re-ran `merge_adjacent_at_rules` over the whole
/// concatenation for *every* additional sibling, re-walking (and re-merging,
/// re-deduplicating) the entire subtree to discover that one boundary: O(K²)
/// walks for a run of K wrappers, which grows with candidate count.
fn append_merged(
  target : Array[CssNode],
  addition : ArrayView[CssNode],
) -> Unit {
  for node in addition {
    let mut absorbed = false
    match (target.last(), node) {
      (
        Some(
          AtRule(
            name=previous_name,
            params=previous_params,
            nodes=Some(previous_children),
            ..
          )
        ),
        AtRule(name~, params~, nodes=Some(children), ..),
      ) =>
        if previous_name == name && previous_params == params {
          append_merged(previous_children, children)
          absorbed = true
        }
      (
        Some(Rule(selector=previous_selector, nodes=previous_children, ..)),
        Rule(selector~, nodes=children, ..),
      ) =>
        if previous_selector == selector {
          append_merged(previous_children, children)
          rededuplicate(previous_children)
          absorbed = true
        }
      _ => ()
    }
    if !absorbed {
      target.push(node)
    }
  }
}

///|
fn flatten_css_rule(
  selector : String,
  children : ArrayView[CssNode],
  span : SourceSpan,
) -> Array[CssNode] {
  if children.is_empty() {
    return []
  }
  // Once a rule declares something, upstream leaves everything below it alone:
  // nested content cannot move out without duplicating this rule.
  if children.iter().any(node_is_declaration) {
    return [Rule(selector~, nodes=children.to_owned(), span~)]
  }
  let output : Array[CssNode] = []
  for child in children {
    match child {
      Rule(selector=child_selector, nodes=grandchildren, span=child_span) =>
        for
          flattened in flatten_css_rule(
            nested_selector(child_selector, selector),
            grandchildren,
            child_span,
          ) {
          output.push(flattened)
        }
      AtRule(name~, params~, nodes=Some(grandchildren), span=child_span) => {
        let inner = flatten_css_rule(selector, grandchildren, child_span)
        if !inner.is_empty() {
          output.push(
            AtRule(name~, params~, nodes=Some(inner), span=child_span),
          )
        }
      }
      _ => output.push(child)
    }
  }
  output
}

///|
let color_mix_supports = "(color: color-mix(in lab, red, red))"

///|
/// Replace `var(--x)` references with their raw theme literal (dropping any
/// fallback), for building the sRGB `color-mix()` compatibility value.
fn resolve_theme_var_literals(
  nodes : ArrayView[ValueNode],
  theme : Map[String, String],
) -> Array[ValueNode] {
  let output : Array[ValueNode] = []
  for node in nodes {
    match node {
      ValueFunction("var", children) => {
        let mut name = ""
        for child in children {
          if child is ValueWord(word) {
            name = word
            break
          }
        }
        match theme.get(name) {
          Some(literal) if !literal.contains("var(") =>
            output.push(ValueWord(literal))
          _ =>
            output.push(
              ValueFunction("var", resolve_theme_var_literals(children, theme)),
            )
        }
      }
      ValueFunction(other, children) =>
        output.push(
          ValueFunction(other, resolve_theme_var_literals(children, theme)),
        )
      _ => output.push(node)
    }
  }
  output
}

///|
/// Rewrite `color-mix(in oklab|oklch, …)` to its sRGB compatibility form:
/// resolve theme variables to literals and switch the interpolation space to
/// `srgb`. Returns `None` when the value has no oklab/oklch `color-mix()`.
fn color_mix_fallback_nodes(
  nodes : ArrayView[ValueNode],
  theme : Map[String, String],
) -> (Array[ValueNode], Bool) {
  let output : Array[ValueNode] = []
  let mut changed = false
  for node in nodes {
    match node {
      ValueFunction("color-mix", children) => {
        let resolved = resolve_theme_var_literals(children, theme)
        let (inner, _) = color_mix_fallback_nodes(resolved, theme)
        // A `color-mix()` involving `currentColor` cannot be reduced to a static
        // colour, and an sRGB `color-mix()` still needs `color-mix` support, so
        // the fallback drops the mix and keeps the bare `currentColor` token.
        let mut current_color : String? = None
        for child in inner {
          if child is ValueWord(word) && word.to_lower() == "currentcolor" {
            current_color = Some(word)
            break
          }
        }
        match current_color {
          Some(word) => {
            changed = true
            output.push(ValueWord(word))
          }
          None => {
            // Switch the first `oklab`/`oklch` color-space keyword to `srgb`.
            let swapped : Array[ValueNode] = []
            let mut did_swap = false
            for child in inner {
              if !did_swap &&
                (child is ValueWord("oklab") || child is ValueWord("oklch")) {
                swapped.push(ValueWord("srgb"))
                did_swap = true
              } else {
                swapped.push(child)
              }
            }
            changed = changed || did_swap
            output.push(ValueFunction("color-mix", swapped))
          }
        }
      }
      ValueFunction(name, children) => {
        let (inner, inner_changed) = color_mix_fallback_nodes(children, theme)
        changed = changed || inner_changed
        output.push(ValueFunction(name, inner))
      }
      _ => output.push(node)
    }
  }
  (output, changed)
}

///|
/// Build the plain sRGB fallback string for a value containing an oklab/oklch
/// `color-mix()`, or `None` when there is nothing to polyfill.
fn color_mix_fallback_value(
  value : String,
  theme : Map[String, String],
) -> String? {
  if !(value.contains("color-mix(in oklab") ||
    value.contains("color-mix(in oklch")) {
    return None
  }
  let (rebuilt, changed) = color_mix_fallback_nodes(parse_value(value), theme)
  if !changed {
    return None
  }
  Some(render_value(rebuilt))
}

///|
/// Add the `color-mix()` compatibility fallback for author declarations: a
/// plain sRGB value followed by the modern value inside
/// `@supports (color: color-mix(in lab, red, red))`.
///
/// Declarations already inside that `@supports` block (the modern branch of an
/// existing fallback, e.g. utility output) are left untouched.
fn add_color_mix_fallback(
  nodes : ArrayView[CssNode],
  theme : Map[String, String],
  inside : Bool,
) -> Array[CssNode] {
  let output : Array[CssNode] = []
  for node in nodes {
    match node {
      AtRule(
        name="@supports",
        params=color_mix_supports,
        nodes=Some(children),
        span~
      ) =>
        output.push(
          AtRule(
            name="@supports",
            params=color_mix_supports,
            nodes=Some(add_color_mix_fallback(children, theme, true)),
            span~,
          ),
        )
      Declaration(name~, value~, important~, span~) => {
        let fallback_value = if inside {
          None
        } else {
          color_mix_fallback_value(value, theme)
        }
        match fallback_value {
          Some(fallback) => {
            output.push(Declaration(name~, value=fallback, important~, span~))
            output.push(
              AtRule(
                name="@supports",
                params=color_mix_supports,
                nodes=Some([Declaration(name~, value~, important~, span~)]),
                span~,
              ),
            )
          }
          None => output.push(node)
        }
      }
      Rule(selector~, nodes=children, span~) =>
        output.push(
          Rule(
            selector~,
            nodes=add_color_mix_fallback(children, theme, inside),
            span~,
          ),
        )
      AtRule(name~, params~, nodes=Some(children), span~) =>
        output.push(
          AtRule(
            name~,
            params~,
            nodes=Some(add_color_mix_fallback(children, theme, inside)),
            span~,
          ),
        )
      Context(values~, nodes=children, span~) =>
        output.push(
          Context(
            values~,
            nodes=add_color_mix_fallback(children, theme, inside),
            span~,
          ),
        )
      _ => output.push(node)
    }
  }
  output
}

///|
/// Remove the `color-mix()` compatibility fallback.
///
/// Each fallback is a plain declaration followed by the modern value inside
/// `@supports (color: color-mix(in lab, red, red))`; dropping the polyfill
/// keeps only the modern value.
fn strip_color_mix_fallback(nodes : ArrayView[CssNode]) -> Array[CssNode] {
  let output : Array[CssNode] = []
  for node in nodes {
    match node {
      AtRule(
        name="@supports",
        params="(color: color-mix(in lab, red, red))",
        nodes=Some(children),
        ..
      ) =>
        for child in children {
          match child {
            Declaration(name~, ..) => {
              // Drop the fallback this modern value replaces.
              let mut index = output.length() - 1
              while index >= 0 {
                match output[index] {
                  Declaration(name=previous, ..) if previous == name => {
                    output.remove(index) |> ignore
                    break
                  }
                  _ => index -= 1
                }
              }
            }
            _ => ()
          }
          output.push(child)
        }
      AtRule(name~, params~, nodes=Some(children), span~) =>
        output.push(
          AtRule(
            name~,
            params~,
            nodes=Some(strip_color_mix_fallback(children)),
            span~,
          ),
        )
      Rule(selector~, nodes=children, span~) =>
        output.push(
          Rule(selector~, nodes=strip_color_mix_fallback(children), span~),
        )
      Context(values~, nodes=children, span~) =>
        output.push(
          Context(values~, nodes=strip_color_mix_fallback(children), span~),
        )
      _ => output.push(node)
    }
  }
  output
}