///|
fn render_ast(
  nodes : Array[Node],
  ctx : Value,
  source : String,
  filters : Array[FilterEntry],
  loader : Loader?,
  includes : Array[CompiledInclude],
  autoescape : Bool,
) -> String raise MoldError {
  render_ast_scoped(
    nodes,
    ctx,
    source,
    Map::from_array([]),
    filters,
    loader,
    includes,
    0,
    autoescape,
  )
}

///|
fn render_ast_scoped(
  nodes : Array[Node],
  ctx : Value,
  source : String,
  scope : Map[String, Value],
  filters : Array[FilterEntry],
  loader : Loader?,
  includes : Array[CompiledInclude],
  depth : Int,
  autoescape : Bool,
) -> String raise MoldError {
  let builder = StringBuilder::new(size_hint=source.length())
  for node in nodes {
    match node {
      Text(text) => builder.write_string(text)
      Interpolation(expr) => {
        let value = eval_expr(expr, ctx, scope, filters)
        if autoescape {
          match value {
            Safe(inner) => builder.write_string(value_to_string(inner, ""))
            _ => {
              let escaped = filter_escape(value, [])
              builder.write_string(value_to_string(escaped, ""))
            }
          }
        } else {
          builder.write_string(value_to_string(value, ""))
        }
      }
      If(cond_expr, then_nodes, else_nodes) => {
        let cond = eval_expr(cond_expr, ctx, scope, filters)
        if is_truthy(cond) {
          builder.write_string(
            render_ast_scoped(
              then_nodes, ctx, source, scope, filters, loader, includes, depth, autoescape,
            ),
          )
        } else {
          builder.write_string(
            render_ast_scoped(
              else_nodes, ctx, source, scope, filters, loader, includes, depth, autoescape,
            ),
          )
        }
      }
      For(item_name, iterable_expr, body) => {
        let iterable = eval_expr(iterable_expr, ctx, scope, filters)
        match iterable {
          Array(items) => {
            let len = items.length()
            let mut i = 0
            for item in items {
              let loop_obj = Object(
                Map::from_array([
                  ("index", Int(i + 1)),
                  ("index0", Int(i)),
                  ("first", Bool(i == 0)),
                  ("last", Bool(i == len - 1)),
                  ("length", Int(len)),
                ]),
              )
              let item_scope : Map[String, Value] = Map::from_array([
                (item_name, item),
                ("loop", loop_obj),
              ])
              builder.write_string(
                render_ast_scoped(
                  body, ctx, source, item_scope, filters, loader, includes, depth,
                  autoescape,
                ),
              )
              i = i + 1
            }
          }
          _ => raise TypeMismatch(("cannot iterate", "array"))
        }
      }
      Include(name) => {
        if depth >= 10 {
          raise IncludeDepthExceeded
        }
        match find_compiled_include(includes, name) {
          Some(entry) =>
            builder.write_string(
              render_ast_scoped(
                entry.ast,
                ctx,
                entry.source,
                scope,
                filters,
                loader,
                includes,
                depth + 1,
                autoescape,
              ),
            )
          None =>
            match loader {
              None => raise MissingInclude(name)
              Some(load) =>
                match load(name) {
                  None => raise MissingInclude(name)
                  Some(included_source) => {
                    let included_tokens = lex(included_source)
                    let included_nodes = parse(included_tokens)
                    builder.write_string(
                      render_ast_scoped(
                        included_nodes,
                        ctx,
                        included_source,
                        scope,
                        filters,
                        loader,
                        includes,
                        depth + 1,
                        autoescape,
                      ),
                    )
                  }
                }
            }
        }
      }
    }
  } nobreak {
    builder.to_string()
  }
}

///|
fn eval_expr(
  expr : Expr,
  ctx : Value,
  scope : Map[String, Value],
  filters : Array[FilterEntry],
) -> Value raise MoldError {
  match expr {
    Path(segments) => {
      let path_str = segments.join(".")
      resolve_with_scope(ctx, segments, path_str, scope)
    }
    StringLiteral(text) => String(text)
    IntLiteral(value) => Int(value)
    FloatLiteral(value) => Float(value)
    BoolLiteral(value) => Bool(value)
    NullLiteral => Null
    Unary(UnaryOp::Not, inner) =>
      Bool(!is_truthy(eval_expr(inner, ctx, scope, filters)))
    FilterCall(base, name, args) => {
      let value = eval_expr(base, ctx, scope, filters)
      let arg_values : Array[Value] = []
      for arg in args {
        arg_values.push(eval_expr(arg, ctx, scope, filters))
      } nobreak {
        apply_filter(filters, value, name, arg_values)
      }
    }
    Binary(left, BinaryOp::And, right) => {
      let left_value = eval_expr(left, ctx, scope, filters)
      if !is_truthy(left_value) {
        Bool(false)
      } else {
        Bool(is_truthy(eval_expr(right, ctx, scope, filters)))
      }
    }
    Binary(left, BinaryOp::Or, right) => {
      let left_value = eval_expr(left, ctx, scope, filters)
      if is_truthy(left_value) {
        Bool(true)
      } else {
        Bool(is_truthy(eval_expr(right, ctx, scope, filters)))
      }
    }
    Binary(left, op, right) => {
      let left_value = eval_expr(left, ctx, scope, filters)
      let right_value = eval_expr(right, ctx, scope, filters)
      Bool(eval_binary_op(left_value, op, right_value))
    }
  }
}

///|
fn eval_binary_op(
  left : Value,
  op : BinaryOp,
  right : Value,
) -> Bool raise MoldError {
  match op {
    Eq => values_equal(left, right)
    Ne => !values_equal(left, right)
    Lt => compare_values(left, right) < 0
    Le => compare_values(left, right) <= 0
    Gt => compare_values(left, right) > 0
    Ge => compare_values(left, right) >= 0
    And => false
    Or => false
  }
}

///|
fn values_equal(left : Value, right : Value) -> Bool {
  let l = match left {
    Safe(v) => v
    _ => left
  }
  let r = match right {
    Safe(v) => v
    _ => right
  }
  match (l, r) {
    (Null, Null) => true
    (Bool(a), Bool(b)) => a == b
    (Int(a), Int(b)) => a == b
    (Float(a), Float(b)) => a == b
    (Int(a), Float(b)) => a.to_double() == b
    (Float(a), Int(b)) => a == b.to_double()
    (String(a), String(b)) => a == b
    _ => false
  }
}

///|
fn compare_values(left : Value, right : Value) -> Int raise MoldError {
  let l = match left {
    Safe(v) => v
    _ => left
  }
  let r = match right {
    Safe(v) => v
    _ => right
  }
  match (l, r) {
    (Int(a), Int(b)) => compare_ints(a, b)
    (Float(a), Float(b)) => compare_doubles(a, b)
    (Int(a), Float(b)) => compare_doubles(a.to_double(), b)
    (Float(a), Int(b)) => compare_doubles(a, b.to_double())
    (String(a), String(b)) => compare_strings(a, b)
    _ => raise TypeMismatch(("cannot compare values", "comparable"))
  }
}

///|
fn compare_ints(left : Int, right : Int) -> Int {
  if left < right {
    -1
  } else if left > right {
    1
  } else {
    0
  }
}

///|
fn compare_doubles(left : Double, right : Double) -> Int {
  if left < right {
    -1
  } else if left > right {
    1
  } else {
    0
  }
}

///|
fn compare_strings(left : String, right : String) -> Int {
  if left < right {
    -1
  } else if left > right {
    1
  } else {
    0
  }
}

///|
fn is_truthy(value : Value) -> Bool {
  match value {
    Null => false
    Bool(b) => b
    String(s) => s.length() > 0
    Safe(inner) => is_truthy(inner)
    _ => true
  }
}