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

package com.hypixel.hytale.codec.lookup;

import java.util.Collection;
import com.hypixel.hytale.codec.EmptyExtraInfo;
import java.lang.ref.WeakReference;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import java.util.Collections;
import org.bson.BsonDocument;
import java.io.IOException;
import com.hypixel.hytale.codec.util.RawJsonReader;
import javax.annotation.Nullable;
import com.hypixel.hytale.codec.ExtraInfo;
import org.bson.BsonValue;
import java.util.Iterator;
import javax.annotation.Nonnull;
import java.util.function.Function;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
import java.util.concurrent.locks.StampedLock;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.Reference;
import java.util.Set;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.codec.Codec;

public class MapKeyMapCodec<V> extends AMapProvidedMapCodec<Class<? extends V>, V, Codec<V>, TypeMap<V>>
{
    private static final HytaleLogger LOGGER;
    private static final Set<Reference<TypeMap<?>>> ACTIVE_MAPS;
    private static final ReferenceQueue<TypeMap<?>> MAP_REFERENCE_QUEUE;
    private static final StampedLock DATA_LOCK;
    protected final Map<String, Class<? extends V>> idToClass;
    protected final Map<Class<? extends V>, String> classToId;
    
    public MapKeyMapCodec() {
        this(true);
    }
    
    public MapKeyMapCodec(final boolean unmodifiable) {
        super(new ConcurrentHashMap<Object, Object>(), (Function<Object, Codec<Object>>)Function.identity(), unmodifiable);
        this.idToClass = new ConcurrentHashMap<String, Class<? extends V>>();
        this.classToId = new ConcurrentHashMap<Class<? extends V>, String>();
    }
    
    public <T extends V> void register(@Nonnull final Class<T> tClass, @Nonnull final String id, @Nonnull final Codec<T> codec) {
        final long lock = MapKeyMapCodec.DATA_LOCK.writeLock();
        try {
            if (this.codecProvider.put((K)tClass, (P)codec) != null) {
                throw new IllegalArgumentException("Id already registered");
            }
            if (this.idToClass.put(id, (Class<? extends V>)tClass) != null) {
                throw new IllegalArgumentException("Id already registered");
            }
            if (this.classToId.put((Class<? extends V>)tClass, id) != null) {
                throw new IllegalArgumentException("Class already registered");
            }
            for (final Reference<TypeMap<?>> mapRef : MapKeyMapCodec.ACTIVE_MAPS) {
                final TypeMap<?> map = mapRef.get();
                if (map != null && map.codec == this) {
                    map.tryUpgrade(tClass, id, codec);
                }
            }
        }
        finally {
            MapKeyMapCodec.DATA_LOCK.unlockWrite(lock);
        }
    }
    
    public <T extends V> void unregister(@Nonnull final Class<T> tClass) {
        final long lock = MapKeyMapCodec.DATA_LOCK.writeLock();
        try {
            final Codec<V> codec = (Codec<V>)this.codecProvider.get(tClass);
            if (codec == null) {
                throw new IllegalStateException(String.valueOf(tClass) + " not registered");
            }
            final String id = this.classToId.get(tClass);
            for (final Reference<TypeMap<?>> mapRef : MapKeyMapCodec.ACTIVE_MAPS) {
                final TypeMap<?> map = mapRef.get();
                if (map != null && map.codec == this) {
                    map.tryDowngrade((Class<V>)tClass, id, codec);
                }
            }
            this.codecProvider.remove(tClass);
            this.classToId.remove(tClass);
            this.idToClass.remove(id);
        }
        finally {
            MapKeyMapCodec.DATA_LOCK.unlockWrite(lock);
        }
    }
    
    @Nullable
    @Deprecated(forRemoval = true)
    public V decodeById(@Nonnull final String id, final BsonValue value, final ExtraInfo extraInfo) {
        final Codec<V> codec = (Codec<V>)this.codecProvider.get(this.getKeyForId(id));
        return codec.decode(value, extraInfo);
    }
    
    @Override
    protected String getIdForKey(final Class<? extends V> key) {
        return this.classToId.get(key);
    }
    
    @Nonnull
    @Override
    public TypeMap<V> createMap() {
        return new TypeMap<V>(this);
    }
    
    @Override
    public void handleUnknown(@Nonnull final TypeMap<V> map, @Nonnull final String key, final BsonValue value, @Nonnull final ExtraInfo extraInfo) {
        extraInfo.addUnknownKey(key);
        map.unknownValues.put(key, value);
    }
    
    @Override
    public void handleUnknown(@Nonnull final TypeMap<V> map, @Nonnull final String key, @Nonnull final RawJsonReader reader, @Nonnull final ExtraInfo extraInfo) throws IOException {
        extraInfo.addUnknownKey(key);
        map.unknownValues.put(key, RawJsonReader.readBsonValue(reader));
    }
    
    @Override
    protected void encodeExtra(@Nonnull final BsonDocument document, @Nonnull final TypeMap<V> map, final ExtraInfo extraInfo) {
        document.putAll(map.unknownValues);
    }
    
    public Class<? extends V> getKeyForId(final String id) {
        return this.idToClass.get(id);
    }
    
    @Nonnull
    @Override
    protected TypeMap<V> emptyMap() {
        return TypeMap.EMPTY;
    }
    
    @Nonnull
    @Override
    protected TypeMap<V> unmodifiableMap(@Nonnull final TypeMap<V> m) {
        return new TypeMap<V>(this, Collections.unmodifiableMap((Map<? extends Class<? extends V>, ? extends V>)m.map), m.map, m.unknownValues);
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
        ACTIVE_MAPS = ConcurrentHashMap.newKeySet();
        MAP_REFERENCE_QUEUE = new ReferenceQueue<TypeMap<?>>();
        DATA_LOCK = new StampedLock();
        final Thread thread = new Thread(() -> {
            while (!Thread.interrupted()) {
                try {
                    MapKeyMapCodec.ACTIVE_MAPS.remove(MapKeyMapCodec.MAP_REFERENCE_QUEUE.remove());
                    continue;
                }
                catch (final InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                }
                break;
            }
            return;
        }, "MapKeyMapCodec");
        thread.setDaemon(true);
        thread.start();
    }
    
    public static class TypeMap<V> implements Map<Class<? extends V>, V>
    {
        private static final TypeMap EMPTY;
        private final MapKeyMapCodec<V> codec;
        @Nonnull
        private final Map<Class<? extends V>, V> map;
        @Nonnull
        private final Map<Class<? extends V>, V> internalMap;
        @Nonnull
        private final Map<String, BsonValue> unknownValues;
        
        public TypeMap(final MapKeyMapCodec<V> codec) {
            this(codec, (Map)new Object2ObjectOpenHashMap(), (Map)new Object2ObjectOpenHashMap());
        }
        
        public TypeMap(final MapKeyMapCodec<V> codec, @Nonnull final Map<Class<? extends V>, V> map, @Nonnull final Map<String, BsonValue> unknownValues) {
            this(codec, map, map, unknownValues);
        }
        
        public TypeMap(final MapKeyMapCodec<V> codec, @Nonnull final Map<Class<? extends V>, V> map, @Nonnull final Map<Class<? extends V>, V> internalMap, @Nonnull final Map<String, BsonValue> unknownValues) {
            this.codec = codec;
            this.map = map;
            this.internalMap = internalMap;
            this.unknownValues = unknownValues;
            MapKeyMapCodec.ACTIVE_MAPS.add(new WeakReference<TypeMap<?>>(this, MapKeyMapCodec.MAP_REFERENCE_QUEUE));
        }
        
        public <T extends V> void tryUpgrade(@Nonnull final Class<T> tClass, @Nonnull final String id, @Nonnull final Codec<T> codec) {
            final BsonValue unknownValue = this.unknownValues.remove(id);
            if (unknownValue == null) {
                return;
            }
            final T value = codec.decode(unknownValue, EmptyExtraInfo.EMPTY);
            this.internalMap.put((Class<? extends V>)tClass, value);
            MapKeyMapCodec.LOGGER.atInfo().log("Upgrade " + id + " from unknown value");
        }
        
        public <T extends V> void tryDowngrade(@Nonnull final Class<T> tClass, @Nonnull final String id, @Nonnull final Codec<T> codec) {
            final V value = this.internalMap.remove(tClass);
            if (value == null) {
                return;
            }
            final BsonValue encoded = codec.encode((T)value, EmptyExtraInfo.EMPTY);
            this.unknownValues.put(id, encoded);
            MapKeyMapCodec.LOGGER.atInfo().log("Downgraded " + id + " to unknown value");
        }
        
        @Override
        public int size() {
            return this.map.size();
        }
        
        @Override
        public boolean isEmpty() {
            return this.map.isEmpty();
        }
        
        @Override
        public boolean containsKey(final Object key) {
            return this.map.containsKey(key);
        }
        
        @Override
        public boolean containsValue(final Object value) {
            return this.map.containsValue(value);
        }
        
        @Override
        public V get(final Object key) {
            return this.map.get(key);
        }
        
        @Nullable
        public <T extends V> T get(final Class<? extends T> key) {
            final long lock = MapKeyMapCodec.DATA_LOCK.readLock();
            try {
                return (T)this.map.get(key);
            }
            finally {
                MapKeyMapCodec.DATA_LOCK.unlockRead(lock);
            }
        }
        
        @Override
        public V put(@Nonnull final Class<? extends V> key, final V value) {
            final long lock = MapKeyMapCodec.DATA_LOCK.readLock();
            try {
                if (!key.isInstance(value)) {
                    throw new IllegalArgumentException("Passed value '" + String.valueOf(value) + "' isn't of type: " + String.valueOf(key));
                }
                return this.map.put(key, value);
            }
            finally {
                MapKeyMapCodec.DATA_LOCK.unlockRead(lock);
            }
        }
        
        @Override
        public V remove(final Object key) {
            return this.map.remove(key);
        }
        
        @Override
        public void putAll(@Nonnull final Map<? extends Class<? extends V>, ? extends V> m) {
            for (final Entry<? extends Class<? extends V>, ? extends V> e : m.entrySet()) {
                this.put((Class<? extends V>)e.getKey(), e.getValue());
            }
        }
        
        @Override
        public void clear() {
            this.map.clear();
        }
        
        @Nonnull
        @Override
        public Set<Class<? extends V>> keySet() {
            return this.map.keySet();
        }
        
        @Nonnull
        @Override
        public Collection<V> values() {
            return this.map.values();
        }
        
        @Nonnull
        @Override
        public Set<Entry<Class<? extends V>, V>> entrySet() {
            return this.map.entrySet();
        }
        
        @Override
        public <T extends V> T computeIfAbsent(final Class<? extends T> key, @Nonnull final Function<? super Class<? extends V>, T> mappingFunction) {
            final long lock = MapKeyMapCodec.DATA_LOCK.readLock();
            try {
                return (T)this.map.computeIfAbsent((Class<? extends V>)key, (Function<? super Class<? extends V>, ? extends V>)mappingFunction);
            }
            finally {
                MapKeyMapCodec.DATA_LOCK.unlockRead(lock);
            }
        }
        
        @Override
        public boolean equals(final Object o) {
            return this == o || (o instanceof Map && this.entrySet().equals(((Map)o).entrySet()));
        }
        
        @Override
        public int hashCode() {
            return this.map.hashCode();
        }
        
        @Nonnull
        @Override
        public String toString() {
            return "TypeMap{map=" + String.valueOf(this.map);
        }
        
        public static <V> TypeMap<V> empty() {
            return TypeMap.EMPTY;
        }
        
        static {
            EMPTY = new TypeMap(null, Collections.emptyMap(), Collections.emptyMap());
        }
    }
}
