# SearchResultCache.java

    package com.university.bookstore.search;

    import java.util.ArrayList;
    import java.util.Deque;
    import java.util.HashMap;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.Map;
    import java.util.Optional;

    import com.university.bookstore.model.Material;

    /**
     * LRU (Least Recently Used) cache for search results.
     * Evicts least recently used items when the cache reaches its maximum size.
     *
     * <p>This implementation provides O(1) average time complexity for get and put operations
     * by using a HashMap for O(1) lookups and a Deque for O(1) access order tracking.</p>
     *
     * @author Navid Mohaghegh
     * @version 3.0
     * @since 2024-09-15
     */
    public class SearchResultCache {

        private final int maxSize;
        private final Map<String, CacheEntry> cache;
        private final Deque<String> accessOrder;

        /**
         * Creates a new search result cache with the specified maximum size.
         *
         * @param maxSize the maximum number of entries the cache can hold
         * @throws IllegalArgumentException if maxSize is not positive
         */
        public SearchResultCache(int maxSize) {
            if (maxSize <= 0) {
                throw new IllegalArgumentException("Cache size must be positive: " + maxSize);
            }

            this.maxSize = maxSize;
            this.cache = new HashMap<>();
            this.accessOrder = new LinkedList<>();
        }

        /**
         * Retrieves cached search results for the given key.
         * Updates the access order to mark this entry as recently used.
         *
         * @param key the cache key
         * @return Optional containing the cached results if found
         */
        public Optional<List<Material>> get(String key) {
            if (key == null) {
                return Optional.empty();
            }

            CacheEntry entry = cache.get(key);
            if (entry != null) {
                // Move to end (most recently used)
                accessOrder.remove(key);
                accessOrder.addLast(key);
                return Optional.of(new ArrayList<>(entry.results));
            }

            return Optional.empty();
        }

        /**
         * Stores search results in the cache with the given key.
         * Evicts the least recently used entry if the cache is full.
         *
         * @param key the cache key
         * @param results the search results to cache
         * @throws IllegalArgumentException if key is null
         */
        public void put(String key, List<Material> results) {
            if (key == null) {
                throw new IllegalArgumentException("Cache key cannot be null");
            }

            if (results == null) {
                throw new IllegalArgumentException("Search results cannot be null");
            }

            // If cache is full, remove least recently used entry
            if (cache.size() >= maxSize && !cache.containsKey(key)) {
                evictLRU();
            }

            // Store the new entry
            cache.put(key, new CacheEntry(results));

            // Update access order
            accessOrder.remove(key); // Remove if already exists
            accessOrder.addLast(key);
        }

        /**
         * Checks if the cache contains results for the given key.
         *
         * @param key the cache key
         * @return true if the key exists in the cache
         */
        public boolean containsKey(String key) {
            return key != null && cache.containsKey(key);
        }

        /**
         * Removes the entry with the given key from the cache.
         *
         * @param key the cache key to remove
         * @return true if an entry was removed, false if key not found
         */
        public boolean remove(String key) {
            if (key == null) {
                return false;
            }

            CacheEntry removed = cache.remove(key);
            accessOrder.remove(key);
            return removed != null;
        }

        /**
         * Clears all entries from the cache.
         */
        public void clear() {
            cache.clear();
            accessOrder.clear();
        }

        /**
         * Gets the current number of entries in the cache.
         *
         * @return the cache size
         */
        public int size() {
            return cache.size();
        }

        /**
         * Gets the maximum number of entries the cache can hold.
         *
         * @return the maximum cache size
         */
        public int getMaxSize() {
            return maxSize;
        }

        /**
         * Checks if the cache is empty.
         *
         * @return true if no entries are cached
         */
        public boolean isEmpty() {
            return cache.isEmpty();
        }

        /**
         * Checks if the cache is full.
         *
         * @return true if the cache has reached its maximum size
         */
        public boolean isFull() {
            return cache.size() >= maxSize;
        }

        /**
         * Gets cache statistics including hit ratio and average age.
         *
         * @return cache statistics
         */
        public CacheStats getStats() {
            long totalRequests = 0;
            long totalHits = 0;
            long totalAge = 0;
            long currentTime = System.currentTimeMillis();

            for (CacheEntry entry : cache.values()) {
                totalRequests += entry.accessCount;
                totalHits += entry.hitCount;
                totalAge += (currentTime - entry.timestamp);
            }

            double hitRatio = totalRequests > 0 ? (double) totalHits / totalRequests : 0.0;
            double averageAge = cache.size() > 0 ? (double) totalAge / cache.size() : 0.0;

            return new CacheStats(
                cache.size(),
                maxSize,
                hitRatio,
                averageAge,
                totalRequests,
                totalHits
            );
        }

        /**
         * Evicts the least recently used entry from the cache.
         */
        private void evictLRU() {
            if (!accessOrder.isEmpty()) {
                String lruKey = accessOrder.removeFirst();
                cache.remove(lruKey);
            }
        }

        /**
         * Internal class representing a cache entry.
         */
        private static class CacheEntry {
            final List<Material> results;
            final long timestamp;
            long accessCount;
            long hitCount;

            CacheEntry(List<Material> results) {
                this.results = new ArrayList<>(results);
                this.timestamp = System.currentTimeMillis();
                this.accessCount = 0;
                this.hitCount = 0;
            }
        }

        /**
         * Statistics class for cache performance monitoring.
         */
        public static class CacheStats {
            private final int currentSize;
            private final int maxSize;
            private final double hitRatio;
            private final double averageAge;
            private final long totalRequests;
            private final long totalHits;

            public CacheStats(int currentSize, int maxSize, double hitRatio,
                             double averageAge, long totalRequests, long totalHits) {
                this.currentSize = currentSize;
                this.maxSize = maxSize;
                this.hitRatio = hitRatio;
                this.averageAge = averageAge;
                this.totalRequests = totalRequests;
                this.totalHits = totalHits;
            }

            public int getCurrentSize() { return currentSize; }
            public int getMaxSize() { return maxSize; }
            public double getHitRatio() { return hitRatio; }
            public double getAverageAge() { return averageAge; }
            public long getTotalRequests() { return totalRequests; }
            public long getTotalHits() { return totalHits; }

            @Override
            public String toString() {
                return String.format("CacheStats[Size=%d/%d, HitRatio=%.2f%%, AvgAge=%.0fms, Requests=%d, Hits=%d]",
                    currentSize, maxSize, hitRatio * 100, averageAge, totalRequests, totalHits);
            }
        }

        @Override
        public String toString() {
            return String.format("SearchResultCache[Size=%d/%d, HitRatio=%.2f%%]",
                size(), maxSize, getStats().getHitRatio() * 100);
        }
    }