// The template VM.
//
// Rendering uses a direct AST-walk interpreter (`eval_nodes`).

///|
/// Evaluate an expression against a context to a `Value`.
fn eval_expr(e : Expr, ctx : Value) -> Value {
  match e {
    EInt(i) => Int(i)
    EFloat(f) => Float(f)
    EStr(s) => Str(s)
    EBool(b) => Bool(b)
    EVar(name) => lookup(name, ctx)
    EMember(obj, field) => lookup(field, eval_expr(obj, ctx))
    EIndex(container, index) =>
      index_value(eval_expr(container, ctx), eval_expr(index, ctx))
    ENot(inner) => Bool(!eval_expr(inner, ctx).is_truthy())
    EBinOp(op, l, r) => eval_binop(op, eval_expr(l, ctx), eval_expr(r, ctx))
    EFilter(input, name, arg_exprs) => {
      let v = eval_expr(input, ctx)
      let arg_vals : Array[Value] = []
      for ae in arg_exprs {
        arg_vals.push(eval_expr(ae, ctx))
      }
      apply_filter(name, v, arg_vals)
    }
    EIs(e, name) => Bool(apply_test(name, eval_expr(e, ctx)))
    ESuper => Str("") // placeholder; handled specially in eval_nodes
  }
}

///|
/// Resolve an array integer index or an object string key.
/// Invalid types and out-of-range indexes evaluate to `Null`.
fn index_value(container : Value, index : Value) -> Value {
  match (container, index) {
    (Array(items), Int(position)) =>
      if position >= 0 && position < items.length() {
        items[position]
      } else {
        Null
      }
    (Object(_), Str(key)) => lookup(key, container)
    _ => Null
  }
}

///|
/// Structural equality of two values (same-type only).
fn value_eq(a : Value, b : Value) -> Bool {
  match (a, b) {
    (Null, Null) => true
    (Bool(x), Bool(y)) => x == y
    (Int(x), Int(y)) => x == y
    (Float(x), Float(y)) => x == y
    (Str(x), Str(y)) => x == y
    _ => false
  }
}

///|
/// Less-than ordering for numbers and strings.
fn value_lt(a : Value, b : Value) -> Bool {
  match (a, b) {
    (Int(x), Int(y)) => x < y
    (Float(x), Float(y)) => x < y
    (Str(x), Str(y)) => x < y
    _ => false
  }
}

///|
/// Evaluate a binary operator.
fn eval_binop(op : String, l : Value, r : Value) -> Value {
  match op {
    "==" => Bool(value_eq(l, r))
    "!=" => Bool(!value_eq(l, r))
    "<" => Bool(value_lt(l, r))
    ">" => Bool(value_lt(r, l))
    "<=" => Bool(!value_lt(r, l))
    ">=" => Bool(!value_lt(l, r))
    "and" => Bool(l.is_truthy() && r.is_truthy())
    "or" => Bool(l.is_truthy() || r.is_truthy())
    "+" => value_add(l, r)
    "-" => value_sub(l, r)
    "*" => value_mul(l, r)
    "/" => value_div(l, r)
    "%" => value_mod(l, r)
    _ => Null
  }
}

///|
fn value_add(l : Value, r : Value) -> Value {
  match (l, r) {
    (Int(a), Int(b)) => Int(a + b)
    (Float(a), Float(b)) => Float(a + b)
    (Str(a), Str(b)) => Str(a + b)
    _ => Null
  }
}

///|
fn value_sub(l : Value, r : Value) -> Value {
  match (l, r) {
    (Int(a), Int(b)) => Int(a - b)
    (Float(a), Float(b)) => Float(a - b)
    _ => Null
  }
}

///|
fn value_mul(l : Value, r : Value) -> Value {
  match (l, r) {
    (Int(a), Int(b)) => Int(a * b)
    (Float(a), Float(b)) => Float(a * b)
    _ => Null
  }
}

///|
fn value_div(l : Value, r : Value) -> Value {
  match (l, r) {
    (Int(a), Int(b)) => if b == 0 { Null } else { Int(a / b) }
    (Float(a), Float(b)) => Float(a / b)
    _ => Null
  }
}

///|
fn value_mod(l : Value, r : Value) -> Value {
  match (l, r) {
    (Int(a), Int(b)) => if b == 0 { Null } else { Int(a % b) }
    _ => Null
  }
}

///|
/// Render a `Value` to its template string form.
fn render_value(v : Value) -> String {
  match v {
    Null => ""
    Bool(b) => b.to_string()
    Int(i) => i.to_string()
    Float(f) => f.to_string()
    Str(s) => s
    Array(_) => v.to_json_string()
    Object(_) => v.to_json_string()
  }
}

///|
/// Evaluate an expression's truthiness (for `{% if %}`).
fn eval_truthy(e : Expr, ctx : Value) -> Bool {
  eval_expr(e, ctx).is_truthy()
}

///|
/// Bind a loop variable on top of a context (loop var takes precedence).
fn bind_var(name : String, val : Value, ctx : Value) -> Value {
  let entries : Array[(String, Value)] = []
  entries.push((name, val))
  match ctx {
    Object(old) =>
      for e in old {
        entries.push(e)
      }
    _ => ()
  }
  Object(entries)
}

///|
/// Find a block's effective body by name (first match wins; children first).
fn lookup_block(
  blocks : Array[(String, Array[Node])],
  name : String,
  default : Array[Node],
) -> Array[Node] {
  for entry in blocks {
    let (n, body) = entry
    if n == name {
      return body
    }
  }
  default
}

///|
/// Find the parent (ancestor) block body: the second match for `name`.
fn lookup_parent_block(
  blocks : Array[(String, Array[Node])],
  name : String,
  default : Array[Node],
) -> Array[Node] {
  let mut found_first = false
  for entry in blocks {
    let (n, body) = entry
    if n == name {
      if found_first {
        return body
      }
      found_first = true
    }
  }
  default
}

///|
/// Evaluate a list of nodes into the output buffer.
/// `blocks` holds overriding block bodies; `tera` resolves includes;
/// `super_body` is the parent block body for `super()`.
fn eval_nodes(
  nodes : Array[Node],
  ctx : Value,
  blocks : Array[(String, Array[Node])],
  tera : Tera,
  super_body : Array[Node],
  out : StringBuilder,
) -> Unit {
  let mut current_ctx = ctx
  for node in nodes {
    match node {
      Text(s) => out.write_string(s)
      Output(e) =>
        match e {
          ESuper =>
            eval_nodes(super_body, current_ctx, blocks, tera, super_body, out)
          _ => out.write_string(render_value(eval_expr(e, current_ctx)))
        }
      If(cond, then_branch, else_branch) =>
        if eval_truthy(cond, current_ctx) {
          eval_nodes(then_branch, current_ctx, blocks, tera, super_body, out)
        } else {
          eval_nodes(else_branch, current_ctx, blocks, tera, super_body, out)
        }
      For(name, iter, body, empty_body) =>
        match eval_expr(iter, current_ctx) {
          Array(items) => {
            let cnt = items.length()
            if cnt == 0 {
              eval_nodes(empty_body, current_ctx, blocks, tera, super_body, out)
            } else {
              for idx = 0; idx < cnt; idx = idx + 1 {
                let loop_val = object_value([
                  ("index", int_value(idx + 1)),
                  ("index0", int_value(idx)),
                  ("first", bool_value(idx == 0)),
                  ("last", bool_value(idx == cnt - 1)),
                  ("length", int_value(cnt)),
                ])
                let ctx1 = bind_var(name, items[idx], current_ctx)
                let ctx2 = bind_var("loop", loop_val, ctx1)
                eval_nodes(body, ctx2, blocks, tera, super_body, out)
              }
            }
          }
          _ =>
            eval_nodes(empty_body, current_ctx, blocks, tera, super_body, out)
        }
      Block(name, body) => {
        let effective = lookup_block(blocks, name, body)
        let parent = lookup_parent_block(blocks, name, body)
        eval_nodes(effective, current_ctx, blocks, tera, parent, out)
      }
      Include(name) =>
        match find_tpl(tera.templates, name) {
          Some(tpl) =>
            eval_nodes(tpl.nodes, current_ctx, blocks, tera, super_body, out)
          None => ()
        }
      Set(name, e) =>
        current_ctx = bind_var(name, eval_expr(e, current_ctx), current_ctx)
    }
  }
}

///|
/// Render a parsed template (list of nodes) against a context (no inheritance).
pub fn render_nodes(nodes : Array[Node], ctx : Value) -> String {
  let out = StringBuilder::new()
  eval_nodes(nodes, ctx, [], Tera::new(), [], out)
  out.to_string()
}