///|
fn is_ascii_digit(c : Char) -> Bool {
  c >= '0' && c <= '9'
}

///|
fn is_nonnegative_integer(input : String) -> Bool {
  if input == "" {
    return false
  }
  for c in input {
    if !is_ascii_digit(c) {
      return false
    }
  }
  true
}

///|
fn is_spacing_number(input : String) -> Bool {
  let parsed = @string.parse_double(input) catch { _ => return false }
  if parsed < 0.0 {
    return false
  }
  let quarters = parsed * 4.0
  quarters == quarters.floor()
}

///|
fn normalize_ratio(input : String, spaced : Bool) -> String? {
  guard input.split_once("/") is Some((left, right)) else { return None }
  let left = trim(left.to_owned())
  let right = trim(right.to_owned())
  if !is_nonnegative_integer(left) ||
    left == "0" ||
    !is_nonnegative_integer(right) ||
    right == "0" {
    return None
  }
  Some(if spaced { "\{left} / \{right}" } else { "\{left}/\{right}" })
}

///|
fn unpack_functional_value(raw : String) -> (String, Bool, String?) {
  if raw.length() >= 2 && raw.has_prefix("[") && raw.has_suffix("]") {
    let inner = raw[1:raw.length() - 1].to_owned()
    match inner.split_once(":") {
      Some((data_type, value)) =>
        (decode_arbitrary(value.to_owned()), true, Some(data_type.to_owned()))
      None => (decode_arbitrary(inner), true, None)
    }
  } else if raw.length() >= 4 && raw.has_prefix("(--") && raw.has_suffix(")") {
    ("var(\{raw[1:raw.length() - 1].to_owned()})", true, None)
  } else if raw.length() >= 5 && raw.has_prefix("(") && raw.has_suffix(")") {
    let inner = raw[1:raw.length() - 1].to_owned()
    match inner.split_once(":") {
      Some((data_type, variable)) =>
        if variable.has_prefix("--") {
          ("var(\{variable.to_owned()})", true, Some(data_type.to_owned()))
        } else {
          (raw, false, None)
        }
      None => (raw, false, None)
    }
  } else {
    (raw, false, None)
  }
}

///|
fn functional_candidate_value(
  root : String,
  candidate : String,
) -> (String?, Bool, String?)? {
  if candidate == root {
    return Some((None, false, None))
  }
  let prefix = "\{root}-"
  guard candidate.has_prefix(prefix) else { return None }
  let raw = candidate[prefix.length():].to_owned()
  if raw == "" {
    None
  } else {
    let (value, arbitrary, data_type) = unpack_functional_value(raw)
    Some((Some(value), arbitrary, data_type))
  }
}

///|
fn resolve_functional_argument(
  argument : String,
  value : String?,
  arbitrary : Bool,
  value_data_type : String?,
  theme : Map[String, String],
) -> String? {
  let argument = replace_all(replace_all(trim(argument), "\\*", "*"), " ", "")
  match value {
    None =>
      if argument.has_prefix("--default(") && argument.has_suffix(")") {
        return Some(trim(argument[10:argument.length() - 1].to_owned()))
      } else {
        return None
      }
    Some(_) => ()
  }
  guard value is Some(value) else { return None }
  if arbitrary {
    if argument.has_prefix("[") && argument.has_suffix("]") {
      let data_type = trim(argument[1:argument.length() - 1].to_owned())
      if data_type == "*" {
        return Some(value)
      }
      match value_data_type {
        Some(hint) => return if hint == data_type { Some(value) } else { None }
        None => ()
      }
      if (data_type == "integer" && is_nonnegative_integer(value)) ||
        (data_type == "number" && is_spacing_number(value)) ||
        (
          data_type == "percentage" &&
          value.has_suffix("%") &&
          is_nonnegative_integer(value[:value.length() - 1].to_owned())
        ) ||
        (data_type == "ratio" && normalize_ratio(value, false) is Some(_)) {
        return Some(value)
      }
    }
    return None
  }
  if argument.length() >= 2 &&
    (
      (argument.has_prefix("'") && argument.has_suffix("'")) ||
      (argument.has_prefix("\"") && argument.has_suffix("\""))
    ) {
    let literal = argument[1:argument.length() - 1].to_owned()
    return if literal == value { Some(value) } else { None }
  }
  if argument == "integer" {
    return if is_nonnegative_integer(value) { Some(value) } else { None }
  }
  if argument == "number" {
    return if is_spacing_number(value) { Some(value) } else { None }
  }
  if argument == "percentage" &&
    value.has_suffix("%") &&
    is_nonnegative_integer(value[:value.length() - 1].to_owned()) {
    return Some(value)
  }
  if argument == "ratio" {
    return normalize_ratio(value, true)
  }
  if argument.has_prefix("--") {
    let key = if argument.contains("-*") {
      replace_all(argument, "*", value)
    } else {
      "\{argument}-\{value}"
    }
    match theme.get(key) {
      Some(_) => return theme_css_value(theme, key)
      None => return None
    }
  }
  None
}

///|
fn substitute_functional_values(
  input : String,
  function_name : String,
  value : String?,
  arbitrary : Bool,
  value_data_type : String?,
  theme : Map[String, String],
) -> (String?, Bool) {
  let (nodes, used) = substitute_functional_value_ast(
    parse_value(input),
    function_name,
    value,
    arbitrary,
    value_data_type,
    theme,
  )
  (nodes.map(fn(nodes) { render_value(nodes) }), used)
}

///|
fn substitute_functional_value_ast(
  nodes : ArrayView[ValueNode],
  function_name : String,
  value : String?,
  arbitrary : Bool,
  value_data_type : String?,
  theme : Map[String, String],
) -> (Array[ValueNode]?, Bool) {
  let output : Array[ValueNode] = []
  let mut used = false
  for node in nodes {
    match node {
      ValueFunction(name, children) =>
        if name == function_name {
          used = true
          let arguments = split_function_arguments(render_value(children))
          let mut resolved : String? = None
          for argument in arguments {
            match
              resolve_functional_argument(
                argument, value, arbitrary, value_data_type, theme,
              ) {
              Some(result) => {
                resolved = Some(result)
                break
              }
              None => ()
            }
          }
          guard resolved is Some(result) else { return (None, true) }
          for replacement in parse_value(result) {
            output.push(replacement)
          }
        } else {
          let (children, child_used) = substitute_functional_value_ast(
            children, function_name, value, arbitrary, value_data_type, theme,
          )
          guard children is Some(children) else {
            return (None, used || child_used)
          }
          used = used || child_used
          output.push(ValueFunction(name, children))
        }
      _ => output.push(node)
    }
  }
  (Some(output), used)
}

///|
priv struct FunctionalContext {
  theme : Map[String, String]
  value : String?
  arbitrary : Bool
  value_data_type : String?
  modifier : String?
  modifier_value : String?
  modifier_arbitrary : Bool
  modifier_data_type : String?
  mut resolved_value : Bool
  mut used_modifier : Bool
  mut resolved_modifier : Bool
  mut resolved_ratio : Bool
  ratios : Array[Bool]
}

///|
/// Substitute `--value()` and `--modifier()` throughout a functional `@utility`
/// body, dropping declarations whose arguments do not resolve.
fn substitute_functional_body(
  nodes : ArrayView[CssNode],
  context : FunctionalContext,
) -> Array[CssNode] {
  let output : Array[CssNode] = []
  for node in nodes {
    match node {
      Declaration(name~, value=raw, important~, span~) => {
        let declaration_ratio = raw.contains("--value(ratio")
        let value_for_declaration = if declaration_ratio {
          match (context.value, context.modifier_value) {
            (Some(left), Some(right)) => Some("\{left}/\{right}")
            _ => context.value
          }
        } else {
          context.value
        }
        let (with_value, used_value) = substitute_functional_values(
          raw,
          "--value",
          value_for_declaration,
          context.arbitrary,
          context.value_data_type,
          context.theme,
        )
        guard with_value is Some(with_value) else { continue }
        if used_value {
          context.resolved_value = true
          if declaration_ratio {
            context.resolved_ratio = true
          }
        }
        let (with_modifier, declaration_used_modifier) = substitute_functional_values(
          with_value,
          "--modifier",
          context.modifier_value,
          context.modifier_arbitrary,
          context.modifier_data_type,
          context.theme,
        )
        context.used_modifier = context.used_modifier ||
          declaration_used_modifier
        match with_modifier {
          Some(resolved) => {
            if declaration_used_modifier {
              context.resolved_modifier = true
            }
            context.ratios.push(declaration_ratio)
            output.push(Declaration(name~, value=resolved, important~, span~))
          }
          None => ()
        }
      }
      Rule(selector~, nodes=children, span~) =>
        output.push(
          Rule(
            selector~,
            nodes=substitute_functional_body(children, context),
            span~,
          ),
        )
      AtRule(name~, params~, nodes=Some(children), span~) =>
        output.push(
          AtRule(
            name~,
            params~,
            nodes=Some(substitute_functional_body(children, context)),
            span~,
          ),
        )
      _ => output.push(node)
    }
  }
  output
}

///|
/// Keep only the ratio form of a declaration set once a ratio value resolved.
fn keep_ratio_declarations(
  nodes : ArrayView[CssNode],
  ratios : ArrayView[Bool],
  cursor : Ref[Int],
) -> Array[CssNode] {
  let output : Array[CssNode] = []
  for node in nodes {
    match node {
      Declaration(..) => {
        let index = cursor.val
        cursor.val = index + 1
        if index < ratios.length() && ratios[index] {
          output.push(node)
        }
      }
      Rule(selector~, nodes=children, span~) =>
        output.push(
          Rule(
            selector~,
            nodes=keep_ratio_declarations(children, ratios, cursor),
            span~,
          ),
        )
      AtRule(name~, params~, nodes=Some(children), span~) =>
        output.push(
          AtRule(
            name~,
            params~,
            nodes=Some(keep_ratio_declarations(children, ratios, cursor)),
            span~,
          ),
        )
      _ => output.push(node)
    }
  }
  output
}

///|
fn compile_functional_utility(
  theme : Map[String, String],
  name : String,
  modifier : String?,
  functional : Map[String, Array[CssNode]],
) -> Array[CssNode]? {
  for root, body in functional {
    guard functional_candidate_value(root, name)
      is Some((value, arbitrary, value_data_type)) else {
      continue
    }
    let (modifier_value, modifier_arbitrary, modifier_data_type) = match
      modifier {
      Some(raw) => {
        let (value, arbitrary, data_type) = unpack_functional_value(raw)
        (Some(value), arbitrary, data_type)
      }
      None => (None, false, None)
    }
    let context : FunctionalContext = {
      theme,
      value,
      arbitrary,
      value_data_type,
      modifier,
      modifier_value,
      modifier_arbitrary,
      modifier_data_type,
      resolved_value: false,
      used_modifier: false,
      resolved_modifier: false,
      resolved_ratio: false,
      ratios: [],
    }
    let substituted = substitute_functional_body(body, context)
    if !context.resolved_value ||
      (
        context.modifier is Some(_) &&
        !context.resolved_ratio &&
        (!context.used_modifier || !context.resolved_modifier)
      ) {
      return None
    }
    let result = if context.resolved_ratio {
      keep_ratio_declarations(substituted, context.ratios, { val: 0 })
    } else {
      substituted
    }
    return if count_declarations(result) == 0 { None } else { Some(result) }
  }
  None
}

///|
fn count_declarations(nodes : ArrayView[CssNode]) -> Int {
  let mut total = 0
  for node in nodes {
    match node {
      Declaration(..) => total += 1
      Rule(nodes=children, ..)
      | AtRule(nodes=Some(children), ..)
      | Context(nodes=children, ..) => total += count_declarations(children)
      _ => ()
    }
  }
  total
}