// Spec for the port of tutuca's value parser (src/value.js).
// READONLY by convention: concrete public data types plus `declare` stubs for
// every function/method. Implement the declares in separate files; this file
// and the value_*_test.mbt suites define the contract.
//
// Why the value layer (this file, value_*.mbt) and the path layer
// (path_spec.mbt, path_*.mbt) share ONE package (this `core/` package)
// instead of splitting into value/ and path/ subpackages: their core types
// form a single dependency
// cycle that MoonBit packages cannot express across an import edge —
// Obj::obj_handler returns Handler (path side), Handler carries &Ctx and
// returns a Value, Ctx returns DispatchPath (whose Steps carry Val), and
// Value::Obj(&Obj) closes the loop. Breaking the cycle would mean removing
// obj_handler from Obj, which needs a trait-object downcast MoonBit doesn't
// have. The value_/path_ file prefixes are the intended organization.

///|
/// Literal constant payload (JS ConstVal.val: string | number | boolean | null)
pub(all) enum Lit {
  LNull
  LBool(Bool)
  LNum(Double)
  LStr(String)
} derive(Debug, Eq)

///|
/// One name an application may head: its arity, and what it does to
/// already-evaluated arguments.
///
/// The table lives in `value_builtin.mbt` and is reachable only through
/// `builtin` / `builtin_names`, so a caller sees a vocabulary rather than an
/// enum it would have to match exhaustively. `apply` is only ever called with
/// exactly `arity` arguments — the parser refuses anything else, which is what
/// lets the implementations index positionally.
pub struct Builtin {
  name : String
  arity : Int
  apply : (Array[Value]) -> Value
}

///|
/// Handler namespaces are a fixed enumeration (JS passes "input"/"alter" strings)
pub(all) enum HandlerNamespace {
  Input // @on.
  Alter // @when, @enrich-with, @loop-with
} derive(Debug, Eq)

///|
/// Parsed value AST. One variant per JS BaseVal subclass. `from_macro` ports
/// ConstVal.fromMacroVar: set when a `^name` macro var resolves to a constant,
/// which makes an enclosing template non-literal. StrTpl parts alternate text
/// constants and placeholder expressions; a `None` part is a placeholder whose
/// inner expression failed to parse (JS keeps null there).
pub(all) enum Val {
  Const(lit~ : Lit, from_macro~ : Bool)
  StrTpl(Array[Val?])
  /// A name applied to arguments — `empty? .items`, `equals? .view 'detail'`.
  /// The name is resolved at EVALUATION time (against `builtin`, and later
  /// against the component's own declared predicates), which is what lets the
  /// vocabulary grow without this enum growing a case.
  App(name~ : String, args~ : Array[Val])
  Name(String) // bare lowercase name
  HandlerName(name~ : String, ns~ : HandlerNamespace)
  TypeName(String) // bare Uppercase name
  Bind(String) // @name
  BindMember(name~ : String, prop~ : String) // @name.member (one level)
  Dyn(String) // *name
  Field(String) // .name
  Method(String) // $name
  SeqAccess(seq~ : String, key~ : String) // .seq[.key] (both plain fields)
} derive(Debug, Eq)

///|
/// A concrete sequence key: list index or map key (JS SeqStep.key is int|string)
pub(all) enum PathKey {
  KInt(Int)
  KStr(String)
} derive(Debug, Eq)

///|
/// Path steps (src/path.js). FieldStep/SeqAccessStep are what values address
/// (Val::to_path_item); SeqStep is a field + concrete key (also the pinned form
/// of SeqAccessStep); EachRenderItStep is an iterated render target that
/// compacts to a SeqStep; Bind/ScopeBind/EachBind are frame-only (stack
/// rebuild). Dynamic-var markers live on DispatchStep in the path package.
pub(all) enum Step {
  FieldStep(String)
  SeqStep(field~ : String, key~ : PathKey)
  SeqAccessStep(seq_field~ : String, key_field~ : String)
  EachRenderItStep(field~ : String, key~ : PathKey)
  // Frame-only: addresses no field (lookup/set pass through), exists so a
  // rebuilt stack can replay the scope bindings a render frame introduced.
  BindStep(binds~ : Map[String, Value])
  // Frame-only: replays a loop-less @enrich-with scope by re-evaluating
  // `val` as a handler against the rebuilt stack (binds computed lazily at
  // rebuild time, matching the renderer).
  ScopeBindStep(val~ : Val)
  // Frame-only: replays the renderer's per-item binds ({key, value} plus any
  // @enrich-with binds) for the @each item addressed by `key`. Carries the
  // iteration Vals (JS IterInfo) so binds are computed lazily at rebuild
  // time; loop-with is deferred with the renderer's @loop-with support.
  EachBindStep(
    val~ : Val,
    when_val~ : Val?,
    enrich_with_val~ : Val?,
    loop_with_val~ : Val?,
    key~ : PathKey
  )
} derive(Debug, Eq)

///|
/// Dynamic runtime value: what eval(stack) reads and returns.
pub(all) enum Value {
  Null
  Bool(Bool)
  Num(Double)
  Str(String)
  List(Array[Value])
  Map(Map[String, Value])
  Fn((Array[Value]) -> Value) // handler / raw-function values
  Obj(&Obj) // component instance (see trait Obj — the hybrid seam)
}

///|
/// A render-cache bucket for one instance: a structural fingerprint fixed when
/// the instance was created, and a revision bumped by each successor.
///
/// NOT an identity, and deliberately not unique. Two unrelated instances can
/// land on the same pair — two bundles loaded at runtime declaring the same
/// component, or two structurally equal siblings of one — and that is fine: a
/// bucket collision costs a MISS, never a wrong subtree, because the cache
/// validates its stored value by physical identity before returning it.
///
/// Giving up on uniqueness is what removes the need for a process-global
/// counter, and with it the question of how two independently loaded
/// components avoid clashing: they do not have to.
pub(all) struct ObjId {
  origin : UInt64
  /// 0 at creation, +1 per successor. Two generations of one instance differ
  /// here even when nothing else does, so a pass that still holds both does
  /// not make them fight for one bucket.
  rev : Int
} derive(Eq, Debug)

///|
/// The fingerprint of a freshly created instance: its component's schema
/// fingerprint mixed with the fields it starts with.
declare pub fn ObjId::of(
  fingerprint : String,
  fields : Map[String, Value],
) -> ObjId

///|
/// The successor's id: same origin, next revision.
declare pub fn ObjId::next(self : ObjId) -> ObjId

///|
/// The origin as 16 hex digits — for looking at, not for keying on.
declare pub fn ObjId::to_hex(self : ObjId) -> String

///|
/// Component-instance protocol (the port of JS's runtime Records tagged with
/// a hidden component symbol). ONE trait serves both hybrid representations:
/// the Value-backed Instance (component package) and user-defined typed
/// structs. Instances live INSIDE Value so they flow through every
/// Value-typed seam (binds, handler args, dynamic lookups, iterated seqs).
/// Fns returned by obj_field / obj_callable and the Handler returned
/// by obj_handler are SELF-PRE-BOUND (they close over the concrete Self —
/// the no-downcast analogue of JS late `this`) and ignore args element 0.
/// Every method defaults to an empty/None answer.
pub(open) trait Obj {
  fn component_id(Self) -> Int? = _
  fn obj_field(Self, name : String) -> Value? = _ // fields; methods surface as pre-bound Fn
  fn obj_with_field(Self, name : String, v : Value) -> Value? = _ // COW -> Some(Obj(new_self))
  fn obj_item(Self, key : PathKey) -> Value? = _
  fn obj_with_item(Self, key : PathKey, v : Value) -> Value? = _
  // Custom-sequence hook (the port of JS SEQ_INFO): ordered (key, value)
  // entries so `@each` can iterate a non-List/Map Obj. None = not a sequence
  // (the JS unkIter default). Keyed entries use KStr keys ("sk" breadcrumbs),
  // indexed ones KInt ("si"); obj_item resolves the same keys for path lookup.
  fn obj_seq_entries(Self) -> Array[(PathKey, Value)]? = _
  // The named CALLABLE this instance exposes for evaluation: the render-time
  // buckets (@when / @enrich-with / @loop-with) under Alter, and whatever
  // answers a bare name otherwise. Distinct from obj_handler, which resolves a
  // leaf-UPDATING handler in a dispatch bucket — different question, different
  // answer, and the near-identical names used to hide that.
  fn obj_callable(Self, name : String, ns : HandlerNamespace) -> Value? = _
  fn obj_handler(Self, bucket : HandlerBucket, name : String) -> Handler? = _ // dispatch buckets
  // What this instance DECLARES: the one method that makes the nine above
  // describable. Without it a holder of a bare `Value` can ask for a NAMED
  // field and nothing else — not which names exist, not what they hold — and
  // every consumer worked around that differently (a `Components` registry, a
  // hardcoded name list, a JSON projection).
  //
  // Answering `Some` is a claim that the schema names every field this
  // instance PUBLISHES, because the defaults below range over it: a field left
  // out is a field that does not exist as far as equality, the debug rendering
  // and the JSON form are concerned. None is the honest answer when even that
  // is more than can be said — an ad-hoc Obj standing in for a record.
  //
  // Keeping state the schema does not name is legal and ordinary — an opaque
  // guest's draft, a cursor, a parser's arena — and costs exactly one thing:
  // two instances that differ only there compare EQUAL. Nothing that detects
  // change may lean on that, which is why identity (`obj_identity`) rather
  // than equality is what decides whether a successor replaces its
  // predecessor.
  //
  // MUST return a STORED value, never build one: the defaults read it in loops.
  fn obj_schema(Self) -> SchemaInfo? = _
  // The sized half of the obj_seq_entries hook, so `size_of` stops answering
  // None for a custom collection. The default counts the entries; override
  // when the length is known without materializing them.
  fn obj_size(Self) -> Int? = _
  // This instance's render-cache bucket (see ObjId). None is the honest answer
  // for an ad-hoc Obj that does not track its own generations, and it is what
  // makes the cache DECLINE at that render site rather than build a key and
  // miss — the reintroduction condition benchmarks/OPTIMIZATIONS.md #8 sets.
  //
  // MUST return a STORED value, never compute one: it is read once per render
  // site per pass.
  fn obj_identity(Self) -> ObjId? = _
  fn obj_eq(Self, other : &Obj) -> Bool = _ // structural over the schema; false when there is none
  fn obj_debug(Self) -> String = _
  // What this instance is CALLED across sessions, if anything. `obj_identity`
  // is the render cache's bucket and is deliberately not this: it is a handle
  // and a revision, both of which are facts about this run of the program.
  // A persist id is the other kind of name — the one a store keys on, so that
  // the thing that comes back after a reload is the same thing.
  //
  // None is the honest answer for an instance nobody named, and it means "do
  // not store me" rather than "store me somewhere arbitrary": a key invented
  // at save time would not be found at load time.
  fn obj_persist_id(Self) -> String? = _
}

///|
impl Obj with fn component_id(_self) {
  None
}

///|
impl Obj with fn obj_field(_self, _name) {
  None
}

///|
impl Obj with fn obj_with_field(_self, _name, _v) {
  None
}

///|
impl Obj with fn obj_item(_self, _key) {
  None
}

///|
impl Obj with fn obj_with_item(_self, _key, _v) {
  None
}

///|
impl Obj with fn obj_seq_entries(_self) {
  None
}

///|
impl Obj with fn obj_callable(_self, _name, _ns) {
  None
}

///|
impl Obj with fn obj_handler(_self, _bucket, _name) {
  None
}

///|
impl Obj with fn obj_schema(_self) {
  None
}

///|
impl Obj with fn obj_size(self) {
  match self.obj_seq_entries() {
    Some(entries) => Some(entries.length())
    None => None
  }
}

///|
impl Obj with fn obj_identity(_self) {
  None
}

///|
impl Obj with fn obj_persist_id(_self) {
  None
}

///|
/// Structural equality over the DECLARED fields, both directions.
///
/// This used to be `false`, and every implementor that wanted real equality
/// wrote the same probe loop — over its OWN fields, so an `other` carrying
/// extra ones compared equal, and an implementor that forgot (`DynObj`) got
/// two instances that never compared equal at all. With a schema in hand the
/// loop belongs here, once.
///
/// Still `false` without a schema: an Obj that declares nothing has not said
/// what equality would even range over, and answering `true` for two opaque
/// values is the worse mistake.
impl Obj with fn obj_eq(self, other) {
  guard self.obj_schema() is Some(schema) else { return false }
  if self.component_id() != other.component_id() {
    return false
  }
  for f in schema.fields {
    if self.obj_field(f.name) != other.obj_field(f.name) {
      return false
    }
  }
  true
}

///|
/// `Name{field: value, …}` from the schema; `` without one.
impl Obj with fn obj_debug(self) {
  guard self.obj_schema() is Some(schema) else { return "" }
  let sb = StringBuilder()
  sb.write_string(schema.name)
  sb.write_string("{")
  let mut first = true
  for f in schema.fields {
    if !first {
      sb.write_string(", ")
    }
    first = false
    sb.write_string(f.name)
    sb.write_string(": ")
    sb.write_string(
      match self.obj_field(f.name) {
        Some(v) => v.to_repr().to_string()
        None => "_"
      },
    )
  }
  sb.write_string("}")
  sb.to_string()
}

///|
/// Structural equality (immutable.js `is`); Fn values are never equal.
declare pub impl Eq for Value

///|
/// Debug rendering for tests/diagnostics; Fn renders opaquely (RFn).
declare pub impl Debug for Value

///|
/// Evaluation environment (JS `stack`). Every method defaults to Null so
/// implementations (and test mocks) provide only the lookups they use.
/// lookup_method returns the RESULT of the no-arg call — the stack invokes.
/// get_handler_for returns Null when no handler is registered.
pub(open) trait Stack {
  fn lookup_name(Self, String) -> Value = _
  fn lookup_bind(Self, String) -> Value = _
  fn lookup_dynamic(Self, String) -> Value = _
  fn lookup_field_raw(Self, String) -> Value = _
  fn lookup_method(Self, String) -> Value = _
  fn get_handler_for(Self, String, HandlerNamespace) -> Value = _
}

///|
impl Stack with fn lookup_name(_self, _name) {
  Null
}

///|
impl Stack with fn lookup_bind(_self, _name) {
  Null
}

///|
impl Stack with fn lookup_dynamic(_self, _name) {
  Null
}

///|
impl Stack with fn lookup_field_raw(_self, _name) {
  Null
}

///|
impl Stack with fn lookup_method(_self, _name) {
  Null
}

///|
impl Stack with fn get_handler_for(_self, _name, _namespace) {
  Null
}

///|
/// Stand-in for JS `eval(null)` on values that touch no stack
pub(all) struct NullStack {}

///|
pub impl Stack for NullStack

///|
/// Evaluate this value against a stack
declare pub fn Val::eval(self : Val, stack : &Stack) -> Value

///|
/// Value in handler position: Method hands back the raw function
/// (lookup_field_raw) instead of invoking; everything else is eval.
declare pub fn Val::eval_as_handler(self : Val, stack : &Stack) -> Value

///|
/// Path step for path-bearing slots: Field and SeqAccess only
declare pub fn Val::to_path_item(self : Val) -> Step?

///|
/// True for constants and for templates whose every part is a plain
/// non-macro constant (a literal written the long way).
declare pub fn Val::is_literal(self : Val) -> Bool

// NOTE(dropped): Val::to_literal_source (the equivalent `'…'` source literal
// of a literal StrTpl) was removed as dead code; re-add it when the
// PLACEHOLDERLESS_TEMPLATE_STRING lint rule is ported — it exists to power
// that nudge.

///|
/// Source round-trip: every Val prints back as its original syntax
declare pub impl Show for Val

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

///|
/// Every builtin name, sorted — the vocabulary a suggestion ranges over.
declare pub fn builtin_names() -> Array[String]

///|
/// JS sizeOf: Some(length) for Str/List/Map, None for unsized values
declare pub fn size_of(v : Value) -> Int?