///|
/// Create a new asset cache with given limits.
pub fn new_asset_cache(max_bytes : Int, max_entries : Int) -> AssetCache {
  AssetCache::{
    entries: {},
    total_bytes: 0,
    max_bytes,
    max_entries,
    access_counter: 0,
  }
}

///|
/// Get a cached audio buffer by key. Returns None if not found.
pub fn cache_get(cache : AssetCache, key : String) -> AudioBuffer? {
  match cache.entries.get(key) {
    Some(entry) => {
      cache.access_counter = cache.access_counter + 1
      entry.last_access = cache.access_counter
      Some(entry.buffer)
    }
    None => None
  }
}

///|
/// Put an audio buffer into the cache. Evicts entries if needed.
pub fn cache_put(
  cache : AssetCache,
  key : String,
  buffer : AudioBuffer,
  policy? : CachePolicy = Normal,
) -> Unit {
  let byte_size = buffer.data.length() * 4 // Float = 4 bytes
  // If key already exists, remove old entry first
  match cache.entries.get(key) {
    Some(old) => {
      cache.total_bytes = cache.total_bytes - old.byte_size
      cache.entries.remove(key)
    }
    None => ()
  }
  // Evict until we have space
  while cache.total_bytes + byte_size > cache.max_bytes ||
        cache.entries.length() >= cache.max_entries {
    if not(evict_one(cache)) {
      break
    }
  }
  cache.access_counter = cache.access_counter + 1
  let entry = CacheEntry::{
    buffer,
    policy,
    byte_size,
    last_access: cache.access_counter,
  }
  cache.entries[key] = entry
  cache.total_bytes = cache.total_bytes + byte_size
}

///|
/// Evict a specific key from the cache. Returns true if found and removed.
pub fn cache_evict(cache : AssetCache, key : String) -> Bool {
  match cache.entries.get(key) {
    Some(entry) => {
      cache.total_bytes = cache.total_bytes - entry.byte_size
      cache.entries.remove(key)
      true
    }
    None => false
  }
}

///|
/// Clear all entries from the cache.
pub fn cache_clear(cache : AssetCache) -> Unit {
  cache.entries.clear()
  cache.total_bytes = 0
}

///|
/// Evict one entry based on LRU policy. StreamPrefer evicted first, then Normal.
/// AlwaysCache entries are never evicted. Returns true if an entry was evicted.
fn evict_one(cache : AssetCache) -> Bool {
  // Find best candidate: StreamPrefer first, then Normal, by oldest access
  let mut best_key : String? = None
  let mut best_access = 2147483647
  let mut best_priority = 0 // 0=none, 1=Normal, 2=StreamPrefer
  for key, entry in cache.entries {
    let priority = match entry.policy {
      StreamPrefer => 2
      Normal => 1
      AlwaysCache => 0
    }
    if priority == 0 {
      continue
    }
    if priority > best_priority ||
      (priority == best_priority && entry.last_access < best_access) {
      best_key = Some(key)
      best_access = entry.last_access
      best_priority = priority
    }
  }
  match best_key {
    Some(key) => cache_evict(cache, key)
    None => false
  }
}