///|
priv struct Evaluator {
  mut activation : Activation
  limits : EvalLimits
  mut cost : Int
  mut depth : Int
  mut iterations : Int
}

///|
fn Evaluator::new(activation : Activation, limits : EvalLimits) -> Evaluator {
  { activation, limits, cost: 0, depth: 0, iterations: 0 }
}

///|
fn literal_value(literal : Literal) -> Value {
  match literal {
    Literal::Null => Value::Null
    Literal::Bool(value) => Value::Bool(value)
    Literal::Int(value) => Value::Int(value)
    Literal::UInt(value) => Value::UInt(value)
    Literal::Double(value) => Value::Double(value)
    Literal::String(value) => Value::String(value)
    Literal::Bytes(value) => Value::Bytes(value)
  }
}

///|
fn type_mismatch(
  expected : String,
  actual : Value,
  span : SourceSpan,
) -> Value raise EvalError {
  raise TypeMismatch(expected~, actual=actual.kind_name(), span~)
}

///|
fn require_bool(value : Value, span : SourceSpan) -> Bool raise EvalError {
  match value {
    Value::Bool(result) => result
    other => {
      ignore(type_mismatch("bool", other, span))
      false
    }
  }
}

///|
fn Evaluator::eval_identifier(
  self : Evaluator,
  name : String,
  span : SourceSpan,
) -> Value raise EvalError {
  match self.activation.lookup(name) {
    Some(value) => value
    None => raise UnboundVariable(name~, span~)
  }
}

///|
fn Evaluator::eval_unary(
  self : Evaluator,
  operator : UnaryOp,
  operand : Expr,
  span : SourceSpan,
) -> Value raise EvalError {
  let value = self.eval_expr(operand)
  match (operator, value) {
    (UnaryOp::Not, Value::Bool(value)) => Value::Bool(!value)
    (UnaryOp::Negate, Value::Int(value)) => safe_negate_int(value, span)
    (UnaryOp::Negate, Value::Double(value)) => Value::Double(-value)
    (UnaryOp::Not, other) => type_mismatch("bool", other, span)
    (UnaryOp::Negate, other) => type_mismatch("int or double", other, span)
  }
}

///|
fn Evaluator::eval_logical(
  self : Evaluator,
  operator : BinaryOp,
  left : Expr,
  right : Expr,
) -> Value raise EvalError {
  let left_value = require_bool(self.eval_expr(left), left.span)
  match operator {
    BinaryOp::And =>
      if !left_value {
        Value::Bool(false)
      } else {
        Value::Bool(require_bool(self.eval_expr(right), right.span))
      }
    BinaryOp::Or =>
      if left_value {
        Value::Bool(true)
      } else {
        Value::Bool(require_bool(self.eval_expr(right), right.span))
      }
    _ => abort("eval_logical requires && or ||")
  }
}

///|
fn compare_int(operator : BinaryOp, left : Int64, right : Int64) -> Bool {
  match operator {
    BinaryOp::Less => left < right
    BinaryOp::LessEqual => left <= right
    BinaryOp::Greater => left > right
    BinaryOp::GreaterEqual => left >= right
    _ => false
  }
}

///|
fn compare_uint(operator : BinaryOp, left : UInt64, right : UInt64) -> Bool {
  match operator {
    BinaryOp::Less => left < right
    BinaryOp::LessEqual => left <= right
    BinaryOp::Greater => left > right
    BinaryOp::GreaterEqual => left >= right
    _ => false
  }
}

///|
fn compare_double(operator : BinaryOp, left : Double, right : Double) -> Bool {
  match operator {
    BinaryOp::Less => left < right
    BinaryOp::LessEqual => left <= right
    BinaryOp::Greater => left > right
    BinaryOp::GreaterEqual => left >= right
    _ => false
  }
}

///|
fn compare_string(operator : BinaryOp, left : String, right : String) -> Bool {
  match operator {
    BinaryOp::Less => left < right
    BinaryOp::LessEqual => left <= right
    BinaryOp::Greater => left > right
    BinaryOp::GreaterEqual => left >= right
    _ => false
  }
}

///|
fn eval_relation(
  operator : BinaryOp,
  left : Value,
  right : Value,
  span : SourceSpan,
) -> Value raise EvalError {
  let result = match (left, right) {
    (Value::Int(a), Value::Int(b)) => compare_int(operator, a, b)
    (Value::UInt(a), Value::UInt(b)) => compare_uint(operator, a, b)
    (Value::Double(a), Value::Double(b)) => compare_double(operator, a, b)
    (Value::String(a), Value::String(b)) => compare_string(operator, a, b)
    (a, _) => {
      ignore(type_mismatch("matching comparable values", a, span))
      false
    }
  }
  Value::Bool(result)
}

///|
fn eval_add(
  left : Value,
  right : Value,
  span : SourceSpan,
) -> Value raise EvalError {
  match (left, right) {
    (Value::Int(a), Value::Int(b)) => safe_add_int(a, b, span)
    (Value::UInt(a), Value::UInt(b)) => safe_add_uint(a, b, span)
    (Value::Double(a), Value::Double(b)) => Value::Double(a + b)
    (Value::String(a), Value::String(b)) => Value::String(a + b)
    (Value::Bytes(a), Value::Bytes(b)) => Value::Bytes(concat_bytes(a, b))
    (Value::List(a), Value::List(b)) => Value::List(concat_lists(a, b))
    (a, _) => type_mismatch("matching addable values", a, span)
  }
}

///|
fn eval_subtract(
  left : Value,
  right : Value,
  span : SourceSpan,
) -> Value raise EvalError {
  match (left, right) {
    (Value::Int(a), Value::Int(b)) => safe_subtract_int(a, b, span)
    (Value::UInt(a), Value::UInt(b)) => safe_subtract_uint(a, b, span)
    (Value::Double(a), Value::Double(b)) => Value::Double(a - b)
    (a, _) => type_mismatch("matching numeric values", a, span)
  }
}

///|
fn eval_multiply(
  left : Value,
  right : Value,
  span : SourceSpan,
) -> Value raise EvalError {
  match (left, right) {
    (Value::Int(a), Value::Int(b)) => safe_multiply_int(a, b, span)
    (Value::UInt(a), Value::UInt(b)) => safe_multiply_uint(a, b, span)
    (Value::Double(a), Value::Double(b)) => Value::Double(a * b)
    (a, _) => type_mismatch("matching numeric values", a, span)
  }
}

///|
fn eval_divide(
  left : Value,
  right : Value,
  span : SourceSpan,
) -> Value raise EvalError {
  match (left, right) {
    (_, Value::UInt(0UL)) | (_, Value::Double(0.0)) =>
      raise DivisionByZero(span~)
    (Value::Int(a), Value::Int(b)) => safe_divide_int(a, b, span)
    (Value::UInt(a), Value::UInt(b)) => Value::UInt(a / b)
    (Value::Double(a), Value::Double(b)) => Value::Double(a / b)
    (a, _) => type_mismatch("matching numeric values", a, span)
  }
}

///|
fn eval_modulo(
  left : Value,
  right : Value,
  span : SourceSpan,
) -> Value raise EvalError {
  match (left, right) {
    (_, Value::UInt(0UL)) => raise DivisionByZero(span~)
    (Value::Int(a), Value::Int(b)) => safe_modulo_int(a, b, span)
    (Value::UInt(a), Value::UInt(b)) => Value::UInt(a % b)
    (a, _) => type_mismatch("matching integral values", a, span)
  }
}

///|
fn Evaluator::eval_binary(
  self : Evaluator,
  operator : BinaryOp,
  left_expr : Expr,
  right_expr : Expr,
  span : SourceSpan,
) -> Value raise EvalError {
  if operator == BinaryOp::And || operator == BinaryOp::Or {
    return self.eval_logical(operator, left_expr, right_expr)
  }
  let left = self.eval_expr(left_expr)
  let right = self.eval_expr(right_expr)
  match operator {
    BinaryOp::Equal => Value::Bool(left == right)
    BinaryOp::NotEqual => Value::Bool(left != right)
    BinaryOp::Less
    | BinaryOp::LessEqual
    | BinaryOp::Greater
    | BinaryOp::GreaterEqual => eval_relation(operator, left, right, span)
    BinaryOp::In => Value::Bool(right.contains(left))
    BinaryOp::Add => eval_add(left, right, span)
    BinaryOp::Subtract => eval_subtract(left, right, span)
    BinaryOp::Multiply => eval_multiply(left, right, span)
    BinaryOp::Divide => eval_divide(left, right, span)
    BinaryOp::Modulo => eval_modulo(left, right, span)
    BinaryOp::And | BinaryOp::Or => abort("logical operators returned early")
  }
}

///|
fn Evaluator::eval_conditional(
  self : Evaluator,
  condition : Expr,
  when_true : Expr,
  when_false : Expr,
) -> Value raise EvalError {
  if require_bool(self.eval_expr(condition), condition.span) {
    self.eval_expr(when_true)
  } else {
    self.eval_expr(when_false)
  }
}

///|
fn Evaluator::eval_expr_unmetered(
  self : Evaluator,
  expr : Expr,
) -> Value raise EvalError {
  match expr.kind {
    ExprKind::Literal(literal) => literal_value(literal)
    ExprKind::Identifier(name) => self.eval_identifier(name, expr.span)
    ExprKind::Unary(operator, operand) =>
      self.eval_unary(operator, operand, expr.span)
    ExprKind::Binary(operator, left, right) =>
      self.eval_binary(operator, left, right, expr.span)
    ExprKind::Conditional(condition, when_true, when_false) =>
      self.eval_conditional(condition, when_true, when_false)
    ExprKind::List(items) => self.eval_list(items)
    ExprKind::Map(entries) => self.eval_map(entries)
    ExprKind::Select(target, field) =>
      self.eval_select(target, field, expr.span)
    ExprKind::Index(target, index) => self.eval_index(target, index, expr.span)
    ExprKind::Call("has", arguments) => self.eval_has(arguments, expr.span)
    ExprKind::Call(name, arguments) =>
      self.eval_call(name, arguments, expr.span)
    ExprKind::ReceiverCall(target, name, arguments) =>
      if is_comprehension_macro(name) {
        self.eval_comprehension(target, name, arguments, expr.span)
      } else {
        self.eval_receiver(target, name, arguments, expr.span)
      }
  }
}

///|
fn Evaluator::eval_expr(self : Evaluator, expr : Expr) -> Value raise EvalError {
  self.cost = self.cost + 1
  if self.cost > self.limits.max_cost {
    raise CostLimitExceeded(limit=self.limits.max_cost, span=expr.span)
  }
  self.depth = self.depth + 1
  if self.depth > self.limits.max_depth {
    raise DepthLimitExceeded(limit=self.limits.max_depth, span=expr.span)
  }
  let result = self.eval_expr_unmetered(expr)
  self.depth = self.depth - 1
  result
}

///|
fn Evaluator::charge_iteration(
  self : Evaluator,
  span : SourceSpan,
) -> Unit raise EvalError {
  self.iterations = self.iterations + 1
  if self.iterations > self.limits.max_iterations {
    raise IterationLimitExceeded(limit=self.limits.max_iterations, span~)
  }
}

///|
/// Evaluate a compiled expression against an immutable activation.
pub fn Program::eval(
  self : Program,
  activation : Activation,
  limits? : EvalLimits = EvalLimits::default(),
) -> Value raise EvalError {
  if !limits.is_valid() {
    raise InvalidArgument(
      message="evaluation limits must all be positive",
      span=self.checked.parsed.root.span,
    )
  }
  Evaluator::new(activation, limits).eval_expr(self.checked.parsed.root)
}