/*
 * @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.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;

import net.ranides.assira.reflection.util.ClassUtils;
import net.ranides.assira.system.RuntimeUtils;

/**
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public abstract class CacheMap<K,V> implements Cache<K,V> {

    protected static final ReferenceQueue QUEUE = new ReferenceQueue<>();

    protected static final boolean USE_THREAD = isThreadEnabled();

    static {
        if(USE_THREAD) {
            Thread thread = new Thread(CacheMap::cleanupRemove, "assira cache cleanup");
            thread.setDaemon(true);
            thread.start();
        }
    }

    protected final Function<K,V> supplier;

    protected CacheMap(Function<K, V> supplier) {
        this.supplier = supplier;
    }

    public static <K,V> CacheMap<K,V> getInstance(Function<K,V> supplier) {
        return new SimpleCache<K, V>(supplier);
    }

    public static <K,V> CacheMap<K,V> getInstance(int capacity, Function<K,V> supplier) {
        return new FixedCache<K, V>(capacity, supplier);
    }

    public static <K,V> CacheMap<K,V> getInstance(Duration ttl, Function<K,V> supplier) {
        return new TTLCache<K, V>(ttl, supplier);
    }

    public final V get(K key) {
        return get(key, supplier);
    }

    public final V get(K key, Function<K,V> function) {
        return get(key, () -> function.apply(key));
    }

    public final V get(K key, Supplier<V> function) {
        Reference<V> ref = iget(key);
        if(null == ref) {
            cleanup();
            return iput(key, function.get());
        }
        V ret = ref.get();
        if(null == ret) {
            cleanup();
            return iput(key, function.get());
        }
        return ret;
    }

    public final void clear() {
        iclear();
        cleanup();
    }

    protected abstract Reference<V> iget(K key);

    protected abstract V iput(K key, V value);

    protected abstract void iremove(K key);

    protected abstract void iclear();

    public static void cleanup(boolean block) {
        if(block) {
            cleanupRemove();
        } else {
            cleanupPoll();
        }
    }

    protected static void cleanup() {
        if(!USE_THREAD) {
            cleanupPoll();
        }
    }

    protected static void cleanupPoll() {
        KRef ref;
        while ( null != (ref = (KRef)QUEUE.poll()) ) {
            ref.src.iremove(ref.key);
        }
    }

    protected static void cleanupRemove() {
        try {
            while (true) {
                KRef ref = (KRef) QUEUE.remove();
                ref.src.iremove(ref.key);
            }
        } catch (InterruptedException cause) {
            // shutdown
        }
    }

    protected static boolean isThreadEnabled() {
        if( !RuntimeUtils.getProperty("assira.cache.use-thread", "false").equals("true")) {
            return false;
        }
        // check if there is cleaner for Java EE
        try {
            ClassUtils.forName("net.ranides.assira.ee.CacheMapCleaner");
            return false;
        } catch (ClassNotFoundException cause) {
            return true;
        }
    }

    private static final class SimpleCache<K,V> extends CacheMap<K,V> {

        private final Map<K,KRef<K,V>> map = new ConcurrentHashMap<>();

        public SimpleCache(Function<K, V> supplier) {
            super(supplier);
        }

        @Override
        protected V iput(K key, V value) {
            map.put(key, new KRef<K, V>(this, key, value));
            return value;
        }

        @Override
        public Reference<V> iget(K key) {
            return map.get(key);
        }

        @Override
        protected void iremove(K key) {
            map.remove(key);
        }

        @Override
        public void iclear() {
            map.clear();
        }
    }

    private static final class FixedCache<K,V> extends CacheMap<K,V> {

        private final Map<K,KRef<K,V>> map;

        public FixedCache(int capacity, Function<K, V> supplier) {
            super(supplier);
            this.map = new FHashMap<>(capacity);
        }

        @Override
        protected V iput(K key, V value) {
            synchronized (map) {
                map.put(key, new KRef<K, V>(this, key, value));
            }
            return value;
        }

        @Override
        public Reference<V> iget(K key) {
            synchronized (map) {
                return map.get(key);
            }
        }

        @Override
        protected void iremove(K key) {
            synchronized (map) {
                map.remove(key);
            }
        }

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

    private static final class TTLCache<K,V> extends CacheMap<K,V> {

        private final Duration ttl;

        private final Map<K,KRef<K,V>> map = new ConcurrentHashMap<>();

        public TTLCache(Duration ttl, Function<K, V> supplier) {
            super(supplier);
            this.ttl = ttl;
        }

        @Override
        protected V iput(K key, V value) {
            map.put(key, new TRef<K, V>(this, key, value, ttl));
            return value;
        }

        @Override
        public Reference<V> iget(K key) {
            return map.get(key);
        }

        @Override
        protected void iremove(K key) {
            map.remove(key);
        }

        @Override
        public void iclear() {
            map.clear();
        }
    }

    private static class KRef<K,V> extends SoftReference<V> {

        public final CacheMap<K,V> src;
        public final K key;

        public KRef(CacheMap<K,V> src, K key, V value) {
            super(value, QUEUE);
            this.src = src;
            this.key = key;
        }
    }

    private static class TRef<K,V> extends KRef<K,V> {

        public Instant time;

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

        @Override
        public V get() {
            return time.isBefore(Instant.now()) ? null : super.get();
        }

    }

    private static class FHashMap<K,V> extends LinkedHashMap<K,V> {

        private final int maxsize;

        public FHashMap(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, V> eldest) {
            return this.size() > maxsize;
        }
    }

}
