///|
priv struct Evaluation {
  mut remaining : Int
  mut calls : Int
  files : Map[String, String]
  loaded : Map[String, Scope]
  loading : Array[String]
  order : Array[String]
  css : Array[String]
  diagnostics : Array[String]
}

///|
fn Evaluation::new() -> Evaluation {
  {
    remaining: 100000,
    calls: 0,
    files: Map([]),
    loaded: Map([]),
    loading: [],
    order: [],
    css: [],
    diagnostics: [],
  }
}

///|
fn Scope::charge(self : Scope) -> Unit raise ParseError {
  self.evaluation.remaining -= 1
  if self.evaluation.remaining < 0 {
    raise Invalid("evaluation budget")
  }
}

///|
fn Scope::evaluate(
  self : Scope,
  source : String,
  division? : Bool = true,
) -> SassValue raise ParseError {
  self.eval_ast(parse_expression(source), division, 0)
}

///|
fn arithmetic_expression(value : Expression) -> Bool {
  match value {
    Literal(_) | Interpolated(_, _) => false
    _ => true
  }
}

///|
fn Scope::eval_ast(
  self : Scope,
  expression : Expression,
  division : Bool,
  depth : Int,
) -> SassValue raise ParseError {
  self.charge()
  if depth > 128 {
    raise Invalid("expression evaluation depth")
  }
  let result = match expression {
    Literal(value) => value
    Variable(name) =>
      match self.get(name) {
        Some(value) => value
        None => raise Invalid("undefined variable " + name)
      }
    Group(value) => self.eval_ast(value, true, depth + 1)
    Interpolated(parts, quoted) => {
      let out = StringBuilder()
      let mut length = 0
      for part in parts {
        let value = self.eval_ast(part, true, depth + 1)
        let text = if value is List([], _, _) || value is Null {
          ""
        } else {
          value.css(unquote=true)
        }
        length += text.length()
        if length > 1000000 {
          raise Invalid("interpolation size limit")
        }
        out.write_string(text)
      }
      Text(out.to_string(), quoted)
    }
    Sequence(values, separator, bracketed) =>
      List(
        values.map(v => self.eval_ast(v, division, depth + 1)),
        separator,
        bracketed,
      )
    Mapping(values) => {
      let result : Array[(SassValue, SassValue)] = []
      for (key, value) in values {
        let key = self.eval_ast(key, true, depth + 1)
        if result.iter().any(p => p.0.same(key)) {
          raise Invalid("duplicate map key")
        }
        result.push((key, self.eval_ast(value, true, depth + 1)))
      }
      Dictionary(result)
    }
    Unary(operator, value) => {
      let v = self.eval_ast(value, true, depth + 1)
      if operator == "not" {
        Boolean(!v.truth())
      } else if v is Number(n) {
        Number({
          ..n,
          amount: if operator == "-" {
            -n.amount
          } else {
            n.amount
          },
        })
      } else {
        Text(operator + v.css(unquote=true), false)
      }
    }
    Binary(operator, a, b) => {
      let left = self.eval_ast(a, division || operator != "/", depth + 1)
      if operator == "and" {
        if left.truth() {
          self.eval_ast(b, division, depth + 1)
        } else {
          left
        }
      } else if operator == "or" {
        if left.truth() {
          left
        } else {
          self.eval_ast(b, division, depth + 1)
        }
      } else {
        let right = self.eval_ast(b, division || operator != "/", depth + 1)
        if operator == "/" &&
          !division &&
          !arithmetic_expression(a) &&
          !arithmetic_expression(b) {
          Text(left.css() + "/" + right.css(), false)
        } else {
          binary_value(operator, left, right)
        }
      }
    }
    RawCall(name, body) => Text(name + "(" + self.expand(body) + ")", false)
    Call(name, args) =>
      if name == "if" &&
        args.length() == 3 &&
        args.iter().all(a => a.0 is None && !a.2) {
        let condition = self.eval_ast(args[0].1, true, depth + 1)
        self.eval_ast(
          args[if condition.truth() { 1 } else { 2 }].1,
          division,
          depth + 1,
        )
      } else {
        let positional = []
        let keywords : Map[String, SassValue] = Map([])
        let mut named = false
        for (key, expr, spread) in args {
          let value = self.eval_ast(
            expr,
            division ||
            name.contains(".") ||
            self.lookup_function(name) is Some(_) ||
            global_builtin(name),
            depth + 1,
          )
          if spread {
            if key is Some(_) {
              raise Invalid("keyword cannot spread")
            }
            match value {
              Dictionary(entries) =>
                for (k, v) in entries {
                  guard k is Text(name, _) else {
                    raise Invalid("keyword map key must be string")
                  }
                  let name = identifier(name)
                  if keywords.contains(name) {
                    raise Invalid("duplicate keyword")
                  }
                  keywords[name] = v
                  named = true
                }
              _ => {
                if named {
                  raise Invalid("positional after keyword")
                }
                for item in value.items() {
                  positional.push(item)
                }
              }
            }
          } else if key is Some(name) {
            if keywords.contains(name) {
              raise Invalid("duplicate keyword")
            }
            keywords[name] = value
            named = true
          } else {
            if named {
              raise Invalid("positional after keyword")
            }
            positional.push(value)
          }
        }
        self.call_function(name, positional, keywords, depth)
      }
  }
  result.check_size(Ref(16384), 0)
  result
}

///|
fn Scope::lookup_function(
  self : Scope,
  name : String,
) -> Mixin? raise ParseError {
  if name.contains(".") {
    return self.qualified_callable(name, true)
  }
  match self.functions.get(name) {
    Some(value) => Some(value)
    None =>
      match self.parent {
        Some(parent) => parent.lookup_function(name)
        None => self.star_callable(name, true)
      }
  }
}

///|
fn bind_arguments(
  definition : Mixin,
  args : Array[SassValue],
  keywords : Map[String, SassValue],
) -> Scope raise ParseError {
  let frame = Scope::new(Some(definition.scope))
  for i in 0.. {
          keywords.remove(key)
          value
        }
        None =>
          match default {
            Some(text) => frame.evaluate(text)
            None => raise Invalid("missing argument " + key)
          }
      }
    }
    frame.vars[key] = value
  }
  if !keywords.is_empty() {
    raise Invalid("unknown keyword argument")
  }
  match definition.rest {
    Some(key) =>
      frame.vars[key] = List(
        if args.length() > definition.params.length() {
          args[definition.params.length():].to_owned()
        } else {
          []
        },
        ",",
        false,
      )
    None =>
      if args.length() > definition.params.length() {
        raise Invalid("too many arguments")
      }
  }
  frame
}

///|
fn Scope::call_function(
  self : Scope,
  name : String,
  args : Array[SassValue],
  keywords : Map[String, SassValue],
  depth : Int,
) -> SassValue raise ParseError {
  if self.lookup_function(name) is Some(definition) {
    self.evaluation.calls += 1
    if self.evaluation.calls > 64 {
      raise Invalid("function recursion limit")
    }
    let frame = bind_arguments(definition, args, keywords)
    let returned = Ref(None)
    let frame = { ..frame, returned: Some(returned), }
    let emitter = Emitter::{
      output: StringBuilder(),
      size: 0,
      budget: 10000,
      pending_size: 0,
      pending: [],
      parents: [],
    }
    render(definition.body, [], frame, emitter, depth + 1, None, false, "")
    self.evaluation.calls -= 1
    match returned.val {
      Some(value) => value
      None => raise Invalid("function finished without return")
    }
  } else if builtin(self, name, args, keywords) is Some(value) {
    value
  } else {
    if name.contains(".") {
      raise Invalid("undefined namespaced function")
    }
    if !keywords.is_empty() {
      raise Invalid("plain CSS function cannot use keyword arguments")
    }
    Text(
      name +
      "(" +
      args.filter(v => !(v is Null)).map(v => v.css()).join(", ") +
      ")",
      false,
    )
  }
}