## LRU Cache Implementation
##
## This module provides a generic Least Recently Used (LRU) cache with:
## - O(1) get/put operations
## - Automatic eviction of least recently used entries
## - Configurable maximum size
## - Thread-safe operations (optional)
##
## **Design:**
## - Doubly-linked list for LRU ordering
## - Hash table for O(1) key lookup
## - Move-to-front on access (most recently used)
## - Evict from tail when capacity exceeded
##
## **Use Cases:**
## - Dependency resolution caching
## - Unification result caching
## - Build hash caching

import tables
import options
import locks
import strutils  # For formatFloat

type
  LRUNode[K, V] = ref object
    ## Node in the doubly-linked list
    key: K
    value: V
    prev: LRUNode[K, V]
    next: LRUNode[K, V]

  LRUCache*[K, V] = ref object
    ## Generic LRU cache with automatic eviction
    capacity: int
    cache: Table[K, LRUNode[K, V]]
    head: LRUNode[K, V]  # Most recently used (dummy head)
    tail: LRUNode[K, V]  # Least recently used (dummy tail)
    lock: Lock           # For thread-safe operations
    threadSafe: bool

  CacheStats* = object
    ## Cache performance statistics
    hits*: int
    misses*: int
    evictions*: int
    size*: int
    capacity*: int

# ============================================================================
# LRU Cache Construction
# ============================================================================

proc newLRUCache*[K, V](capacity: int, threadSafe: bool = false): LRUCache[K, V] =
  ## Create a new LRU cache with specified capacity.
  ##
  ## **Parameters:**
  ## - capacity: Maximum number of entries (must be > 0)
  ## - threadSafe: Enable thread-safe operations (default: false)
  ##
  ## **Returns:** New LRU cache instance
  ##
  ## **Example:**
  ## ```nim
  ## let cache = newLRUCache[string, int](capacity = 100)
  ## cache.put("key", 42)
  ## let value = cache.get("key")
  ## ```
  
  assert capacity > 0, "Cache capacity must be positive"
  
  result = LRUCache[K, V](
    capacity: capacity,
    cache: initTable[K, LRUNode[K, V]](),
    threadSafe: threadSafe
  )
  
  # Create dummy head and tail nodes
  result.head = LRUNode[K, V]()
  result.tail = LRUNode[K, V]()
  result.head.next = result.tail
  result.tail.prev = result.head
  
  if threadSafe:
    initLock(result.lock)

# ============================================================================
# Internal List Operations
# ============================================================================

proc removeNode[K, V](cache: LRUCache[K, V], node: LRUNode[K, V]) =
  ## Remove node from doubly-linked list (internal)
  let prev = node.prev
  let next = node.next
  prev.next = next
  next.prev = prev

proc addToHead[K, V](cache: LRUCache[K, V], node: LRUNode[K, V]) =
  ## Add node to head of list (most recently used)
  node.prev = cache.head
  node.next = cache.head.next
  cache.head.next.prev = node
  cache.head.next = node

proc moveToHead[K, V](cache: LRUCache[K, V], node: LRUNode[K, V]) =
  ## Move existing node to head (mark as most recently used)
  cache.removeNode(node)
  cache.addToHead(node)

proc removeTail[K, V](cache: LRUCache[K, V]): LRUNode[K, V] =
  ## Remove and return tail node (least recently used)
  result = cache.tail.prev
  cache.removeNode(result)

# ============================================================================
# Public Cache Operations
# ============================================================================

proc get*[K, V](cache: LRUCache[K, V], key: K): Option[V] =
  ## Get value from cache, marking it as recently used.
  ##
  ## **Parameters:**
  ## - key: Key to lookup
  ##
  ## **Returns:** Some(value) if found, None if not found
  ##
  ## **Complexity:** O(1)
  ##
  ## **Side Effect:** Moves accessed entry to front (most recently used)
  
  if cache.threadSafe:
    acquire(cache.lock)
  
  defer:
    if cache.threadSafe:
      release(cache.lock)
  
  if key in cache.cache:
    let node = cache.cache[key]
    cache.moveToHead(node)
    return some(node.value)
  else:
    return none(V)

proc put*[K, V](cache: LRUCache[K, V], key: K, value: V) =
  ## Put value into cache, evicting least recently used if necessary.
  ##
  ## **Parameters:**
  ## - key: Key to store
  ## - value: Value to store
  ##
  ## **Complexity:** O(1)
  ##
  ## **Side Effect:** May evict least recently used entry if at capacity
  
  if cache.threadSafe:
    acquire(cache.lock)
  
  defer:
    if cache.threadSafe:
      release(cache.lock)
  
  if key in cache.cache:
    # Update existing entry
    let node = cache.cache[key]
    node.value = value
    cache.moveToHead(node)
  else:
    # Add new entry
    let newNode = LRUNode[K, V](key: key, value: value)
    cache.cache[key] = newNode
    cache.addToHead(newNode)
    
    # Evict if over capacity
    if cache.cache.len > cache.capacity:
      let tail = cache.removeTail()
      cache.cache.del(tail.key)

proc contains*[K, V](cache: LRUCache[K, V], key: K): bool =
  ## Check if key exists in cache without affecting LRU order.
  ##
  ## **Parameters:**
  ## - key: Key to check
  ##
  ## **Returns:** true if key exists, false otherwise
  ##
  ## **Complexity:** O(1)
  ##
  ## **Note:** Does NOT mark entry as recently used
  
  if cache.threadSafe:
    acquire(cache.lock)
  
  defer:
    if cache.threadSafe:
      release(cache.lock)
  
  return key in cache.cache

proc delete*[K, V](cache: LRUCache[K, V], key: K): bool =
  ## Delete entry from cache.
  ##
  ## **Parameters:**
  ## - key: Key to delete
  ##
  ## **Returns:** true if entry was deleted, false if not found
  ##
  ## **Complexity:** O(1)
  
  if cache.threadSafe:
    acquire(cache.lock)
  
  defer:
    if cache.threadSafe:
      release(cache.lock)
  
  if key in cache.cache:
    let node = cache.cache[key]
    cache.removeNode(node)
    cache.cache.del(key)
    return true
  else:
    return false

proc clear*[K, V](cache: LRUCache[K, V]) =
  ## Clear all entries from cache.
  ##
  ## **Complexity:** O(n)
  
  if cache.threadSafe:
    acquire(cache.lock)
  
  defer:
    if cache.threadSafe:
      release(cache.lock)
  
  cache.cache.clear()
  cache.head.next = cache.tail
  cache.tail.prev = cache.head

proc len*[K, V](cache: LRUCache[K, V]): int =
  ## Get current number of entries in cache.
  ##
  ## **Returns:** Number of entries
  ##
  ## **Complexity:** O(1)
  
  if cache.threadSafe:
    acquire(cache.lock)
  
  defer:
    if cache.threadSafe:
      release(cache.lock)
  
  return cache.cache.len

proc capacity*[K, V](cache: LRUCache[K, V]): int =
  ## Get maximum capacity of cache.
  ##
  ## **Returns:** Maximum number of entries
  
  return cache.capacity

proc isFull*[K, V](cache: LRUCache[K, V]): bool =
  ## Check if cache is at capacity.
  ##
  ## **Returns:** true if cache is full, false otherwise
  
  return cache.len >= cache.capacity

# ============================================================================
# Cache Statistics
# ============================================================================

type
  LRUCacheWithStats*[K, V] = ref object
    ## LRU cache with performance statistics tracking
    cache: LRUCache[K, V]
    hits: int
    misses: int
    evictions: int

proc newLRUCacheWithStats*[K, V](capacity: int, threadSafe: bool = false): LRUCacheWithStats[K, V] =
  ## Create LRU cache with statistics tracking.
  ##
  ## **Parameters:**
  ## - capacity: Maximum number of entries
  ## - threadSafe: Enable thread-safe operations
  ##
  ## **Returns:** New cache with stats tracking
  
  result = LRUCacheWithStats[K, V](
    cache: newLRUCache[K, V](capacity, threadSafe),
    hits: 0,
    misses: 0,
    evictions: 0
  )

proc get*[K, V](cache: LRUCacheWithStats[K, V], key: K): Option[V] =
  ## Get value from cache with statistics tracking.
  
  let result = cache.cache.get(key)
  if result.isSome:
    cache.hits += 1
  else:
    cache.misses += 1
  return result

proc put*[K, V](cache: LRUCacheWithStats[K, V], key: K, value: V) =
  ## Put value into cache with statistics tracking.
  
  let wasFull = cache.cache.isFull
  let hadKey = key in cache.cache
  
  cache.cache.put(key, value)
  
  if wasFull and not hadKey:
    cache.evictions += 1

proc getStats*[K, V](cache: LRUCacheWithStats[K, V]): CacheStats =
  ## Get cache performance statistics.
  ##
  ## **Returns:** Statistics including hits, misses, evictions
  
  result = CacheStats(
    hits: cache.hits,
    misses: cache.misses,
    evictions: cache.evictions,
    size: cache.cache.len,
    capacity: cache.cache.capacity
  )

proc hitRate*[K, V](cache: LRUCacheWithStats[K, V]): float =
  ## Calculate cache hit rate.
  ##
  ## **Returns:** Hit rate as percentage (0.0 - 1.0)
  
  let total = cache.hits + cache.misses
  if total == 0:
    return 0.0
  return cache.hits.float / total.float

proc resetStats*[K, V](cache: LRUCacheWithStats[K, V]) =
  ## Reset statistics counters to zero.
  
  cache.hits = 0
  cache.misses = 0
  cache.evictions = 0

proc clear*[K, V](cache: LRUCacheWithStats[K, V]) =
  ## Clear all entries from cache (keeps statistics).
  
  cache.cache.clear()

proc delete*[K, V](cache: LRUCacheWithStats[K, V], key: K): bool =
  ## Delete entry from cache.
  result = cache.cache.delete(key)

# ============================================================================
# Iteration Support
# ============================================================================

iterator items*[K, V](cache: LRUCache[K, V]): (K, V) =
  ## Iterate over cache entries (no particular order).
  ##
  ## **Note:** Does NOT affect LRU order
  
  if cache.threadSafe:
    acquire(cache.lock)
  
  defer:
    if cache.threadSafe:
      release(cache.lock)
  
  for key, node in cache.cache.pairs:
    yield (key, node.value)

iterator itemsLRU*[K, V](cache: LRUCache[K, V]): (K, V) =
  ## Iterate over cache entries in LRU order (most recent first).
  ##
  ## **Note:** Does NOT affect LRU order
  
  if cache.threadSafe:
    acquire(cache.lock)
  
  defer:
    if cache.threadSafe:
      release(cache.lock)
  
  var current = cache.head.next
  while current != cache.tail:
    yield (current.key, current.value)
    current = current.next

# ============================================================================
# Debug and Inspection
# ============================================================================

proc `$`*[K, V](cache: LRUCache[K, V]): string =
  ## String representation of cache for debugging.
  
  result = "LRUCache(size=" & $cache.len & ", capacity=" & $cache.capacity & ")"

proc `$`*(stats: CacheStats): string =
  ## String representation of cache statistics.
  
  let hitRate = if stats.hits + stats.misses > 0:
    (stats.hits.float / (stats.hits + stats.misses).float * 100.0)
  else:
    0.0
  
  result = "CacheStats(hits=" & $stats.hits &
           ", misses=" & $stats.misses &
           ", evictions=" & $stats.evictions &
           ", size=" & $stats.size &
           ", capacity=" & $stats.capacity &
           ", hitRate=" & hitRate.formatFloat(ffDecimal, 2) & "%)"

# ============================================================================
# Unit Tests
# ============================================================================

when isMainModule:
  import unittest
  
  suite "LRU Cache Basic Operations":
    test "Create cache with capacity":
      let cache = newLRUCache[string, int](capacity = 3)
      check cache.len == 0
      check cache.capacity == 3
      check not cache.isFull
    
    test "Put and get single entry":
      let cache = newLRUCache[string, int](capacity = 3)
      cache.put("key1", 100)
      
      let value = cache.get("key1")
      check value.isSome
      check value.get == 100
    
    test "Get non-existent key returns None":
      let cache = newLRUCache[string, int](capacity = 3)
      let value = cache.get("missing")
      check value.isNone
    
    test "Update existing key":
      let cache = newLRUCache[string, int](capacity = 3)
      cache.put("key1", 100)
      cache.put("key1", 200)
      
      let value = cache.get("key1")
      check value.get == 200
      check cache.len == 1
    
    test "Contains check":
      let cache = newLRUCache[string, int](capacity = 3)
      cache.put("key1", 100)
      
      check "key1" in cache
      check "missing" notin cache
    
    test "Delete entry":
      let cache = newLRUCache[string, int](capacity = 3)
      cache.put("key1", 100)
      
      check cache.delete("key1")
      check "key1" notin cache
      check not cache.delete("missing")
    
    test "Clear cache":
      let cache = newLRUCache[string, int](capacity = 3)
      cache.put("key1", 100)
      cache.put("key2", 200)
      
      cache.clear()
      check cache.len == 0
      check "key1" notin cache
  
  suite "LRU Eviction":
    test "Evict least recently used when at capacity":
      let cache = newLRUCache[string, int](capacity = 3)
      cache.put("key1", 100)
      cache.put("key2", 200)
      cache.put("key3", 300)
      cache.put("key4", 400)  # Should evict key1
      
      check cache.len == 3
      check "key1" notin cache
      check "key2" in cache
      check "key3" in cache
      check "key4" in cache
    
    test "Access updates LRU order":
      let cache = newLRUCache[string, int](capacity = 3)
      cache.put("key1", 100)
      cache.put("key2", 200)
      cache.put("key3", 300)
      
      # Access key1 to make it most recently used
      discard cache.get("key1")
      
      # Add key4, should evict key2 (least recently used)
      cache.put("key4", 400)
      
      check "key1" in cache
      check "key2" notin cache
      check "key3" in cache
      check "key4" in cache
    
    test "Update preserves entry":
      let cache = newLRUCache[string, int](capacity = 3)
      cache.put("key1", 100)
      cache.put("key2", 200)
      cache.put("key3", 300)
      
      # Update key1
      cache.put("key1", 150)
      
      # Add key4, should evict key2
      cache.put("key4", 400)
      
      check "key1" in cache
      check cache.get("key1").get == 150
  
  suite "Cache Statistics":
    test "Track hits and misses":
      let cache = newLRUCacheWithStats[string, int](capacity = 3)
      cache.put("key1", 100)
      
      discard cache.get("key1")  # Hit
      discard cache.get("key2")  # Miss
      discard cache.get("key1")  # Hit
      
      let stats = cache.getStats()
      check stats.hits == 2
      check stats.misses == 1
      check cache.hitRate() > 0.6
    
    test "Track evictions":
      let cache = newLRUCacheWithStats[string, int](capacity = 2)
      cache.put("key1", 100)
      cache.put("key2", 200)
      cache.put("key3", 300)  # Eviction
      
      let stats = cache.getStats()
      check stats.evictions == 1
    
    test "Reset statistics":
      let cache = newLRUCacheWithStats[string, int](capacity = 3)
      cache.put("key1", 100)
      discard cache.get("key1")
      
      cache.resetStats()
      
      let stats = cache.getStats()
      check stats.hits == 0
      check stats.misses == 0
  
  suite "Iteration":
    test "Iterate over entries":
      let cache = newLRUCache[string, int](capacity = 3)
      cache.put("key1", 100)
      cache.put("key2", 200)
      cache.put("key3", 300)
      
      var count = 0
      for (key, value) in cache.items:
        count += 1
      
      check count == 3
    
    test "Iterate in LRU order":
      let cache = newLRUCache[string, int](capacity = 3)
      cache.put("key1", 100)
      cache.put("key2", 200)
      cache.put("key3", 300)
      
      var keys: seq[string]
      for (key, value) in cache.itemsLRU:
        keys.add(key)
      
      # Most recent first
      check keys[0] == "key3"
      check keys[2] == "key1"
