// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.worldgen.util.cache;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import java.util.logging.Level;
import com.hypixel.hytale.server.worldgen.util.LogUtil;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import java.util.Objects;
import com.hypixel.hytale.math.util.MathUtil;
import java.util.concurrent.locks.StampedLock;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import java.util.ArrayDeque;
import java.lang.ref.WeakReference;
import com.hypixel.hytale.server.core.HytaleServer;
import it.unimi.dsi.fastutil.HashCommon;
import javax.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import java.lang.ref.Cleaner;
import java.util.concurrent.ScheduledFuture;
import java.util.function.BiConsumer;
import java.util.function.Function;
import javax.annotation.Nonnull;

public class ConcurrentSizedTimeoutCache<K, V> implements Cache<K, V>
{
    private static final int BUCKET_MIN_CAPACITY = 16;
    private static final float BUCKET_LOAD_FACTOR = 0.75f;
    private final int bucketMask;
    @Nonnull
    private final Bucket<K, V>[] buckets;
    @Nonnull
    private final Function<K, K> computeKey;
    @Nonnull
    private final Function<K, V> computeValue;
    @Nonnull
    private final BiConsumer<K, V> destroyer;
    @Nonnull
    private final ScheduledFuture<?> future;
    @Nonnull
    private final Cleaner.Cleanable cleanable;
    
    public ConcurrentSizedTimeoutCache(final int capacity, final int concurrencyLevel, final long timeout, @Nonnull final TimeUnit timeoutUnit, @Nonnull final Function<K, K> computeKey, @Nonnull final Function<K, V> computeValue, @Nullable final BiConsumer<K, V> destroyer) {
        final long timeout_ns = timeoutUnit.toNanos(timeout);
        final int bucketCount = HashCommon.nextPowerOfTwo(concurrencyLevel);
        final int bucketCapacity = Math.max(16, HashCommon.nextPowerOfTwo(capacity / bucketCount));
        this.bucketMask = bucketCount - 1;
        this.buckets = new Bucket[bucketCount];
        for (int i = 0; i < bucketCount; ++i) {
            this.buckets[i] = new Bucket<K, V>(bucketCapacity, timeout_ns);
        }
        this.computeKey = computeKey;
        this.computeValue = computeValue;
        this.destroyer = ((destroyer != null) ? destroyer : ConcurrentSizedTimeoutCache::noopDestroy);
        this.future = HytaleServer.SCHEDULED_EXECUTOR.scheduleWithFixedDelay(new CleanupRunnable<Object, Object>(new WeakReference<Cache<?, ?>>(this)), timeout, timeout, timeoutUnit);
        this.cleanable = CleanupFutureAction.CLEANER.register(this, new CleanupFutureAction(this.future));
    }
    
    @Override
    public void shutdown() {
        this.cleanable.clean();
        for (final Bucket<K, V> bucket : this.buckets) {
            bucket.clear(this.destroyer);
        }
    }
    
    @Override
    public void cleanup() {
        for (final Bucket<K, V> bucket : this.buckets) {
            bucket.cleanup(this.destroyer);
        }
    }
    
    @Nonnull
    @Override
    public V get(final K key) {
        if (this.future.isCancelled()) {
            throw new IllegalStateException("Cache has been shutdown!");
        }
        final int hash = HashCommon.mix(key.hashCode());
        return this.buckets[hash & this.bucketMask].compute(key, this.computeKey, this.computeValue, this.destroyer);
    }
    
    private static <K, V> void noopDestroy(final K key, final V value) {
    }
    
    private static class Bucket<K, V>
    {
        private final int capacity;
        private final int trimThreshold;
        private final long timeout_ns;
        private final ArrayDeque<CacheEntry<K, V>> pool;
        private final Object2ObjectOpenHashMap<K, CacheEntry<K, V>> map;
        private final StampedLock lock;
        
        public Bucket(final int capacity, final long timeout_ns) {
            this.lock = new StampedLock();
            this.capacity = capacity;
            this.trimThreshold = MathUtil.fastFloor(capacity * 0.75f);
            this.timeout_ns = timeout_ns;
            this.pool = new ArrayDeque<CacheEntry<K, V>>(capacity);
            this.map = new Object2ObjectOpenHashMap<K, CacheEntry<K, V>>(capacity, 0.75f);
        }
        
        @Nonnull
        public V compute(@Nonnull final K key, @Nonnull final Function<K, K> computeKey, @Nonnull final Function<K, V> computeValue, @Nonnull final BiConsumer<K, V> destroyer) {
            final long timestamp = System.nanoTime();
            final long readStamp = this.lock.readLock();
            try {
                final CacheEntry<K, V> entry = this.map.get(key);
                if (entry != null) {
                    return entry.markAndGet(timestamp);
                }
            }
            finally {
                this.lock.unlockRead(readStamp);
            }
            final K newKey = computeKey.apply(key);
            V resultValue;
            final V newValue = resultValue = computeValue.apply(key);
            final long writeStamp = this.lock.writeLock();
            try {
                final CacheEntry<K, V> newEntry = this.pool.isEmpty() ? new CacheEntry<K, V>() : this.pool.poll();
                Objects.requireNonNull(newEntry, "CacheEntry pool returned null entry!");
                newEntry.key = newKey;
                newEntry.value = newValue;
                newEntry.timestamp = timestamp;
                final CacheEntry<K, V> currentEntry = this.map.putIfAbsent(newKey, newEntry);
                if (currentEntry != null) {
                    Objects.requireNonNull(currentEntry.value);
                    resultValue = currentEntry.value;
                    currentEntry.timestamp = timestamp;
                    newEntry.key = null;
                    newEntry.value = null;
                    if (this.pool.size() < this.capacity) {
                        this.pool.offer(newEntry);
                    }
                }
            }
            finally {
                this.lock.unlockWrite(writeStamp);
            }
            if (newValue != resultValue) {
                destroyer.accept(newKey, newValue);
            }
            return resultValue;
        }
        
        public void cleanup(@Nullable final BiConsumer<K, V> destroyer) {
            final long writeStamp = this.lock.writeLock();
            try {
                final boolean needsTrim = this.map.size() >= this.trimThreshold;
                final long expireTimestamp = System.nanoTime() - this.timeout_ns;
                final ObjectIterator<Object2ObjectMap.Entry<K, CacheEntry<K, V>>> it = this.map.object2ObjectEntrySet().fastIterator();
                while (it.hasNext()) {
                    final CacheEntry<K, V> entry = (CacheEntry<K, V>)it.next().getValue();
                    if (entry == null) {
                        LogUtil.getLogger().at(Level.SEVERE).log("Found null entry in cache bucket during cleanup!");
                        it.remove();
                    }
                    else {
                        if (entry.timestamp < expireTimestamp) {
                            continue;
                        }
                        it.remove();
                        if (destroyer != null) {
                            destroyer.accept(entry.key, entry.value);
                        }
                        entry.key = null;
                        entry.value = null;
                        if (this.pool.size() >= this.capacity) {
                            continue;
                        }
                        this.pool.offer(entry);
                    }
                }
                if (needsTrim && this.map.size() < this.capacity) {
                    this.map.trim(this.capacity);
                }
            }
            finally {
                this.lock.unlockWrite(writeStamp);
            }
        }
        
        public void clear(@Nonnull final BiConsumer<K, V> destroyer) {
            final long writeStamp = this.lock.writeLock();
            try {
                final ObjectIterator<Object2ObjectMap.Entry<K, CacheEntry<K, V>>> it = this.map.object2ObjectEntrySet().fastIterator();
                while (it.hasNext()) {
                    final CacheEntry<K, V> entry = (CacheEntry<K, V>)it.next().getValue();
                    destroyer.accept(entry.key, entry.value);
                    it.remove();
                }
            }
            finally {
                this.lock.unlockWrite(writeStamp);
            }
        }
    }
    
    private static class CacheEntry<K, V>
    {
        private static final VarHandle TIMESTAMP;
        @Nullable
        public K key;
        @Nullable
        public V value;
        public long timestamp;
        
        private CacheEntry() {
            this.key = null;
            this.value = null;
            this.timestamp = 0L;
        }
        
        @Nonnull
        protected V markAndGet(final long timestamp) {
            Objects.requireNonNull(this.value);
            CacheEntry.TIMESTAMP.setVolatile(this, timestamp);
            return this.value;
        }
        
        static {
            try {
                TIMESTAMP = MethodHandles.lookup().findVarHandle(CacheEntry.class, "timestamp", Long.TYPE);
            }
            catch (final ReflectiveOperationException e) {
                throw new ExceptionInInitializerError(e);
            }
        }
    }
}
