///|
fn Scope::standard(self : Scope, prefix : String) -> String? {
  match self.standard_modules.get(prefix) {
    Some(name) => Some(name)
    None =>
      match self.parent {
        Some(parent) => parent.standard(prefix)
        None => None
      }
  }
}

///|
fn require_number(value : SassValue) -> SassNumber raise ParseError {
  match value {
    Number(n) => n
    _ => raise Invalid("expected number")
  }
}

///|
fn require_text(value : SassValue) -> (String, Bool) raise ParseError {
  match value {
    Text(s, q) => (s, q)
    _ => raise Invalid("expected string")
  }
}

///|
fn require_int(value : SassValue) -> Int raise ParseError {
  let n = require_number(value)
  if !n.unitless() ||
    n.amount.is_nan() ||
    n.amount.is_inf() ||
    n.amount != n.amount.floor() ||
    n.amount.abs() > 2147483647.0 {
    raise Invalid("expected bounded integer")
  }
  n.amount.to_int()
}

///|
fn list_index(value : SassValue, length : Int) -> Int raise ParseError {
  let raw = require_int(value)
  let index = if raw < 0 { length + raw } else { raw - 1 }
  if index < 0 || index >= length {
    raise Invalid("list index out of range")
  }
  index
}

///|
fn builtin(
  scope : Scope,
  original : String,
  positional : Array[SassValue],
  keywords : Map[String, SassValue],
) -> SassValue? raise ParseError {
  let pieces = original.split(".").to_array()
  let name = if pieces.length() == 2 {
    let library = scope.standard(pieces[0].to_owned()).unwrap_or("")
    if library.is_empty() {
      return None
    }
    library + "." + pieces[1].to_owned()
  } else {
    original
  }
  // Parameter names also define the keyword-binding contract for each function.
  let spec = match name {
    "math.div" => ["number1", "number2"]
    "abs"
    | "math.abs"
    | "ceil"
    | "math.ceil"
    | "floor"
    | "math.floor"
    | "round"
    | "math.round"
    | "percentage"
    | "math.percentage"
    | "unit"
    | "math.unit"
    | "unitless"
    | "math.is-unitless" => ["number"]
    "comparable" | "math.compatible" => ["number1", "number2"]
    "math.pow" => ["base", "exponent"]
    "math.sqrt" => ["number"]
    "length" | "list.length" | "list.separator" | "list.is-bracketed" =>
      ["list"]
    "nth" | "list.nth" => ["list", "n"]
    "set-nth" | "list.set-nth" => ["list", "n", "value"]
    "index" | "list.index" => ["list", "value"]
    "append" | "list.append" => ["list", "val", "separator"]
    "join" | "list.join" => ["list1", "list2", "separator", "bracketed"]
    "list.slash" => []
    "map-get" | "map.get" | "map-has-key" | "map.has-key" => ["map", "key"]
    "map-keys" | "map.keys" | "map-values" | "map.values" => ["map"]
    "map-merge" | "map.merge" => ["map1", "map2"]
    "map-remove" | "map.remove" => ["map", "key"]
    "quote"
    | "string.quote"
    | "unquote"
    | "string.unquote"
    | "str-length"
    | "string.length"
    | "to-upper-case"
    | "string.to-upper-case"
    | "to-lower-case"
    | "string.to-lower-case" => ["string"]
    "str-slice" | "string.slice" => ["string", "start-at", "end-at"]
    "str-index" | "string.index" => ["string", "substring"]
    "str-insert" | "string.insert" => ["string", "insert", "index"]
    "type-of" | "meta.type-of" | "inspect" | "meta.inspect" => ["value"]
    "variable-exists"
    | "meta.variable-exists"
    | "global-variable-exists"
    | "meta.global-variable-exists" => ["name"]
    "function-exists"
    | "meta.function-exists"
    | "mixin-exists"
    | "meta.mixin-exists" => ["name"]
    "min" | "math.min" | "max" | "math.max" => []
    _ => return None
  }
  let variadic = [
    "min", "math.min", "max", "math.max", "list.slash", "map-remove", "map.remove",
  ].contains(name)
  let args = positional.copy()
  let supplied : Map[Int, Bool] = Map([])
  for i in 0.. spec.length() {
    raise Invalid("too many builtin arguments")
  }
  let minimum = match name {
    "append"
    | "list.append"
    | "join"
    | "list.join"
    | "str-slice"
    | "string.slice" => 2
    "min" | "math.min" | "max" | "math.max" => 1
    "list.slash" => 2
    _ => spec.length()
  }
  if args.length() < minimum {
    raise Invalid("missing builtin argument")
  }
  for i in 0.. SassValue { args.get(i).unwrap_or(Null) }
  let result : SassValue = match name {
    "math.div" => binary_value("/", at(0), at(1))
    "abs"
    | "math.abs"
    | "ceil"
    | "math.ceil"
    | "floor"
    | "math.floor"
    | "round"
    | "math.round" => {
      let n = require_number(at(0))
      Number({
        ..n,
        amount: if name.has_suffix("abs") {
          n.amount.abs()
        } else if name.has_suffix("ceil") {
          n.amount.ceil()
        } else if name.has_suffix("floor") {
          n.amount.floor()
        } else if n.amount < 0.0 {
          -(n.amount.abs() + 0.5).floor()
        } else {
          (n.amount + 0.5).floor()
        },
      })
    }
    "percentage" | "math.percentage" => {
      let n = require_number(at(0))
      if !n.unitless() {
        raise Invalid("percentage expects unitless number")
      }
      numeric(n.amount * 100.0, unit="%")
    }
    "unit" | "math.unit" => {
      let n = require_number(at(0))
      Text(
        n.numerator.join("*") +
        (if n.denominator.is_empty() {
          ""
        } else {
          "/" + n.denominator.join("*")
        }),
        true,
      )
    }
    "unitless" | "math.is-unitless" => Boolean(require_number(at(0)).unitless())
    "comparable" | "math.compatible" =>
      Boolean(
        require_number(at(0)).convert(require_number(at(1)), unitless=true)
        is Some(_),
      )
    "math.pow" | "math.sqrt" => {
      let n = require_number(at(0))
      if !n.unitless() {
        raise Invalid("math function expects unitless number")
      }
      if name == "math.sqrt" {
        numeric(n.amount.sqrt())
      } else {
        let power = require_number(at(1))
        if !power.unitless() {
          raise Invalid("power must be unitless")
        }
        numeric(@math.pow(n.amount, power.amount))
      }
    }
    "min" | "math.min" | "max" | "math.max" => {
      let mut best = require_number(args[0])
      for value in args[1:] {
        let n = require_number(value)
        let converted = match n.convert(best, unitless=true) {
          Some(v) => v
          None => raise Invalid("incompatible min/max units")
        }
        if (name.has_suffix("min") && converted < best.amount) ||
          (name.has_suffix("max") && converted > best.amount) {
          best = n
        }
      }
      Number(best)
    }
    "length" | "list.length" => numeric(at(0).items().length().to_double())
    "list.separator" =>
      Text(
        match at(0) {
          List(_, sep, _) =>
            if sep == "," {
              "comma"
            } else if sep == " / " {
              "slash"
            } else {
              "space"
            }
          Dictionary(_) => "comma"
          _ => "space"
        },
        false,
      )
    "list.is-bracketed" => Boolean(at(0) is List(_, _, true))
    "nth" | "list.nth" => {
      let values = at(0).items()
      values[list_index(at(1), values.length())]
    }
    "set-nth" | "list.set-nth" => {
      let values = at(0).items()
      values[list_index(at(1), values.length())] = at(2)
      match at(0) {
        List(_, sep, bracket) => List(values, sep, bracket)
        _ => List(values, " ", false)
      }
    }
    "index" | "list.index" => {
      let values = at(0).items()
      let mut result = Null
      for i in 0.. {
      let values = at(0).items()
      if name.has_suffix("append") {
        values.push(at(1))
      } else {
        for value in at(1).items() {
          values.push(value)
        }
      }
      let separator = if at(2) is Null || at(2) is Text("auto", _) {
        match at(0) {
          List(_, sep, _) => sep
          _ =>
            if name.has_suffix("join") {
              match at(1) {
                List(_, sep, _) => sep
                _ => " "
              }
            } else {
              " "
            }
        }
      } else {
        match require_text(at(2)).0 {
          "comma" => ","
          "space" => " "
          _ => raise Invalid("invalid separator")
        }
      }
      let bracketed = if at(3) is Null || at(3) is Text("auto", _) {
        at(0) is List(_, _, true)
      } else {
        match at(3) {
          Boolean(v) => v
          _ => raise Invalid("bracketed must be boolean")
        }
      }
      List(values, separator, bracketed)
    }
    "list.slash" => List(args, " / ", false)
    "map-get" | "map.get" | "map-has-key" | "map.has-key" => {
      let values = map_entries(at(0))
      let mut result = Null
      let mut found = false
      for (key, value) in values {
        if key.same(at(1)) {
          result = value
          found = true
          break
        }
      }
      if name.has_suffix("has-key") {
        Boolean(found)
      } else {
        result
      }
    }
    "map-keys" | "map.keys" =>
      List(map_entries(at(0)).map(p => p.0), ",", false)
    "map-values" | "map.values" =>
      List(map_entries(at(0)).map(p => p.1), ",", false)
    "map-merge" | "map.merge" => {
      let values = map_entries(at(0)).copy()
      for (key, value) in map_entries(at(1)) {
        let mut replaced = false
        for i in 0..
      Dictionary(
        map_entries(at(0)).filter(p => !args[1:].iter().any(k => k.same(p.0))),
      )
    "quote" | "string.quote" | "unquote" | "string.unquote" =>
      Text(require_text(at(0)).0, !name.has_suffix("unquote"))
    "str-length" | "string.length" =>
      numeric(require_text(at(0)).0.to_array().length().to_double())
    "to-upper-case"
    | "string.to-upper-case"
    | "to-lower-case"
    | "string.to-lower-case" => {
      let (text, quoted) = require_text(at(0))
      Text(
        if name.has_suffix("upper-case") {
          text.to_upper()
        } else {
          text.to_lower()
        },
        quoted,
      )
    }
    "str-slice" | "string.slice" => {
      let (text, quoted) = require_text(at(0))
      let chars = text.to_array()
      let start = require_int(at(1))
      let finish = if at(2) is Null {
        chars.length()
      } else {
        require_int(at(2))
      }
      let first = (if start < 0 {
          chars.length() + start
        } else {
          (start - 1).max(0)
        })
        .max(0)
        .min(chars.length())
      let last = (if finish < 0 { chars.length() + finish + 1 } else { finish })
        .max(0)
        .min(chars.length())
      Text(
        if last < first {
          ""
        } else {
          String::from_array(chars[first:last])
        },
        quoted,
      )
    }
    "str-index" | "string.index" => {
      let chars = require_text(at(0)).0.to_array()
      let needle = require_text(at(1)).0.to_array()
      let mut index = Null
      for i = 0; i + needle.length() <= chars.length(); i = i + 1 {
        if chars[i:i + needle.length()].to_owned() == needle {
          index = numeric((i + 1).to_double())
          break
        }
      }
      index
    }
    "str-insert" | "string.insert" => {
      let (text, quoted) = require_text(at(0))
      let chars = text.to_array()
      let insert = require_text(at(1)).0
      let index = require_int(at(2))
      let at = (if index < 0 {
          chars.length() + index + 1
        } else {
          (index - 1).max(0)
        })
        .max(0)
        .min(chars.length())
      Text(
        String::from_array(chars[:at]) + insert + String::from_array(chars[at:]),
        quoted,
      )
    }
    "type-of" | "meta.type-of" =>
      Text(
        match at(0) {
          Number(_) => "number"
          Text(_, _) => "string"
          Boolean(_) => "bool"
          Null => "null"
          List(_, _, _) => "list"
          Dictionary(_) => "map"
        },
        false,
      )
    "inspect" | "meta.inspect" => Text(at(0).css(inspect=true), false)
    "variable-exists" | "meta.variable-exists" =>
      Boolean(scope.get(require_text(at(0)).0) is Some(_))
    "global-variable-exists" | "meta.global-variable-exists" =>
      Boolean(scope.global().vars.contains(identifier(require_text(at(0)).0)))
    "function-exists" | "meta.function-exists" => {
      let name = identifier(require_text(at(0)).0)
      Boolean(scope.lookup_function(name) is Some(_) || global_builtin(name))
    }
    "mixin-exists" | "meta.mixin-exists" =>
      Boolean(scope.lookup_mixin(require_text(at(0)).0) is Some(_))
    _ => raise Invalid("unimplemented builtin")
  }
  Some(result)
}

///|
fn map_entries(
  value : SassValue,
) -> Array[(SassValue, SassValue)] raise ParseError {
  match value {
    Dictionary(values) => values
    List([], _, false) => []
    _ => raise Invalid("expected map")
  }
}

///|
fn global_builtin(name : String) -> Bool {
  [
    "abs", "ceil", "floor", "round", "percentage", "unit", "unitless", "comparable",
    "min", "max", "length", "nth", "set-nth", "index", "append", "join", "map-get",
    "map-has-key", "map-keys", "map-values", "map-merge", "map-remove", "quote",
    "unquote", "str-length", "to-upper-case", "to-lower-case", "str-slice", "str-index",
    "str-insert", "type-of", "inspect", "variable-exists", "global-variable-exists",
    "function-exists", "mixin-exists",
  ].contains(name)
}