/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.collection.map;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

/**
 * <p>
 * Kolekcja zaprojektowana jako podręczna pamięć na obiekty, których koszt 
 * utworzenia jest relatywnie wysoki. Kolekcja przechowuje {@code holdsize} 
 * ostatnio używanych elementów, bez względu na to, czy istnieją inne referencje 
 * do nich. Przez "użycie" rozumiemy dowolną operację zapis lub odczytu. 
 * W szczególności taka strategia pozwala polegać na metodzie "containsKey", 
 * która uchroni wartość przed natychmiastowym usunięciem - jeśli tylko stwierdzono, 
 * że klucz istnieje. Dodatkowo kolekcja przechowuje {@code cachesize} elementów 
 * za pomocą WeakReference - obiekty te mogą zostać zwolnione przez GC, jeśli 
 * zajdzie taka potrzeba.
 * </p><p>
 * Uwaga: wszystkie widoki zwracane przez "cache" są niebezpieczne w tym sensie, 
 * że nie można wywoływać <b>żadnej</b> metody na obiekcie "cache" w czasie 
 * iterowania po widoku. Jest to spowodowane tym, że cache uwzględnia czas dostępu 
 * do obiektów: nawet odczyt zmienia wewnętrzną reprezentację. W efekcie, jeśli 
 * podczas iteracji zostanie wywołana jakakolwiek inna metoda, to zostanie rzucone 
 * ConcurentModificationException. 
 * 
 * @todo (prev # 8) issue: holdlist contains duplicate entries
 *        if you invoke #get(key) x times, it will put x "key" entries inside list
 * </p>
 * <div class="message-note">thread-safe class</div>
 * @param <K> 
 * @param <V> 
 * @author ranides
 */
@SuppressWarnings("element-type-mismatch")
public class Cache<K,V> extends AbstractMap<K,V> {

    private final CacheMap<K,V> map;

    private final CacheList<V> list;
    
    // może zrobić ciekawy interfejs (oraz implementację): LockingIterator. Taki
    // iterator blokowałby kolekcję, dopóki nie doiterowałby do końca zbioru.
    // po osiągnięciu końca zwalniałby lock
    private final Map<K,V> view;
    
    private final ReferenceQueue<V> queue = new ReferenceQueue<>();

    /**
     * Tworzy cache, który przechowuje maksymalnie 1024 elementy, w tym 32 
     * ostatnio użyte są chronione przez usunięciem przez GC.
     */
    public Cache() {
        this(1024, 32);
        
    }
    
    /**
     * Tworzy cache, którzy przechowuje {@code cachesize} ostatnio używanych 
     * elementów, bez względu na to, czy istnieją do nich inne referencje.
     * @param cachesize ilość przechowywanych elementów
     */
    public Cache(int cachesize) {
        this(cachesize, cachesize);
    }

    /**
     * Tworzy cache, którzy przechowuje {@code cachesize} ostatnio używanych 
     * elementów, w tym chroni przez usunięciem przez GC ostatnie {@code holdsize}
     * elementów.
     * @param cachesize ilość przechowywanych elementów
     * @param holdsize ilość elementów chronionych przed GC
     */
    public Cache(int cachesize, int holdsize) {
        if( cachesize < holdsize ) {
            throw new IllegalArgumentException("cachesize must be greater or equal to holdsize");
        }
        this.list   = new CacheList<>(Math.max(1,holdsize));
        this.map    = new CacheMap<>(cachesize);
        this.view   = new CacheView<>(map);
    }

    @Override
    public boolean containsKey(Object key) {
        synchronized(this) {
            return null != get(key);
        }
    }

    @Override
    public V get(Object key) {
        synchronized(this) {
            Reference<V> ref = map.get(key);
            if(ref == null) {
                return null;
            }
            // From the SoftReference we get the value, which can be
            // null if it was removed in the processQueue() method defined below
            V result = ref.get();
            if (result == null) {
                map.remove(key);
            } else {
                // hold value for a while...
                list.hold(result);
            }
            return result;
        }
    }
    
    /**
     * Wstawia wartość do cache'u i zwraca ją jako wynik. Jeśli w mapie
     * istniała już jakaś wartość, to ją zastępuje.
     * @param key
     * @param value
     * @return 
     * @since 0.67.2
     */
    public V pass(K key, V value) {
        synchronized(this) {
            release();
            map.put(key, new ReferenceEntry<>(key, value, queue));
            return value;
        }
    }

    @Override
    public V put(K key, V value) {
        synchronized(this) {
            release();
            V prev = asValue(map.get(key));
            map.put(key, new ReferenceEntry<>(key, value, queue));
            return prev;
        }
    }

    @Override
    public V remove(Object key) {
        synchronized(this) {
            release();
            return asValue(map.remove(key));
        }
    }

    @Override
    public void clear() {
        synchronized(this) {
            list.clear();
            release();
            map.clear();
        }
    }

    @Override
    public int size() {
        synchronized(this) {
            release();
            return map.size();
        }
    }
    
    /**
     * Returns number of holded entries
     * Please note, that holdlist could contain duplicated entries, 
     * so returned number could be deceptively greater than size of map. 
     * @return
     */
    public int holdsize() {
        synchronized(this) {
            release();
            return list.size();
        }
    }
    
    /**
     * Holds value specified by key
     * @param key
     * @return 
     */
    public boolean hold(K key) {
        synchronized(this) {
            return null != get(key);
        }
    }
    
    /**
     * Holds as many entries as possible
     * @return number of holded values
     */
    public int hold() {
        synchronized(this) {
            for(V value : values() ) {
                if(null != value) {
                    list.hold(value);
                }
            }
            return list.size();
        }
    }
    
    @Override
    public Set<Entry<K,V>> entrySet() {
        release();
        return view.entrySet();
    }

    @Override
    public Set<K> keySet() {
        release();
        return view.keySet();
    }

    @Override
    public Collection<V> values() {
        release();
        return view.values();
    }
    
    private void release() {
        synchronized(this) {
            Reference<? extends V> ref;
            while ( null != (ref = queue.poll()) ) { map.remove( asKey(ref) ); }
        }
    }
    
    private static <TV> TV asValue(Reference<? extends TV> ref) {
        return (ref != null) ? ref.get() : null;
    }
    
    @SuppressWarnings("unchecked")
    private static <TK> TK asKey(Reference<?> ref) {
        return (ref != null) ? ((ReferenceEntry<TK,?>)ref).key : null;
    }
    
    private static class ReferenceEntry<KT,VT> extends SoftReference<VT> {
        
        private final KT key;

        public ReferenceEntry(KT key, VT value, ReferenceQueue<VT> queue) {
            super(value, queue);
            this.key = key;
        }
    }
    
    private static class CacheMap<TK,TV> extends LinkedHashMap<TK, Reference<TV>> {
        
        private final int maxsize;

        public CacheMap(int maxsize) {
            super( ic(maxsize), 0.75f, true);
            this.maxsize = maxsize;
        }
        
        private static int ic(int maxsize) {
            return ( maxsize == Integer.MAX_VALUE) ? 16 :Math.min(Math.round(maxsize / 0.75f)+2, 2048);
        }

        @Override
        protected boolean removeEldestEntry(Entry<TK, Reference<TV>> eldest) {
            return size() > maxsize;
        }
        
    };
    
    private static class CacheList<TV> extends LinkedList<TV> {
        
        private final int maxsize;

        public CacheList(int maxsize) {
            this.maxsize = maxsize;
        }
        
        public void hold(TV value) {
            addFirst(value);
            if (size() > maxsize) { removeLast(); }
        }
        
    }
    
    private static class CacheView<TK,TV> extends MapAdapter<TK, Reference<TV>, TV> {

        public CacheView(Map<TK, Reference<TV>> content) {
            super(content);
        }

        @Override
        public TV apply(Entry<TK, Reference<TV>> source) {
            return asValue(source.getValue());
        }
    }

}
