///|
fn split_function_arguments(input : String) -> Array[String] {
  let arguments : Array[String] = []
  let mut start = 0
  let mut depth = 0
  for i in 0.. depth += 1
      ')' | ']' => depth -= 1
      ',' =>
        if depth == 0 {
          arguments.push(trim(input[start:i].to_owned()))
          start = i + 1
        }
      _ => ()
    }
  }
  arguments.push(trim(input[start:].to_owned()))
  arguments
}

///|
fn find_function_end(input : String, open : Int) -> Int raise CompileError {
  let mut depth = 1
  let mut quote : UInt16? = None
  for i = open + 1; i < input.length(); i = i + 1 {
    let c = input[i]
    match quote {
      Some(q) => if c == q && (i == 0 || input[i - 1] != '\\') { quote = None }
      None =>
        match c {
          '\'' | '"' => quote = Some(c)
          '(' => depth += 1
          ')' => {
            depth -= 1
            if depth == 0 {
              return i
            }
          }
          _ => ()
        }
    }
  }
  raise InvalidCss("Unclosed CSS function")
}

///|
fn resolve_theme_function(
  theme : Map[String, String],
  raw_path : String,
  fallback : ArrayView[String],
  inline : Bool,
) -> String raise CompileError {
  let path = trim(raw_path)
  let (path, force_inline) = if path.has_suffix(" inline") {
    (trim(path[:path.length() - 7].to_owned()), true)
  } else {
    (path, false)
  }
  if !path.has_prefix("--") {
    raise InvalidCss("--theme() requires a CSS theme variable")
  }
  match theme.get(path) {
    Some(value) =>
      if inline || force_inline {
        value
      } else if fallback.is_empty() || trim(fallback.join(", ")) == "initial" {
        // `var(x, initial)` is equivalent to `var(x)`; upstream drops the
        // redundant `initial` fallback.
        "var(\{path})"
      } else {
        "var(\{path}, \{fallback.join(", ")})"
      }
    None =>
      if fallback.is_empty() {
        raise InvalidCss("Could not resolve theme value: \{path}")
      } else {
        fallback.join(", ")
      }
  }
}

///|
fn legacy_theme_key(path : String) -> String {
  let unquoted = if path.length() >= 2 &&
    (
      (path.has_prefix("\"") && path.has_suffix("\"")) ||
      (path.has_prefix("'") && path.has_suffix("'"))
    ) {
    path[1:path.length() - 1].to_owned()
  } else {
    path
  }
  replace_all(replace_all(replace_all(unquoted, ".", "-"), "[", "-"), "]", "")
}

///|
fn evaluate_css_function(
  name : String,
  content : String,
  theme : Map[String, String],
  inline_theme : Bool,
) -> String raise CompileError {
  let arguments = split_function_arguments(content)
  match name {
    "--spacing" => {
      if arguments.length() != 1 || arguments[0] == "" {
        raise InvalidCss("--spacing() requires one argument")
      }
      guard theme.get("--spacing") is Some(_) else {
        raise InvalidCss("--spacing() requires --spacing in the theme")
      }
      match arguments[0] {
        "0" => "0px"
        "1" => "var(--spacing)"
        value => "calc(var(--spacing) * \{value})"
      }
    }
    "--alpha" => {
      if arguments.length() != 1 {
        raise InvalidCss("--alpha() requires one color/alpha argument")
      }
      guard arguments[0].split_once("/") is Some((color, alpha)) else {
        raise InvalidCss("--alpha() requires `color / alpha`")
      }
      "color-mix(in oklab, \{trim(color.to_owned())} \{trim(alpha.to_owned())}, transparent)"
    }
    "--theme" => {
      if arguments.is_empty() {
        raise InvalidCss("--theme() requires a theme variable")
      }
      resolve_theme_function(theme, arguments[0], arguments[1:], inline_theme)
    }
    "theme" => {
      if arguments.is_empty() {
        raise InvalidCss("theme() requires a theme path")
      }
      let key = legacy_theme_key(arguments[0])
      let variable = if key.has_prefix("--") { key } else { "--\{key}" }
      resolve_theme_function(theme, variable, arguments[1:], inline_theme)
    }
    _ => content
  }
}

///|
fn substitute_value_functions(
  input : String,
  theme : Map[String, String],
  inline_theme~ : Bool,
) -> (String, Bool) raise CompileError {
  // Only these four calls are compile-time functions, so a value naming none of
  // them cannot change. Every declaration in the stylesheet reaches this point,
  // and the parse/render round trip below is the single biggest cost on large
  // import graphs — skip it rather than rebuild an identical value. (`theme(`
  // also covers `--theme(`.)
  if !input.contains("theme(") &&
    !input.contains("--spacing(") &&
    !input.contains("--alpha(") {
    return (input, false)
  }
  let ast = parse_value(input)
  let (ast, changed) = substitute_value_ast(ast, theme, inline_theme)
  (render_value(ast), changed)
}

///|
fn substitute_value_ast(
  nodes : ArrayView[ValueNode],
  theme : Map[String, String],
  inline_theme : Bool,
) -> (Array[ValueNode], Bool) raise CompileError {
  let output : Array[ValueNode] = []
  let mut changed = false
  for node in nodes {
    match node {
      ValueFunction(name, children) => {
        let (children, child_changed) = substitute_value_ast(
          children, theme, inline_theme,
        )
        changed = changed || child_changed
        if name == "--alpha" ||
          name == "--spacing" ||
          name == "--theme" ||
          name == "theme" {
          output.push(
            ValueWord(
              evaluate_css_function(
                name,
                render_value(children),
                theme,
                inline_theme,
              ),
            ),
          )
          changed = true
        } else {
          output.push(ValueFunction(name, children))
        }
      }
      _ => output.push(node)
    }
  }
  (output, changed)
}

///|
fn substitute_css_functions(
  nodes : ArrayView[CssNode],
  theme : Map[String, String],
) -> (Array[CssNode], Bool) raise CompileError {
  let output : Array[CssNode] = []
  let mut changed = false
  for node in nodes {
    match node {
      Declaration(name~, value~, important~, span~) => {
        let (value, did_change) = substitute_value_functions(
          value,
          theme,
          inline_theme=false,
        )
        changed = changed || did_change
        output.push(Declaration(name~, value~, important~, span~))
      }
      Rule(selector~, nodes~, span~) => {
        let (children, child_changed) = substitute_css_functions(nodes, theme)
        changed = changed || child_changed
        output.push(Rule(selector~, nodes=children, span~))
      }
      AtRule(name~, params~, nodes~, span~) => {
        let conditional = name == "@media" ||
          name == "@custom-media" ||
          name == "@container" ||
          name == "@supports"
        let (params, params_changed) = if conditional {
          substitute_value_functions(params, theme, inline_theme=true)
        } else {
          (params, false)
        }
        let children = match nodes {
          Some(children) => {
            let (children, child_changed) = substitute_css_functions(
              children, theme,
            )
            changed = changed || child_changed
            Some(children)
          }
          None => None
        }
        changed = changed || params_changed
        output.push(AtRule(name~, params~, nodes=children, span~))
      }
      Context(values~, nodes=children, span~) => {
        let (inner, inner_changed) = substitute_css_functions(children, theme)
        changed = changed || inner_changed
        output.push(Context(values~, nodes=inner, span~))
      }
      Comment(..) | AtRoot(..) => output.push(node)
    }
  }
  (output, changed)
}