package net.minecraft.server.level;

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 java.util.function.Consumer;
import net.minecraft.world.level.storage.LevelData;
import java.util.Collections;
import com.google.common.collect.Lists;
import net.minecraft.world.level.GameRules;
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 net.minecraft.world.level.chunk.LightChunkGetter;
import net.minecraft.util.thread.BlockableEventLoop;
import java.io.File;
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 net.minecraft.world.level.storage.LevelStorageSource;
import javax.annotation.Nullable;
import net.minecraft.world.level.NaturalSpawner;
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 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;
    @Nullable
    private NaturalSpawner.SpawnState lastSpawnState;
    
    public ServerChunkCache(final ServerLevel zd, final LevelStorageSource.LevelStorageAccess a, final DataFixer dataFixer, final StructureManager cva, final Executor executor, final ChunkGenerator cha, final int integer, final boolean boolean8, final ChunkProgressListener zm, 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 = zd;
        this.mainThreadProcessor = new MainThreadExecutor((Level)zd);
        this.generator = cha;
        this.mainThread = Thread.currentThread();
        final File file12 = a.getDimensionPath(zd.dimension());
        final File file13 = new File(file12, "data");
        file13.mkdirs();
        this.dataStorage = new DimensionDataStorage(file13, dataFixer);
        this.chunkMap = new ChunkMap(zd, a, dataFixer, cva, executor, this.mainThreadProcessor, this, this.getGenerator(), zm, supplier, integer, boolean8);
        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 cgy, final ChunkStatus chc) {
        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] = chc;
        this.lastChunk[0] = cgy;
    }
    
    @Nullable
    @Override
    public ChunkAccess getChunk(final int integer1, final int integer2, final ChunkStatus chc, final boolean boolean4) {
        if (Thread.currentThread() != this.mainThread) {
            return CompletableFuture.<ChunkAccess>supplyAsync(() -> this.getChunk(integer1, integer2, chc, boolean4), this.mainThreadProcessor).join();
        }
        final ProfilerFiller ami6 = this.level.getProfiler();
        ami6.incrementCounter("getChunk");
        final long long7 = ChunkPos.asLong(integer1, integer2);
        for (int integer3 = 0; integer3 < 4; ++integer3) {
            if (long7 == this.lastChunkPos[integer3] && chc == this.lastChunkStatus[integer3]) {
                final ChunkAccess cgy2 = this.lastChunk[integer3];
                if (cgy2 != null || !boolean4) {
                    return cgy2;
                }
            }
        }
        ami6.incrementCounter("getChunkCacheMiss");
        final CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completableFuture9 = this.getChunkFutureMainThread(integer1, integer2, chc, boolean4);
        this.mainThreadProcessor.managedBlock(completableFuture9::isDone);
        final IllegalStateException throwable;
        final ChunkAccess cgy2 = (ChunkAccess)completableFuture9.join().map(cgy -> cgy, a -> {
            if (boolean4) {
                new IllegalStateException("Chunk not there when requested: " + a);
                throw Util.<IllegalStateException>pauseInIde(throwable);
            }
            else {
                return null;
            }
        });
        this.storeInCache(long7, cgy2, chc);
        return cgy2;
    }
    
    @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 cgy7 = this.lastChunk[integer3];
                return (cgy7 instanceof LevelChunk) ? ((LevelChunk)cgy7) : null;
            }
        }
        final ChunkHolder yo6 = this.getVisibleChunkIfPresent(long4);
        if (yo6 == null) {
            return null;
        }
        final Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure> either7 = yo6.getFutureIfPresent(ChunkStatus.FULL).getNow(null);
        if (either7 == null) {
            return null;
        }
        final ChunkAccess cgy8 = either7.left().orElse(null);
        if (cgy8 != null) {
            this.storeInCache(long4, cgy8, ChunkStatus.FULL);
            if (cgy8 instanceof LevelChunk) {
                return (LevelChunk)cgy8;
            }
        }
        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 chc, final boolean boolean4) {
        final boolean boolean5 = Thread.currentThread() == this.mainThread;
        CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completableFuture2;
        if (boolean5) {
            completableFuture2 = this.getChunkFutureMainThread(integer1, integer2, chc, boolean4);
            this.mainThreadProcessor.managedBlock(completableFuture2::isDone);
        }
        else {
            completableFuture2 = CompletableFuture.<CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>>supplyAsync(() -> this.getChunkFutureMainThread(integer1, integer2, chc, 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 chc, final boolean boolean4) {
        final ChunkPos bph6 = new ChunkPos(integer1, integer2);
        final long long7 = bph6.toLong();
        final int integer3 = 33 + ChunkStatus.getDistance(chc);
        ChunkHolder yo10 = this.getVisibleChunkIfPresent(long7);
        if (boolean4) {
            this.distanceManager.<ChunkPos>addTicket(TicketType.UNKNOWN, bph6, integer3, bph6);
            if (this.chunkAbsent(yo10, integer3)) {
                final ProfilerFiller ami11 = this.level.getProfiler();
                ami11.push("chunkLoad");
                this.runDistanceManagerUpdates();
                yo10 = this.getVisibleChunkIfPresent(long7);
                ami11.pop();
                if (this.chunkAbsent(yo10, integer3)) {
                    throw Util.<IllegalStateException>pauseInIde(new IllegalStateException("No chunk holder after ticket has been added"));
                }
            }
        }
        if (this.chunkAbsent(yo10, integer3)) {
            return ChunkHolder.UNLOADED_CHUNK_FUTURE;
        }
        return yo10.getOrScheduleFuture(chc, this.chunkMap);
    }
    
    private boolean chunkAbsent(@Nullable final ChunkHolder yo, final int integer) {
        return yo == null || yo.getTicketLevel() > integer;
    }
    
    @Override
    public boolean hasChunk(final int integer1, final int integer2) {
        final ChunkHolder yo4 = this.getVisibleChunkIfPresent(new ChunkPos(integer1, integer2).toLong());
        final int integer3 = 33 + ChunkStatus.getDistance(ChunkStatus.FULL);
        return !this.chunkAbsent(yo4, integer3);
    }
    
    @Override
    public BlockGetter getChunkForLighting(final int integer1, final int integer2) {
        final long long4 = ChunkPos.asLong(integer1, integer2);
        final ChunkHolder yo6 = this.getVisibleChunkIfPresent(long4);
        if (yo6 == null) {
            return null;
        }
        int integer3 = ServerChunkCache.CHUNK_STATUSES.size() - 1;
        while (true) {
            final ChunkStatus chc8 = ServerChunkCache.CHUNK_STATUSES.get(integer3);
            final Optional<ChunkAccess> optional9 = (Optional<ChunkAccess>)yo6.getFutureIfPresentUnchecked(chc8).getNow(ChunkHolder.UNLOADED_CHUNK).left();
            if (optional9.isPresent()) {
                return optional9.get();
            }
            if (chc8 == 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 aom) {
        final long long3 = ChunkPos.asLong(Mth.floor(aom.getX()) >> 4, Mth.floor(aom.getZ()) >> 4);
        return this.checkChunkFuture(long3, ChunkHolder::getEntityTickingChunkFuture);
    }
    
    @Override
    public boolean isEntityTickingChunk(final ChunkPos bph) {
        return this.checkChunkFuture(bph.toLong(), ChunkHolder::getEntityTickingChunkFuture);
    }
    
    @Override
    public boolean isTickingChunk(final BlockPos fu) {
        final long long3 = ChunkPos.asLong(fu.getX() >> 4, fu.getZ() >> 4);
        return this.checkChunkFuture(long3, ChunkHolder::getTickingChunkFuture);
    }
    
    private boolean checkChunkFuture(final long long1, final Function<ChunkHolder, CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>>> function) {
        final ChunkHolder yo5 = this.getVisibleChunkIfPresent(long1);
        if (yo5 == null) {
            return false;
        }
        final Either<LevelChunk, ChunkHolder.ChunkLoadingFailure> either6 = function.apply(yo5).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();
    }
    
    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 dab6 = this.level.getLevelData();
        final boolean boolean7 = this.level.isDebug();
        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 boolean boolean9 = dab6.getGameTime() % 400L == 0L;
            this.level.getProfiler().push("naturalSpawnCount");
            final int integer10 = this.distanceManager.getNaturalSpawnChunkCount();
            final NaturalSpawner.SpawnState d12 = NaturalSpawner.createState(integer10, this.level.getAllEntities(), this::getFullChunk);
            this.lastSpawnState = d12;
            this.level.getProfiler().pop();
            final List<ChunkHolder> list13 = Lists.newArrayList(this.chunkMap.getChunks());
            Collections.shuffle(list13);
            final Optional<LevelChunk> optional9;
            Optional<LevelChunk> optional10;
            LevelChunk chj11;
            ChunkPos bph12;
            final long n;
            final boolean b;
            final NaturalSpawner.SpawnState d13;
            final boolean boolean10;
            final int integer11;
            list13.forEach(yo -> {
                optional9 = (Optional<LevelChunk>)yo.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).left();
                if (!optional9.isPresent()) {
                    return;
                }
                else {
                    this.level.getProfiler().push("broadcast");
                    yo.broadcastChanges(optional9.get());
                    this.level.getProfiler().pop();
                    optional10 = (Optional<LevelChunk>)yo.getEntityTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).left();
                    if (!optional10.isPresent()) {
                        return;
                    }
                    else {
                        chj11 = optional10.get();
                        bph12 = yo.getPos();
                        if (this.chunkMap.noPlayersCloseForSpawning(bph12)) {
                            return;
                        }
                        else {
                            chj11.setInhabitedTime(chj11.getInhabitedTime() + n);
                            if (b && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chj11.getPos())) {
                                NaturalSpawner.spawnForChunk(this.level, chj11, d13, this.spawnFriendlies, this.spawnEnemies, boolean10);
                            }
                            this.level.tickChunk(chj11, integer11);
                            return;
                        }
                    }
                }
            });
            this.level.getProfiler().push("customSpawners");
            if (boolean8) {
                this.level.tickCustomSpawners(this.spawnEnemies, this.spawnFriendlies);
            }
            this.level.getProfiler().pop();
            this.level.getProfiler().pop();
        }
        this.chunkMap.tick();
    }
    
    private void getFullChunk(final long long1, final Consumer<LevelChunk> consumer) {
        final ChunkHolder yo5 = this.getVisibleChunkIfPresent(long1);
        if (yo5 != null) {
            yo5.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).left().ifPresent(consumer);
        }
    }
    
    @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 fu) {
        final int integer3 = fu.getX() >> 4;
        final int integer4 = fu.getZ() >> 4;
        final ChunkHolder yo5 = this.getVisibleChunkIfPresent(ChunkPos.asLong(integer3, integer4));
        if (yo5 != null) {
            yo5.blockChanged(fu.getX() & 0xF, fu.getY(), fu.getZ() & 0xF);
        }
    }
    
    @Override
    public void onLightUpdate(final LightLayer bqi, final SectionPos go) {
        final ChunkHolder yo4;
        this.mainThreadProcessor.execute(() -> {
            yo4 = this.getVisibleChunkIfPresent(go.chunk().toLong());
            if (yo4 != null) {
                yo4.sectionLightChanged(bqi, go.y());
            }
        });
    }
    
    public <T> void addRegionTicket(final TicketType<T> zi, final ChunkPos bph, final int integer, final T object) {
        this.distanceManager.<T>addRegionTicket(zi, bph, integer, object);
    }
    
    public <T> void removeRegionTicket(final TicketType<T> zi, final ChunkPos bph, final int integer, final T object) {
        this.distanceManager.<T>removeRegionTicket(zi, bph, integer, object);
    }
    
    @Override
    public void updateChunkForced(final ChunkPos bph, final boolean boolean2) {
        this.distanceManager.updateChunkForced(bph, boolean2);
    }
    
    public void move(final ServerPlayer ze) {
        this.chunkMap.move(ze);
    }
    
    public void removeEntity(final Entity aom) {
        this.chunkMap.removeEntity(aom);
    }
    
    public void addEntity(final Entity aom) {
        this.chunkMap.addEntity(aom);
    }
    
    public void broadcastAndSend(final Entity aom, final Packet<?> ni) {
        this.chunkMap.broadcastAndSend(aom, ni);
    }
    
    public void broadcast(final Entity aom, final Packet<?> ni) {
        this.chunkMap.broadcast(aom, ni);
    }
    
    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 bph) {
        return this.chunkMap.getChunkDebugData(bph);
    }
    
    public DimensionDataStorage getDataStorage() {
        return this.dataStorage;
    }
    
    public PoiManager getPoiManager() {
        return this.chunkMap.getPoiManager();
    }
    
    @Nullable
    public NaturalSpawner.SpawnState getLastSpawnState() {
        return this.lastSpawnState;
    }
    
    static {
        CHUNK_STATUSES = ChunkStatus.getStatusList();
    }
    
    final class MainThreadExecutor extends BlockableEventLoop<Runnable> {
        private MainThreadExecutor(final Level bqb) {
            super("Chunk source main thread executor for " + bqb.dimension().location());
        }
        
        @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();
        }
    }
}
