///|
/// Mutable per-session state shared by serialization and deserialization:
/// the deduplicated-string table and (reserved for later phases) the object
/// reference table. A fresh state is created for every top-level
/// serialize/deserialize call, so ids never leak across calls.
///
/// The tables are allocated lazily on first use, matching desert-rust:
/// sessions that never touch deduplication (the common case for small
/// payloads) pay nothing for them.
pub struct SerializerState {
  priv mut strings_by_id : @hashmap.HashMap[Int32, String]?
  priv mut ids_by_string : @hashmap.HashMap[String, Int32]?
  priv mut last_string_id : Int32
}

///|
/// Result of `store_string` on the serializer side.
priv enum StoreStringResult {
  StringAlreadyStored(Int32)
  StringIsNew(Int32)
}

///|
pub fn SerializerState::new() -> SerializerState {
  { strings_by_id: None, ids_by_string: None, last_string_id: 0, }
}

///|
/// Register `s` in the dedup table. Returns `StringAlreadyStored(id)` when
/// `s` was seen before (with the id it was first stored under), or
/// `StringIsNew(id)` for a first occurrence (ids are assigned consecutively
/// starting from 1, in first-occurrence order).
fn SerializerState::store_string(
  self : SerializerState,
  s : String,
) -> StoreStringResult {
  let ids_by_string = match self.ids_by_string {
    Some(map) => map
    None => {
      let map : @hashmap.HashMap[String, Int32] = @hashmap.HashMap(
        [],
        capacity=64,
      )
      self.ids_by_string = Some(map)
      map
    }
  }
  match ids_by_string.get(s) {
    Some(id) => StringAlreadyStored(id)
    None => {
      let strings_by_id = match self.strings_by_id {
        Some(map) => map
        None => {
          let map : @hashmap.HashMap[Int32, String] = @hashmap.HashMap(
            [],
            capacity=64,
          )
          self.strings_by_id = Some(map)
          map
        }
      }
      let id = self.last_string_id + 1
      self.last_string_id = id
      ids_by_string.set(s, id)
      strings_by_id.set(id, s)
      StringIsNew(id)
    }
  }
}

///|
/// Look up a previously stored string by id. The deserializer registers
/// every string it reads inline, so a miss means a corrupted stream.
fn SerializerState::get_string(self : SerializerState, id : Int32) -> String? {
  match self.strings_by_id {
    Some(strings_by_id) => strings_by_id.get(id)
    None => None
  }
}

///|
/// Register a string that was just read inline (new-occurrence path).
fn SerializerState::store_read_string(
  self : SerializerState,
  s : String,
) -> Unit {
  let _ = self.store_string(s)
}