package net.minecraft.server.level;

import net.minecraft.core.Registry;
import java.util.concurrent.CompletionStage;
import net.minecraft.world.level.lighting.LevelLightEngine;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.network.protocol.Packet;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.LightLayer;
import com.google.common.annotations.VisibleForTesting;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.minecraft.world.level.storage.LevelData;
import net.minecraft.world.level.NaturalSpawner;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.GameRules;
import net.minecraft.world.level.LevelType;
import java.util.function.BooleanSupplier;
import java.io.IOException;
import java.util.function.Function;
import net.minecraft.core.BlockPos;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import java.util.Optional;
import net.minecraft.world.level.BlockGetter;
import java.util.Arrays;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.Util;
import com.mojang.datafixers.util.Either;
import net.minecraft.world.level.ChunkPos;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
import net.minecraft.world.level.chunk.LightChunkGetter;
import net.minecraft.util.thread.BlockableEventLoop;
import net.minecraft.world.level.Level;
import java.util.function.Supplier;
import net.minecraft.server.level.progress.ChunkProgressListener;
import java.util.concurrent.Executor;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureManager;
import com.mojang.datafixers.DataFixer;
import java.io.File;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.storage.DimensionDataStorage;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkStatus;
import java.util.List;
import net.minecraft.world.level.chunk.ChunkSource;

public class ServerChunkCache extends ChunkSource {
    private static final int MAGIC_NUMBER;
    private static final List<ChunkStatus> CHUNK_STATUSES;
    private final DistanceManager distanceManager;
    private final ChunkGenerator<?> generator;
    private final ServerLevel level;
    private final Thread mainThread;
    private final ThreadedLevelLightEngine lightEngine;
    private final MainThreadExecutor mainThreadProcessor;
    public final ChunkMap chunkMap;
    private final DimensionDataStorage dataStorage;
    private long lastInhabitedUpdate;
    private boolean spawnEnemies;
    private boolean spawnFriendlies;
    private final long[] lastChunkPos;
    private final ChunkStatus[] lastChunkStatus;
    private final ChunkAccess[] lastChunk;
    
    public ServerChunkCache(final ServerLevel xd, final File file, final DataFixer dataFixer, final StructureManager cmo, final Executor executor, final ChunkGenerator<?> caa, final int integer, final ChunkProgressListener xm, final Supplier<DimensionDataStorage> supplier) {
        this.spawnEnemies = true;
        this.spawnFriendlies = true;
        this.lastChunkPos = new long[4];
        this.lastChunkStatus = new ChunkStatus[4];
        this.lastChunk = new ChunkAccess[4];
        this.level = xd;
        this.mainThreadProcessor = new MainThreadExecutor((Level)xd);
        this.generator = caa;
        this.mainThread = Thread.currentThread();
        final File file2 = xd.getDimension().getType().getStorageFolder(file);
        final File file3 = new File(file2, "data");
        file3.mkdirs();
        this.dataStorage = new DimensionDataStorage(file3, dataFixer);
        this.chunkMap = new ChunkMap(xd, file, dataFixer, cmo, executor, this.mainThreadProcessor, this, this.getGenerator(), xm, supplier, integer);
        this.lightEngine = this.chunkMap.getLightEngine();
        this.distanceManager = this.chunkMap.getDistanceManager();
        this.clearCache();
    }
    
    @Override
    public ThreadedLevelLightEngine getLightEngine() {
        return this.lightEngine;
    }
    
    @Nullable
    private ChunkHolder getVisibleChunkIfPresent(final long long1) {
        return this.chunkMap.getVisibleChunkIfPresent(long1);
    }
    
    public int getTickingGenerated() {
        return this.chunkMap.getTickingGenerated();
    }
    
    private void storeInCache(final long long1, final ChunkAccess bzy, final ChunkStatus cae) {
        for (int integer6 = 3; integer6 > 0; --integer6) {
            this.lastChunkPos[integer6] = this.lastChunkPos[integer6 - 1];
            this.lastChunkStatus[integer6] = this.lastChunkStatus[integer6 - 1];
            this.lastChunk[integer6] = this.lastChunk[integer6 - 1];
        }
        this.lastChunkPos[0] = long1;
        this.lastChunkStatus[0] = cae;
        this.lastChunk[0] = bzy;
    }
    
    @Nullable
    @Override
    public ChunkAccess getChunk(final int integer1, final int integer2, final ChunkStatus cae, final boolean boolean4) {
        if (Thread.currentThread() != this.mainThread) {
            return CompletableFuture.<ChunkAccess>supplyAsync(() -> this.getChunk(integer1, integer2, cae, boolean4), this.mainThreadProcessor).join();
        }
        final ProfilerFiller aio6 = this.level.getProfiler();
        aio6.incrementCounter("getChunk");
        final long long7 = ChunkPos.asLong(integer1, integer2);
        for (int integer3 = 0; integer3 < 4; ++integer3) {
            if (long7 == this.lastChunkPos[integer3] && cae == this.lastChunkStatus[integer3]) {
                final ChunkAccess bzy2 = this.lastChunk[integer3];
                if (bzy2 != null || !boolean4) {
                    return bzy2;
                }
            }
        }
        aio6.incrementCounter("getChunkCacheMiss");
        final CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completableFuture9 = this.getChunkFutureMainThread(integer1, integer2, cae, boolean4);
        this.mainThreadProcessor.managedBlock(completableFuture9::isDone);
        final IllegalStateException throwable;
        final ChunkAccess bzy2 = (ChunkAccess)completableFuture9.join().map(bzy -> bzy, a -> {
            if (boolean4) {
                new IllegalStateException("Chunk not there when requested: " + a);
                throw Util.<IllegalStateException>pauseInIde(throwable);
            }
            else {
                return null;
            }
        });
        this.storeInCache(long7, bzy2, cae);
        return bzy2;
    }
    
    @Nullable
    @Override
    public LevelChunk getChunkNow(final int integer1, final int integer2) {
        if (Thread.currentThread() != this.mainThread) {
            return null;
        }
        this.level.getProfiler().incrementCounter("getChunkNow");
        final long long4 = ChunkPos.asLong(integer1, integer2);
        for (int integer3 = 0; integer3 < 4; ++integer3) {
            if (long4 == this.lastChunkPos[integer3] && this.lastChunkStatus[integer3] == ChunkStatus.FULL) {
                final ChunkAccess bzy7 = this.lastChunk[integer3];
                return (bzy7 instanceof LevelChunk) ? ((LevelChunk)bzy7) : null;
            }
        }
        final ChunkHolder wo6 = this.getVisibleChunkIfPresent(long4);
        if (wo6 == null) {
            return null;
        }
        final Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure> either7 = wo6.getFutureIfPresent(ChunkStatus.FULL).getNow(null);
        if (either7 == null) {
            return null;
        }
        final ChunkAccess bzy8 = either7.left().orElse(null);
        if (bzy8 != null) {
            this.storeInCache(long4, bzy8, ChunkStatus.FULL);
            if (bzy8 instanceof LevelChunk) {
                return (LevelChunk)bzy8;
            }
        }
        return null;
    }
    
    private void clearCache() {
        Arrays.fill(this.lastChunkPos, ChunkPos.INVALID_CHUNK_POS);
        Arrays.fill(this.lastChunkStatus, null);
        Arrays.fill(this.lastChunk, null);
    }
    
    public CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> getChunkFuture(final int integer1, final int integer2, final ChunkStatus cae, final boolean boolean4) {
        final boolean boolean5 = Thread.currentThread() == this.mainThread;
        CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completableFuture2;
        if (boolean5) {
            completableFuture2 = this.getChunkFutureMainThread(integer1, integer2, cae, boolean4);
            this.mainThreadProcessor.managedBlock(completableFuture2::isDone);
        }
        else {
            completableFuture2 = CompletableFuture.<CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>>supplyAsync(() -> this.getChunkFutureMainThread(integer1, integer2, cae, boolean4), this.mainThreadProcessor).<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>thenCompose(completableFuture -> completableFuture);
        }
        return completableFuture2;
    }
    
    private CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> getChunkFutureMainThread(final int integer1, final int integer2, final ChunkStatus cae, final boolean boolean4) {
        final ChunkPos bjh6 = new ChunkPos(integer1, integer2);
        final long long7 = bjh6.toLong();
        final int integer3 = 33 + ChunkStatus.getDistance(cae);
        ChunkHolder wo10 = this.getVisibleChunkIfPresent(long7);
        if (boolean4) {
            this.distanceManager.<ChunkPos>addTicket(TicketType.UNKNOWN, bjh6, integer3, bjh6);
            if (this.chunkAbsent(wo10, integer3)) {
                final ProfilerFiller aio11 = this.level.getProfiler();
                aio11.push("chunkLoad");
                this.runDistanceManagerUpdates();
                wo10 = this.getVisibleChunkIfPresent(long7);
                aio11.pop();
                if (this.chunkAbsent(wo10, integer3)) {
                    throw Util.<IllegalStateException>pauseInIde(new IllegalStateException("No chunk holder after ticket has been added"));
                }
            }
        }
        if (this.chunkAbsent(wo10, integer3)) {
            return ChunkHolder.UNLOADED_CHUNK_FUTURE;
        }
        return wo10.getOrScheduleFuture(cae, this.chunkMap);
    }
    
    private boolean chunkAbsent(@Nullable final ChunkHolder wo, final int integer) {
        return wo == null || wo.getTicketLevel() > integer;
    }
    
    @Override
    public boolean hasChunk(final int integer1, final int integer2) {
        final ChunkHolder wo4 = this.getVisibleChunkIfPresent(new ChunkPos(integer1, integer2).toLong());
        final int integer3 = 33 + ChunkStatus.getDistance(ChunkStatus.FULL);
        return !this.chunkAbsent(wo4, integer3);
    }
    
    @Override
    public BlockGetter getChunkForLighting(final int integer1, final int integer2) {
        final long long4 = ChunkPos.asLong(integer1, integer2);
        final ChunkHolder wo6 = this.getVisibleChunkIfPresent(long4);
        if (wo6 == null) {
            return null;
        }
        int integer3 = ServerChunkCache.CHUNK_STATUSES.size() - 1;
        while (true) {
            final ChunkStatus cae8 = ServerChunkCache.CHUNK_STATUSES.get(integer3);
            final Optional<ChunkAccess> optional9 = (Optional<ChunkAccess>)wo6.getFutureIfPresentUnchecked(cae8).getNow(ChunkHolder.UNLOADED_CHUNK).left();
            if (optional9.isPresent()) {
                return optional9.get();
            }
            if (cae8 == ChunkStatus.LIGHT.getParent()) {
                return null;
            }
            --integer3;
        }
    }
    
    @Override
    public Level getLevel() {
        return this.level;
    }
    
    public boolean pollTask() {
        return this.mainThreadProcessor.pollTask();
    }
    
    private boolean runDistanceManagerUpdates() {
        final boolean boolean2 = this.distanceManager.runAllUpdates(this.chunkMap);
        final boolean boolean3 = this.chunkMap.promoteChunkMap();
        if (boolean2 || boolean3) {
            this.clearCache();
            return true;
        }
        return false;
    }
    
    @Override
    public boolean isEntityTickingChunk(final Entity akq) {
        final long long3 = ChunkPos.asLong(Mth.floor(akq.getX()) >> 4, Mth.floor(akq.getZ()) >> 4);
        return this.checkChunkFuture(long3, ChunkHolder::getEntityTickingChunkFuture);
    }
    
    @Override
    public boolean isEntityTickingChunk(final ChunkPos bjh) {
        return this.checkChunkFuture(bjh.toLong(), ChunkHolder::getEntityTickingChunkFuture);
    }
    
    @Override
    public boolean isTickingChunk(final BlockPos fk) {
        final long long3 = ChunkPos.asLong(fk.getX() >> 4, fk.getZ() >> 4);
        return this.checkChunkFuture(long3, ChunkHolder::getTickingChunkFuture);
    }
    
    public boolean isInAccessibleChunk(final Entity akq) {
        final long long3 = ChunkPos.asLong(Mth.floor(akq.getX()) >> 4, Mth.floor(akq.getZ()) >> 4);
        return this.checkChunkFuture(long3, ChunkHolder::getFullChunkFuture);
    }
    
    private boolean checkChunkFuture(final long long1, final Function<ChunkHolder, CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>>> function) {
        final ChunkHolder wo5 = this.getVisibleChunkIfPresent(long1);
        if (wo5 == null) {
            return false;
        }
        final Either<LevelChunk, ChunkHolder.ChunkLoadingFailure> either6 = function.apply(wo5).getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK);
        return either6.left().isPresent();
    }
    
    public void save(final boolean boolean1) {
        this.runDistanceManagerUpdates();
        this.chunkMap.saveAllChunks(boolean1);
    }
    
    @Override
    public void close() throws IOException {
        this.save(true);
        this.lightEngine.close();
        this.chunkMap.close();
    }
    
    @Override
    public void tick(final BooleanSupplier booleanSupplier) {
        this.level.getProfiler().push("purge");
        this.distanceManager.purgeStaleTickets();
        this.runDistanceManagerUpdates();
        this.level.getProfiler().popPush("chunks");
        this.tickChunks();
        this.level.getProfiler().popPush("unload");
        this.chunkMap.tick(booleanSupplier);
        this.level.getProfiler().pop();
        this.clearCache();
    }
    
    private void tickChunks() {
        final long long2 = this.level.getGameTime();
        final long long3 = long2 - this.lastInhabitedUpdate;
        this.lastInhabitedUpdate = long2;
        final LevelData crm6 = this.level.getLevelData();
        final boolean boolean7 = crm6.getGeneratorType() == LevelType.DEBUG_ALL_BLOCK_STATES;
        final boolean boolean8 = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING);
        if (!boolean7) {
            this.level.getProfiler().push("pollingChunks");
            final int integer9 = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING);
            final BlockPos fk10 = this.level.getSharedSpawnPos();
            final boolean boolean9 = crm6.getGameTime() % 400L == 0L;
            this.level.getProfiler().push("naturalSpawnCount");
            final int integer10 = this.distanceManager.getNaturalSpawnChunkCount();
            final MobCategory[] arr13 = MobCategory.values();
            final Object2IntMap<MobCategory> object2IntMap14 = this.level.getMobCategoryCounts();
            this.level.getProfiler().pop();
            final Optional<LevelChunk> optional12;
            LevelChunk cal13;
            ChunkPos bjh14;
            final long n;
            final boolean b;
            final MobCategory[] array;
            int length;
            int i = 0;
            MobCategory alb18;
            final boolean b2;
            final int n2;
            int integer11;
            final Object2IntMap object2IntMap15;
            final BlockPos fk11;
            final int integer12;
            this.chunkMap.getChunks().forEach(wo -> {
                optional12 = (Optional<LevelChunk>)wo.getEntityTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).left();
                if (!optional12.isPresent()) {
                    return;
                }
                else {
                    cal13 = optional12.get();
                    this.level.getProfiler().push("broadcast");
                    wo.broadcastChanges(cal13);
                    this.level.getProfiler().pop();
                    bjh14 = wo.getPos();
                    if (this.chunkMap.noPlayersCloseForSpawning(bjh14)) {
                        return;
                    }
                    else {
                        cal13.setInhabitedTime(cal13.getInhabitedTime() + n);
                        if (b && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(cal13.getPos())) {
                            this.level.getProfiler().push("spawner");
                            for (length = array.length; i < length; ++i) {
                                alb18 = array[i];
                                if (alb18 != MobCategory.MISC) {
                                    if (!alb18.isFriendly() || this.spawnFriendlies) {
                                        if (alb18.isFriendly() || this.spawnEnemies) {
                                            if (!alb18.isPersistent() || b2) {
                                                integer11 = alb18.getMaxInstancesPerChunk() * n2 / ServerChunkCache.MAGIC_NUMBER;
                                                if (object2IntMap15.getInt(alb18) <= integer11) {
                                                    NaturalSpawner.spawnCategoryForChunk(alb18, this.level, cal13, fk11);
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                            this.level.getProfiler().pop();
                        }
                        this.level.tickChunk(cal13, integer12);
                        return;
                    }
                }
            });
            this.level.getProfiler().push("customSpawners");
            if (boolean8) {
                this.generator.tickCustomSpawners(this.level, this.spawnEnemies, this.spawnFriendlies);
            }
            this.level.getProfiler().pop();
            this.level.getProfiler().pop();
        }
        this.chunkMap.tick();
    }
    
    @Override
    public String gatherStats() {
        return "ServerChunkCache: " + this.getLoadedChunksCount();
    }
    
    @VisibleForTesting
    public int getPendingTasksCount() {
        return this.mainThreadProcessor.getPendingTasksCount();
    }
    
    public ChunkGenerator<?> getGenerator() {
        return this.generator;
    }
    
    public int getLoadedChunksCount() {
        return this.chunkMap.size();
    }
    
    public void blockChanged(final BlockPos fk) {
        final int integer3 = fk.getX() >> 4;
        final int integer4 = fk.getZ() >> 4;
        final ChunkHolder wo5 = this.getVisibleChunkIfPresent(ChunkPos.asLong(integer3, integer4));
        if (wo5 != null) {
            wo5.blockChanged(fk.getX() & 0xF, fk.getY(), fk.getZ() & 0xF);
        }
    }
    
    @Override
    public void onLightUpdate(final LightLayer bkf, final SectionPos gd) {
        final ChunkHolder wo4;
        this.mainThreadProcessor.execute(() -> {
            wo4 = this.getVisibleChunkIfPresent(gd.chunk().toLong());
            if (wo4 != null) {
                wo4.sectionLightChanged(bkf, gd.y());
            }
        });
    }
    
    public <T> void addRegionTicket(final TicketType<T> xi, final ChunkPos bjh, final int integer, final T object) {
        this.distanceManager.<T>addRegionTicket(xi, bjh, integer, object);
    }
    
    public <T> void removeRegionTicket(final TicketType<T> xi, final ChunkPos bjh, final int integer, final T object) {
        this.distanceManager.<T>removeRegionTicket(xi, bjh, integer, object);
    }
    
    @Override
    public void updateChunkForced(final ChunkPos bjh, final boolean boolean2) {
        this.distanceManager.updateChunkForced(bjh, boolean2);
    }
    
    public void move(final ServerPlayer xe) {
        this.chunkMap.move(xe);
    }
    
    public void removeEntity(final Entity akq) {
        this.chunkMap.removeEntity(akq);
    }
    
    public void addEntity(final Entity akq) {
        this.chunkMap.addEntity(akq);
    }
    
    public void broadcastAndSend(final Entity akq, final Packet<?> lt) {
        this.chunkMap.broadcastAndSend(akq, lt);
    }
    
    public void broadcast(final Entity akq, final Packet<?> lt) {
        this.chunkMap.broadcast(akq, lt);
    }
    
    public void setViewDistance(final int integer) {
        this.chunkMap.setViewDistance(integer);
    }
    
    @Override
    public void setSpawnSettings(final boolean boolean1, final boolean boolean2) {
        this.spawnEnemies = boolean1;
        this.spawnFriendlies = boolean2;
    }
    
    public String getChunkDebugData(final ChunkPos bjh) {
        return this.chunkMap.getChunkDebugData(bjh);
    }
    
    public DimensionDataStorage getDataStorage() {
        return this.dataStorage;
    }
    
    public PoiManager getPoiManager() {
        return this.chunkMap.getPoiManager();
    }
    
    static {
        MAGIC_NUMBER = (int)Math.pow(17.0, 2.0);
        CHUNK_STATUSES = ChunkStatus.getStatusList();
    }
    
    final class MainThreadExecutor extends BlockableEventLoop<Runnable> {
        private MainThreadExecutor(final Level bjw) {
            super("Chunk source main thread executor for " + Registry.DIMENSION_TYPE.getKey(bjw.getDimension().getType()));
        }
        
        @Override
        protected Runnable wrapRunnable(final Runnable runnable) {
            return runnable;
        }
        
        @Override
        protected boolean shouldRun(final Runnable runnable) {
            return true;
        }
        
        @Override
        protected boolean scheduleExecutables() {
            return true;
        }
        
        @Override
        protected Thread getRunningThread() {
            return ServerChunkCache.this.mainThread;
        }
        
        @Override
        protected void doRunTask(final Runnable runnable) {
            ServerChunkCache.this.level.getProfiler().incrementCounter("runTask");
            super.doRunTask(runnable);
        }
        
        @Override
        protected boolean pollTask() {
            if (ServerChunkCache.this.runDistanceManagerUpdates()) {
                return true;
            }
            ServerChunkCache.this.lightEngine.tryScheduleUpdate();
            return super.pollTask();
        }
    }
}
