// JSON in, JSON out. The only place in the library that mentions `Json`.
//
// Three JSON objects are not objects:
//
//   {"$link": "bafy..."}   a CID
//   {"$bytes": "AQID"}     a byte string
//   {"$type": "blob", ...} a blob reference (handled in blob.mbt, on top of
//                          the object this file produces)
//
// The first two are recognised by having that key AND NO OTHER. That rule is
// upstream's and it matters: a record is allowed to contain a genuine map whose
// only key happens to be `$link`, and treating it as a CID would corrupt it.
// The extra-key check is what keeps the two apart.

///|
fn of_json(json : Json, path : String) -> LexValue raise DecodeError {
  match json {
    Null => LexValue::Null
    True => Bool(true)
    False => Bool(false)
    String(s) => Str(s)
    Number(value, repr~) => Int(integer_of(value, repr, path))
    Array(items) => {
      let out = Array::new(capacity=items.length())
      for i, item in items {
        out.push(of_json(item, index_path(path, i)))
      }
      Arr(out)
    }
    Object(fields) => of_json_object(fields, path)
  }
}

///|
fn of_json_object(
  fields : Map[String, Json],
  path : String,
) -> LexValue raise DecodeError {
  if fields.length() == 1 {
    if fields.get("$link") is Some(String(text)) {
      let cid = @syntax.Cid::parse(text) catch {
        e => raise DecodeError(path~, reason="invalid $link: \{e.reason()}")
      }
      return Link(cid)
    }
    if fields.get("$bytes") is Some(String(text)) {
      guard base64_decode(text) is Some(bytes) else {
        raise DecodeError(path~, reason="invalid $bytes: not base64")
      }
      return Bytes(bytes)
    }
  }
  let out : Map[String, LexValue] = Map([])
  for key, value in fields {
    out[key] = of_json(value, field_path(path, key))
  }
  Obj(out)
}

///|
/// JSON numbers arrive as `Double`, which cannot hold every 53-bit integer's
/// identity through arithmetic -- so the original text is used when it is
/// available, and the `Double` only as a fallback.
///
/// A non-integer is an error rather than something that rounds. DAG-CBOR
/// forbids floats, so a value this accepted could not be written back.
fn integer_of(
  value : Double,
  repr : String?,
  path : String,
) -> Int64 raise DecodeError {
  if repr is Some(text) {
    try {
      return @string.parse_int64(text)
    } catch {
      _ => raise DecodeError(path~, reason="not an integer: \{text}")
    }
  }
  guard value == value.trunc() && !value.is_inf() && value == value else {
    raise DecodeError(path~, reason="not an integer: \{value}")
  }
  value.to_int64()
}

///|
fn to_json(value : LexValue) -> Json {
  match value {
    Null => Json::null()
    Bool(b) => Json::boolean(b)
    Int(i) =>
      // `repr` carries the exact digits, so a 53-bit integer survives even
      // though the `Double` beside it may not represent it exactly.
      Json::number(i.to_double(), repr=i.to_string())
    Str(s) => Json::string(s)
    Bytes(b) => Json::object({ "$bytes": Json::string(base64_encode(b)) })
    Link(cid) => Json::object({ "$link": Json::string(cid.to_string()) })
    Arr(items) => Json::array(items.map(to_json))
    Obj(fields) => {
      let out : Map[String, Json] = Map([])
      for key, item in fields {
        out[key] = to_json(item)
      }
      Json::object(out)
    }
  }
}