package net.minecraft.server.level;

import java.util.function.IntConsumer;
import java.util.function.IntSupplier;
import java.util.stream.Stream;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
import net.minecraft.util.Mth;
import net.minecraft.Util;
import java.util.function.Consumer;
import java.util.concurrent.CompletionStage;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.Level;
import net.minecraft.network.protocol.game.ClientboundChunkBlocksUpdatePacket;
import net.minecraft.network.protocol.game.ClientboundLevelChunkPacket;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket;
import net.minecraft.core.BlockPos;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientboundLightUpdatePacket;
import net.minecraft.world.level.LightLayer;
import java.util.Optional;
import javax.annotation.Nullable;
import net.minecraft.world.level.lighting.LevelLightEngine;
import net.minecraft.world.level.ChunkPos;
import java.util.concurrent.atomic.AtomicReferenceArray;
import net.minecraft.world.level.chunk.ChunkStatus;
import java.util.List;
import net.minecraft.world.level.chunk.LevelChunk;
import java.util.concurrent.CompletableFuture;
import net.minecraft.world.level.chunk.ChunkAccess;
import com.mojang.datafixers.util.Either;

public class ChunkHolder {
    public static final Either<ChunkAccess, ChunkLoadingFailure> UNLOADED_CHUNK;
    public static final CompletableFuture<Either<ChunkAccess, ChunkLoadingFailure>> UNLOADED_CHUNK_FUTURE;
    public static final Either<LevelChunk, ChunkLoadingFailure> UNLOADED_LEVEL_CHUNK;
    private static final CompletableFuture<Either<LevelChunk, ChunkLoadingFailure>> UNLOADED_LEVEL_CHUNK_FUTURE;
    private static final List<ChunkStatus> CHUNK_STATUSES;
    private static final FullChunkStatus[] FULL_CHUNK_STATUSES;
    private final AtomicReferenceArray<CompletableFuture<Either<ChunkAccess, ChunkLoadingFailure>>> futures;
    private volatile CompletableFuture<Either<LevelChunk, ChunkLoadingFailure>> fullChunkFuture;
    private volatile CompletableFuture<Either<LevelChunk, ChunkLoadingFailure>> tickingChunkFuture;
    private volatile CompletableFuture<Either<LevelChunk, ChunkLoadingFailure>> entityTickingChunkFuture;
    private CompletableFuture<ChunkAccess> chunkToSave;
    private int oldTicketLevel;
    private int ticketLevel;
    private int queueLevel;
    private final ChunkPos pos;
    private final short[] changedBlocks;
    private int changes;
    private int changedSectionFilter;
    private int sectionsToForceSendLightFor;
    private int blockChangedLightSectionFilter;
    private int skyChangedLightSectionFilter;
    private final LevelLightEngine lightEngine;
    private final LevelChangeListener onLevelChange;
    private final PlayerProvider playerProvider;
    private boolean wasAccessibleSinceLastSave;
    
    public ChunkHolder(final ChunkPos bjh, final int integer, final LevelLightEngine coa, final LevelChangeListener c, final PlayerProvider d) {
        this.futures = new AtomicReferenceArray<CompletableFuture<Either<ChunkAccess, ChunkLoadingFailure>>>(ChunkHolder.CHUNK_STATUSES.size());
        this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
        this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
        this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
        this.chunkToSave = CompletableFuture.<ChunkAccess>completedFuture((ChunkAccess)null);
        this.changedBlocks = new short[64];
        this.pos = bjh;
        this.lightEngine = coa;
        this.onLevelChange = c;
        this.playerProvider = d;
        this.oldTicketLevel = ChunkMap.MAX_CHUNK_DISTANCE + 1;
        this.ticketLevel = this.oldTicketLevel;
        this.queueLevel = this.oldTicketLevel;
        this.setTicketLevel(integer);
    }
    
    public CompletableFuture<Either<ChunkAccess, ChunkLoadingFailure>> getFutureIfPresentUnchecked(final ChunkStatus cae) {
        final CompletableFuture<Either<ChunkAccess, ChunkLoadingFailure>> completableFuture3 = this.futures.get(cae.getIndex());
        return (completableFuture3 == null) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : completableFuture3;
    }
    
    public CompletableFuture<Either<ChunkAccess, ChunkLoadingFailure>> getFutureIfPresent(final ChunkStatus cae) {
        if (getStatus(this.ticketLevel).isOrAfter(cae)) {
            return this.getFutureIfPresentUnchecked(cae);
        }
        return ChunkHolder.UNLOADED_CHUNK_FUTURE;
    }
    
    public CompletableFuture<Either<LevelChunk, ChunkLoadingFailure>> getTickingChunkFuture() {
        return this.tickingChunkFuture;
    }
    
    public CompletableFuture<Either<LevelChunk, ChunkLoadingFailure>> getEntityTickingChunkFuture() {
        return this.entityTickingChunkFuture;
    }
    
    public CompletableFuture<Either<LevelChunk, ChunkLoadingFailure>> getFullChunkFuture() {
        return this.fullChunkFuture;
    }
    
    @Nullable
    public LevelChunk getTickingChunk() {
        final CompletableFuture<Either<LevelChunk, ChunkLoadingFailure>> completableFuture2 = this.getTickingChunkFuture();
        final Either<LevelChunk, ChunkLoadingFailure> either3 = completableFuture2.getNow(null);
        if (either3 == null) {
            return null;
        }
        return either3.left().orElse(null);
    }
    
    @Nullable
    public ChunkStatus getLastAvailableStatus() {
        for (int integer2 = ChunkHolder.CHUNK_STATUSES.size() - 1; integer2 >= 0; --integer2) {
            final ChunkStatus cae3 = ChunkHolder.CHUNK_STATUSES.get(integer2);
            final CompletableFuture<Either<ChunkAccess, ChunkLoadingFailure>> completableFuture4 = this.getFutureIfPresentUnchecked(cae3);
            if (completableFuture4.getNow(ChunkHolder.UNLOADED_CHUNK).left().isPresent()) {
                return cae3;
            }
        }
        return null;
    }
    
    @Nullable
    public ChunkAccess getLastAvailable() {
        for (int integer2 = ChunkHolder.CHUNK_STATUSES.size() - 1; integer2 >= 0; --integer2) {
            final ChunkStatus cae3 = ChunkHolder.CHUNK_STATUSES.get(integer2);
            final CompletableFuture<Either<ChunkAccess, ChunkLoadingFailure>> completableFuture4 = this.getFutureIfPresentUnchecked(cae3);
            if (!completableFuture4.isCompletedExceptionally()) {
                final Optional<ChunkAccess> optional5 = (Optional<ChunkAccess>)completableFuture4.getNow(ChunkHolder.UNLOADED_CHUNK).left();
                if (optional5.isPresent()) {
                    return optional5.get();
                }
            }
        }
        return null;
    }
    
    public CompletableFuture<ChunkAccess> getChunkToSave() {
        return this.chunkToSave;
    }
    
    public void blockChanged(final int integer1, final int integer2, final int integer3) {
        final LevelChunk cal5 = this.getTickingChunk();
        if (cal5 == null) {
            return;
        }
        this.changedSectionFilter |= 1 << (integer2 >> 4);
        if (this.changes < 64) {
            final short short6 = (short)(integer1 << 12 | integer3 << 8 | integer2);
            for (int integer4 = 0; integer4 < this.changes; ++integer4) {
                if (this.changedBlocks[integer4] == short6) {
                    return;
                }
            }
            this.changedBlocks[this.changes++] = short6;
        }
    }
    
    public void sectionLightChanged(final LightLayer bkf, final int integer) {
        final LevelChunk cal4 = this.getTickingChunk();
        if (cal4 == null) {
            return;
        }
        cal4.setUnsaved(true);
        if (bkf == LightLayer.SKY) {
            this.skyChangedLightSectionFilter |= 1 << integer + 1;
        }
        else {
            this.blockChangedLightSectionFilter |= 1 << integer + 1;
        }
    }
    
    public void broadcastChanges(final LevelChunk cal) {
        if (this.changes == 0 && this.skyChangedLightSectionFilter == 0 && this.blockChangedLightSectionFilter == 0) {
            return;
        }
        final Level bjw3 = cal.getLevel();
        if (this.changes == 64) {
            this.sectionsToForceSendLightFor = -1;
        }
        if (this.skyChangedLightSectionFilter != 0 || this.blockChangedLightSectionFilter != 0) {
            this.broadcast(new ClientboundLightUpdatePacket(cal.getPos(), this.lightEngine, this.skyChangedLightSectionFilter & ~this.sectionsToForceSendLightFor, this.blockChangedLightSectionFilter & ~this.sectionsToForceSendLightFor), true);
            final int integer4 = this.skyChangedLightSectionFilter & this.sectionsToForceSendLightFor;
            final int integer5 = this.blockChangedLightSectionFilter & this.sectionsToForceSendLightFor;
            if (integer4 != 0 || integer5 != 0) {
                this.broadcast(new ClientboundLightUpdatePacket(cal.getPos(), this.lightEngine, integer4, integer5), false);
            }
            this.skyChangedLightSectionFilter = 0;
            this.blockChangedLightSectionFilter = 0;
            this.sectionsToForceSendLightFor &= ~(this.skyChangedLightSectionFilter & this.blockChangedLightSectionFilter);
        }
        if (this.changes == 1) {
            final int integer4 = (this.changedBlocks[0] >> 12 & 0xF) + this.pos.x * 16;
            final int integer5 = this.changedBlocks[0] & 0xFF;
            final int integer6 = (this.changedBlocks[0] >> 8 & 0xF) + this.pos.z * 16;
            final BlockPos fk7 = new BlockPos(integer4, integer5, integer6);
            this.broadcast(new ClientboundBlockUpdatePacket(bjw3, fk7), false);
            if (bjw3.getBlockState(fk7).getBlock().isEntityBlock()) {
                this.broadcastBlockEntity(bjw3, fk7);
            }
        }
        else if (this.changes == 64) {
            this.broadcast(new ClientboundLevelChunkPacket(cal, this.changedSectionFilter), false);
        }
        else if (this.changes != 0) {
            this.broadcast(new ClientboundChunkBlocksUpdatePacket(this.changes, this.changedBlocks, cal), false);
            for (int integer4 = 0; integer4 < this.changes; ++integer4) {
                final int integer5 = (this.changedBlocks[integer4] >> 12 & 0xF) + this.pos.x * 16;
                final int integer6 = this.changedBlocks[integer4] & 0xFF;
                final int integer7 = (this.changedBlocks[integer4] >> 8 & 0xF) + this.pos.z * 16;
                final BlockPos fk8 = new BlockPos(integer5, integer6, integer7);
                if (bjw3.getBlockState(fk8).getBlock().isEntityBlock()) {
                    this.broadcastBlockEntity(bjw3, fk8);
                }
            }
        }
        this.changes = 0;
        this.changedSectionFilter = 0;
    }
    
    private void broadcastBlockEntity(final Level bjw, final BlockPos fk) {
        final BlockEntity bwl4 = bjw.getBlockEntity(fk);
        if (bwl4 != null) {
            final ClientboundBlockEntityDataPacket mh5 = bwl4.getUpdatePacket();
            if (mh5 != null) {
                this.broadcast(mh5, false);
            }
        }
    }
    
    private void broadcast(final Packet<?> lt, final boolean boolean2) {
        this.playerProvider.getPlayers(this.pos, boolean2).forEach(xe -> xe.connection.send(lt));
    }
    
    public CompletableFuture<Either<ChunkAccess, ChunkLoadingFailure>> getOrScheduleFuture(final ChunkStatus cae, final ChunkMap wp) {
        final int integer4 = cae.getIndex();
        final CompletableFuture<Either<ChunkAccess, ChunkLoadingFailure>> completableFuture5 = this.futures.get(integer4);
        if (completableFuture5 != null) {
            final Either<ChunkAccess, ChunkLoadingFailure> either6 = completableFuture5.getNow(null);
            if (either6 == null || either6.left().isPresent()) {
                return completableFuture5;
            }
        }
        if (getStatus(this.ticketLevel).isOrAfter(cae)) {
            final CompletableFuture<Either<ChunkAccess, ChunkLoadingFailure>> completableFuture6 = wp.schedule(this, cae);
            this.updateChunkToSave(completableFuture6);
            this.futures.set(integer4, completableFuture6);
            return completableFuture6;
        }
        return (completableFuture5 == null) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : completableFuture5;
    }
    
    private void updateChunkToSave(final CompletableFuture<? extends Either<? extends ChunkAccess, ChunkLoadingFailure>> completableFuture) {
        this.chunkToSave = this.chunkToSave.<Object, ChunkAccess>thenCombine(completableFuture, (bzy, either) -> (ChunkAccess)either.map(bzy -> bzy, a -> bzy));
    }
    
    public FullChunkStatus getFullStatus() {
        return getFullChunkStatus(this.ticketLevel);
    }
    
    public ChunkPos getPos() {
        return this.pos;
    }
    
    public int getTicketLevel() {
        return this.ticketLevel;
    }
    
    public int getQueueLevel() {
        return this.queueLevel;
    }
    
    private void setQueueLevel(final int integer) {
        this.queueLevel = integer;
    }
    
    public void setTicketLevel(final int integer) {
        this.ticketLevel = integer;
    }
    
    protected void updateFutures(final ChunkMap wp) {
        final ChunkStatus cae3 = getStatus(this.oldTicketLevel);
        final ChunkStatus cae4 = getStatus(this.ticketLevel);
        final boolean boolean5 = this.oldTicketLevel <= ChunkMap.MAX_CHUNK_DISTANCE;
        final boolean boolean6 = this.ticketLevel <= ChunkMap.MAX_CHUNK_DISTANCE;
        final FullChunkStatus b7 = getFullChunkStatus(this.oldTicketLevel);
        final FullChunkStatus b8 = getFullChunkStatus(this.ticketLevel);
        if (boolean5) {
            final Either<ChunkAccess, ChunkLoadingFailure> either2 = (Either<ChunkAccess, ChunkLoadingFailure>)Either.right(new ChunkLoadingFailure() {
                @Override
                public String toString() {
                    return "Unloaded ticket level " + ChunkHolder.this.pos.toString();
                }
            });
            for (int integer10 = boolean6 ? (cae4.getIndex() + 1) : 0; integer10 <= cae3.getIndex(); ++integer10) {
                final CompletableFuture<Either<ChunkAccess, ChunkLoadingFailure>> completableFuture11 = this.futures.get(integer10);
                if (completableFuture11 != null) {
                    completableFuture11.complete(either2);
                }
                else {
                    this.futures.set(integer10, CompletableFuture.<Either<ChunkAccess, ChunkLoadingFailure>>completedFuture(either2));
                }
            }
        }
        final boolean boolean7 = b7.isOrAfter(FullChunkStatus.BORDER);
        final boolean boolean8 = b8.isOrAfter(FullChunkStatus.BORDER);
        this.wasAccessibleSinceLastSave |= boolean8;
        if (!boolean7 && boolean8) {
            this.updateChunkToSave(this.fullChunkFuture = wp.unpackTicks(this));
        }
        if (boolean7 && !boolean8) {
            final CompletableFuture<Either<LevelChunk, ChunkLoadingFailure>> completableFuture12 = this.fullChunkFuture;
            this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
            this.updateChunkToSave(completableFuture12.thenApply(either -> either.ifLeft((Consumer)wp::packTicks)));
        }
        final boolean boolean9 = b7.isOrAfter(FullChunkStatus.TICKING);
        final boolean boolean10 = b8.isOrAfter(FullChunkStatus.TICKING);
        if (!boolean9 && boolean10) {
            this.updateChunkToSave(this.tickingChunkFuture = wp.postProcess(this));
        }
        if (boolean9 && !boolean10) {
            this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
            this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
        }
        final boolean boolean11 = b7.isOrAfter(FullChunkStatus.ENTITY_TICKING);
        final boolean boolean12 = b8.isOrAfter(FullChunkStatus.ENTITY_TICKING);
        if (!boolean11 && boolean12) {
            if (this.entityTickingChunkFuture != ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE) {
                throw Util.<IllegalStateException>pauseInIde(new IllegalStateException());
            }
            this.updateChunkToSave(this.entityTickingChunkFuture = wp.getEntityTickingRangeFuture(this.pos));
        }
        if (boolean11 && !boolean12) {
            this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
            this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
        }
        this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel);
        this.oldTicketLevel = this.ticketLevel;
    }
    
    public static ChunkStatus getStatus(final int integer) {
        if (integer < 33) {
            return ChunkStatus.FULL;
        }
        return ChunkStatus.getStatus(integer - 33);
    }
    
    public static FullChunkStatus getFullChunkStatus(final int integer) {
        return ChunkHolder.FULL_CHUNK_STATUSES[Mth.clamp(33 - integer + 1, 0, ChunkHolder.FULL_CHUNK_STATUSES.length - 1)];
    }
    
    public boolean wasAccessibleSinceLastSave() {
        return this.wasAccessibleSinceLastSave;
    }
    
    public void refreshAccessibility() {
        this.wasAccessibleSinceLastSave = getFullChunkStatus(this.ticketLevel).isOrAfter(FullChunkStatus.BORDER);
    }
    
    public void replaceProtoChunk(final ImposterProtoChunk cak) {
        for (int integer3 = 0; integer3 < this.futures.length(); ++integer3) {
            final CompletableFuture<Either<ChunkAccess, ChunkLoadingFailure>> completableFuture4 = this.futures.get(integer3);
            if (completableFuture4 != null) {
                final Optional<ChunkAccess> optional5 = (Optional<ChunkAccess>)completableFuture4.getNow(ChunkHolder.UNLOADED_CHUNK).left();
                if (optional5.isPresent()) {
                    if (optional5.get() instanceof ProtoChunk) {
                        this.futures.set(integer3, CompletableFuture.<Either<ChunkAccess, ChunkLoadingFailure>>completedFuture(Either.left(cak)));
                    }
                }
            }
        }
        this.updateChunkToSave(CompletableFuture.<Either>completedFuture(Either.left(cak.getWrapped())));
    }
    
    static {
        UNLOADED_CHUNK = Either.right(ChunkLoadingFailure.UNLOADED);
        UNLOADED_CHUNK_FUTURE = CompletableFuture.<Either<ChunkAccess, ChunkLoadingFailure>>completedFuture(ChunkHolder.UNLOADED_CHUNK);
        UNLOADED_LEVEL_CHUNK = Either.right(ChunkLoadingFailure.UNLOADED);
        UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.<Either<LevelChunk, ChunkLoadingFailure>>completedFuture(ChunkHolder.UNLOADED_LEVEL_CHUNK);
        CHUNK_STATUSES = ChunkStatus.getStatusList();
        FULL_CHUNK_STATUSES = FullChunkStatus.values();
    }
    
    public enum FullChunkStatus {
        INACCESSIBLE, 
        BORDER, 
        TICKING, 
        ENTITY_TICKING;
        
        public boolean isOrAfter(final FullChunkStatus b) {
            return this.ordinal() >= b.ordinal();
        }
    }
    
    public interface ChunkLoadingFailure {
        public static final ChunkLoadingFailure UNLOADED = new ChunkLoadingFailure() {
            @Override
            public String toString() {
                return "UNLOADED";
            }
        };
    }
    
    public interface PlayerProvider {
        Stream<ServerPlayer> getPlayers(final ChunkPos bjh, final boolean boolean2);
    }
    
    public interface LevelChangeListener {
        void onLevelChange(final ChunkPos bjh, final IntSupplier intSupplier, final int integer, final IntConsumer intConsumer);
    }
}
