package net.minecraft.client.multiplayer;

import java.util.concurrent.atomic.AtomicReferenceArray;
import org.apache.logging.log4j.LogManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.core.BlockPos;
import net.minecraft.client.Minecraft;
import net.minecraft.world.level.LightLayer;
import java.util.function.BooleanSupplier;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.core.SectionPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.level.chunk.ChunkBiomeContainer;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.chunk.ChunkStatus;
import javax.annotation.Nullable;
import net.minecraft.world.level.chunk.LightChunkGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.EmptyLevelChunk;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.lighting.LevelLightEngine;
import net.minecraft.world.level.chunk.LevelChunk;
import org.apache.logging.log4j.Logger;
import net.minecraft.world.level.chunk.ChunkSource;

public class ClientChunkCache extends ChunkSource {
    private static final Logger LOGGER;
    private final LevelChunk emptyChunk;
    private final LevelLightEngine lightEngine;
    private volatile Storage storage;
    private final ClientLevel level;
    
    public ClientChunkCache(final ClientLevel dym, final int integer) {
        this.level = dym;
        this.emptyChunk = new EmptyLevelChunk(dym, new ChunkPos(0, 0));
        this.lightEngine = new LevelLightEngine(this, true, dym.dimensionType().hasSkyLight());
        this.storage = new Storage(calculateStorageRange(integer));
    }
    
    @Override
    public LevelLightEngine getLightEngine() {
        return this.lightEngine;
    }
    
    private static boolean isValidChunk(@Nullable final LevelChunk chj, final int integer2, final int integer3) {
        if (chj == null) {
            return false;
        }
        final ChunkPos bph4 = chj.getPos();
        return bph4.x == integer2 && bph4.z == integer3;
    }
    
    public void drop(final int integer1, final int integer2) {
        if (!this.storage.inRange(integer1, integer2)) {
            return;
        }
        final int integer3 = this.storage.getIndex(integer1, integer2);
        final LevelChunk chj5 = this.storage.getChunk(integer3);
        if (isValidChunk(chj5, integer1, integer2)) {
            this.storage.replace(integer3, chj5, null);
        }
    }
    
    @Nullable
    @Override
    public LevelChunk getChunk(final int integer1, final int integer2, final ChunkStatus chc, final boolean boolean4) {
        if (this.storage.inRange(integer1, integer2)) {
            final LevelChunk chj6 = this.storage.getChunk(this.storage.getIndex(integer1, integer2));
            if (isValidChunk(chj6, integer1, integer2)) {
                return chj6;
            }
        }
        if (boolean4) {
            return this.emptyChunk;
        }
        return null;
    }
    
    @Override
    public BlockGetter getLevel() {
        return this.level;
    }
    
    @Nullable
    public LevelChunk replaceWithPacketData(final int integer1, final int integer2, @Nullable final ChunkBiomeContainer cgz, final FriendlyByteBuf mg, final CompoundTag le, final int integer6, final boolean boolean7) {
        if (!this.storage.inRange(integer1, integer2)) {
            ClientChunkCache.LOGGER.warn("Ignoring chunk since it's not in the view range: {}, {}", integer1, integer2);
            return null;
        }
        final int integer7 = this.storage.getIndex(integer1, integer2);
        LevelChunk chj10 = this.storage.chunks.get(integer7);
        if (boolean7 || !isValidChunk(chj10, integer1, integer2)) {
            if (cgz == null) {
                ClientChunkCache.LOGGER.warn("Ignoring chunk since we don't have complete data: {}, {}", integer1, integer2);
                return null;
            }
            chj10 = new LevelChunk(this.level, new ChunkPos(integer1, integer2), cgz);
            chj10.replaceWithPacketData(cgz, mg, le, integer6);
            this.storage.replace(integer7, chj10);
        }
        else {
            chj10.replaceWithPacketData(cgz, mg, le, integer6);
        }
        final LevelChunkSection[] arr11 = chj10.getSections();
        final LevelLightEngine cwr12 = this.getLightEngine();
        cwr12.enableLightSources(new ChunkPos(integer1, integer2), true);
        for (int integer8 = 0; integer8 < arr11.length; ++integer8) {
            final LevelChunkSection chk14 = arr11[integer8];
            cwr12.updateSectionStatus(SectionPos.of(integer1, integer8, integer2), LevelChunkSection.isEmpty(chk14));
        }
        this.level.onChunkLoaded(integer1, integer2);
        return chj10;
    }
    
    public void tick(final BooleanSupplier booleanSupplier) {
    }
    
    public void updateViewCenter(final int integer1, final int integer2) {
        this.storage.viewCenterX = integer1;
        this.storage.viewCenterZ = integer2;
    }
    
    public void updateViewRadius(final int integer) {
        final int integer2 = this.storage.chunkRadius;
        final int integer3 = calculateStorageRange(integer);
        if (integer2 != integer3) {
            final Storage a5 = new Storage(integer3);
            a5.viewCenterX = this.storage.viewCenterX;
            a5.viewCenterZ = this.storage.viewCenterZ;
            for (int integer4 = 0; integer4 < this.storage.chunks.length(); ++integer4) {
                final LevelChunk chj7 = this.storage.chunks.get(integer4);
                if (chj7 != null) {
                    final ChunkPos bph8 = chj7.getPos();
                    if (a5.inRange(bph8.x, bph8.z)) {
                        a5.replace(a5.getIndex(bph8.x, bph8.z), chj7);
                    }
                }
            }
            this.storage = a5;
        }
    }
    
    private static int calculateStorageRange(final int integer) {
        return Math.max(2, integer) + 3;
    }
    
    @Override
    public String gatherStats() {
        return "Client Chunk Cache: " + this.storage.chunks.length() + ", " + this.getLoadedChunksCount();
    }
    
    public int getLoadedChunksCount() {
        return this.storage.chunkCount;
    }
    
    @Override
    public void onLightUpdate(final LightLayer bqi, final SectionPos go) {
        Minecraft.getInstance().levelRenderer.setSectionDirty(go.x(), go.y(), go.z());
    }
    
    @Override
    public boolean isTickingChunk(final BlockPos fu) {
        return this.hasChunk(fu.getX() >> 4, fu.getZ() >> 4);
    }
    
    @Override
    public boolean isEntityTickingChunk(final ChunkPos bph) {
        return this.hasChunk(bph.x, bph.z);
    }
    
    @Override
    public boolean isEntityTickingChunk(final Entity aom) {
        return this.hasChunk(Mth.floor(aom.getX()) >> 4, Mth.floor(aom.getZ()) >> 4);
    }
    
    static {
        LOGGER = LogManager.getLogger();
    }
    
    final class Storage {
        private final AtomicReferenceArray<LevelChunk> chunks;
        private final int chunkRadius;
        private final int viewRange;
        private volatile int viewCenterX;
        private volatile int viewCenterZ;
        private int chunkCount;
        
        private Storage(final int integer) {
            this.chunkRadius = integer;
            this.viewRange = integer * 2 + 1;
            this.chunks = new AtomicReferenceArray<LevelChunk>(this.viewRange * this.viewRange);
        }
        
        private int getIndex(final int integer1, final int integer2) {
            return Math.floorMod(integer2, this.viewRange) * this.viewRange + Math.floorMod(integer1, this.viewRange);
        }
        
        protected void replace(final int integer, @Nullable final LevelChunk chj) {
            final LevelChunk chj2 = this.chunks.getAndSet(integer, chj);
            if (chj2 != null) {
                --this.chunkCount;
                ClientChunkCache.this.level.unload(chj2);
            }
            if (chj != null) {
                ++this.chunkCount;
            }
        }
        
        protected LevelChunk replace(final int integer, final LevelChunk chj2, @Nullable final LevelChunk chj3) {
            if (this.chunks.compareAndSet(integer, chj2, chj3) && chj3 == null) {
                --this.chunkCount;
            }
            ClientChunkCache.this.level.unload(chj2);
            return chj2;
        }
        
        private boolean inRange(final int integer1, final int integer2) {
            return Math.abs(integer1 - this.viewCenterX) <= this.chunkRadius && Math.abs(integer2 - this.viewCenterZ) <= this.chunkRadius;
        }
        
        @Nullable
        protected LevelChunk getChunk(final int integer) {
            return this.chunks.get(integer);
        }
    }
}
