package org.ggp.base.util.statemachine.cache;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * This is a generic implementation of a Time-To-Live cache
 * that maps keys of type K to values of type V. It's backed
 * by a hashmap, and whenever a pair (K,V) is accessed, their
 * TTL is reset to the starting TTL (which is the parameter
 * passed to the constructor). On the other hand, when the
 * method prune() is called, the TTL of all of the pairs in the
 * map is decremented, and pairs whose TTL has reached zero are
 * removed.
 *
 * While this class implements the Map interface, keep in mind
 * that it only decrements the TTL of an entry when that entry
 * is accessed directly.
 *
 * @param <K> Key type
 * @param <V> Value type
 */
public final class TtlCache<K, V> implements Map<K,V>
{
    private final class Entry
    {
        public int ttl;
        public final V value;

        public Entry(V value, int ttl)
        {
            this.value = value;
            this.ttl = ttl;
        }

        @Override
        @SuppressWarnings("unchecked")
        public boolean equals(Object o) {
            if (o instanceof TtlCache.Entry) {
                return ((Entry)o).value.equals(value);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return value.hashCode();
        }
    }

    private final Map<K, Entry> contents;
    private final int ttl;

    public TtlCache(int ttl)
    {
        this.contents = new HashMap<K, Entry>();
        this.ttl = ttl;
    }

    @Override
    public synchronized boolean containsKey(Object key)
    {
        return contents.containsKey(key);
    }

    @Override
    public synchronized V get(Object key)
    {
        Entry entry = contents.get(key);
        if (entry == null)
            return null;

        // Reset the TTL when a value is accessed directly.
        entry.ttl = ttl;
        return entry.value;
    }

    public synchronized void prune()
    {
        List<K> toPrune = new ArrayList<K>();
        for (K key : contents.keySet())
        {
            Entry entry = contents.get(key);
            if (entry.ttl == 0)
            {
                toPrune.add(key);
            }
            entry.ttl--;
        }

        for (K key : toPrune)
        {
            contents.remove(key);
        }
    }

    @Override
    public synchronized V put(K key, V value)
    {
        Entry x = contents.put(key, new Entry(value, ttl));
        if(x == null) return null;
        return x.value;
    }

    @Override
    public synchronized int size()
    {
        return contents.size();
    }

    @Override
    public synchronized void clear() {
        contents.clear();
    }

    @Override
    public synchronized boolean containsValue(Object value) {
        return contents.containsValue(value);
    }

    @Override
    public synchronized boolean isEmpty() {
        return contents.isEmpty();
    }

    @Override
    public synchronized Set<K> keySet() {
        return contents.keySet();
    }

    @Override
    public synchronized void putAll(Map<? extends K, ? extends V> m) {
        for(Map.Entry<? extends K, ? extends V> anEntry : m.entrySet()) {
            this.put(anEntry.getKey(), anEntry.getValue());
        }
    }

    @Override
    public synchronized V remove(Object key) {
        return contents.remove(key).value;
    }

    @Override
    public synchronized Collection<V> values() {
        Collection<V> theValues = new HashSet<V>();
        for (Entry e : contents.values())
            theValues.add(e.value);
        return theValues;
    }

    private class entrySetMapEntry implements Map.Entry<K,V> {
        private K key;
        private V value;

        entrySetMapEntry(K k, V v) {
            key = k;
            value = v;
        }

        @Override
        public K getKey() { return key; }
        @Override
        public V getValue() { return value; }
        @Override
        public V setValue(V value) { return (this.value = value); }
    }

    @Override
    public synchronized Set<java.util.Map.Entry<K, V>> entrySet() {
        Set<Map.Entry<K,V>> theEntries = new HashSet<Map.Entry<K, V>>();
        for (Map.Entry<K, Entry> e : contents.entrySet())
            theEntries.add(new entrySetMapEntry(e.getKey(), e.getValue().value));
        return theEntries;
    }
}