// A field that can be absent, present-and-null, or present with a value.
//
// Three fields in the whole in-scope corpus declare `nullable`, and only one of
// them is a client's to set -- but that one carries real meaning:
// `putRecord`'s `swapRecord` is an optimistic-concurrency check where
//
//   absent    don't care whether the record already exists
//   null      it must NOT already exist
//   a CID     it must exist AND be exactly this version
//
// An `Option` cannot say that. Collapsing the two empty cases -- which is what
// rsky does, its serde structs having nowhere to put the distinction -- turns
// "create only if absent" into "overwrite whatever is there".
//
// Used only where the Lexicon says `nullable`. Everywhere else an absent field
// is an `Option`, and encoding omits it.

///|
pub(all) enum Nullable[T] {
  Absent
  Null
  Value(T)
} derive(Eq, Debug)

///|
/// The value, if there is one. Both empty cases collapse here, which is fine
/// for a reader -- it is the WRITER that needs all three.
pub fn[T] Nullable::value(self : Self[T]) -> T? {
  match self {
    Value(v) => Some(v)
    _ => None
  }
}

///|
pub fn[T] Nullable::is_absent(self : Self[T]) -> Bool {
  self is Absent
}

///|
pub fn[T] Nullable::is_null(self : Self[T]) -> Bool {
  self is Null
}

///|
/// Reads a nullable field, consuming it. `Absent` when the key is not there,
/// `Null` when it is there and null.
pub fn[T] take_nullable(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
  parse : (LexValue, String) -> T raise DecodeError,
) -> Nullable[T] raise DecodeError {
  match rest.get(key) {
    None => Absent
    Some(Null) => {
      rest.remove(key)
      Null
    }
    Some(value) => {
      rest.remove(key)
      Value(parse(value, field_path(path, key)))
    }
  }
}

///|
/// Writes a nullable field. `Absent` writes nothing; `Null` writes an explicit
/// null, which is the whole point -- this is the one place in the library where
/// emitting a null is correct rather than a bug.
pub fn[T] put_nullable(
  out : Map[String, LexValue],
  key : String,
  value : Nullable[T],
  encode : (T) -> LexValue,
) -> Unit {
  match value {
    Absent => ()
    Null => out[key] = LexValue::Null
    Value(v) => out[key] = encode(v)
  }
}