// JSON bridge for runtime payloads that cross the JS boundary: CustomEvent
// detail objects, file-input metadata (app glue) and structured vdom Data
// props (render). JS gets this for free — the event payload IS a JS value.

///|
/// Build a Value from parsed JSON. Lossless: every JSON shape has a Value.
pub fn Value::from_json(j : Json) -> Value {
  match j {
    Null => Null
    True => Bool(true)
    False => Bool(false)
    Number(n, ..) => Num(n)
    String(s) => Str(s)
    Array(items) => List(items.map(i => Value::from_json(i)))
    Object(m) => {
      let out : Map[String, Value] = Map([])
      for k, item in m {
        out[k] = Value::from_json(item)
      }
      Map(out)
    }
  }
}

///|
/// Stash for lossless Obj/Fn round-trips through the ToJson/FromJson trait
/// impls: active only inside with_value_stash (typed-state encode/decode).
/// Inactive (None) keeps the plain semantics: Obj/Fn degrade to null.
let value_stash : Ref[Array[Value]?] = { val: None }

///|
/// Marker key for stashed values; NUL bytes keep it out of real data.
const STASH_KEY = "\u{0}tutuca-ref\u{0}"

///|
/// Run `f` with an active Value stash: within it, the ToJson/FromJson trait
/// impls for Value carry Obj/Fn values through Json losslessly (encoded as
/// `{STASH_KEY: index}` markers into a side array). Strictly synchronous;
/// nesting saves and restores the outer stash.
pub fn[T] with_value_stash(f : () -> T) -> T {
  let saved = value_stash.val
  value_stash.val = Some([])
  let r = f()
  value_stash.val = saved
  r
}

///|
fn stash_to_json(v : Value, stash : Array[Value]) -> Json {
  match v {
    Fn(_) | Obj(_) => {
      stash.push(v)
      let marker : Map[String, Json] = Map([])
      marker[STASH_KEY] = Json::number((stash.length() - 1).to_double())
      Json::object(marker)
    }
    List(items) => Json::array(items.map(i => stash_to_json(i, stash)))
    Map(m) => {
      let obj : Map[String, Json] = Map([])
      for k, item in m {
        obj[k] = stash_to_json(item, stash)
      }
      Json::object(obj)
    }
    _ => v.to_json()
  }
}

///|
fn stash_from_json(j : Json, stash : Array[Value]) -> Value {
  match j {
    Object(m) =>
      match m.get(STASH_KEY) {
        Some(Number(n, ..)) if n.to_int() >= 0 && n.to_int() < stash.length() =>
          stash[n.to_int()]
        _ => {
          let out : Map[String, Value] = Map([])
          for k, item in m {
            out[k] = stash_from_json(item, stash)
          }
          Map(out)
        }
      }
    Array(items) => List(items.map(i => stash_from_json(i, stash)))
    _ => Value::from_json(j)
  }
}

///|
/// Trait form of Value::to_json, stash-aware: lets `derive(ToJson)` state
/// structs hold Value fields (see with_value_stash).
pub impl ToJson for Value with fn to_json(self) {
  match value_stash.val {
    Some(stash) => stash_to_json(self, stash)
    None => self.to_json()
  }
}

///|
/// Trait form of Value::from_json, stash-aware and total (never raises).
pub impl @json.FromJson for Value with fn from_json(j, _path) {
  match value_stash.val {
    Some(stash) => stash_from_json(j, stash)
    None => Value::from_json(j)
  }
}

///|
/// Json shape of a Value. `Fn`/`Obj` have no JSON form and become null (JS
/// structured clone would throw on a function too).
pub fn Value::to_json(self : Value) -> Json {
  match self {
    Null => Json::null()
    Bool(b) => Json::boolean(b)
    Num(n) => Json::number(n)
    Str(s) => Json::string(s)
    List(items) => Json::array(items.map(i => i.to_json()))
    Map(m) => {
      let obj : Map[String, Json] = Map([])
      for k, item in m {
        obj[k] = item.to_json()
      }
      Json::object(obj)
    }
    Fn(_) | Obj(_) => Json::null()
  }
}