///|
priv struct ResourceCacheItem[V] {
  value : V
  byte_size : Int64
  last_used : Int64
}

///|
/// Observable counters for a resource cache.
pub(all) struct ResourceCacheStats {
  count : Int
  byte_size : Int64
  max_bytes : Int64
  hits : Int
  misses : Int
  evictions : Int
} derive(Debug, Eq)

///|
/// Small deterministic LRU cache for renderer-owned resources.
pub(all) struct ResourceCache[K, V] {
  priv entries : Map[K, ResourceCacheItem[V]]
  priv mut max_bytes : Int64
  priv mut current_bytes : Int64
  priv mut clock : Int64
  priv mut hits : Int
  priv mut misses : Int
  priv mut evictions : Int
}

///|
fn non_negative_i64(value : Int64) -> Int64 {
  if value < 0L {
    0L
  } else {
    value
  }
}

///|
pub fn[K, V] ResourceCache::new(max_bytes? : Int64 = 0L) -> ResourceCache[K, V] {
  {
    entries: Default::default(),
    max_bytes: non_negative_i64(max_bytes),
    current_bytes: 0L,
    clock: 0L,
    hits: 0,
    misses: 0,
    evictions: 0,
  }
}

///|
pub fn[K, V] ResourceCache::length(self : ResourceCache[K, V]) -> Int {
  self.entries.length()
}

///|
pub fn[K, V] ResourceCache::is_empty(self : ResourceCache[K, V]) -> Bool {
  self.entries.is_empty()
}

///|
pub fn[K, V] ResourceCache::byte_size(self : ResourceCache[K, V]) -> Int64 {
  self.current_bytes
}

///|
pub fn[K, V] ResourceCache::max_bytes(self : ResourceCache[K, V]) -> Int64 {
  self.max_bytes
}

///|
pub fn[K, V] ResourceCache::stats(
  self : ResourceCache[K, V],
) -> ResourceCacheStats {
  {
    count: self.length(),
    byte_size: self.current_bytes,
    max_bytes: self.max_bytes,
    hits: self.hits,
    misses: self.misses,
    evictions: self.evictions,
  }
}

///|
fn[K, V] ResourceCache::least_recent_key(self : ResourceCache[K, V]) -> K? {
  let mut oldest_key : K? = None
  let mut oldest_tick = 0L
  self.entries.each(fn(key, item) {
    match oldest_key {
      None => {
        oldest_key = Some(key)
        oldest_tick = item.last_used
      }
      Some(_) =>
        if item.last_used < oldest_tick {
          oldest_key = Some(key)
          oldest_tick = item.last_used
        }
    }
  })
  oldest_key
}

///|
fn[K : Hash + Eq, V] ResourceCache::remove_without_stats(
  self : ResourceCache[K, V],
  key : K,
) -> Bool {
  match self.entries.get(key) {
    None => false
    Some(item) => {
      self.entries.remove(key)
      self.current_bytes -= item.byte_size
      true
    }
  }
}

///|
fn[K : Hash + Eq, V] ResourceCache::evict_until_fit(
  self : ResourceCache[K, V],
) -> Unit {
  if self.max_bytes > 0L && self.current_bytes > self.max_bytes {
    match self.least_recent_key() {
      None => ()
      Some(key) => {
        if self.remove_without_stats(key) {
          self.evictions += 1
        }
        self.evict_until_fit()
      }
    }
  }
}

///|
pub fn[K : Hash + Eq, V] ResourceCache::contains(
  self : ResourceCache[K, V],
  key : K,
) -> Bool {
  self.entries.contains(key)
}

///|
pub fn[K : Hash + Eq, V] ResourceCache::insert(
  self : ResourceCache[K, V],
  key : K,
  value : V,
  byte_size? : Int64 = 0L,
) -> Bool {
  let byte_size = non_negative_i64(byte_size)
  if self.max_bytes > 0L && byte_size > self.max_bytes {
    ignore(self.remove_without_stats(key))
    false
  } else {
    match self.entries.get(key) {
      None => ()
      Some(item) => self.current_bytes -= item.byte_size
    }
    self.clock += 1L
    self.entries.set(key, { value, byte_size, last_used: self.clock })
    self.current_bytes += byte_size
    self.evict_until_fit()
    true
  }
}

///|
pub fn[K : Hash + Eq, V] ResourceCache::peek(
  self : ResourceCache[K, V],
  key : K,
) -> V? {
  match self.entries.get(key) {
    None => None
    Some(item) => Some(item.value)
  }
}

///|
pub fn[K : Hash + Eq, V] ResourceCache::get(
  self : ResourceCache[K, V],
  key : K,
) -> V? {
  match self.entries.get(key) {
    None => {
      self.misses += 1
      None
    }
    Some(item) => {
      self.hits += 1
      self.clock += 1L
      self.entries.set(key, { ..item, last_used: self.clock })
      Some(item.value)
    }
  }
}

///|
pub fn[K : Hash + Eq, V] ResourceCache::remove(
  self : ResourceCache[K, V],
  key : K,
) -> Bool {
  self.remove_without_stats(key)
}

///|
pub fn[K, V] ResourceCache::clear(self : ResourceCache[K, V]) -> Unit {
  self.entries.clear()
  self.current_bytes = 0L
}

///|
pub fn[K : Hash + Eq, V] ResourceCache::set_max_bytes(
  self : ResourceCache[K, V],
  max_bytes : Int64,
) -> Unit {
  self.max_bytes = non_negative_i64(max_bytes)
  self.evict_until_fit()
}