package ca.recrown.islandsurvivalcraft.utilities.caching;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

public class Cache<K, V> {
    private final int maxSize;
    private final ConcurrentHashMap<K, CacheValue<K, V>> data;
    private final CacheUsageStack<K, V> usage = new CacheUsageStack<>();
    private final ReentrantLock lock = new ReentrantLock();

    public Cache(int maxSize) {
        data = new ConcurrentHashMap<>(maxSize + 1, 0.75f, 16);
        this.maxSize = maxSize;
    }

    public Cache() {
        this(1024);
    }

    /**
     * Sets the value with the respective associated key.
     * If the addition of this value and key cause the cache to be bigger than its designated max size,
     * It will remove one of the least used values in the cache to accomodate.
     * @param key The key to associate with the value.
     * @param value The value to store.
     */
    public void set(K key, V value) {
        if (data.containsKey(key)) {
            data.get(key).value = value;
        } else {
            lock.lock();
            try {
                if (data.containsKey(key)) return;
                CacheValue<K, V> val = new CacheValue<>(key, value);
                data.put(key, val);
                usage.add(val);
                if (data.size() > maxSize) {
                    data.remove(usage.pop().key);
                }
            } finally {
                lock.unlock();
            }
        }
    }

    /**
     * Retrieves cached value associated to the key.
     * Uses equals.
     * If does not exist, returns null.
     * @param key the key associated with the value.
     * @return the value associated to the key.
     */
    public V get(K key) {
        CacheValue<K, V> cacheValue = data.get(key);
        if (cacheValue == null) return null;
        usage.tryMoveToTop(cacheValue);
        return cacheValue.value;
    }

    /**
     * Clears the cache of all values.
     */
    public void clearCache() {
        lock.lock();
        try {
            data.clear();
            usage.clear();
        } finally {
            lock.unlock();
        }
    }
}