///|
priv enum Scope {
  Empty
  Var(String, Json, Scope)
  Func(String, Array[String], Filter, Scope)
  FilterArg(String, Filter, Scope)
}

///|
let empty_scope : Scope = Scope::Empty

///|
fn Scope::bind_var(self : Scope, name : String, value : Json) -> Scope {
  Var(name, value, self)
}

///|
fn Scope::get_var(self : Scope, name : String) -> Json? {
  match self {
    Empty => None
    Var(n, v, parent) => if n == name { Some(v) } else { parent.get_var(name) }
    Func(_, _, _, parent) => parent.get_var(name)
    FilterArg(_, _, parent) => parent.get_var(name)
  }
}

///|
fn Scope::bind_func(
  self : Scope,
  name : String,
  params : Array[String],
  body : Filter,
) -> Scope {
  Func(name, params, body, self)
}

///|
fn Scope::get_func(self : Scope, name : String) -> (Array[String], Filter)? {
  match self {
    Empty => None
    Var(_, _, parent) => parent.get_func(name)
    Func(n, params, body, parent) =>
      if n == name {
        Some((params, body))
      } else {
        parent.get_func(name)
      }
    FilterArg(_, _, parent) => parent.get_func(name)
  }
}

///|
fn Scope::bind_filter_arg(
  self : Scope,
  name : String,
  filter : Filter,
) -> Scope {
  FilterArg(name, filter, self)
}

///|
fn Scope::get_filter_arg(self : Scope, name : String) -> Filter? {
  match self {
    Empty => None
    Var(_, _, parent) => parent.get_filter_arg(name)
    Func(_, _, _, parent) => parent.get_filter_arg(name)
    FilterArg(n, f, parent) =>
      if n == name {
        Some(f)
      } else {
        parent.get_filter_arg(name)
      }
  }
}

///|
/// Fast path for single-output filters. Returns the value directly
/// without callback overhead. Falls back to eval() for multi-output filters.
fn eval_single(
  filter : Filter,
  input : Json,
  env : Scope,
) -> Json raise JqError {
  match filter {
    Identity => input
    Field(name) =>
      match input {
        Object(map) =>
          match map.get(name) {
            Some(v) => v
            None => Json::null()
          }
        Null => Json::null()
        _ =>
          raise JqError(
            "Cannot index " + json_type(input) + " with string \"" + name + "\"",
          )
      }
    Index(i) =>
      match input {
        Array(arr) => {
          let idx = if i < 0 { arr.length() + i } else { i }
          if idx >= 0 && idx < arr.length() {
            arr[idx]
          } else {
            Json::null()
          }
        }
        _ =>
          raise JqError(
            "Cannot index " +
            json_type(input) +
            " with number (" +
            i.to_string() +
            ")",
          )
      }
    Literal(v) => v
    Pipe(left, right) => eval_single(right, eval_single(left, input, env), env)
    Arith(op, left, right) =>
      arith_op(
        op,
        eval_single(left, input, env),
        eval_single(right, input, env),
      )
    Compare(op, left, right) =>
      Json::boolean(
        compare_op(
          op,
          eval_single(left, input, env),
          eval_single(right, input, env),
        ),
      )
    Neg(expr) => {
      let v = eval_single(expr, input, env)
      match v {
        Number(n, ..) => Json::number(-n)
        _ =>
          raise JqError(
            json_type(v) + " (" + neg_error_desc(v) + ") cannot be negated",
          )
      }
    }
    FuncCall(name, _args) =>
      if name.has_prefix("$") {
        if name == "$__loc__" {
          let m : Map[String, Json] = Map::new()
          m["file"] = Json::string("")
          m["line"] = Json::number(1.0)
          Json::object(m)
        } else {
          match env.get_var(name) {
            Some(v) => v
            None => raise JqError("undefined variable: " + name)
          }
        }
      } else {
        // Fall back to callback eval for builtins
        let mut result = Json::null()
        eval(filter, input, env, v => result = v)
        result
      }
    _ => {
      // Fall back to callback eval
      let mut result = Json::null()
      eval(filter, input, env, v => result = v)
      result
    }
  }
}

///|
/// Core eval: evaluate a filter against input, yielding results via callback.
fn eval(
  filter : Filter,
  input : Json,
  env : Scope,
  yield_ : (Json) -> Unit raise JqError,
) -> Unit raise JqError {
  match filter {
    Identity => yield_(input)
    RecurseOp => recurse_impl(input, yield_)
    Field(name) =>
      match input {
        Object(map) =>
          match map.get(name) {
            Some(v) => yield_(v)
            None => yield_(Json::null())
          }
        Null => yield_(Json::null())
        _ =>
          raise JqError(
            "Cannot index " + json_type(input) + " with string \"" + name + "\"",
          )
      }
    Index(i) =>
      match input {
        Array(arr) => {
          let idx = if i < 0 { arr.length() + i } else { i }
          if idx >= 0 && idx < arr.length() {
            yield_(arr[idx])
          } else {
            yield_(Json::null())
          }
        }
        _ =>
          raise JqError(
            "Cannot index " +
            json_type(input) +
            " with number (" +
            i.to_string() +
            ")",
          )
      }
    Slice(start, end) =>
      match input {
        Array(arr) => {
          let len = arr.length()
          let s = match start {
            Some(v) => if v < 0 { len + v } else { v }
            None => 0
          }
          let e = match end {
            Some(v) => if v < 0 { len + v } else { v }
            None => len
          }
          let result : Array[Json] = []
          for i = s; i < e && i < len; i = i + 1 {
            if i >= 0 {
              result.push(arr[i])
            }
          }
          yield_(Json::array(result))
        }
        String(str) => {
          let chars = Array::from_iter(str.iter())
          let len = chars.length()
          let s = match start {
            Some(v) => if v < 0 { len + v } else { v }
            None => 0
          }
          let e = match end {
            Some(v) => if v < 0 { len + v } else { v }
            None => len
          }
          let buf = StringBuilder::new()
          for i = s; i < e && i < len; i = i + 1 {
            if i >= 0 {
              buf.write_char(chars[i])
            }
          }
          yield_(Json::string(buf.to_string()))
        }
        Null => yield_(Json::null())
        _ => raise JqError("cannot slice " + json_type(input))
      }
    Iterate =>
      match input {
        Array(arr) =>
          for v in arr {
            yield_(v)
          }
        Object(map) => map.each((_k, v) => yield_(v))
        _ => raise JqError("cannot iterate over " + json_type(input))
      }
    Pipe(left, right) =>
      eval(left, input, env, mid => eval(right, mid, env, yield_))
    Comma(left, right) => {
      eval(left, input, env, yield_)
      eval(right, input, env, yield_)
    }
    Literal(v) => yield_(v)
    ArrayConstruct(None) => yield_(Json::array([]))
    ArrayConstruct(Some(expr)) => {
      let result : Array[Json] = []
      eval(expr, input, env, v => result.push(v))
      yield_(Json::array(result))
    }
    ObjectConstruct(fields) =>
      eval_object_construct(fields, 0, Map::new(), input, env, yield_)
    Paren(expr) => eval(expr, input, env, yield_)
    Try(expr) => {
      let results : Array[Json] = []
      eval(expr, input, env, v => results.push(v)) catch {
        JqError(_) => ()
      }
      for r in results {
        yield_(r)
      }
    }
    TryCatch(expr, handler) => {
      let results : Array[Json] = []
      let mut caught_msg : String? = None
      eval(expr, input, env, v => results.push(v)) catch {
        JqError(msg) => caught_msg = Some(msg)
      }
      for r in results {
        yield_(r)
      }
      match caught_msg {
        Some(msg) => eval(handler, jq_error_decode(msg), env, yield_)
        None => ()
      }
    }
    IfThenElse(cond, then_branch, else_branch) =>
      eval(cond, input, env, cv => {
        if is_truthy(cv) {
          eval(then_branch, input, env, yield_)
        } else {
          eval(else_branch, input, env, yield_)
        }
      })
    Binding(pat, expr, body) =>
      eval(expr, input, env, v => {
        let new_env = bind_pattern(env, pat, v)
        eval(body, input, new_env, yield_)
      })
    Reduce(iter_expr, pat, init, update) => {
      let mut acc = eval_single(init, input, env)
      eval(iter_expr, input, env, v => {
        let new_env = bind_pattern(env, pat, v)
        acc = eval_single(update, acc, new_env)
      })
      yield_(acc)
    }
    FuncDef(name, params, body, rest) => {
      let new_env = env.bind_func(name, params, body)
      eval(rest, input, new_env, yield_)
    }
    FuncCall(name, args) => eval_func_call(name, args, input, env, yield_)
    DynIndex(expr) =>
      eval(expr, input, env, idx => dyn_index_impl(input, idx, yield_))
    PostfixDynIndex(base, idx_expr) =>
      eval(base, input, env, base_val => {
        eval(idx_expr, input, env, idx => dyn_index_impl(base_val, idx, yield_))
      })
    DynSlice(base, start_expr, end_expr) =>
      eval(base, input, env, base_val => {
        let mut s : Int? = None
        let mut e : Int? = None
        match start_expr {
          Some(se) =>
            eval(se, input, env, sv => {
              match sv {
                Number(n, ..) =>
                  if not(is_nan(n)) {
                    s = Some(n.floor().to_int())
                  }
                _ => raise JqError("slice index must be a number")
              }
            })
          None => ()
        }
        match end_expr {
          Some(ee) =>
            eval(ee, input, env, ev => {
              match ev {
                Number(n, ..) =>
                  if not(is_nan(n)) {
                    let floored = n.floor()
                    // jq uses ceil for end index on non-integer floats
                    let idx = if floored < n {
                      floored.to_int() + 1
                    } else {
                      floored.to_int()
                    }
                    e = Some(idx)
                  }
                _ => raise JqError("slice index must be a number")
              }
            })
          None => ()
        }
        match base_val {
          Array(arr) => {
            let len = arr.length()
            let start = match s {
              Some(v) => if v < 0 { len + v } else { v }
              None => 0
            }
            let end = match e {
              Some(v) => if v < 0 { len + v } else { v }
              None => len
            }
            let result : Array[Json] = []
            for i = start; i < end && i < len; i = i + 1 {
              if i >= 0 {
                result.push(arr[i])
              }
            }
            yield_(Json::array(result))
          }
          String(str) => {
            let chars = Array::from_iter(str.iter())
            let len = chars.length()
            let start = match s {
              Some(v) => if v < 0 { len + v } else { v }
              None => 0
            }
            let end = match e {
              Some(v) => if v < 0 { len + v } else { v }
              None => len
            }
            let buf = StringBuilder::new()
            for i = start; i < end && i < len; i = i + 1 {
              if i >= 0 {
                buf.write_char(chars[i])
              }
            }
            yield_(Json::string(buf.to_string()))
          }
          Null => yield_(Json::null())
          _ => raise JqError("cannot slice " + json_type(base_val))
        }
      })
    Neg(expr) =>
      eval(expr, input, env, v => {
        match v {
          Number(n, ..) => yield_(Json::number(-n))
          _ =>
            raise JqError(
              json_type(v) + " (" + neg_error_desc(v) + ") cannot be negated",
            )
        }
      })
    Arith(op, left, right) =>
      eval(right, input, env, rv => {
        eval(left, input, env, lv => yield_(arith_op(op, lv, rv)))
      })
    Compare(op, left, right) =>
      eval(right, input, env, rv => {
        eval(left, input, env, lv => {
          yield_(Json::boolean(compare_op(op, lv, rv)))
        })
      })
    LogicAnd(left, right) =>
      eval(left, input, env, lv => {
        if is_truthy(lv) {
          eval(right, input, env, rv => yield_(Json::boolean(is_truthy(rv))))
        } else {
          yield_(Json::boolean(false))
        }
      })
    LogicOr(left, right) =>
      eval(left, input, env, lv => {
        if is_truthy(lv) {
          yield_(Json::boolean(true))
        } else {
          eval(right, input, env, rv => yield_(Json::boolean(is_truthy(rv))))
        }
      })
    Alternative(left, right) => {
      let mut has_result = false
      eval(left, input, env, v => {
        if is_truthy(v) {
          has_result = true
          yield_(v)
        }
      })
      if not(has_result) {
        eval(right, input, env, yield_)
      }
    }
    Foreach(iter_expr, pat, init, update, extract) =>
      eval(init, input, env, init_val => {
        let mut acc = init_val
        eval(iter_expr, input, env, v => {
          let new_env = bind_pattern(env, pat, v)
          acc = eval_single(update, acc, new_env)
          match extract {
            Some(ext) => {
              let new_env2 = bind_pattern(env, pat, v)
              eval(ext, acc, new_env2, yield_)
            }
            None => yield_(acc)
          }
        })
      })
    StringInterp(parts) => {
      let buf = StringBuilder::new()
      for part in parts {
        let (lit, filter_opt) = part
        if not(lit.is_empty()) {
          buf.write_string(lit)
        }
        match filter_opt {
          Some(f) => {
            let v = eval_single(f, input, env)
            buf.write_string(json_to_interp_string(v))
          }
          None => ()
        }
      }
      yield_(Json::string(buf.to_string()))
    }
    Assign(path_expr, value_expr) =>
      eval(value_expr, input, env, value => {
        let mut result = input
        eval_path(path_expr, input, env, path => {
          result = setpath_impl(result, path, value)
        })
        yield_(result)
      })
    Update(path_expr, update_filter) => {
      let mut result = input
      let paths : Array[Array[Json]] = []
      eval_path(path_expr, input, env, p => paths.push(p))
      let del_paths : Array[Array[Json]] = []
      for p in paths {
        let current = getpath_impl(result, p)
        let vals : Array[Json] = []
        eval(update_filter, current, env, v => vals.push(v))
        if vals.is_empty() {
          del_paths.push(p)
        } else {
          result = setpath_impl(result, p, vals[0])
        }
      }
      if not(del_paths.is_empty()) {
        result = delpaths_impl(result, del_paths)
      }
      yield_(result)
    }
    Label(name, body) => {
      let tag = "__label_" + name
      eval(body, input, env, yield_) catch {
        JqError(msg) => if msg == tag { () } else { raise JqError(msg) }
      }
    }
    Break(name) => raise JqError("__label_" + name)
    BindingAlt(patterns, expr, body) =>
      eval(expr, input, env, v => {
        // Collect all variable names from all patterns
        let all_vars : Array[String] = []
        for pat in patterns {
          collect_pattern_vars(pat, all_vars)
        }
        // Pre-bind all variables as null
        let mut base_env = env
        for var_name in all_vars {
          base_env = base_env.bind_var(var_name, Json::null())
        }
        let mut succeeded = false
        for i = 0; i < patterns.length(); i = i + 1 {
          if not(succeeded) {
            let results : Array[Json] = []
            let mut had_error = false
            (fn() -> Unit raise JqError {
              let new_env = bind_pattern_strict(base_env, patterns[i], v)
              eval(body, input, new_env, r => results.push(r))
            })() catch {
              JqError(_) => had_error = true
            }
            if not(had_error) {
              succeeded = true
              for r in results {
                yield_(r)
              }
            }
          }
        }
      })
    UpdateAlt(path_expr, value_expr) => {
      let mut result = input
      let paths : Array[Array[Json]] = []
      eval_path(path_expr, input, env, p => paths.push(p))
      for p in paths {
        let current = getpath_impl(result, p)
        if not(is_truthy(current)) {
          let new_val = eval_single(value_expr, input, env)
          result = setpath_impl(result, p, new_val)
        }
      }
      yield_(result)
    }
  }
}

///|
fn eval_object_construct(
  fields : Array[(ObjKey, Filter)],
  idx : Int,
  acc : Map[String, Json],
  input : Json,
  env : Scope,
  yield_ : (Json) -> Unit raise JqError,
) -> Unit raise JqError {
  if idx >= fields.length() {
    yield_(Json::object(acc))
    return
  }
  let (key, value_filter) = fields[idx]
  match key {
    StrKey(name) =>
      eval(value_filter, input, env, v => {
        let new_acc = acc.copy()
        new_acc[name] = v
        eval_object_construct(fields, idx + 1, new_acc, input, env, yield_)
      })
    ExprKey(key_expr) =>
      eval(key_expr, input, env, k => {
        match k {
          String(name) =>
            eval(value_filter, input, env, v => {
              let new_acc = acc.copy()
              new_acc[name] = v
              eval_object_construct(
                fields,
                idx + 1,
                new_acc,
                input,
                env,
                yield_,
              )
            })
          _ => raise JqError("object key must be a string")
        }
      })
  }
}

///|
fn dyn_index_impl(
  base : Json,
  idx : Json,
  yield_ : (Json) -> Unit raise JqError,
) -> Unit raise JqError {
  match (base, idx) {
    (Array(arr), Number(n, ..)) =>
      if is_nan(n) {
        yield_(Json::null())
      } else {
        let i = n.to_int()
        let actual = if i < 0 { arr.length() + i } else { i }
        if actual >= 0 && actual < arr.length() {
          yield_(arr[actual])
        } else {
          yield_(Json::null())
        }
      }
    (Object(map), String(k)) =>
      match map.get(k) {
        Some(v) => yield_(v)
        None => yield_(Json::null())
      }
    (Null, _) => yield_(Json::null())
    _ =>
      raise JqError(
        "Cannot index " +
        json_type(base) +
        " with " +
        json_type(idx) +
        " " +
        idx.stringify(),
      )
  }
}