/*
 * @author Ranides Atterwim {@literal <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.RuntimeConfiguration;

/**
 * This class provides few cache implementations with different behaviour.
 *
 * Please note, that CacheMap can be configured how to compute non-existing values, so you don't
 * have to provide computation function in every call to {@link #get(Object)}.
 *
 * @param <K> K
 * @param <V> V
 * @author Ranides Atterwim {@literal <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;
    }

    /**
     * Returns new cache, which does not remove any entries on their own.
     * Cache entries are removed only by garbage collector.
     *
     * It uses provided function to generate results if plain {@code get} method was called.
     *
     * @param supplier supplier
     * @param <K> K
     * @param <V> V
     * @return cache
     */
    public static <K,V> CacheMap<K,V> getInstance(Function<K,V> supplier) {
        return new SimpleCache<>(supplier);
    }

    /**
     * Returns new cache with limited capacity.
     * Cache entries are removed if limit capacity was reached. Removes oldest accessed entries.
     * Cache entries are removed by garbage collector.
     *
     * It uses provided function to generate results if plain {@code get} method was called.
     *
     * @param capacity capacity
     * @param supplier supplier
     * @param <K> K
     * @param <V> V
     * @return cache
     */
    public static <K,V> CacheMap<K,V> getInstance(int capacity, Function<K,V> supplier) {
        return new FixedCache<>(capacity, supplier);
    }

    /**
     * Returns new cache with limited lifetime.
     * Cache entries are removed if they were created more than "duration" time ago.
     * Cache entries are removed by garbage collector.
     *
     * It uses provided function to generate results if plain {@code get} method was called.
     *
     * @param ttl ttl
     * @param supplier supplier
     * @param <K> K
     * @param <V> V
     * @return cache
     */
    public static <K,V> CacheMap<K,V> getInstance(Duration ttl, Function<K,V> supplier) {
        return new TTLCache<>(ttl, supplier);
    }

    /**
     * This method returns cached result or it creates new one
     * by calling "supplier" associated with this cache.
     *
     * @param key key
     * @return value
     */
    public final V get(K key) {
        return get(key, supplier);
    }

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

    @Override
    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();

    /**
     * This static method invalidates cache entries in ALL caches created inside application.
     * It can do work in current thread (blocking it) or in the background (returning instantly).
     *
     * @param block block
     */
    public static void cleanup(boolean block) {
        if(block) {
            cleanupRemove();
        } else {
            cleanupPoll();
        }
    }

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

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

    @SuppressWarnings("unchecked")
    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( !RuntimeConfiguration.FEATURE_CACHE_DAEMON()) {
            return false;
        }
        // check if there is cleaner for Java EE
        return !ClassUtils.forName("net.ranides.assira.ee.CacheMapCleaner").isPresent();
    }

    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<>(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<>(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<>(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;

        @SuppressWarnings("unchecked")
        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 final 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;
        }
    }

}
