// The atproto data model -- the eight kinds of value a record can be made of.
//
// https://atproto.com/specs/data-model
//
// This is what stands in for `Json` throughout the library, and the difference
// is not cosmetic. JSON has one number type and no bytes; the atproto data
// model has integers only and two byte-ish kinds (`bytes` and `cid-link`) that
// JSON can only spell as tagged objects. A caller handed a `Json` would have to
// know that `{"$link": "..."}` is a CID rather than a map with one odd key --
// which is exactly the knowledge a typed client exists to remove.
//
// Two consequences of "integers only" worth stating:
//
//   - There is no `Float` case, and `1.5` is a decode error rather than
//     something that rounds. DAG-CBOR forbids floats, so a float that got in
//     here could not be written back out.
//   - Integers are `Int64`, but the protocol's own limit is 53 bits, because
//     that is what a JavaScript number holds exactly. Nothing here enforces 53
//     bits; a server that sends more is unusual, not malformed.

///|
/// A value in the atproto data model.
pub(all) enum LexValue {
  Null
  Bool(Bool)
  Int(Int64)
  Str(String)
  /// JSON: `{"$bytes": ""}`.
  Bytes(Bytes)
  /// JSON: `{"$link": "bafy..."}`.
  Link(@syntax.Cid)
  Arr(Array[LexValue])
  Obj(Map[String, LexValue])
} derive(Eq, Debug)

///|
/// The kind name, for error messages. Matches the Lexicon spelling of each
/// type, so a mismatch reads in the same vocabulary the schema uses.
pub fn LexValue::kind(self : Self) -> String {
  match self {
    Null => "null"
    Bool(_) => "boolean"
    Int(_) => "integer"
    Str(_) => "string"
    Bytes(_) => "bytes"
    Link(_) => "cid-link"
    Arr(_) => "array"
    Obj(_) => "object"
  }
}

///|
pub fn LexValue::as_bool(self : Self) -> Bool? {
  match self {
    Bool(b) => Some(b)
    _ => None
  }
}

///|
pub fn LexValue::as_int(self : Self) -> Int64? {
  match self {
    Int(i) => Some(i)
    _ => None
  }
}

///|
pub fn LexValue::as_string(self : Self) -> String? {
  match self {
    Str(s) => Some(s)
    _ => None
  }
}

///|
pub fn LexValue::as_bytes(self : Self) -> Bytes? {
  match self {
    Bytes(b) => Some(b)
    _ => None
  }
}

///|
pub fn LexValue::as_link(self : Self) -> @syntax.Cid? {
  match self {
    Link(c) => Some(c)
    _ => None
  }
}

///|
pub fn LexValue::as_array(self : Self) -> Array[LexValue]? {
  match self {
    Arr(a) => Some(a)
    _ => None
  }
}

///|
pub fn LexValue::as_object(self : Self) -> Map[String, LexValue]? {
  match self {
    Obj(o) => Some(o)
    _ => None
  }
}

///|
/// One field of an object, or `None` if this is not an object or has no such
/// field. The two cases are deliberately not distinguished: a caller reaching
/// for a field on a non-object has already gone wrong somewhere else.
pub fn LexValue::get(self : Self, key : String) -> LexValue? {
  match self {
    Obj(o) => o.get(key)
    _ => None
  }
}

///|
/// The `$type` discriminator, if this is a tagged object.
///
/// Every record and every open-union member carries one, and it is what
/// dispatch is done on -- never the shape of the object, which is how
/// TypeScript does it and how rsky's untagged unions go wrong.
pub fn LexValue::type_tag(self : Self) -> String? {
  match self.get("$type") {
    Some(Str(t)) => Some(t)
    _ => None
  }
}

///|
/// Parses JSON into the data model, decoding `$link` and `$bytes` on the way.
///
/// This and `stringify` are the only doors between JSON and everything above,
/// which is what keeps `Json` out of the library's public API.
pub fn LexValue::parse(text : String) -> LexValue raise DecodeError {
  let json = @json.parse(text) catch {
    e => raise DecodeError(path="", reason="invalid JSON: \{e}")
  }
  of_json(json, "")
}

///|
pub fn LexValue::stringify(self : Self) -> String {
  to_json(self).stringify()
}