// The builtin vocabulary: the names an application may head, each with the
// arity that makes application unambiguous and the semantics evaluation uses.
//
// This was `enum Pred` — five constructors, an arity match and an apply match,
// all of them closed. Nothing an author could write joined the set, which is
// why a predicate needed its own AST node (`Val::Predicate`), its own parse
// path and its own no-kind exception. Applying a NAME to arguments is the
// general shape, and the five become the first rows of a table.
//
// The table is data rather than an enum for one reason worth stating plainly:
// `Val::App` carries the NAME, so `Val` still derives `Eq` and `Debug` even
// though the behaviour here is a closure. An enum in the AST would have had to
// grow a case per user-declared predicate, which is exactly the thing that
// cannot work.
//
// It is ONE table for both languages. A conditional slot and a block body ask
// the same names of the same implementations, so `truthy? .items` cannot mean
// one thing in a `@show` and another in a `pred`. The operators are here for
// the same reason: `and` in a slot is `and` in a block, not a second spelling
// that happens to agree today. `tscript/script_builtin.mbt` is the block's
// view of this list and re-exports rather than restates it.
//
// TWO ROWS DO NOT EVALUATE HERE. `and` and `or` short-circuit, and `apply`
// takes ARGUMENTS THAT ARE ALREADY EVALUATED, so they cannot. `Val::eval`
// answers them ahead of the generic path; the rows exist so the name resolves
// and the arity is checked in one place with everything else.

///|
/// Truthiness, for the operators. `Value::is_truthy` and NOT `pred_truthy`:
/// the block coerces a condition the same way (`tscript/emit_mbt/emit.mbt`'s
/// `at_bool`, which says so out loud), and the two backends have to agree
/// about `@show=".count"`.
fn op_truthy(v : Value) -> Bool {
  v.is_truthy()
}

///|
/// Every builtin, by name. Built once: `builtin` is on the parse path and the
/// eval path, and neither wants to rebuild the map per call.
let builtin_table : Map[String, Builtin] = {
  // -- The shape predicates ------------------------------------------------
  //
  // These are the ones no operator says, which is why they keep the `?` and
  // why both languages keep all four. `empty?` is total over any value where
  // `(len x) is 0` is not, and `truthy?` differs from plain truthiness ON
  // PURPOSE — see `pred_truthy`.

  // `empty?` is deliberately not `falsy?`: a `0` is falsy and is not empty,
  // and an unsized value is neither.
  "empty?": {
    name: "empty?",
    arity: 1,
    apply: args => Bool(args[0] is Null || size_of(args[0]) is Some(0)),
  },
  // `truthy?` / `falsy?` are `pred_truthy`, NOT `Value::is_truthy`: the
  // predicates treat an empty collection as falsy and plain truthiness does
  // not. The difference is deliberate and is the reason both exist.
  "truthy?": {
    name: "truthy?",
    arity: 1,
    apply: args => Bool(pred_truthy(args[0])),
  },
  "null?": { name: "null?", arity: 1, apply: args => Bool(args[0] is Null) },

  // -- Logic ---------------------------------------------------------------
  //
  // `not` evaluates here; `and` / `or` do not — `Val::eval` short-circuits
  // them, because `apply` is handed evaluated arguments and a guard like
  // `and (truthy? .items) ($firstLabel)` has to be able to stop.
  "not": { name: "not", arity: 1, apply: args => Bool(!op_truthy(args[0])) },
  "and": {
    name: "and",
    arity: 2,
    apply: args => Bool(op_truthy(args[0]) && op_truthy(args[1])),
  },
  "or": {
    name: "or",
    arity: 2,
    apply: args => Bool(op_truthy(args[0]) || op_truthy(args[1])),
  },
  // `a implies b` is `(not a) or b`, the shape most cross-field rules take.
  "implies": {
    name: "implies",
    arity: 2,
    apply: args => Bool(!op_truthy(args[0]) || op_truthy(args[1])),
  },

  // -- Comparison ----------------------------------------------------------
  //
  // `is` is STRUCTURAL, through `Eq for Value`, so two lists with equal
  // contents compare equal here as they do in a compiled handler. The ordering
  // four read both sides as numbers, which a non-number answers 0 for —
  // the interpreter's analogue of the type error the compiled backend raises.
  "is": { name: "is", arity: 2, apply: args => Bool(args[0] == args[1]) },
  "is not": {
    name: "is not",
    arity: 2,
    apply: args => Bool(args[0] != args[1]),
  },
  "<": {
    name: "<",
    arity: 2,
    apply: args => Bool(args[0].num() < args[1].num()),
  },
  "<=": {
    name: "<=",
    arity: 2,
    apply: args => Bool(args[0].num() <= args[1].num()),
  },
  ">": {
    name: ">",
    arity: 2,
    apply: args => Bool(args[0].num() > args[1].num()),
  },
  ">=": {
    name: ">=",
    arity: 2,
    apply: args => Bool(args[0].num() >= args[1].num()),
  },

  // -- Reading -------------------------------------------------------------
  //
  // The block's list, which is here because the block's list IS this list.
  // Each of them is in it because the census found it hand-written; they are
  // not a general standard library and should not become one.
  "len": {
    name: "len",
    arity: 1,
    apply: args => {
      match size_of(args[0]) {
        Some(n) => Num(n.to_double())
        None => Null
      }
    },
  },
  // A KEY in a set or map, a VALUE in a list. The keyed case keys on the
  // DISPLAY string, which is the key the generated `hasIn` mutator looks up.
  "has": {
    name: "has",
    arity: 2,
    apply: args => {
      match args[0] {
        Map(m) => Bool(m.contains(args[1].to_display_string()))
        List(a) => Bool(a.iter().any(v => v == args[1]))
        // A custom collection answers both halves at once: it is asked by KEY
        // like a map and by VALUE like a list, because nothing here knows which
        // of the two shapes it meant to be.
        Obj(o) =>
          Bool(
            o
            .obj_seq_entries()
            .unwrap_or([])
            .iter()
            .any(e => {
              let (k, v) = e
              k.to_label() == args[1].to_display_string() || v == args[1]
            }),
          )
        _ => Bool(false)
      }
    },
  },
  "contains": {
    name: "contains",
    arity: 2,
    apply: args => Bool(args[0].str().contains(args[1].str())),
  },
  "min": {
    name: "min",
    arity: 2,
    apply: args => {
      let (a, b) = (args[0].num(), args[1].num())
      Num(if a < b { a } else { b })
    },
  },
  "max": {
    name: "max",
    arity: 2,
    apply: args => {
      let (a, b) = (args[0].num(), args[1].num())
      Num(if a > b { a } else { b })
    },
  },
  // The empty-range answer is `lo`, matching the compiled backend: an empty
  // list makes `hi` -1 and a negative index is not an honest result.
  "clamp": {
    name: "clamp",
    arity: 3,
    apply: args => {
      let (v, lo, hi) = (args[0].num(), args[1].num(), args[2].num())
      Num(
        if hi < lo {
          lo
        } else if v < lo {
          lo
        } else if v > hi {
          hi
        } else {
          v
        },
      )
    },
  },
  "int": {
    name: "int",
    arity: 1,
    apply: args => Num(args[0].num().to_int().to_double()),
  },
  "num": { name: "num", arity: 1, apply: args => Num(args[0].num()) },
  "str": {
    name: "str",
    arity: 1,
    apply: args => Str(args[0].to_display_string()),
  },
  "lower": {
    name: "lower",
    arity: 1,
    apply: args => Str(args[0].str().to_lower()),
  },
  "upper": {
    name: "upper",
    arity: 1,
    apply: args => Str(args[0].str().to_upper()),
  },
  "trim": {
    name: "trim",
    arity: 1,
    apply: args => Str(args[0].str().trim(chars=" \t\n\r").to_owned()),
  },

  // -- Retired ---------------------------------------------------------------
  //
  // Still here, and still answering, because a compiled dyncomp bundle carries
  // its view markup as a STRING inside the wasm and the host parses it at load
  // time (`dyncomp/host/manifest.mbt`). Deleting a spelling would break a
  // bundle nobody can recompile. `retired` is what points an author at the
  // one that stays; these go at the next apiVersion.
  "equals?": {
    name: "equals?",
    arity: 2,
    apply: args => Bool(args[0] == args[1]),
  },
  "falsy?": {
    name: "falsy?",
    arity: 1,
    apply: args => Bool(!pred_truthy(args[0])),
  },
}

///|
/// The names that still parse but are no longer the spelling, each with the
/// one that is.
///
/// One table rather than a sentence at each of the four sites that report an
/// unknown name — the slot generator, the block checker, and the two block
/// backends — because a retirement the four word differently is a retirement
/// an author has to learn four times.
let retired_table : Map[String, String] = {
  "equals?": "`is` says this — write `a is b`",
  "falsy?": "`not` says this — write `not (truthy? x)`",
}

///|
/// The builtin a name denotes, or None when nothing does.
pub fn builtin(name : String) -> Builtin? {
  builtin_table.get(name)
}

///|
/// For a name that still answers but is no longer the spelling, what to write
/// instead. `None` for every name in current use.
pub fn retired(name : String) -> String? {
  retired_table.get(name)
}

///|
/// Every builtin name in BYTE order — the vocabulary a "did you mean" search
/// ranges over, and the reason it is public rather than a private table.
///
/// Retired names are NOT in it: a suggestion is advice about what to write,
/// and the answer is never a name on its way out.
///
/// Not `Array::sort`: MoonBit's `Compare for String` is length-first, which
/// would answer `null?, empty?, falsy?, equals?, truthy?`. That is still
/// deterministic, and it reads as unordered to the person the list is for.
pub fn builtin_names() -> Array[String] {
  let out : Array[String] = []
  for name, _ in builtin_table {
    if !retired_table.contains(name) {
      out.push(name)
    }
  }
  out.sort_by((a, b) => a.lexical_compare(b))
  out
}