///|
/// Creates an empty TTL cache.
pub fn[T] TTL::new(
  default_ttl_ms? : Int64 = 10_000L,
  capacity? : Int = 1_000_000_000,
) -> TTL[T] {
  {
    store: Map([]),
    listeners: Map([]),
    default_ttl_ms,
    capacity,
    next_listener_id: 0,
  }
}

///|
/// Returns the configured default TTL in milliseconds.
pub fn[T] TTL::default_ttl_ms(self : TTL[T]) -> Int64 {
  self.default_ttl_ms
}

///|
/// Updates the default TTL in milliseconds.
pub fn[T] TTL::set_default_ttl_ms(self : TTL[T], ttl_ms : Int64) -> Unit {
  self.default_ttl_ms = ttl_ms
}

///|
/// Returns the maximum number of keys accepted by the cache.
pub fn[T] TTL::capacity(self : TTL[T]) -> Int {
  self.capacity
}

///|
/// Updates cache capacity. Negative values are rejected.
pub fn[T] TTL::set_capacity(self : TTL[T], capacity : Int) -> Bool {
  if capacity < 0 {
    false
  } else {
    self.capacity = capacity
    true
  }
}

///|
/// Sets a key-value pair. Returns `false` when a new key is dropped because the
/// cache is at capacity.
pub fn[T] TTL::set(
  self : TTL[T],
  key : String,
  val : T,
  now_ms~ : Int64,
  ttl_ms? : Int64,
) -> Bool {
  let ttl = match ttl_ms {
    Some(ttl) => ttl
    None => self.default_ttl_ms
  }
  let already_present = self.store.contains(key)
  if !already_present && self.store.length() >= self.capacity {
    self.emit(Drop, key, Some(val), None)
    false
  } else {
    if already_present {
      ignore(self.del(key))
    }
    let expire_ms = now_ms + ttl
    self.store[key] = { val, expire_ms }
    self.emit(Set, key, Some(val), Some(expire_ms))
    true
  }
}

///|
/// Sets multiple keys and returns the number accepted by the cache.
pub fn[T] TTL::mset(
  self : TTL[T],
  entries : ArrayView[SetEntry[T]],
  now_ms~ : Int64,
) -> Int {
  let mut accepted = 0
  for entry in entries {
    let ok = match entry.ttl_ms {
      Some(ttl) => self.set(entry.key, entry.val, now_ms~, ttl_ms=ttl)
      None => self.set(entry.key, entry.val, now_ms~)
    }
    if ok {
      accepted += 1
    }
  }
  accepted
}

///|
fn[T] entry_key(pair : (String, Item[T])) -> String {
  let (key, _) = pair
  key
}

///|
fn[T] entry_item(pair : (String, Item[T])) -> Item[T] {
  let (_, item) = pair
  item
}

///|
fn[T] cache_entry_from_pair(pair : (String, Item[T])) -> CacheEntry[T] {
  let (key, item) = pair
  { key, val: item.val, expire_ms: item.expire_ms }
}

///|
/// Gets a value by key and removes it first if it has expired.
pub fn[T] TTL::get(self : TTL[T], key : String, now_ms~ : Int64) -> T? {
  match self.store.get(key) {
    None => {
      self.emit(Miss, key, None, None)
      None
    }
    Some(item) =>
      if item.expire_ms <= now_ms {
        self.remove_expired_key(key, item)
        self.emit(Miss, key, None, None)
        None
      } else {
        self.emit(Hit, key, Some(item.val), Some(item.expire_ms))
        Some(item.val)
      }
  }
}

///|
/// Returns whether a key exists and has not expired.
pub fn[T] TTL::has(self : TTL[T], key : String, now_ms~ : Int64) -> Bool {
  match self.store.get(key) {
    None => false
    Some(item) =>
      if item.expire_ms <= now_ms {
        self.remove_expired_key(key, item)
        false
      } else {
        true
      }
  }
}

///|
/// Deletes a key and returns its value when present.
pub fn[T] TTL::del(self : TTL[T], key : String) -> T? {
  match self.store.get(key) {
    None => None
    Some(item) => {
      self.store.remove(key)
      self.emit(Del, key, Some(item.val), Some(item.expire_ms))
      Some(item.val)
    }
  }
}

///|
/// Removes all keys, emitting `Del` for each removed item.
pub fn[T] TTL::clear(self : TTL[T]) -> Unit {
  let entries = self.store.to_array()
  for pair in entries {
    let key = entry_key(pair)
    ignore(self.del(key))
  }
}

///|
/// Removes all expired keys and returns the number removed.
pub fn[T] TTL::purge_expired(self : TTL[T], now_ms~ : Int64) -> Int {
  let entries = self.store.to_array()
  let mut removed = 0
  for pair in entries {
    let key = entry_key(pair)
    let item = entry_item(pair)
    if item.expire_ms <= now_ms {
      self.remove_expired_key(key, item)
      removed += 1
    }
  }
  removed
}

///|
/// Returns the number of stored keys. When `now_ms` is supplied, expired keys
/// are purged before counting.
pub fn[T] TTL::size(self : TTL[T], now_ms? : Int64) -> Int {
  match now_ms {
    Some(now) => ignore(self.purge_expired(now_ms=now))
    None => ()
  }
  self.store.length()
}

///|
/// Returns unexpired cache entries. Expired keys are purged before the snapshot
/// is produced.
pub fn[T] TTL::entries(self : TTL[T], now_ms~ : Int64) -> Array[CacheEntry[T]] {
  ignore(self.purge_expired(now_ms~))
  self.store.to_array().map(cache_entry_from_pair)
}