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

package com.hypixel.hytale.server.core.universe.world.chunk.section;

import com.hypixel.hytale.server.core.universe.world.chunk.BlockChunk;
import io.netty.buffer.Unpooled;
import java.util.Map;
import com.hypixel.hytale.server.core.util.FillerBlockUtil;
import com.hypixel.hytale.server.core.asset.type.fluid.Fluid;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.Rotation;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import com.hypixel.hytale.server.core.universe.world.chunk.section.palette.PaletteTypeEnum;
import java.util.function.Function;
import java.util.function.ToIntFunction;
import com.hypixel.hytale.sneakythrow.SneakyThrow;
import com.hypixel.hytale.server.core.util.io.ByteBufUtil;
import io.netty.buffer.ByteBufAllocator;
import com.hypixel.hytale.codec.ExtraInfo;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockMigration;
import com.hypixel.hytale.protocol.packets.world.PaletteType;
import io.netty.buffer.ByteBuf;
import com.hypixel.hytale.assetstore.map.BlockTypeAssetMap;
import com.hypixel.hytale.assetstore.map.IndexedLookupTableAssetMap;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
import com.hypixel.hytale.server.core.asset.type.blockhitbox.BlockBoundingBoxes;
import com.hypixel.hytale.server.core.asset.type.blocktick.BlockTickStrategy;
import com.hypixel.hytale.function.predicate.ObjectPositionBlockFunction;
import com.hypixel.hytale.common.util.BitSetUtil;
import java.time.Instant;
import it.unimi.dsi.fastutil.ints.Int2ShortMap;
import java.util.function.IntConsumer;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.ints.IntList;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.RotationTuple;
import com.hypixel.hytale.math.util.ChunkUtil;
import java.util.Objects;
import com.hypixel.hytale.server.core.universe.world.chunk.section.palette.EmptySectionPalette;
import com.hypixel.hytale.server.core.modules.LegacyModule;
import com.hypixel.hytale.component.ComponentType;
import java.util.Comparator;
import com.hypixel.hytale.server.core.blocktype.component.BlockPhysics;
import javax.annotation.Nullable;
import com.hypixel.hytale.protocol.packets.world.SetChunk;
import com.hypixel.hytale.protocol.CachedPacket;
import java.util.concurrent.CompletableFuture;
import java.lang.ref.SoftReference;
import it.unimi.dsi.fastutil.objects.ObjectHeapPriorityQueue;
import java.util.BitSet;
import com.hypixel.hytale.server.core.universe.world.chunk.section.palette.ISectionPalette;
import javax.annotation.Nonnull;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import java.util.concurrent.locks.StampedLock;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.component.Component;

public class BlockSection implements Component<ChunkStore>
{
    public static final int VERSION = 6;
    public static final BuilderCodec<BlockSection> CODEC;
    private final StampedLock chunkSectionLock;
    public boolean loaded;
    @Nonnull
    private IntOpenHashSet changedPositions;
    @Nonnull
    private IntOpenHashSet swapChangedPositions;
    private ISectionPalette chunkSection;
    private ISectionPalette fillerSection;
    private ISectionPalette rotationSection;
    private ChunkLightData localLight;
    private short localChangeCounter;
    private ChunkLightData globalLight;
    private short globalChangeCounter;
    private BitSet tickingBlocks;
    private final BitSet tickingBlocksCopy;
    @Nonnull
    private final BitSet tickingWaitAdjacentBlocks;
    private int tickingBlocksCount;
    private int tickingBlocksCountCopy;
    private int tickingWaitAdjacentBlockCount;
    private final ObjectHeapPriorityQueue<TickRequest> tickRequests;
    private double maximumHitboxExtent;
    @Nullable
    private transient SoftReference<CompletableFuture<CachedPacket<SetChunk>>> cachedChunkPacket;
    @Nullable
    @Deprecated(forRemoval = true)
    private FluidSection migratedFluidSection;
    @Nullable
    @Deprecated(forRemoval = true)
    private BlockPhysics migratedBlockPhysics;
    private static final Comparator<TickRequest> TICK_REQUEST_COMPARATOR;
    
    public static ComponentType<ChunkStore, BlockSection> getComponentType() {
        return LegacyModule.get().getBlockSectionComponentType();
    }
    
    public BlockSection() {
        this(EmptySectionPalette.INSTANCE, EmptySectionPalette.INSTANCE, EmptySectionPalette.INSTANCE);
    }
    
    public BlockSection(final ISectionPalette chunkSection, final ISectionPalette fillerSection, final ISectionPalette rotationSection) {
        this.chunkSectionLock = new StampedLock();
        this.loaded = false;
        this.changedPositions = new IntOpenHashSet(0);
        this.swapChangedPositions = new IntOpenHashSet(0);
        this.tickRequests = new ObjectHeapPriorityQueue<TickRequest>(BlockSection.TICK_REQUEST_COMPARATOR);
        this.maximumHitboxExtent = -1.0;
        this.chunkSection = chunkSection;
        this.fillerSection = fillerSection;
        this.rotationSection = rotationSection;
        this.tickingBlocks = new BitSet();
        this.tickingBlocksCopy = new BitSet();
        this.tickingWaitAdjacentBlocks = new BitSet();
        this.tickingBlocksCount = 0;
        this.tickingBlocksCountCopy = 0;
        this.localLight = ChunkLightData.EMPTY;
        this.localChangeCounter = 0;
        this.globalLight = ChunkLightData.EMPTY;
        this.globalChangeCounter = 0;
    }
    
    public ISectionPalette getChunkSection() {
        return this.chunkSection;
    }
    
    public void setChunkSection(final ISectionPalette chunkSection) {
        this.chunkSection = chunkSection;
    }
    
    public void setLocalLight(@Nonnull final ChunkLightDataBuilder localLight) {
        Objects.requireNonNull(localLight);
        this.localLight = localLight.build();
    }
    
    public void setGlobalLight(@Nonnull final ChunkLightDataBuilder globalLight) {
        Objects.requireNonNull(globalLight);
        this.globalLight = globalLight.build();
    }
    
    public ChunkLightData getLocalLight() {
        return this.localLight;
    }
    
    public ChunkLightData getGlobalLight() {
        return this.globalLight;
    }
    
    public boolean hasLocalLight() {
        return this.localLight.getChangeId() == this.localChangeCounter;
    }
    
    public boolean hasGlobalLight() {
        return this.globalLight.getChangeId() == this.globalChangeCounter;
    }
    
    public void invalidateLocalLight() {
        ++this.localChangeCounter;
        this.invalidateGlobalLight();
    }
    
    public void invalidateGlobalLight() {
        ++this.globalChangeCounter;
    }
    
    public short getLocalChangeCounter() {
        return this.localChangeCounter;
    }
    
    public short getGlobalChangeCounter() {
        return this.globalChangeCounter;
    }
    
    public void invalidate() {
        this.cachedChunkPacket = null;
    }
    
    public int get(final int index) {
        long lock = this.chunkSectionLock.tryOptimisticRead();
        final int i = this.chunkSection.get(index);
        if (!this.chunkSectionLock.validate(lock)) {
            lock = this.chunkSectionLock.readLock();
            try {
                return this.chunkSection.get(index);
            }
            finally {
                this.chunkSectionLock.unlockRead(lock);
            }
        }
        return i;
    }
    
    public int getFiller(final int index) {
        long lock = this.chunkSectionLock.tryOptimisticRead();
        final int i = this.fillerSection.get(index);
        if (!this.chunkSectionLock.validate(lock)) {
            lock = this.chunkSectionLock.readLock();
            try {
                return this.fillerSection.get(index);
            }
            finally {
                this.chunkSectionLock.unlockRead(lock);
            }
        }
        return i;
    }
    
    public int getFiller(final int x, final int y, final int z) {
        return this.getFiller(ChunkUtil.indexBlock(x, y, z));
    }
    
    public int getRotationIndex(final int index) {
        long lock = this.chunkSectionLock.tryOptimisticRead();
        final int i = this.rotationSection.get(index);
        if (!this.chunkSectionLock.validate(lock)) {
            lock = this.chunkSectionLock.readLock();
            try {
                return this.rotationSection.get(index);
            }
            finally {
                this.chunkSectionLock.unlockRead(lock);
            }
        }
        return i;
    }
    
    public int getRotationIndex(final int x, final int y, final int z) {
        return this.getRotationIndex(ChunkUtil.indexBlock(x, y, z));
    }
    
    public RotationTuple getRotation(final int index) {
        return RotationTuple.get(this.getRotationIndex(index));
    }
    
    public RotationTuple getRotation(final int x, final int y, final int z) {
        return this.getRotation(ChunkUtil.indexBlock(x, y, z));
    }
    
    public boolean set(final int blockIdx, final int blockId, final int rotation, final int filler) {
        final long lock = this.chunkSectionLock.writeLock();
        boolean changed;
        try {
            ISectionPalette.SetResult result = this.chunkSection.set(blockIdx, blockId);
            if (result == ISectionPalette.SetResult.REQUIRES_PROMOTE) {
                this.chunkSection = this.chunkSection.promote();
                final ISectionPalette.SetResult repeatResult = this.chunkSection.set(blockIdx, blockId);
                if (repeatResult != ISectionPalette.SetResult.ADDED_OR_REMOVED) {
                    throw new IllegalStateException("Promoted chunk section failed to correctly add the new block!");
                }
            }
            else {
                if (result == ISectionPalette.SetResult.ADDED_OR_REMOVED) {
                    this.maximumHitboxExtent = -1.0;
                }
                if (this.chunkSection.shouldDemote()) {
                    this.chunkSection = this.chunkSection.demote();
                }
            }
            changed = (result != ISectionPalette.SetResult.UNCHANGED);
            result = this.fillerSection.set(blockIdx, filler);
            if (result == ISectionPalette.SetResult.REQUIRES_PROMOTE) {
                this.fillerSection = this.fillerSection.promote();
                final ISectionPalette.SetResult repeatResult = this.fillerSection.set(blockIdx, filler);
                if (repeatResult != ISectionPalette.SetResult.ADDED_OR_REMOVED) {
                    throw new IllegalStateException("Promoted chunk section failed to correctly add the new block!");
                }
            }
            else if (this.fillerSection.shouldDemote()) {
                this.fillerSection = this.fillerSection.demote();
            }
            changed |= (result != ISectionPalette.SetResult.UNCHANGED);
            result = this.rotationSection.set(blockIdx, rotation);
            if (result == ISectionPalette.SetResult.REQUIRES_PROMOTE) {
                this.rotationSection = this.rotationSection.promote();
                final ISectionPalette.SetResult repeatResult = this.rotationSection.set(blockIdx, rotation);
                if (repeatResult != ISectionPalette.SetResult.ADDED_OR_REMOVED) {
                    throw new IllegalStateException("Promoted chunk section failed to correctly add the new block!");
                }
            }
            else if (this.rotationSection.shouldDemote()) {
                this.rotationSection = this.rotationSection.demote();
            }
            changed |= (result != ISectionPalette.SetResult.UNCHANGED);
            if (changed && this.loaded) {
                this.changedPositions.add(blockIdx);
            }
        }
        finally {
            this.chunkSectionLock.unlockWrite(lock);
        }
        if (changed) {
            this.invalidateLocalLight();
        }
        return changed;
    }
    
    @Nonnull
    public IntOpenHashSet getAndClearChangedPositions() {
        final long stamp = this.chunkSectionLock.writeLock();
        try {
            this.swapChangedPositions.clear();
            final IntOpenHashSet tmp = this.changedPositions;
            this.changedPositions = this.swapChangedPositions;
            return this.swapChangedPositions = tmp;
        }
        finally {
            this.chunkSectionLock.unlockWrite(stamp);
        }
    }
    
    public boolean contains(final int id) {
        long lock = this.chunkSectionLock.tryOptimisticRead();
        final boolean contains = this.chunkSection.contains(id);
        if (!this.chunkSectionLock.validate(lock)) {
            lock = this.chunkSectionLock.readLock();
            try {
                return this.chunkSection.contains(id);
            }
            finally {
                this.chunkSectionLock.unlockRead(lock);
            }
        }
        return contains;
    }
    
    public boolean containsAny(final IntList ids) {
        long lock = this.chunkSectionLock.tryOptimisticRead();
        final boolean contains = this.chunkSection.containsAny(ids);
        if (!this.chunkSectionLock.validate(lock)) {
            lock = this.chunkSectionLock.readLock();
            try {
                return this.chunkSection.containsAny(ids);
            }
            finally {
                this.chunkSectionLock.unlockRead(lock);
            }
        }
        return contains;
    }
    
    public int count() {
        long lock = this.chunkSectionLock.tryOptimisticRead();
        final int count = this.chunkSection.count();
        if (!this.chunkSectionLock.validate(lock)) {
            lock = this.chunkSectionLock.readLock();
            try {
                return this.chunkSection.count();
            }
            finally {
                this.chunkSectionLock.unlockRead(lock);
            }
        }
        return count;
    }
    
    public int count(final int id) {
        long lock = this.chunkSectionLock.tryOptimisticRead();
        final int count = this.chunkSection.count(id);
        if (!this.chunkSectionLock.validate(lock)) {
            lock = this.chunkSectionLock.readLock();
            try {
                return this.chunkSection.count(id);
            }
            finally {
                this.chunkSectionLock.unlockRead(lock);
            }
        }
        return count;
    }
    
    public IntSet values() {
        long lock = this.chunkSectionLock.tryOptimisticRead();
        final IntSet values = this.chunkSection.values();
        if (!this.chunkSectionLock.validate(lock)) {
            lock = this.chunkSectionLock.readLock();
            try {
                return this.chunkSection.values();
            }
            finally {
                this.chunkSectionLock.unlockRead(lock);
            }
        }
        return values;
    }
    
    public void forEachValue(final IntConsumer consumer) {
        final long lock = this.chunkSectionLock.readLock();
        try {
            this.chunkSection.forEachValue(consumer);
        }
        finally {
            this.chunkSectionLock.unlockRead(lock);
        }
    }
    
    public Int2ShortMap valueCounts() {
        long lock = this.chunkSectionLock.tryOptimisticRead();
        final Int2ShortMap valueCounts = this.chunkSection.valueCounts();
        if (!this.chunkSectionLock.validate(lock)) {
            lock = this.chunkSectionLock.readLock();
            try {
                return this.chunkSection.valueCounts();
            }
            finally {
                this.chunkSectionLock.unlockRead(lock);
            }
        }
        return valueCounts;
    }
    
    public boolean isSolidAir() {
        long lock = this.chunkSectionLock.tryOptimisticRead();
        final boolean isSolid = this.chunkSection.isSolid(0);
        if (!this.chunkSectionLock.validate(lock)) {
            lock = this.chunkSectionLock.readLock();
            try {
                return this.chunkSection.isSolid(0);
            }
            finally {
                this.chunkSectionLock.unlockRead(lock);
            }
        }
        return isSolid;
    }
    
    public void find(final IntList ids, final IntSet internalIdHolder, final IntConsumer indexConsumer) {
        final long lock = this.chunkSectionLock.readLock();
        try {
            this.chunkSection.find(ids, internalIdHolder, indexConsumer);
        }
        finally {
            this.chunkSectionLock.unlockRead(lock);
        }
    }
    
    public boolean setTicking(final int blockIdx, final boolean ticking) {
        final long readStamp = this.chunkSectionLock.readLock();
        try {
            if (this.tickingBlocks.get(blockIdx) == ticking) {
                return false;
            }
        }
        finally {
            this.chunkSectionLock.unlockRead(readStamp);
        }
        final long writeStamp = this.chunkSectionLock.writeLock();
        try {
            if (this.tickingBlocks.get(blockIdx) != ticking) {
                if (ticking) {
                    ++this.tickingBlocksCount;
                }
                else {
                    --this.tickingBlocksCount;
                }
                this.tickingBlocks.set(blockIdx, ticking);
                return true;
            }
            return false;
        }
        finally {
            this.chunkSectionLock.unlockWrite(writeStamp);
        }
    }
    
    public int getTickingBlocksCount() {
        return (this.tickingBlocksCount > 0) ? this.tickingBlocksCount : 0;
    }
    
    public int getTickingBlocksCountCopy() {
        return this.tickingBlocksCountCopy;
    }
    
    public boolean hasTicking() {
        return this.tickingBlocksCount > 0;
    }
    
    public boolean isTicking(final int blockIdx) {
        if (this.tickingBlocksCount > 0) {
            final long readStamp = this.chunkSectionLock.readLock();
            try {
                return this.tickingBlocks.get(blockIdx);
            }
            finally {
                this.chunkSectionLock.unlockRead(readStamp);
            }
        }
        return false;
    }
    
    public void scheduleTick(final int index, @Nullable final Instant gameTime) {
        if (gameTime == null) {
            return;
        }
        this.tickRequests.enqueue(new TickRequest(index, gameTime));
    }
    
    public void preTick(final Instant gameTime) {
        TickRequest request;
        while (!this.tickRequests.isEmpty() && (request = this.tickRequests.first()).requestedGameTime.isBefore(gameTime)) {
            this.tickRequests.dequeue();
            this.setTicking(request.index, true);
        }
        final long writeStamp = this.chunkSectionLock.writeLock();
        try {
            if (this.tickingBlocksCount == 0) {
                this.tickingBlocksCountCopy = 0;
                return;
            }
            BitSetUtil.copyValues(this.tickingBlocks, this.tickingBlocksCopy);
            this.tickingBlocksCountCopy = this.tickingBlocksCount;
            this.tickingBlocks.clear();
            this.tickingBlocksCount = 0;
        }
        finally {
            this.chunkSectionLock.unlockWrite(writeStamp);
        }
    }
    
    public <T, V> int forEachTicking(final T t, final V v, final int sectionIndex, @Nonnull final ObjectPositionBlockFunction<T, V, BlockTickStrategy> acceptor) {
        if (this.tickingBlocksCountCopy == 0) {
            return 0;
        }
        final int sectionStartYBlock = sectionIndex << 5;
        int ticked = 0;
        for (int index = this.tickingBlocksCopy.nextSetBit(0); index >= 0; index = this.tickingBlocksCopy.nextSetBit(index + 1)) {
            final int x = ChunkUtil.xFromIndex(index);
            final int y = ChunkUtil.yFromIndex(index);
            final int z = ChunkUtil.zFromIndex(index);
            final BlockTickStrategy strategy = acceptor.accept(t, v, x, y | sectionStartYBlock, z, this.get(index));
            final long writeStamp = this.chunkSectionLock.writeLock();
            try {
                switch (strategy) {
                    case WAIT_FOR_ADJACENT_CHUNK_LOAD: {
                        if (!this.tickingWaitAdjacentBlocks.get(index)) {
                            ++this.tickingWaitAdjacentBlockCount;
                            this.tickingWaitAdjacentBlocks.set(index, true);
                            break;
                        }
                        break;
                    }
                    case CONTINUE: {
                        if (!this.tickingBlocks.get(index)) {
                            ++this.tickingBlocksCount;
                            this.tickingBlocks.set(index, true);
                            break;
                        }
                        break;
                    }
                }
            }
            finally {
                this.chunkSectionLock.unlockWrite(writeStamp);
            }
            ++ticked;
        }
        return ticked;
    }
    
    public void mergeTickingBlocks() {
        final long writeStamp = this.chunkSectionLock.writeLock();
        try {
            this.tickingBlocks.or(this.tickingWaitAdjacentBlocks);
            this.tickingBlocksCount = this.tickingBlocks.cardinality();
            this.tickingWaitAdjacentBlocks.clear();
            this.tickingWaitAdjacentBlockCount = 0;
        }
        finally {
            this.chunkSectionLock.unlockWrite(writeStamp);
        }
    }
    
    public double getMaximumHitboxExtent() {
        final double extent = this.maximumHitboxExtent;
        if (extent != -1.0) {
            return extent;
        }
        double maximumExtent = BlockBoundingBoxes.UNIT_BOX_MAXIMUM_EXTENT;
        final long lock = this.chunkSectionLock.readLock();
        try {
            final IndexedLookupTableAssetMap<String, BlockBoundingBoxes> hitBoxAssetMap = BlockBoundingBoxes.getAssetMap();
            final BlockTypeAssetMap<String, BlockType> blockTypeMap = BlockType.getAssetMap();
            for (int idx = 0; idx < 32768; ++idx) {
                final int blockId = this.chunkSection.get(idx);
                if (blockId != 0) {
                    final int rotation = this.rotationSection.get(idx);
                    final BlockType blockType = blockTypeMap.getAsset(blockId);
                    if (blockType != null) {
                        if (!blockType.isUnknown()) {
                            final BlockBoundingBoxes asset = hitBoxAssetMap.getAsset(blockType.getHitboxTypeIndex());
                            if (asset != BlockBoundingBoxes.UNIT_BOX) {
                                final double boxMaximumExtent = asset.get(rotation).getBoundingBox().getMaximumExtent();
                                if (boxMaximumExtent > maximumExtent) {
                                    maximumExtent = boxMaximumExtent;
                                }
                            }
                        }
                    }
                }
            }
        }
        finally {
            this.chunkSectionLock.unlockRead(lock);
        }
        return this.maximumHitboxExtent = maximumExtent;
    }
    
    @Deprecated
    public void invalidateBlock(final int x, final int y, final int z) {
        final long stamp = this.chunkSectionLock.writeLock();
        try {
            this.changedPositions.add(ChunkUtil.indexBlock(x, y, z));
        }
        finally {
            this.chunkSectionLock.unlockWrite(stamp);
        }
    }
    
    @Nullable
    @Deprecated(forRemoval = true)
    public FluidSection takeMigratedFluid() {
        final FluidSection temp = this.migratedFluidSection;
        this.migratedFluidSection = null;
        return temp;
    }
    
    @Nullable
    @Deprecated(forRemoval = true)
    public BlockPhysics takeMigratedDecoBlocks() {
        final BlockPhysics temp = this.migratedBlockPhysics;
        this.migratedBlockPhysics = null;
        return temp;
    }
    
    public void serializeForPacket(@Nonnull final ByteBuf buf) {
        final long lock = this.chunkSectionLock.readLock();
        try {
            final PaletteType paletteType = this.chunkSection.getPaletteType();
            final byte paletteTypeId = (byte)paletteType.ordinal();
            buf.writeByte(paletteTypeId);
            this.chunkSection.serializeForPacket(buf);
            final PaletteType fillerType = this.fillerSection.getPaletteType();
            final byte fillerTypeId = (byte)fillerType.ordinal();
            buf.writeByte(fillerTypeId);
            this.fillerSection.serializeForPacket(buf);
            final PaletteType rotationType = this.rotationSection.getPaletteType();
            final byte rotationTypeId = (byte)rotationType.ordinal();
            buf.writeByte(rotationTypeId);
            this.rotationSection.serializeForPacket(buf);
        }
        finally {
            this.chunkSectionLock.unlockRead(lock);
        }
    }
    
    public void serialize(final ISectionPalette.KeySerializer keySerializer, @Nonnull final ByteBuf buf) {
        final long lock = this.chunkSectionLock.readLock();
        try {
            buf.writeInt(BlockMigration.getAssetMap().getAssetCount());
            final PaletteType paletteType = this.chunkSection.getPaletteType();
            buf.writeByte(paletteType.ordinal());
            this.chunkSection.serialize(keySerializer, buf);
            if (paletteType != PaletteType.Empty) {
                final BitSet combinedTickingBlock = (BitSet)this.tickingBlocks.clone();
                combinedTickingBlock.or(this.tickingWaitAdjacentBlocks);
                buf.writeShort(combinedTickingBlock.cardinality());
                final long[] data = combinedTickingBlock.toLongArray();
                buf.writeShort(data.length);
                for (final long l : data) {
                    buf.writeLong(l);
                }
            }
            buf.writeByte(this.fillerSection.getPaletteType().ordinal());
            this.fillerSection.serialize(ByteBuf::writeShort, buf);
            buf.writeByte(this.rotationSection.getPaletteType().ordinal());
            this.rotationSection.serialize(ByteBuf::writeByte, buf);
            this.localLight.serialize(buf);
            this.globalLight.serialize(buf);
            buf.writeShort(this.localChangeCounter);
            buf.writeShort(this.globalChangeCounter);
        }
        finally {
            this.chunkSectionLock.unlockRead(lock);
        }
    }
    
    public byte[] serialize(final ExtraInfo extraInfo) {
        final ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
        try {
            this.serialize(BlockType.KEY_SERIALIZER, buf);
            return ByteBufUtil.getBytesRelease(buf);
        }
        catch (final Throwable t) {
            buf.release();
            throw SneakyThrow.sneakyThrow(t);
        }
    }
    
    public void deserialize(final ToIntFunction<ByteBuf> keyDeserializer, @Nonnull final ByteBuf buf, final int version) {
        int blockMigrationVersion = 0;
        if (version >= 6) {
            blockMigrationVersion = buf.readInt();
        }
        Function<String, String> blockMigration = null;
        final Map<Integer, BlockMigration> blockMigrationMap = BlockMigration.getAssetMap().getAssetMap();
        for (BlockMigration migration = blockMigrationMap.get(blockMigrationVersion); migration != null; migration = blockMigrationMap.get(++blockMigrationVersion)) {
            if (blockMigration == null) {
                final BlockMigration obj = migration;
                Objects.requireNonNull(obj);
                blockMigration = obj::getMigration;
            }
            else {
                final Function<String, String> function = blockMigration;
                final BlockMigration obj2 = migration;
                Objects.requireNonNull(obj2);
                blockMigration = (Function<String, String>)function.andThen((Function<? super String, ?>)obj2::getMigration);
            }
        }
        final PaletteTypeEnum typeEnum = PaletteTypeEnum.get(buf.readByte());
        final PaletteType paletteType = typeEnum.getPaletteType();
        this.chunkSection = (ISectionPalette)typeEnum.getConstructor().get();
        if (version <= 4) {
            final ISectionPalette tempSection = (ISectionPalette)typeEnum.getConstructor().get();
            final boolean[] foundMigratable = { false };
            final boolean[] needsPhysics = { false };
            final int[] nextTempIndex = { -1 };
            final Int2ObjectOpenHashMap<String> types = new Int2ObjectOpenHashMap<String>();
            final Object2IntOpenHashMap<String> typesRev = new Object2IntOpenHashMap<String>();
            typesRev.defaultReturnValue(Integer.MIN_VALUE);
            final Function<String, String> finalBlockMigration = blockMigration;
            tempSection.deserialize(bytebuf -> {
                String key2 = ByteBufUtil.readUTF(bytebuf);
                if (finalBlockMigration != null) {
                    key2 = finalBlockMigration.apply(key2);
                }
                final int index2 = typesRev.getInt(key2);
                if (index2 != Integer.MIN_VALUE) {
                    return index2;
                }
                else {
                    final boolean migratable = key2.startsWith("Fluid_") || key2.contains("|Fluid=") || key2.contains("|Deco") || key2.contains("|Support") || key2.contains("|Filler") || key2.contains("|Yaw=") || key2.contains("|Pitch=") || key2.contains("|Roll=");
                    final int n;
                    foundMigratable[n] |= migratable;
                    int index3;
                    if (migratable) {
                        index3 = nextTempIndex[0]--;
                    }
                    else {
                        index3 = BlockType.getBlockIdOrUnknown(key2, "Unknown BlockType %s", key2);
                        final int n2;
                        needsPhysics[n2] |= BlockType.getAssetMap().getAsset(index3).hasSupport();
                    }
                    types.put(index3, key2);
                    typesRev.put(key2, index3);
                    return index3;
                }
            }, buf, version);
            if (needsPhysics[0]) {
                this.migratedBlockPhysics = new BlockPhysics();
            }
            if (foundMigratable[0]) {
                for (int index = 0; index < 32768; ++index) {
                    final int id = tempSection.get(index);
                    if (id >= 0) {
                        this.chunkSection.set(index, id);
                    }
                    else {
                        Rotation rotationYaw = Rotation.None;
                        Rotation rotationPitch = Rotation.None;
                        Rotation rotationRoll = Rotation.None;
                        String key = types.get(id);
                        if (key.startsWith("Fluid_") || key.contains("|Fluid=")) {
                            if (this.migratedFluidSection == null) {
                                this.migratedFluidSection = new FluidSection();
                            }
                            final Fluid.ConversionResult result = Fluid.convertBlockToFluid(key);
                            if (result == null) {
                                throw new RuntimeException("Invalid Fluid Key " + key);
                            }
                            if (result.blockTypeStr == null) {
                                this.migratedFluidSection.setFluid(index, result.fluidId, result.fluidLevel);
                                continue;
                            }
                            key = result.blockTypeStr;
                            this.migratedFluidSection.setFluid(index, result.fluidId, result.fluidLevel);
                        }
                        if (key.contains("|Deco")) {
                            if (this.migratedBlockPhysics == null) {
                                this.migratedBlockPhysics = new BlockPhysics();
                            }
                            this.migratedBlockPhysics.set(index, 15);
                        }
                        if (key.contains("|Support=")) {
                            if (this.migratedBlockPhysics == null) {
                                this.migratedBlockPhysics = new BlockPhysics();
                            }
                            final int start = key.indexOf("|Support=") + "|Support=".length();
                            int end = key.indexOf(124, start);
                            if (end == -1) {
                                end = key.length();
                            }
                            this.migratedBlockPhysics.set(index, Integer.parseInt(key, start, end, 10));
                        }
                        if (key.contains("|Filler=")) {
                            final int start = key.indexOf("|Filler=") + "|Filler=".length();
                            final int firstComma = key.indexOf(44, start);
                            if (firstComma == -1) {
                                throw new IllegalArgumentException("Invalid filler metadata! Missing comma");
                            }
                            final int secondComma = key.indexOf(44, firstComma + 1);
                            if (secondComma == -1) {
                                throw new IllegalArgumentException("Invalid filler metadata! Missing second comma");
                            }
                            int end2 = key.indexOf(124, start);
                            if (end2 == -1) {
                                end2 = key.length();
                            }
                            final int fillerX = Integer.parseInt(key, start, firstComma, 10);
                            final int fillerY = Integer.parseInt(key, firstComma + 1, secondComma, 10);
                            final int fillerZ = Integer.parseInt(key, secondComma + 1, end2, 10);
                            final int filler = FillerBlockUtil.pack(fillerX, fillerY, fillerZ);
                            final ISectionPalette.SetResult result2 = this.fillerSection.set(index, filler);
                            if (result2 == ISectionPalette.SetResult.REQUIRES_PROMOTE) {
                                (this.fillerSection = this.fillerSection.promote()).set(index, filler);
                            }
                        }
                        if (key.contains("|Yaw=")) {
                            final int start = key.indexOf("|Yaw=") + "|Yaw=".length();
                            int end = key.indexOf(124, start);
                            if (end == -1) {
                                end = key.length();
                            }
                            rotationYaw = Rotation.ofDegrees(Integer.parseInt(key, start, end, 10));
                        }
                        if (key.contains("|Pitch=")) {
                            final int start = key.indexOf("|Pitch=") + "|Pitch=".length();
                            int end = key.indexOf(124, start);
                            if (end == -1) {
                                end = key.length();
                            }
                            rotationPitch = Rotation.ofDegrees(Integer.parseInt(key, start, end, 10));
                        }
                        if (key.contains("|Roll=")) {
                            final int start = key.indexOf("|Roll=") + "|Roll=".length();
                            int end = key.indexOf(124, start);
                            if (end == -1) {
                                end = key.length();
                            }
                            rotationRoll = Rotation.ofDegrees(Integer.parseInt(key, start, end, 10));
                        }
                        if (rotationYaw != Rotation.None || rotationPitch != Rotation.None || rotationRoll != Rotation.None) {
                            final int rotation = RotationTuple.index(rotationYaw, rotationPitch, rotationRoll);
                            final ISectionPalette.SetResult result3 = this.rotationSection.set(index, rotation);
                            if (result3 == ISectionPalette.SetResult.REQUIRES_PROMOTE) {
                                (this.rotationSection = this.rotationSection.promote()).set(index, rotation);
                            }
                        }
                        final int endOfName = key.indexOf(124);
                        if (endOfName != -1) {
                            key = key.substring(0, endOfName);
                        }
                        this.chunkSection.set(index, BlockType.getBlockIdOrUnknown(key, "Unknown BlockType: %s", key));
                    }
                }
                if (this.chunkSection.shouldDemote()) {
                    this.chunkSection.demote();
                }
            }
            else {
                this.chunkSection = tempSection;
            }
        }
        else if (blockMigration != null) {
            final Function<String, String> finalBlockMigration2 = blockMigration;
            this.chunkSection.deserialize(bytebuf -> {
                final String key3 = ByteBufUtil.readUTF(bytebuf);
                final String key4 = finalBlockMigration1.apply(key3);
                return BlockType.getBlockIdOrUnknown(key4, "Unknown BlockType %s", key4);
            }, buf, version);
        }
        else {
            this.chunkSection.deserialize(keyDeserializer, buf, version);
        }
        if (paletteType != PaletteType.Empty) {
            this.tickingBlocksCount = buf.readUnsignedShort();
            final int len = buf.readUnsignedShort();
            final long[] tickingBlocksData = new long[len];
            for (int i = 0; i < tickingBlocksData.length; ++i) {
                tickingBlocksData[i] = buf.readLong();
            }
            this.tickingBlocks = BitSet.valueOf(tickingBlocksData);
            this.tickingBlocksCount = this.tickingBlocks.cardinality();
        }
        if (version >= 4) {
            final PaletteTypeEnum fillerTypeEnum = PaletteTypeEnum.get(buf.readByte());
            (this.fillerSection = (ISectionPalette)fillerTypeEnum.getConstructor().get()).deserialize(ByteBuf::readUnsignedShort, buf, version);
        }
        if (version >= 5) {
            final PaletteTypeEnum rotationTypeEnum = PaletteTypeEnum.get(buf.readByte());
            (this.rotationSection = (ISectionPalette)rotationTypeEnum.getConstructor().get()).deserialize(ByteBuf::readUnsignedByte, buf, version);
        }
        this.localLight = ChunkLightData.deserialize(buf, version);
        this.globalLight = ChunkLightData.deserialize(buf, version);
        this.localChangeCounter = buf.readShort();
        this.globalChangeCounter = buf.readShort();
    }
    
    public void deserialize(@Nonnull final byte[] bytes, @Nonnull final ExtraInfo extraInfo) {
        final ByteBuf buf = Unpooled.wrappedBuffer(bytes);
        this.deserialize(BlockType.KEY_DESERIALIZER, buf, extraInfo.getVersion());
    }
    
    @Override
    public Component<ChunkStore> clone() {
        throw new UnsupportedOperationException("Not implemented!");
    }
    
    @Nonnull
    @Override
    public Component<ChunkStore> cloneSerializable() {
        return this;
    }
    
    @Nonnull
    public CompletableFuture<CachedPacket<SetChunk>> getCachedChunkPacket(final int x, final int y, final int z) {
        final SoftReference<CompletableFuture<CachedPacket<SetChunk>>> ref = this.cachedChunkPacket;
        CompletableFuture<CachedPacket<SetChunk>> future = (ref != null) ? ref.get() : null;
        if (future != null) {
            return future;
        }
        future = CompletableFuture.supplyAsync(() -> {
            byte[] localLightArr = null;
            byte[] globalLightArr = null;
            byte[] data = null;
            if (BlockChunk.SEND_LOCAL_LIGHTING_DATA && this.hasLocalLight()) {
                final ChunkLightData localLight = this.getLocalLight();
                final ByteBuf buffer = Unpooled.buffer();
                localLight.serializeForPacket(buffer);
                if (this.getLocalChangeCounter() == localLight.getChangeId()) {
                    localLightArr = ByteBufUtil.getBytesRelease(buffer);
                }
            }
            if (BlockChunk.SEND_GLOBAL_LIGHTING_DATA && this.hasGlobalLight()) {
                final ByteBuf buffer2 = Unpooled.buffer();
                final ChunkLightData globalLight = this.getGlobalLight();
                globalLight.serializeForPacket(buffer2);
                if (this.getGlobalChangeCounter() == globalLight.getChangeId()) {
                    globalLightArr = ByteBufUtil.getBytesRelease(buffer2);
                }
            }
            if (!this.isSolidAir()) {
                final ByteBuf buf = Unpooled.buffer(65536);
                this.serializeForPacket(buf);
                data = ByteBufUtil.getBytesRelease(buf);
            }
            final SetChunk setChunk = new SetChunk(x, y, z, localLightArr, globalLightArr, data);
            return CachedPacket.cache(setChunk);
        });
        this.cachedChunkPacket = new SoftReference<CompletableFuture<CachedPacket<SetChunk>>>(future);
        return future;
    }
    
    public int get(final int x, final int y, final int z) {
        return this.get(ChunkUtil.indexBlock(x, y, z));
    }
    
    public boolean set(final int x, final int y, final int z, final int blockId, final int rotation, final int filler) {
        return this.set(ChunkUtil.indexBlock(x, y, z), blockId, rotation, filler);
    }
    
    public boolean setTicking(final int x, final int y, final int z, final boolean ticking) {
        return this.setTicking(ChunkUtil.indexBlock(x, y, z), ticking);
    }
    
    public boolean isTicking(final int x, final int y, final int z) {
        return this.isTicking(ChunkUtil.indexBlock(x, y, z));
    }
    
    static {
        // 
        // This method could not be decompiled.
        // 
        // Original Bytecode:
        // 
        //     2: invokedynamic   BootstrapMethod #9, get:()Ljava/util/function/Supplier;
        //     7: invokestatic    com/hypixel/hytale/codec/builder/BuilderCodec.builder:(Ljava/lang/Class;Ljava/util/function/Supplier;)Lcom/hypixel/hytale/codec/builder/BuilderCodec$Builder;
        //    10: invokevirtual   com/hypixel/hytale/codec/builder/BuilderCodec$Builder.versioned:()Lcom/hypixel/hytale/codec/builder/BuilderCodec$BuilderBase;
        //    13: checkcast       Lcom/hypixel/hytale/codec/builder/BuilderCodec$Builder;
        //    16: bipush          6
        //    18: invokevirtual   com/hypixel/hytale/codec/builder/BuilderCodec$Builder.codecVersion:(I)Lcom/hypixel/hytale/codec/builder/BuilderCodec$BuilderBase;
        //    21: checkcast       Lcom/hypixel/hytale/codec/builder/BuilderCodec$Builder;
        //    24: new             Lcom/hypixel/hytale/codec/KeyedCodec;
        //    27: dup            
        //    28: ldc_w           "Data"
        //    31: getstatic       com/hypixel/hytale/codec/Codec.BYTE_ARRAY:Lcom/hypixel/hytale/codec/Codec;
        //    34: invokespecial   com/hypixel/hytale/codec/KeyedCodec.<init>:(Ljava/lang/String;Lcom/hypixel/hytale/codec/Codec;)V
        //    37: invokedynamic   BootstrapMethod #10, accept:()Lcom/hypixel/hytale/function/consumer/TriConsumer;
        //    42: invokedynamic   BootstrapMethod #11, apply:()Ljava/util/function/BiFunction;
        //    47: invokevirtual   com/hypixel/hytale/codec/builder/BuilderCodec$Builder.append:(Lcom/hypixel/hytale/codec/KeyedCodec;Lcom/hypixel/hytale/function/consumer/TriConsumer;Ljava/util/function/BiFunction;)Lcom/hypixel/hytale/codec/builder/BuilderField$FieldBuilder;
        //    50: invokevirtual   com/hypixel/hytale/codec/builder/BuilderField$FieldBuilder.add:()Lcom/hypixel/hytale/codec/builder/BuilderCodec$BuilderBase;
        //    53: checkcast       Lcom/hypixel/hytale/codec/builder/BuilderCodec$Builder;
        //    56: invokevirtual   com/hypixel/hytale/codec/builder/BuilderCodec$Builder.build:()Lcom/hypixel/hytale/codec/builder/BuilderCodec;
        //    59: putstatic       com/hypixel/hytale/server/core/universe/world/chunk/section/BlockSection.CODEC:Lcom/hypixel/hytale/codec/builder/BuilderCodec;
        //    62: invokedynamic   BootstrapMethod #12, apply:()Ljava/util/function/Function;
        //    67: invokestatic    java/util/Comparator.comparing:(Ljava/util/function/Function;)Ljava/util/Comparator;
        //    70: putstatic       com/hypixel/hytale/server/core/universe/world/chunk/section/BlockSection.TICK_REQUEST_COMPARATOR:Ljava/util/Comparator;
        //    73: return         
        // 
        // The error that occurred was:
        // 
        // java.lang.UnsupportedOperationException: The requested operation is not supported.
        //     at com.strobel.util.ContractUtils.unsupported(ContractUtils.java:27)
        //     at com.strobel.assembler.metadata.TypeReference.getRawType(TypeReference.java:284)
        //     at com.strobel.assembler.metadata.TypeReference.getRawType(TypeReference.java:279)
        //     at com.strobel.assembler.metadata.TypeReference.makeGenericType(TypeReference.java:154)
        //     at com.strobel.assembler.metadata.TypeSubstitutionVisitor.visitParameterizedType(TypeSubstitutionVisitor.java:225)
        //     at com.strobel.assembler.metadata.TypeSubstitutionVisitor.visitParameterizedType(TypeSubstitutionVisitor.java:25)
        //     at com.strobel.assembler.metadata.ParameterizedType.accept(ParameterizedType.java:103)
        //     at com.strobel.assembler.metadata.TypeSubstitutionVisitor.visit(TypeSubstitutionVisitor.java:40)
        //     at com.strobel.assembler.metadata.TypeSubstitutionVisitor.visitMethod(TypeSubstitutionVisitor.java:314)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferCall(TypeAnalysis.java:2611)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1040)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:782)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:778)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1510)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:790)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferCall(TypeAnalysis.java:2689)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1040)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:790)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferCall(TypeAnalysis.java:2689)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1040)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:782)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:778)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1510)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:790)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferCall(TypeAnalysis.java:2689)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1040)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:782)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:778)
        //     at com.strobel.decompiler.ast.TypeAnalysis.doInferTypeForExpression(TypeAnalysis.java:1083)
        //     at com.strobel.decompiler.ast.TypeAnalysis.inferTypeForExpression(TypeAnalysis.java:815)
        //     at com.strobel.decompiler.ast.TypeAnalysis.runInference(TypeAnalysis.java:684)
        //     at com.strobel.decompiler.ast.TypeAnalysis.runInference(TypeAnalysis.java:667)
        //     at com.strobel.decompiler.ast.TypeAnalysis.runInference(TypeAnalysis.java:373)
        //     at com.strobel.decompiler.ast.TypeAnalysis.run(TypeAnalysis.java:95)
        //     at com.strobel.decompiler.ast.AstOptimizer.optimize(AstOptimizer.java:344)
        //     at com.strobel.decompiler.ast.AstOptimizer.optimize(AstOptimizer.java:42)
        //     at com.strobel.decompiler.languages.java.ast.AstMethodBodyBuilder.createMethodBody(AstMethodBodyBuilder.java:206)
        //     at com.strobel.decompiler.languages.java.ast.AstMethodBodyBuilder.createMethodBody(AstMethodBodyBuilder.java:93)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.createMethodBody(AstBuilder.java:868)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.createMethod(AstBuilder.java:761)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.addTypeMembers(AstBuilder.java:638)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.createTypeCore(AstBuilder.java:605)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.createTypeNoCache(AstBuilder.java:195)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.createType(AstBuilder.java:162)
        //     at com.strobel.decompiler.languages.java.ast.AstBuilder.addType(AstBuilder.java:137)
        //     at com.strobel.decompiler.languages.java.JavaLanguage.buildAst(JavaLanguage.java:71)
        //     at com.strobel.decompiler.languages.java.JavaLanguage.decompileType(JavaLanguage.java:59)
        //     at com.strobel.decompiler.DecompilerDriver.decompileType(DecompilerDriver.java:333)
        //     at com.strobel.decompiler.DecompilerDriver.decompileJar(DecompilerDriver.java:254)
        //     at com.strobel.decompiler.DecompilerDriver.main(DecompilerDriver.java:129)
        // 
        throw new IllegalStateException("An error occurred while decompiling this method.");
    }
    
    record TickRequest(int index, @Nonnull Instant requestedGameTime) {
        @Nonnull
        public Instant requestedGameTime() {
            return this.requestedGameTime;
        }
    }
}
