///|
/// Small keyed cache for reusable pixmap resources.
pub struct PixmapCache {
  mut pixmaps : Map[String, Pixmap]
  mut access_order : Array[String]
  capacity : Int
  is_bounded : Bool
} derive(Debug)

///|
pub extend PixmapCache with Debug::{to_repr}

///|
/// Result of reading a pixmap from cache or inserting it on miss.
pub struct PixmapCacheInsertResult {
  pixmap : Pixmap
  did_insert : Bool
} derive(Debug)

///|
pub extend PixmapCacheInsertResult with Debug::{to_repr}

///|
/// Result of reading a pixmap from cache or decoding bytes on miss.
pub struct PixmapCacheDecodeResult {
  pixmap : Pixmap
  did_decode : Bool
} derive(Debug)

///|
pub extend PixmapCacheDecodeResult with Debug::{to_repr}

///|
/// Create an empty pixmap cache.
pub fn PixmapCache::new() -> PixmapCache {
  { pixmaps: Map([]), access_order: [], capacity: 0, is_bounded: false, }
}

///|
/// Create an empty pixmap cache with least-recently-used eviction.
pub fn PixmapCache::with_capacity(capacity : Int) -> PixmapCache {
  {
    pixmaps: Map([]),
    access_order: [],
    capacity: capacity.max(1),
    is_bounded: true,
  }
}

///|
/// Get the resulting pixmap.
pub fn PixmapCacheInsertResult::get_pixmap(
  self : PixmapCacheInsertResult,
) -> Pixmap {
  self.pixmap
}

///|
/// Return whether this lookup inserted a new pixmap instead of hitting cache.
pub fn PixmapCacheInsertResult::did_insert(
  self : PixmapCacheInsertResult,
) -> Bool {
  self.did_insert
}

///|
/// Return whether this lookup reused a cached pixmap.
pub fn PixmapCacheInsertResult::did_hit_cache(
  self : PixmapCacheInsertResult,
) -> Bool {
  !self.did_insert
}

///|
/// Get the resulting pixmap.
pub fn PixmapCacheDecodeResult::get_pixmap(
  self : PixmapCacheDecodeResult,
) -> Pixmap {
  self.pixmap
}

///|
/// Return whether this lookup decoded bytes instead of hitting cache.
pub fn PixmapCacheDecodeResult::did_decode(
  self : PixmapCacheDecodeResult,
) -> Bool {
  self.did_decode
}

///|
/// Return whether this lookup reused a cached pixmap.
pub fn PixmapCacheDecodeResult::did_hit_cache(
  self : PixmapCacheDecodeResult,
) -> Bool {
  !self.did_decode
}

///|
/// Return the number of cached pixmaps.
pub fn PixmapCache::len(self : PixmapCache) -> Int {
  self.pixmaps.length()
}

///|
/// Return whether the cache currently has no pixmaps.
pub fn PixmapCache::is_empty(self : PixmapCache) -> Bool {
  self.len() is 0
}

///|
/// Return the maximum number of cached pixmaps, or 0 for an unbounded cache.
pub fn PixmapCache::capacity(self : PixmapCache) -> Int {
  self.capacity
}

///|
/// Return whether the cache has a configured entry limit.
pub fn PixmapCache::is_capacity_bounded(self : PixmapCache) -> Bool {
  self.is_bounded
}

///|
/// Return whether the cache has a pixmap for this key without refreshing LRU.
pub fn PixmapCache::contains(self : PixmapCache, key : String) -> Bool {
  self.pixmaps.contains(key)
}

///|
/// Look up a pixmap by caller-provided key.
pub fn PixmapCache::get(self : PixmapCache, key : String) -> Pixmap? {
  match self.pixmaps.get(key) {
    Some(pixmap) => {
      self.touch_key(key)
      Some(pixmap.copy())
    }
    None => None
  }
}

///|
/// Insert or replace a pixmap under a caller-provided key.
pub fn PixmapCache::insert(
  self : PixmapCache,
  key : String,
  pixmap : Pixmap,
) -> Pixmap {
  let stored = pixmap.copy()
  self.pixmaps[key] = stored
  self.touch_key(key)
  self.evict_over_capacity()
  stored.copy()
}

///|
/// Return a cached pixmap or insert this fallback pixmap on a miss.
pub fn PixmapCache::get_or_insert(
  self : PixmapCache,
  key : String,
  pixmap : Pixmap,
) -> Pixmap {
  self.get_or_insert_result(key, pixmap).get_pixmap()
}

///|
/// Return a cached pixmap or insert this fallback pixmap, reporting cache hits.
pub fn PixmapCache::get_or_insert_result(
  self : PixmapCache,
  key : String,
  pixmap : Pixmap,
) -> PixmapCacheInsertResult {
  match self.get(key) {
    Some(cached) => { pixmap: cached, did_insert: false, }
    None => { pixmap: self.insert(key, pixmap), did_insert: true, }
  }
}

///|
/// Return a cached pixmap or decode BMP bytes and cache the decoded pixmap.
pub fn PixmapCache::get_or_decode_bmp(
  self : PixmapCache,
  key : String,
  data : Bytes,
) -> Result[Pixmap, PixmapDecodeError] {
  match self.get_or_decode_bmp_result(key, data) {
    Ok(result) => Ok(result.get_pixmap())
    Err(error) => Err(error)
  }
}

///|
/// Return a cached pixmap or decode BMP bytes, reporting cache hits.
pub fn PixmapCache::get_or_decode_bmp_result(
  self : PixmapCache,
  key : String,
  data : Bytes,
) -> Result[PixmapCacheDecodeResult, PixmapDecodeError] {
  match self.get(key) {
    Some(cached) => Ok({ pixmap: cached, did_decode: false, })
    None =>
      match Pixmap::from_bmp_bytes(data) {
        Ok(decoded) => {
          let pixmap = self.insert(key, decoded)
          Ok({ pixmap, did_decode: true, })
        }
        Err(error) => Err(error)
      }
  }
}

///|
/// Return a cached pixmap or decode PNG bytes and cache the decoded pixmap.
pub fn PixmapCache::get_or_decode_png(
  self : PixmapCache,
  key : String,
  data : Bytes,
) -> Result[Pixmap, PixmapDecodeError] {
  match self.get_or_decode_png_result(key, data) {
    Ok(result) => Ok(result.get_pixmap())
    Err(error) => Err(error)
  }
}

///|
/// Return a cached pixmap or decode PNG bytes, reporting cache hits.
pub fn PixmapCache::get_or_decode_png_result(
  self : PixmapCache,
  key : String,
  data : Bytes,
) -> Result[PixmapCacheDecodeResult, PixmapDecodeError] {
  match self.get(key) {
    Some(cached) => Ok({ pixmap: cached, did_decode: false, })
    None =>
      match Pixmap::from_png_bytes(data) {
        Ok(decoded) => {
          let pixmap = self.insert(key, decoded)
          Ok({ pixmap, did_decode: true, })
        }
        Err(error) => Err(error)
      }
  }
}

///|
/// Return a cached pixmap or decode raw RGBA bytes and cache the pixmap.
pub fn PixmapCache::get_or_decode_rgba(
  self : PixmapCache,
  key : String,
  width : Int,
  height : Int,
  data : Bytes,
) -> Result[Pixmap, PixmapDecodeError] {
  match self.get_or_decode_rgba_result(key, width, height, data) {
    Ok(result) => Ok(result.get_pixmap())
    Err(error) => Err(error)
  }
}

///|
/// Return a cached pixmap or decode raw RGBA bytes, reporting cache hits.
pub fn PixmapCache::get_or_decode_rgba_result(
  self : PixmapCache,
  key : String,
  width : Int,
  height : Int,
  data : Bytes,
) -> Result[PixmapCacheDecodeResult, PixmapDecodeError] {
  match self.get(key) {
    Some(cached) => Ok({ pixmap: cached, did_decode: false, })
    None =>
      match Pixmap::from_rgba_bytes(width, height, data) {
        Ok(decoded) => {
          let pixmap = self.insert(key, decoded)
          Ok({ pixmap, did_decode: true, })
        }
        Err(error) => Err(error)
      }
  }
}

///|
/// Remove a cached pixmap by key.
pub fn PixmapCache::remove(self : PixmapCache, key : String) -> Unit {
  self.pixmaps.remove(key)
  self.remove_access_key(key)
}

///|
/// Drop all cached pixmaps.
pub fn PixmapCache::clear(self : PixmapCache) -> Unit {
  self.pixmaps = Map([])
  self.access_order = []
}

///|
fn PixmapCache::touch_key(self : PixmapCache, key : String) -> Unit {
  self.remove_access_key(key)
  self.access_order.push(key)
}

///|
fn PixmapCache::remove_access_key(self : PixmapCache, key : String) -> Unit {
  let mut index = 0
  while index < self.access_order.length() {
    if self.access_order[index] == key {
      ignore(self.access_order.remove(index))
    } else {
      index += 1
    }
  }
}

///|
fn PixmapCache::evict_over_capacity(self : PixmapCache) -> Unit {
  guard self.is_bounded && self.capacity > 0 else { return }
  while self.pixmaps.length() > self.capacity && self.access_order.length() > 0 {
    let key = self.access_order.remove(0)
    self.pixmaps.remove(key)
  }
}