// SPDX-License-Identifier: Apache-2.0
// mbel playground browser entry — wasm-gc exports callable from JS.
// Strings cross the boundary natively via the js-string builtins
// integration (moon.pkg link: use-js-builtin-string); on the JS side
// the module must be compiled with builtins ["js-string"] and
// importedStringConstants "_". Every entry point returns the
// canonical serializer's output, or {"$error":"..."} on failure.

///|
fn main {
  ignore(0)
}

///|
/// Runs `f` against a fresh engine instance (dialect demo transforms
/// registered, execution engine selected) and the parsed env JSON,
/// canonicalizing the result or formatting the error for the page.
fn with_env(
  env : String,
  engine : String,
  f : (@engine.Engine, @ast.Value) -> @ast.Value raise @engine.EngineError,
) -> String {
  let ctx = json_to_value(@json.parse(env)) catch {
    _ => return code_out("env is not valid JSON", "ENV_JSON")
  }
  let inst = make_instance(engine)
  let v = f(inst, ctx) catch { @engine.EngineErr(msg) => return error_out(msg) }
  canon(v)
}

///|
/// A fresh instance with the dialect demo transforms registered and the
/// execution engine selected. Shared by the stateless entries and by
/// session creation so both paths see the same baseline.
fn make_instance(engine : String) -> @engine.Engine {
  let inst = @engine.new()
  register_demo_transforms(inst)
  if engine == "vm" {
    @engine.Engine::set_engine(inst, Vm)
  }
  inst
}

///|
/// Demo transforms mirroring cmd/main: the legacy dialect's `|` is a
/// Jexl transform pipe, so `x | first` needs "first" registered on
/// the instance (kept in sync with the CLI's set).
fn register_demo_transforms(inst : @engine.Engine) -> Unit {
  @engine.Engine::add_transform(inst, "dbl", fn(
    args : Array[@ast.Value],
  ) -> @ast.Value {
    @ast.NumVal(@evaluator.to_number(args[0]) * 2.0)
  })
  @engine.Engine::add_transform(inst, "first", fn(
    args : Array[@ast.Value],
  ) -> @ast.Value {
    match args[0] {
      @ast.ArrayVal(xs) => if xs.length() > 0 { xs[0] } else { @ast.UndefVal }
      _ => @ast.UndefVal
    }
  })
  @engine.Engine::add_transform(inst, "concatWith", fn(
    args : Array[@ast.Value],
  ) -> @ast.Value {
    @evaluator.plus(args[0], args[1])
  })
}

///|
/// Classic dialect (JS semantics) — the locked-corpus entry.
pub fn eval_jexl(expr : String, env : String, engine : String) -> String {
  with_env(env, engine, (inst, ctx) => @engine.Engine::eval(inst, expr, ctx))
}

///|
/// Standard dialect, Eval mode — dynamic typed runtime errors.
pub fn eval_expr(expr : String, env : String, engine : String) -> String {
  with_env(env, engine, (inst, ctx) => {
    @engine.Engine::eval_expr(inst, expr, ctx)
  })
}

///|
/// Standard dialect, Compile mode — static checker errors.
pub fn eval_checked(expr : String, env : String, engine : String) -> String {
  with_env(env, engine, (inst, ctx) => {
    @engine.Engine::eval_expr_checked(inst, expr, ctx)
  })
}

///|
/// VM bytecode listing for the expr front end — the same lowering the
/// Vm engine executes, after constant folding. The env argument is
/// unused but kept so all page entry points share one JS signature.
pub fn disassemble(source : String, _env : String) -> String {
  let node = @expr.compile_expr_limited(source, @parser.default_max_nodes()) catch {
    @evaluator.EvalErr(msg) => return error_out(msg)
  }
  match node {
    Some(n) => {
      let prog = @evaluator.compile_program(
        @evaluator.constant_fold(n),
        @evaluator.default_grammar(),
      ) catch {
        @evaluator.EvalErr(msg) => return error_out(msg)
      }
      @evaluator.disassemble(prog)
    }
    None => error_out("Cannot disassemble an empty expression")
  }
}

///|
/// expr front-end parse-tree dump (the ENode debug representation).
pub fn dump_ast(source : String) -> String {
  let node = @expr.parse(source) catch {
    @expr.ParseErrExpr(msg, line, col) =>
      return error_out(
        "parse error at " +
        line.to_string() +
        ":" +
        col.to_string() +
        ": " +
        msg,
      )
  }
  @expr.dump(node)
}

///|
/// Parses a JSON object string into an engine context Value. JSON
/// null maps to NullVal, numbers to NumVal (parsed doubles), objects
/// to ObjectVal in insertion order. Shared with cmd/main (kept in
/// sync — main packages cannot be imported).
fn json_to_value(j : Json) -> @ast.Value {
  match j {
    Null => @ast.NullVal
    True => @ast.BoolVal(true)
    False => @ast.BoolVal(false)
    // Integer-exact input (0002 P2a, Decision 4 = (a)): an integer-valued
    // JSON number within ±(2^53 - 1) maps to IntVal. `5.0` is indistinguishable
    // from `5` at the Json layer (core keeps the raw repr only for
    // non-round-trippable numbers) and becomes IntVal too; NaN/±Inf stay
    // NumVal. The bound is 2^53 - 1, not 2^53: an integer literal at or
    // beyond 2^53 arrives as Number(double, repr~Some(_)) where the double
    // already aliases neighbors (2^53 + 1 parses to the same double as 2^53),
    // so mapping it to IntVal would claim exactness it does not have — such
    // numbers stay NumVal (documented out of P2a scope; exact repr-based
    // int64 mapping is future work). Keep in sync with cmd/main/main.mbt and
    // builtin/helpers.mbt.
    Number(d, ..) =>
      if d.floor() == d && Double::abs(d) <= 9007199254740991.0 {
        @ast.IntVal(d.to_int64())
      } else {
        @ast.NumVal(d)
      }
    String(s) => @ast.StrVal(s)
    Array(xs) => @ast.ArrayVal(xs.map(json_to_value))
    Object(m) => {
      let pairs = m
        .to_array()
        .map(kv => {
          let (k, v) = kv
          (k, json_to_value(v))
        })
      @ast.ObjectVal(pairs)
    }
  }
}

///|
/// Canonical serializer shared with cmd/main — identical output keeps
/// the playground consistent with the corpus driver.
fn canon(v : @ast.Value) -> String {
  match v {
    @ast.BoolVal(b) => if b { "true" } else { "false" }
    @ast.NumVal(d) => canon_num(d)
    @ast.IntVal(i) => i.to_string()
    // Bytes serialize like Go's json.Marshal of []byte (base64)
    @ast.BytesVal(b) => canon_str(@builtin.encode64_bytes(b))
    @ast.StrVal(s) => canon_str(s)
    @ast.NullVal => "null"
    @ast.UndefVal => "{\"$undef\":true}"
    @ast.ArrayVal(xs) => {
      let parts = xs.map(canon)
      "[" + parts.join(",") + "]"
    }
    @ast.ObjectVal(pairs) => {
      let parts = pairs.map(fn(kv) {
        let (k, val) = kv
        canon_str(k) + ":" + canon(val)
      })
      "{" + parts.join(",") + "}"
    }
  }
}

///|
fn canon_num(d : Double) -> String {
  if d != d {
    return "{\"$nan\":true}"
  }
  if d == 1.0 / 0.0 {
    return "{\"$inf\":true}"
  }
  if d == -1.0 / 0.0 {
    return "{\"$ninf\":true}"
  }
  d.to_string()
}

///|
/// JSON-style string escaping, matching JS JSON.stringify.
fn canon_str(s : String) -> String {
  let out = StringBuilder::new()
  out.write_char('"')
  for c in s {
    let code = c.to_int()
    match c {
      '"' => out.write_string("\\\"")
      '\\' => out.write_string("\\\\")
      '\n' => out.write_string("\\n")
      '\r' => out.write_string("\\r")
      '\t' => out.write_string("\\t")
      '\u{8}' => out.write_string("\\b")
      '\u{C}' => out.write_string("\\f")
      _ =>
        if code < 0x20 || code == 0x2028 || code == 0x2029 {
          out.write_string("\\u" + hex4(code))
        } else {
          out.write_char(c)
        }
    }
  }
  out.write_char('"')
  out.to_string()
}

///|
fn hex4(code : Int32) -> String {
  let digits = "0123456789abcdef".to_array()
  let out = StringBuilder::new()
  for i in 0..<4 {
    let shift = (3 - i) * 4
    let nibble = (code >> shift) & 0xF
    out.write_char(digits[nibble])
  }
  out.to_string()
}

///|
/// A user function parsed from a functions file (format v1).
struct UserFnSpec {
  name : String
  params : Array[String]?
  body : String
  description : String
}

///|
/// Validates a functions file (format v1) and returns the parsed
/// specs. Known top-level keys: `version` (must be 1), `functions`
/// (array), `env` (object — a page-level example, ignored by the
/// engine). Known function keys: `name`, `params`, `body`,
/// `description`; unknown keys are rejected as likely typos. Errors
/// carry the `functions[i]` index.
fn parse_user_functions(j : Json) -> Result[Array[UserFnSpec], String] {
  let obj = match j {
    Object(m) => m
    _ => return Err("functions file must be a JSON object")
  }
  let mut version = 0.0
  let mut seen_version = false
  let mut funcs : Array[Json] = []
  let mut seen_funcs = false
  for kv in obj.to_array() {
    let (k, v) = kv
    match k {
      "version" =>
        match v {
          Number(d, ..) => {
            version = d
            seen_version = true
          }
          _ => return Err("functions file: \"version\" must be a number")
        }
      "functions" =>
        match v {
          Array(xs) => {
            funcs = xs
            seen_funcs = true
          }
          _ => return Err("functions file: \"functions\" must be an array")
        }
      // page-level example environment; the engine ignores it
      "env" =>
        match v {
          Object(_) => ()
          _ => return Err("functions file: \"env\" must be an object")
        }
      _ => return Err("functions file: unknown field \"" + k + "\"")
    }
  }
  if !seen_version {
    return Err("functions file: missing \"version\"")
  }
  if version != 1.0 {
    return Err("functions file: unsupported version " + version.to_string())
  }
  if !seen_funcs {
    return Err("functions file: missing \"functions\"")
  }
  let specs : Array[UserFnSpec] = []
  for i in 0.. m
      _ => return Err("functions[" + i.to_string() + "]: must be an object")
    }
    let mut name = ""
    let mut params : Array[String]? = None
    let mut body = ""
    let mut description = ""
    let mut has_name = false
    let mut has_body = false
    for kv in fo.to_array() {
      let (k, v) = kv
      match k {
        "name" =>
          match v {
            String(s) => {
              name = s
              has_name = true
            }
            _ =>
              return Err(
                "functions[" + i.to_string() + "]: \"name\" must be a string",
              )
          }
        "params" =>
          match v {
            Array(xs) => {
              let ps : Array[String] = []
              for pi in 0.. ps.push(s)
                  _ =>
                    return Err(
                      "functions[" +
                      i.to_string() +
                      "]: \"params\" must be an array of strings",
                    )
                }
              }
              params = Some(ps)
            }
            _ =>
              return Err(
                "functions[" +
                i.to_string() +
                "]: \"params\" must be an array of strings",
              )
          }
        "body" =>
          match v {
            String(s) => {
              body = s
              has_body = true
            }
            _ =>
              return Err(
                "functions[" + i.to_string() + "]: \"body\" must be a string",
              )
          }
        "description" =>
          match v {
            String(s) => description = s
            _ =>
              return Err(
                "functions[" +
                i.to_string() +
                "]: \"description\" must be a string",
              )
          }
        _ =>
          return Err(
            "functions[" + i.to_string() + "]: unknown field \"" + k + "\"",
          )
      }
    }
    if !has_name {
      return Err("functions[" + i.to_string() + "]: missing \"name\"")
    }
    if !has_body {
      return Err("functions[" + i.to_string() + "]: missing \"body\"")
    }
    specs.push({ name, params, body, description, })
  }
  Ok(specs)
}

///|
/// Validates a functions file and returns the resolved signature list
/// `[{"name":...,"params":[...],"description":...}]` — params are the
/// explicit list or auto-extracted from the body (first-appearance
/// order). Page-facing: used to render the function chips.
pub fn describe_functions(funcs_json : String) -> String {
  let fj = @json.parse(funcs_json) catch {
    _ => return code_out("functions file is not valid JSON", "FUNCS_JSON")
  }
  let specs = match parse_user_functions(fj) {
    Ok(specs) => specs
    Err(msg) => return code_out(msg, functions_file_code(msg))
  }
  let out = StringBuilder::new()
  out.write_string("[")
  let mut first = true
  for sp in specs {
    if !first {
      out.write_string(",")
    }
    first = false
    let ps = match sp.params {
      Some(ps) => ps
      None =>
        @engine.user_function_params(sp.name, sp.body) catch {
          @engine.EngineErr(m) => return code_out(m, registration_code(m))
        }
    }
    out.write_string("{\"name\":")
    out.write_string(canon_str(sp.name))
    out.write_string(",\"params\":[")
    out.write_string(ps.map(fn(p) { canon_str(p) }).join(","))
    out.write_string("],\"description\":")
    out.write_string(canon_str(sp.description))
    out.write_string("}")
  }
  out.write_string("]")
  out.to_string()
}

///|
/// Evaluates with user functions loaded from a functions file (format
/// v1). `mode` selects the dialect entry — "eval" (standard, default),
/// "checked" (standard, strict), "legacy" (classic); `engine` selects
/// "walk" (default) or "vm". The evaluation env doubles as the
/// functions' default bindings: unpassed parameters fall back to
/// same-named env keys, explicit arguments win. In "checked" mode the
/// bodies are type-checked at registration (lexical params, complete
/// symbol table) and the expression's calls are checked against the
/// registered arities. All failures (file format, registration,
/// evaluation) return {"$error": "..."}.
pub fn eval_with_functions(
  expr : String,
  env : String,
  funcs_json : String,
  mode : String,
  engine : String,
) -> String {
  let ctx = json_to_value(@json.parse(env)) catch {
    _ => return code_out("env is not valid JSON", "ENV_JSON")
  }
  let fj = @json.parse(funcs_json) catch {
    _ => return code_out("functions file is not valid JSON", "FUNCS_JSON")
  }
  let specs = match parse_user_functions(fj) {
    Ok(specs) => specs
    Err(msg) => return code_out(msg, functions_file_code(msg))
  }
  let inst = make_instance(engine)
  let defs : Array[@engine.UserFunctionDef] = []
  for sp in specs {
    defs.push({ name: sp.name, params: sp.params, body: sp.body, })
  }
  // non-object envs simply provide no default bindings
  let defaults : @ast.Value? = match ctx {
    @ast.ObjectVal(_) => Some(ctx)
    _ => None
  }
  let registered = try {
    if mode == "checked" {
      @engine.Engine::add_expression_functions_checked(inst, defs, defaults)
    } else {
      @engine.Engine::add_expression_functions(inst, defs, defaults)
    }
  } catch {
    @engine.EngineErr(m) => return code_out(m, registration_code(m))
  }
  ignore(registered)
  let v = try {
    if mode == "checked" {
      @engine.Engine::eval_expr_checked(inst, expr, ctx)
    } else if mode == "legacy" {
      @engine.Engine::eval(inst, expr, ctx)
    } else {
      @engine.Engine::eval_expr(inst, expr, ctx)
    }
  } catch {
    @engine.EngineErr(m) => return error_out(m)
  }
  canon(v)
}