package ca.recrown.islandsurvivalcraft.caching;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;

public class Cache<K, V> {
    private final int maxSize;
    private final ConcurrentHashMap<K, CacheValue<K, V>> data;
    private final UsageStack<K, V> usage = new UsageStack<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final WriteLock writeLock = lock.writeLock();
    private final ReadLock readLock = lock.readLock();

    public Cache(int maxSize) {
        data = new ConcurrentHashMap<>(maxSize + 1, 0.75f, 6);
        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 {
            writeLock.lock();
            try {
                CacheValue<K, V> val = new CacheValue<>();
                val.key = key;
                val.value = value;
                data.put(key, val);
                usage.add(val);
                if (data.size() > maxSize) {
                    data.remove(usage.pop().key);
                }
            } finally {
                writeLock.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) {
        readLock.lock();
        CacheValue<K, V> value = null;
        try {
            if (!data.containsKey(key)) return null;
            value = data.get(key);
            usage.tryMoveToTop(value);
        } finally {
            readLock.unlock();
        }
        return value.value;
    }

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