///|
fn is_comprehension_macro(name : String) -> Bool {
  name == "all" ||
  name == "exists" ||
  name == "exists_one" ||
  name == "map" ||
  name == "filter"
}

///|
fn macro_element_type(collection : CelType) -> CelType? {
  match collection {
    CelType::List(element) => Some(element)
    CelType::Map(key, _) => Some(key)
    CelType::Dyn => Some(CelType::Dyn)
    _ => None
  }
}

///|
fn macro_variable(argument : Expr) -> String? {
  match argument.kind {
    ExprKind::Identifier(name) => Some(name)
    _ => None
  }
}

///|
fn Checker::infer_has_macro(
  self : Checker,
  expr : Expr,
  arguments : Array[Expr],
) -> CelType {
  if arguments.length() != 1 {
    self.error(
      "CEL-CHECK-MACRO-ARITY",
      "has requires exactly one field selection or index",
      expr.span,
    )
    for argument in arguments {
      ignore(self.infer(argument))
    }
    return self.record(expr, CelType::Bool)
  }
  let argument = arguments[0]
  match argument.kind {
    ExprKind::Select(_, _) | ExprKind::Index(_, _) =>
      ignore(self.infer(argument))
    _ => {
      ignore(self.infer(argument))
      self.error(
        "CEL-CHECK-HAS",
        "has argument must be a field selection or index",
        argument.span,
      )
    }
  }
  self.record(expr, CelType::Bool)
}

///|
fn Checker::with_macro_variable(
  self : Checker,
  name : String,
  cel_type : CelType,
  body : Expr,
) -> CelType {
  let previous = self.env
  self.env = self.env.with_variable(name, cel_type)
  let result = self.infer(body)
  self.env = previous
  result
}

///|
fn Checker::macro_predicate(
  self : Checker,
  variable : String,
  element : CelType,
  predicate : Expr,
) -> Unit {
  let predicate_type = self.with_macro_variable(variable, element, predicate)
  ignore(
    self.require(
      predicate_type,
      CelType::Bool,
      predicate.span,
      "macro predicate",
    ),
  )
}

///|
fn Checker::infer_map_macro(
  self : Checker,
  expr : Expr,
  element_type : CelType,
  variable : String,
  arguments : Array[Expr],
) -> CelType {
  let result = if arguments.length() == 2 {
    self.with_macro_variable(variable, element_type, arguments[1])
  } else if arguments.length() == 3 {
    self.macro_predicate(variable, element_type, arguments[1])
    self.with_macro_variable(variable, element_type, arguments[2])
  } else {
    self.error(
      "CEL-CHECK-MACRO-ARITY",
      "map requires (variable, transform) or (variable, predicate, transform)",
      expr.span,
    )
    CelType::Dyn
  }
  self.record(expr, CelType::List(result))
}

///|
fn Checker::infer_comprehension_macro(
  self : Checker,
  expr : Expr,
  receiver : Expr,
  name : String,
  arguments : Array[Expr],
) -> CelType {
  let collection_type = self.infer(receiver)
  let element_type = match macro_element_type(collection_type) {
    Some(value) => value
    None => {
      self.error(
        "CEL-CHECK-MACRO-TARGET",
        "\{name} requires a list or map receiver, found \{collection_type.display()}",
        receiver.span,
      )
      CelType::Dyn
    }
  }
  if arguments.is_empty() {
    self.error(
      "CEL-CHECK-MACRO-ARITY",
      "\{name} requires an iteration variable and body",
      expr.span,
    )
    return self.record(expr, CelType::Dyn)
  }
  let variable = match macro_variable(arguments[0]) {
    Some(value) => value
    None => {
      self.error(
        "CEL-CHECK-MACRO-VARIABLE",
        "first \{name} argument must be an identifier",
        arguments[0].span,
      )
      "__invalid_macro_variable"
    }
  }
  if name == "map" {
    return self.infer_map_macro(expr, element_type, variable, arguments)
  }
  if arguments.length() != 2 {
    self.error(
      "CEL-CHECK-MACRO-ARITY",
      "\{name} requires exactly an iteration variable and predicate",
      expr.span,
    )
    return self.record(expr, CelType::Dyn)
  }
  self.macro_predicate(variable, element_type, arguments[1])
  let result = if name == "filter" { collection_type } else { CelType::Bool }
  self.record(expr, result)
}

///|
fn macro_items(
  value : Value,
  span : SourceSpan,
) -> Array[Value] raise EvalError {
  match value {
    Value::List(items) => items
    Value::Map(entries) => entries.map(entry => entry.key)
    other => {
      ignore(type_mismatch("list or map", other, span))
      []
    }
  }
}

///|
fn macro_binding(
  arguments : Array[Expr],
  span : SourceSpan,
) -> String raise EvalError {
  if arguments.is_empty() {
    raise InvalidArgument(message="macro requires an iteration variable", span~)
  }
  match arguments[0].kind {
    ExprKind::Identifier(name) => name
    _ =>
      raise InvalidArgument(
        message="macro variable must be an identifier",
        span~,
      )
  }
}

///|
fn Evaluator::eval_scoped(
  self : Evaluator,
  variable : String,
  value : Value,
  body : Expr,
) -> Value raise EvalError {
  let previous = self.activation
  self.activation = self.activation.bind(variable, value)
  let result = self.eval_expr(body)
  self.activation = previous
  result
}

///|
fn Evaluator::eval_has(
  self : Evaluator,
  arguments : Array[Expr],
  span : SourceSpan,
) -> Value raise EvalError {
  if arguments.length() != 1 {
    raise InvalidArgument(message="has requires one argument", span~)
  }
  let present = match arguments[0].kind {
    ExprKind::Select(target, field) =>
      match self.eval_expr(target) {
        Value::Map(entries) =>
          entries.any(entry => entry.key == Value::String(field))
        _ => false
      }
    ExprKind::Index(target, index) => {
      let key = self.eval_expr(index)
      match self.eval_expr(target) {
        Value::List(items) =>
          match key {
            Value::Int(position) => {
              let position = position.to_int()
              position >= 0 && position < items.length()
            }
            _ => false
          }
        Value::Map(entries) => entries.any(entry => entry.key == key)
        _ => false
      }
    }
    _ => false
  }
  Value::Bool(present)
}

///|
fn Evaluator::eval_quantifier(
  self : Evaluator,
  name : String,
  variable : String,
  items : Array[Value],
  predicate : Expr,
) -> Value raise EvalError {
  let mut matches = 0
  for item in items {
    self.charge_iteration(predicate.span)
    let accepted = require_bool(
      self.eval_scoped(variable, item, predicate),
      predicate.span,
    )
    if name == "all" && !accepted {
      return Value::Bool(false)
    }
    if name == "exists" && accepted {
      return Value::Bool(true)
    }
    if accepted {
      matches = matches + 1
      if name == "exists_one" && matches > 1 {
        return Value::Bool(false)
      }
    }
  }
  match name {
    "all" => Value::Bool(true)
    "exists" => Value::Bool(false)
    "exists_one" => Value::Bool(matches == 1)
    _ => raise UnknownFunction(name~, span=predicate.span)
  }
}

///|
fn Evaluator::eval_filter(
  self : Evaluator,
  variable : String,
  items : Array[Value],
  predicate : Expr,
) -> Value raise EvalError {
  let selected : Array[Value] = []
  for item in items {
    self.charge_iteration(predicate.span)
    if require_bool(self.eval_scoped(variable, item, predicate), predicate.span) {
      selected.push(item)
    }
  }
  Value::List(selected)
}

///|
fn Evaluator::eval_map_macro(
  self : Evaluator,
  variable : String,
  items : Array[Value],
  arguments : Array[Expr],
  span : SourceSpan,
) -> Value raise EvalError {
  if arguments.length() != 2 && arguments.length() != 3 {
    raise InvalidArgument(
      message="map macro expects two or three arguments",
      span~,
    )
  }
  let output : Array[Value] = []
  for item in items {
    self.charge_iteration(span)
    if arguments.length() == 3 &&
      !require_bool(
        self.eval_scoped(variable, item, arguments[1]),
        arguments[1].span,
      ) {
      continue
    }
    let transform = arguments[arguments.length() - 1]
    output.push(self.eval_scoped(variable, item, transform))
  }
  Value::List(output)
}

///|
fn Evaluator::eval_comprehension(
  self : Evaluator,
  target : Expr,
  name : String,
  arguments : Array[Expr],
  span : SourceSpan,
) -> Value raise EvalError {
  let variable = macro_binding(arguments, span)
  let items = macro_items(self.eval_expr(target), target.span)
  if name == "map" {
    return self.eval_map_macro(variable, items, arguments, span)
  }
  if arguments.length() != 2 {
    raise InvalidArgument(message="\{name} expects two arguments", span~)
  }
  if name == "filter" {
    self.eval_filter(variable, items, arguments[1])
  } else {
    self.eval_quantifier(name, variable, items, arguments[1])
  }
}