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

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

import java.lang.ref.WeakReference;
import com.hypixel.hytale.server.core.HytaleServer;
import java.util.concurrent.TimeUnit;
import java.lang.ref.Cleaner;
import javax.annotation.Nonnull;
import java.util.concurrent.ScheduledFuture;
import java.util.function.BiConsumer;
import javax.annotation.Nullable;
import java.util.function.Function;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import java.util.ArrayDeque;

public class SizedTimeoutCache<K, V> implements Cache<K, V>
{
    private final ArrayDeque<CacheEntry<K, V>> pool;
    private final Object2ObjectLinkedOpenHashMap<K, CacheEntry<K, V>> map;
    private final long timeout;
    private final int maxSize;
    @Nullable
    private final Function<K, V> func;
    @Nullable
    private final BiConsumer<K, V> destroyer;
    @Nonnull
    private final ScheduledFuture<?> future;
    @Nonnull
    private final Cleaner.Cleanable cleanable;
    
    public SizedTimeoutCache(final long expire, @Nonnull final TimeUnit unit, final int maxSize, @Nullable final Function<K, V> func, @Nullable final BiConsumer<K, V> destroyer) {
        this.pool = new ArrayDeque<CacheEntry<K, V>>();
        this.map = new Object2ObjectLinkedOpenHashMap<K, CacheEntry<K, V>>();
        this.timeout = unit.toNanos(expire);
        this.maxSize = maxSize;
        this.func = func;
        this.destroyer = destroyer;
        this.future = HytaleServer.SCHEDULED_EXECUTOR.scheduleWithFixedDelay(new CleanupRunnable<Object, Object>(new WeakReference<Cache<?, ?>>(this)), expire, expire, unit);
        this.cleanable = CleanupFutureAction.CLEANER.register(this, new CleanupFutureAction(this.future));
    }
    
    @Override
    public void cleanup() {
        this.reduceLength(this.maxSize);
        final long expire = System.nanoTime() - this.timeout;
        while (true) {
            final K key;
            final V value;
            synchronized (this.map) {
                if (this.map.isEmpty()) {
                    break;
                }
                key = this.map.lastKey();
                final CacheEntry<K, V> entry = this.map.get(key);
                if (entry.timestamp > expire) {
                    break;
                }
                this.map.remove(key);
                value = entry.value;
                if (this.pool.size() < this.maxSize) {
                    entry.key = null;
                    entry.value = null;
                    entry.timestamp = 0L;
                    this.pool.addLast(entry);
                }
            }
            if (this.destroyer != null) {
                this.destroyer.accept(key, value);
            }
        }
    }
    
    private void reduceLength(final int targetSize) {
        while (true) {
            final K key;
            final V value;
            synchronized (this.map) {
                if (this.map.size() <= targetSize) {
                    break;
                }
                final CacheEntry<K, V> entry = this.map.removeLast();
                key = entry.key;
                value = entry.value;
                if (this.pool.size() < this.maxSize) {
                    entry.key = null;
                    entry.value = null;
                    entry.timestamp = 0L;
                    this.pool.addLast(entry);
                }
            }
            if (this.destroyer != null) {
                this.destroyer.accept(key, value);
            }
        }
    }
    
    @Override
    public void shutdown() {
        this.cleanable.clean();
        if (this.destroyer != null) {
            this.reduceLength(0);
        }
        else {
            synchronized (this.map) {
                this.map.clear();
            }
        }
    }
    
    @Nullable
    @Override
    public V get(final K key) {
        if (this.future.isCancelled()) {
            throw new IllegalStateException("Cache has been shutdown!");
        }
        long timestamp = System.nanoTime();
        synchronized (this.map) {
            final CacheEntry<K, V> entry = this.map.getAndMoveToFirst(key);
            if (entry != null) {
                entry.timestamp = timestamp;
                return entry.value;
            }
        }
        if (this.func == null) {
            return null;
        }
        final V value = this.func.apply(key);
        timestamp = System.nanoTime();
        final CacheEntry<K, V> newEntry;
        CacheEntry<K, V> resultEntry;
        final V resultValue;
        synchronized (this.map) {
            newEntry = (this.pool.isEmpty() ? new CacheEntry<K, V>() : this.pool.removeLast());
            newEntry.key = key;
            newEntry.value = value;
            newEntry.timestamp = timestamp;
            resultEntry = this.map.getAndMoveToFirst(key);
            if (resultEntry != null) {
                resultEntry.timestamp = timestamp;
            }
            else {
                resultEntry = newEntry;
                this.map.put(key, resultEntry);
            }
            resultValue = resultEntry.value;
        }
        if (resultEntry != newEntry && this.destroyer != null) {
            this.destroyer.accept(key, value);
        }
        return resultValue;
    }
    
    public void put(final K key, final V value) {
        if (this.future.isCancelled()) {
            throw new IllegalStateException("Cache has been shutdown!");
        }
        final long timestamp = System.nanoTime();
        final CacheEntry<K, V> oldEntry;
        synchronized (this.map) {
            final CacheEntry<K, V> entry = this.pool.isEmpty() ? new CacheEntry<K, V>() : this.pool.removeLast();
            entry.key = key;
            entry.value = value;
            entry.timestamp = timestamp;
            oldEntry = this.map.putAndMoveToFirst(key, entry);
            if (oldEntry != null) {
                entry.key = oldEntry.key;
            }
        }
        if (oldEntry != null && this.destroyer != null) {
            this.destroyer.accept(key, oldEntry.value);
        }
    }
    
    @Nullable
    public V getWithReusedKey(final K reusedKey, @Nonnull final Function<K, K> keyPool) {
        if (this.future.isCancelled()) {
            throw new IllegalStateException("Cache has been shutdown!");
        }
        long timestamp = System.nanoTime();
        synchronized (this.map) {
            final CacheEntry<K, V> entry = this.map.getAndMoveToFirst(reusedKey);
            if (entry != null) {
                entry.timestamp = timestamp;
                return entry.value;
            }
        }
        if (this.func == null) {
            return null;
        }
        final K newKey = keyPool.apply(reusedKey);
        final V value = this.func.apply(newKey);
        timestamp = System.nanoTime();
        final CacheEntry<K, V> newEntry;
        CacheEntry<K, V> resultEntry;
        final V resultValue;
        synchronized (this.map) {
            newEntry = (this.pool.isEmpty() ? new CacheEntry<K, V>() : this.pool.removeLast());
            newEntry.key = newKey;
            newEntry.value = value;
            newEntry.timestamp = timestamp;
            resultEntry = this.map.getAndMoveToFirst(newKey);
            if (resultEntry != null) {
                resultEntry.timestamp = timestamp;
            }
            else {
                resultEntry = newEntry;
                this.map.put(newKey, resultEntry);
            }
            resultValue = resultEntry.value;
        }
        if (resultEntry != newEntry && this.destroyer != null) {
            this.destroyer.accept(newKey, value);
        }
        return resultValue;
    }
    
    private static class CacheEntry<K, V>
    {
        @Nullable
        private V value;
        @Nullable
        private K key;
        private long timestamp;
    }
}
