///|
/// A change to the number of cached entries holding one user.
priv enum UserStep {
  Hold
  Release
}

///|
/// What a `UserStep` does to the user's record.
priv enum UserMove {
  /// The first holder: the record moves from the recent users to the held
  /// ones.
  Promote
  /// Another holder, or one of several letting go: the record stays held.
  Keep
  /// The last holder let go: the record leaves the cache.
  Drop
  /// A release with no holder recorded. Never happens while every holder
  /// releases exactly what it held; ignored rather than driven negative.
  Stray
}

///|
fn user_move(count : Int32, step : UserStep) -> UserMove {
  match (step, count) {
    (Hold, 0) => Promote
    (Hold, _) => Keep
    (Release, 0) => Stray
    (Release, 1) => Drop
    (Release, _) => Keep
  }
}

///|
/// Cached users, kept only while something holds them.
///
/// A user is *held* by each cached entry that refers to it — a member entry,
/// a cached message (its author), and the current user — and leaves the
/// cache when the last holder goes, whatever removed it. `counts_` is the
/// exact holder count; the held records sit in `held_` under the `users`
/// limit, which may drop a record early but never changes a count. Users
/// seen without a holder (a ban, a departed member's last view, the author of
/// a message the cache does not keep, an interaction's user) go to `recent_`
/// instead, under the `recent_users` limit. A user is in at most one of the
/// two maps.
priv struct UserStore {
  enabled_ : Bool
  held_ : TimedMap[@model.UserId, @model.User]
  counts_ : Map[@model.UserId, Int32]
  recent_ : TimedMap[@model.UserId, @model.User]
}

///|
fn UserStore::new(
  enabled~ : Bool,
  held~ : CacheLimit,
  recent~ : CacheLimit,
  now~ : () -> Int64,
) -> UserStore {
  {
    enabled_: enabled,
    held_: TimedMap::new(held, now),
    counts_: Map([]),
    recent_: TimedMap::new(recent, now),
  }
}

///|
/// A cached entry starts holding `user`, whose view becomes the current one.
fn UserStore::hold(self : UserStore, user : @model.User) -> Unit {
  guard self.enabled_ else { return }
  let count = self.counts_.get(user.id).unwrap_or(0)
  if user_move(count, Hold) is Promote {
    self.recent_.remove(user.id) |> ignore
  }
  self.counts_[user.id] = count + 1
  self.held_.put(user.id, user) |> ignore
}

///|
/// A cached entry holding user `id` left the cache.
fn UserStore::release(self : UserStore, id : @model.UserId) -> Unit {
  guard self.enabled_ else { return }
  let count = self.counts_.get(id).unwrap_or(0)
  match user_move(count, Release) {
    Drop => {
      self.counts_.remove(id)
      self.held_.remove(id) |> ignore
    }
    Keep => self.counts_[id] = count - 1
    // `Stray`; `Promote` only answers a hold.
    _ => ()
  }
}

///|
/// `user` was seen in an event: a held record takes the newer view, and an
/// unheld user becomes the most recent of the recent users.
fn UserStore::see(self : UserStore, user : @model.User) -> Unit {
  guard self.enabled_ else { return }
  if self.counts_.contains(user.id) {
    self.held_.put(user.id, user) |> ignore
  } else {
    self.recent_.put(user.id, user) |> ignore
  }
}

///|
/// Drops the records whose own `users` or `recent_users` TTL has passed.
fn UserStore::expire(self : UserStore) -> Unit {
  self.held_.sweep()
  self.recent_.sweep()
}

///|
fn UserStore::get(self : UserStore, id : @model.UserId) -> @model.User? {
  match self.held_.get(id) {
    Some(user) => Some(user)
    None => self.recent_.get(id)
  }
}

///|
fn UserStore::values(self : UserStore) -> Array[@model.User] {
  let users = self.held_.values()
  users.append(self.recent_.values())
  users
}