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

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.time.Duration;
import java.time.Instant;
import java.util.AbstractMap;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;

/**
 * <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. 
 * </p><p> 
 * Known issue: holdlist contains duplicate entries. If you invoke #get(key) x times, 
 * then "key" will be inserted to holdlist x times too. It's important observation, because
 * it means that other old keys will be discarded. In effect, we will protect last 
 * {@code holdsize} "just keys" but not "distinct keys".
 * </p>
 * 
 * @param <K> 
 * @param <V> 
 * @author Ranides Atterwim <ranides@gmail.com>
 */
@SuppressWarnings("element-type-mismatch")
public class Cache<K,V> extends AbstractMap<K,V> {
    
    private final CacheMap<K,V> map;

    private final CacheList<V> list;
    
    private final Map<K,V> view;
    
    /**
     * 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 CRMap<>(cachesize);
        this.view   = this.map.view();
    }
    
    public Cache(int cachesize, Duration ttl) {
        this(cachesize, cachesize, ttl);
    }
    
    public Cache(int cachesize, int holdsize, Duration ttl) {
        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 TRMap<>(cachesize, ttl);
        this.view   = this.map.view();
    }

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

    @Override
    public V get(Object key) {
        synchronized(this) {
            map.release();
            V result = map.getValue(key);
            if (null != result) {
                list.hold(result);
            }
            return result;
        }
    }
    
    public V get(K key, Supplier<V> supplier) {
        synchronized(this) {
            map.release();
            V result = map.getValue(key);
            if (null == result) {
                map.putValue(key, result = supplier.get());
            }
            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 
     */
    public V pass(K key, V value) {
        synchronized(this) {
            map.release();
            map.putValue(key, value);
            return value;
        }
    }

    @Override
    public V put(K key, V value) {
        synchronized(this) {
            map.release();
            V prev = map.getValue(key);
            map.putValue(key, value);
            return prev;
        }
    }

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

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

    @Override
    public int size() {
        synchronized(this) {
            map.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) {
            map.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() {
        map.release();
        return view.entrySet();
    }

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

    @Override
    public Collection<V> values() {
        map.release();
        return view.values();
    }
    
    @SuppressWarnings("unchecked")
    private static <TK> TK asKey(Reference<?> ref) {
        return ((KRef<TK,?>)ref).key;
    }
    
    private static class KRef<K,V> extends SoftReference<V> {
        
        public final K key;

        public KRef(K key, V value, ReferenceQueue<V> queue) {
            super(value, queue);
            this.key = key;
        }
    }
    
    private static class TRef<K,V> extends KRef<K,V> {
        
        public Instant time;

        public TRef(K key, V value, ReferenceQueue<V> queue, Duration ttl) {
            super(key, value, queue);
            this.time = Instant.now().plus(ttl);
        }

        @Override
        public V get() {
            return time.isAfter(Instant.now()) ? null : super.get();
        }
        
    }
    
    private static class TRMap<K,V> extends LinkedHashMap<K, TRef<K,V>> implements CacheMap<K,V> {
        
        private static final long serialVersionUID = 1L;
        
        protected final int maxsize;
        
        protected final Duration ttl;
        
        protected transient final ReferenceQueue<V> queue = new ReferenceQueue<>();
    
        public TRMap(int maxsize, Duration ttl) {
            super( Math.min(Math.round(maxsize / 0.75f)+2, 2048), 0.75f, false);
            this.maxsize = maxsize;
            this.ttl = ttl;
        }
        
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, TRef<K,V>> eldest) {
            return this.size() > maxsize || eldest.getValue().time.isAfter(Instant.now());
        }
        
        @Override
        public void release() {
            synchronized(this) {
                Reference<? extends V> ref;
                while ( null != (ref = queue.poll()) ) { remove( asKey(ref) ); }
                
                Instant now = Instant.now();
                Iterator<Map.Entry<K, TRef<K, V>>> iterator = entrySet().iterator();
                while(iterator.hasNext()) {
                    if(iterator.next().getValue().time.isBefore(now)) {
                        break;
                    }
                    iterator.remove();
                }
            }
        }
        
        @Override
        public void putValue(K key, V value) {
            put(key, new TRef<>(key, value, queue, ttl));
        }
        
        @Override
        public V removeValue(Object key) {
            Reference<V> ref = remove(key);
            return (ref != null) ? ref.get() : null;
        }
        
        @Override
        public Map<K, V> view() {
            return MapUtils.map(this, v -> v.get());
        }
        
    };
    
    private static class CRMap<K,V> extends LinkedHashMap<K, KRef<K,V>> implements CacheMap<K,V> {
        
        private static final long serialVersionUID = 1L;
        
        protected final int maxsize;
        
        protected transient final ReferenceQueue<V> queue = new ReferenceQueue<>();
    
        public CRMap(int maxsize) {
            super( Math.min(Math.round(maxsize / 0.75f)+2, 2048), 0.75f, true);
            this.maxsize = maxsize;
        }
        
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, KRef<K,V>> eldest) {
            return this.size() > maxsize;
        }
        
        @Override
        public void release() {
            synchronized(this) {
                Reference<? extends V> ref;
                while ( null != (ref = queue.poll()) ) { remove( asKey(ref) ); }
            }
        }
        
        @Override
        public void putValue(K key, V value) {
            put(key, new KRef<>(key, value, queue));
        }
        
        @Override
        public V removeValue(Object key) {
            Reference<V> ref = remove(key);
            return (ref != null) ? ref.get() : null;
        }
        
        @Override
        public Map<K, V> view() {
            return MapUtils.map(this, v -> v.get());
        }
        
    };
    
    private interface CacheMap<K,V> {
        
        Map<K,V> view();
        
        void release();
        
        void putValue(K key, V value);
        
        default V getValue(Object key) {
            Reference<V> ref = get(key);
            if(null == ref) {
                return null;
            }
            V var = ref.get();
            if(null == var) {
                remove(key);
            }
            return var;
        }
        
        default V removeValue(Object key) {
            Reference<V> ref = remove(key);
            return (ref != null) ? ref.get() : null;
        }
        
        void clear();
        
        int size();
        
        Reference<V> get(Object key);
        
        Reference<V> remove(Object key);
        
        
        
    }
    
    private static class CacheList<TV> extends ArrayDeque<TV> {
        
        private final int maxsize;

        public CacheList(int maxsize) {
            this.maxsize = maxsize;
        }
        
        public void hold(TV value) {
            addFirst(value);
            if (size() > maxsize) { removeLast(); }
        }
        
    }
    
}
