package net.minecraft.server.level;

import it.unimi.dsi.fastutil.longs.Long2IntMaps;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2IntMap;
import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ByteMap;
import net.minecraft.world.level.chunk.ChunkStatus;
import org.apache.logging.log4j.LogManager;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.LevelChunk;
import com.mojang.datafixers.util.Either;
import java.util.concurrent.CompletableFuture;
import it.unimi.dsi.fastutil.longs.LongIterator;
import javax.annotation.Nullable;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import java.util.List;
import com.google.common.collect.ImmutableList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import com.google.common.collect.Sets;
import java.util.concurrent.Executor;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.util.thread.ProcessorHandle;
import java.util.Set;
import net.minecraft.util.SortedArraySet;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectSet;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import org.apache.logging.log4j.Logger;

public abstract class DistanceManager {
    private static final Logger LOGGER;
    private static final int PLAYER_TICKET_LEVEL;
    private final Long2ObjectMap<ObjectSet<ServerPlayer>> playersPerChunk;
    private final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> tickets;
    private final ChunkTicketTracker ticketTracker;
    private final FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter;
    private final PlayerTicketTracker playerTicketManager;
    private final Set<ChunkHolder> chunksToUpdateFutures;
    private final ChunkTaskPriorityQueueSorter ticketThrottler;
    private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> ticketThrottlerInput;
    private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Release> ticketThrottlerReleaser;
    private final LongSet ticketsToRelease;
    private final Executor mainThreadExecutor;
    private long ticketTickCounter;
    
    protected DistanceManager(final Executor executor1, final Executor executor2) {
        this.playersPerChunk = (Long2ObjectMap<ObjectSet<ServerPlayer>>)new Long2ObjectOpenHashMap();
        this.tickets = (Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>>)new Long2ObjectOpenHashMap();
        this.ticketTracker = new ChunkTicketTracker();
        this.naturalSpawnChunkCounter = new FixedPlayerDistanceChunkTracker(8);
        this.playerTicketManager = new PlayerTicketTracker(33);
        this.chunksToUpdateFutures = Sets.newHashSet();
        this.ticketsToRelease = (LongSet)new LongOpenHashSet();
        final ProcessorHandle<Runnable> aiu4 = ProcessorHandle.<Runnable>of("player ticket throttler", executor2::execute);
        final ChunkTaskPriorityQueueSorter wr5 = new ChunkTaskPriorityQueueSorter(ImmutableList.of(aiu4), executor1, 4);
        this.ticketThrottler = wr5;
        this.ticketThrottlerInput = wr5.<Runnable>getProcessor(aiu4, true);
        this.ticketThrottlerReleaser = wr5.getReleaseProcessor(aiu4);
        this.mainThreadExecutor = executor2;
    }
    
    protected void purgeStaleTickets() {
        ++this.ticketTickCounter;
        final ObjectIterator<Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>>> objectIterator2 = (ObjectIterator<Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>>>)this.tickets.long2ObjectEntrySet().fastIterator();
        while (objectIterator2.hasNext()) {
            final Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>> entry3 = (Long2ObjectMap.Entry<SortedArraySet<Ticket<?>>>)objectIterator2.next();
            if (((SortedArraySet)entry3.getValue()).removeIf(xh -> xh.timedOut(this.ticketTickCounter))) {
                this.ticketTracker.update(entry3.getLongKey(), getTicketLevelAt((SortedArraySet<Ticket<?>>)entry3.getValue()), false);
            }
            if (((SortedArraySet)entry3.getValue()).isEmpty()) {
                objectIterator2.remove();
            }
        }
    }
    
    private static int getTicketLevelAt(final SortedArraySet<Ticket<?>> abw) {
        return abw.isEmpty() ? (ChunkMap.MAX_CHUNK_DISTANCE + 1) : abw.first().getTicketLevel();
    }
    
    protected abstract boolean isChunkToRemove(final long long1);
    
    @Nullable
    protected abstract ChunkHolder getChunk(final long long1);
    
    @Nullable
    protected abstract ChunkHolder updateChunkScheduling(final long long1, final int integer2, @Nullable final ChunkHolder wo, final int integer4);
    
    public boolean runAllUpdates(final ChunkMap wp) {
        this.naturalSpawnChunkCounter.runAllUpdates();
        this.playerTicketManager.runAllUpdates();
        final int integer3 = Integer.MAX_VALUE - this.ticketTracker.runDistnaceUpdates(Integer.MAX_VALUE);
        final boolean boolean4 = integer3 != 0;
        if (boolean4) {}
        if (!this.chunksToUpdateFutures.isEmpty()) {
            this.chunksToUpdateFutures.forEach(wo -> wo.updateFutures(wp));
            this.chunksToUpdateFutures.clear();
            return true;
        }
        if (!this.ticketsToRelease.isEmpty()) {
            final LongIterator longIterator5 = this.ticketsToRelease.iterator();
            while (longIterator5.hasNext()) {
                final long long6 = longIterator5.nextLong();
                if (this.getTickets(long6).stream().anyMatch(xh -> xh.getType() == TicketType.PLAYER)) {
                    final ChunkHolder wo2 = wp.getUpdatingChunkIfPresent(long6);
                    if (wo2 == null) {
                        throw new IllegalStateException();
                    }
                    final CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> completableFuture9 = wo2.getEntityTickingChunkFuture();
                    completableFuture9.thenAccept(either -> this.mainThreadExecutor.execute(() -> this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> {}, long6, false))));
                }
            }
            this.ticketsToRelease.clear();
        }
        return boolean4;
    }
    
    private void addTicket(final long long1, final Ticket<?> xh) {
        final SortedArraySet<Ticket<?>> abw5 = this.getTickets(long1);
        final int integer6 = getTicketLevelAt(abw5);
        final Ticket<?> xh2 = abw5.addOrGet(xh);
        xh2.setCreatedTick(this.ticketTickCounter);
        if (xh.getTicketLevel() < integer6) {
            this.ticketTracker.update(long1, xh.getTicketLevel(), true);
        }
    }
    
    private void removeTicket(final long long1, final Ticket<?> xh) {
        final SortedArraySet<Ticket<?>> abw5 = this.getTickets(long1);
        if (abw5.remove(xh)) {}
        if (abw5.isEmpty()) {
            this.tickets.remove(long1);
        }
        this.ticketTracker.update(long1, getTicketLevelAt(abw5), false);
    }
    
    public <T> void addTicket(final TicketType<T> xi, final ChunkPos bjh, final int integer, final T object) {
        this.addTicket(bjh.toLong(), new Ticket<>(xi, integer, object));
    }
    
    public <T> void removeTicket(final TicketType<T> xi, final ChunkPos bjh, final int integer, final T object) {
        final Ticket<T> xh6 = new Ticket<T>(xi, integer, object);
        this.removeTicket(bjh.toLong(), xh6);
    }
    
    public <T> void addRegionTicket(final TicketType<T> xi, final ChunkPos bjh, final int integer, final T object) {
        this.addTicket(bjh.toLong(), new Ticket<>(xi, 33 - integer, object));
    }
    
    public <T> void removeRegionTicket(final TicketType<T> xi, final ChunkPos bjh, final int integer, final T object) {
        final Ticket<T> xh6 = new Ticket<T>(xi, 33 - integer, object);
        this.removeTicket(bjh.toLong(), xh6);
    }
    
    private SortedArraySet<Ticket<?>> getTickets(final long long1) {
        return (SortedArraySet<Ticket<?>>)this.tickets.computeIfAbsent(long1, long1 -> SortedArraySet.<Comparable>create(4));
    }
    
    protected void updateChunkForced(final ChunkPos bjh, final boolean boolean2) {
        final Ticket<ChunkPos> xh4 = new Ticket<ChunkPos>(TicketType.FORCED, 31, bjh);
        if (boolean2) {
            this.addTicket(bjh.toLong(), xh4);
        }
        else {
            this.removeTicket(bjh.toLong(), xh4);
        }
    }
    
    public void addPlayer(final SectionPos gd, final ServerPlayer xe) {
        final long long4 = gd.chunk().toLong();
        ((ObjectSet)this.playersPerChunk.computeIfAbsent(long4, long1 -> new ObjectOpenHashSet())).add(xe);
        this.naturalSpawnChunkCounter.update(long4, 0, true);
        this.playerTicketManager.update(long4, 0, true);
    }
    
    public void removePlayer(final SectionPos gd, final ServerPlayer xe) {
        final long long4 = gd.chunk().toLong();
        final ObjectSet<ServerPlayer> objectSet6 = (ObjectSet<ServerPlayer>)this.playersPerChunk.get(long4);
        objectSet6.remove(xe);
        if (objectSet6.isEmpty()) {
            this.playersPerChunk.remove(long4);
            this.naturalSpawnChunkCounter.update(long4, Integer.MAX_VALUE, false);
            this.playerTicketManager.update(long4, Integer.MAX_VALUE, false);
        }
    }
    
    protected String getTicketDebugString(final long long1) {
        final SortedArraySet<Ticket<?>> abw4 = (SortedArraySet<Ticket<?>>)this.tickets.get(long1);
        String string5;
        if (abw4 == null || abw4.isEmpty()) {
            string5 = "no_ticket";
        }
        else {
            string5 = abw4.first().toString();
        }
        return string5;
    }
    
    protected void updatePlayerTickets(final int integer) {
        this.playerTicketManager.updateViewDistance(integer);
    }
    
    public int getNaturalSpawnChunkCount() {
        this.naturalSpawnChunkCounter.runAllUpdates();
        return this.naturalSpawnChunkCounter.chunks.size();
    }
    
    public boolean hasPlayersNearby(final long long1) {
        this.naturalSpawnChunkCounter.runAllUpdates();
        return this.naturalSpawnChunkCounter.chunks.containsKey(long1);
    }
    
    public String getDebugStatus() {
        return this.ticketThrottler.getDebugStatus();
    }
    
    static {
        LOGGER = LogManager.getLogger();
        PLAYER_TICKET_LEVEL = 33 + ChunkStatus.getDistance(ChunkStatus.FULL) - 2;
    }
    
    class FixedPlayerDistanceChunkTracker extends ChunkTracker {
        protected final Long2ByteMap chunks;
        protected final int maxDistance;
        
        protected FixedPlayerDistanceChunkTracker(final int integer) {
            super(integer + 2, 16, 256);
            this.chunks = (Long2ByteMap)new Long2ByteOpenHashMap();
            this.maxDistance = integer;
            this.chunks.defaultReturnValue((byte)(integer + 2));
        }
        
        @Override
        protected int getLevel(final long long1) {
            return this.chunks.get(long1);
        }
        
        @Override
        protected void setLevel(final long long1, final int integer) {
            byte byte5;
            if (integer > this.maxDistance) {
                byte5 = this.chunks.remove(long1);
            }
            else {
                byte5 = this.chunks.put(long1, (byte)integer);
            }
            this.onLevelChange(long1, byte5, integer);
        }
        
        protected void onLevelChange(final long long1, final int integer2, final int integer3) {
        }
        
        @Override
        protected int getLevelFromSource(final long long1) {
            return this.havePlayer(long1) ? 0 : Integer.MAX_VALUE;
        }
        
        private boolean havePlayer(final long long1) {
            final ObjectSet<ServerPlayer> objectSet4 = (ObjectSet<ServerPlayer>)DistanceManager.this.playersPerChunk.get(long1);
            return objectSet4 != null && !objectSet4.isEmpty();
        }
        
        public void runAllUpdates() {
            this.runUpdates(Integer.MAX_VALUE);
        }
    }
    
    class PlayerTicketTracker extends FixedPlayerDistanceChunkTracker {
        private int viewDistance;
        private final Long2IntMap queueLevels;
        private final LongSet toUpdate;
        
        protected PlayerTicketTracker(final int integer) {
            super(integer);
            this.queueLevels = Long2IntMaps.synchronize((Long2IntMap)new Long2IntOpenHashMap());
            this.toUpdate = (LongSet)new LongOpenHashSet();
            this.viewDistance = 0;
            this.queueLevels.defaultReturnValue(integer + 2);
        }
        
        @Override
        protected void onLevelChange(final long long1, final int integer2, final int integer3) {
            this.toUpdate.add(long1);
        }
        
        public void updateViewDistance(final int integer) {
            for (final Long2ByteMap.Entry entry4 : this.chunks.long2ByteEntrySet()) {
                final byte byte5 = entry4.getByteValue();
                final long long6 = entry4.getLongKey();
                this.onLevelChange(long6, byte5, this.haveTicketFor(byte5), byte5 <= integer - 2);
            }
            this.viewDistance = integer;
        }
        
        private void onLevelChange(final long long1, final int integer, final boolean boolean3, final boolean boolean4) {
            if (boolean3 != boolean4) {
                final Ticket<?> xh7 = new Ticket<>(TicketType.PLAYER, DistanceManager.PLAYER_TICKET_LEVEL, new ChunkPos(long1));
                if (boolean4) {
                    final Ticket xh8;
                    DistanceManager.this.ticketThrottlerInput.tell(ChunkTaskPriorityQueueSorter.message(() -> DistanceManager.this.mainThreadExecutor.execute(() -> {
                        if (this.haveTicketFor(this.getLevel(long1))) {
                            DistanceManager.this.addTicket(long1, xh8);
                            DistanceManager.this.ticketsToRelease.add(long1);
                        }
                        else {
                            DistanceManager.this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> {}, long1, false));
                        }
                    }), long1, () -> integer));
                }
                else {
                    DistanceManager.this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> DistanceManager.this.mainThreadExecutor.execute(() -> DistanceManager.this.removeTicket(long1, xh7)), long1, true));
                }
            }
        }
        
        @Override
        public void runAllUpdates() {
            super.runAllUpdates();
            if (!this.toUpdate.isEmpty()) {
                final LongIterator longIterator2 = this.toUpdate.iterator();
                while (longIterator2.hasNext()) {
                    final long long3 = longIterator2.nextLong();
                    final int integer2 = this.queueLevels.get(long3);
                    final int integer3 = this.getLevel(long3);
                    if (integer2 != integer3) {
                        final long n;
                        DistanceManager.this.ticketThrottler.onLevelChange(new ChunkPos(long3), () -> this.queueLevels.get(long3), integer3, integer -> {
                            if (integer >= this.queueLevels.defaultReturnValue()) {
                                this.queueLevels.remove(n);
                            }
                            else {
                                this.queueLevels.put(n, integer);
                            }
                            return;
                        });
                        this.onLevelChange(long3, integer3, this.haveTicketFor(integer2), this.haveTicketFor(integer3));
                    }
                }
                this.toUpdate.clear();
            }
        }
        
        private boolean haveTicketFor(final int integer) {
            return integer <= this.viewDistance - 2;
        }
    }
    
    class ChunkTicketTracker extends ChunkTracker {
        public ChunkTicketTracker() {
            super(ChunkMap.MAX_CHUNK_DISTANCE + 2, 16, 256);
        }
        
        @Override
        protected int getLevelFromSource(final long long1) {
            final SortedArraySet<Ticket<?>> abw4 = (SortedArraySet<Ticket<?>>)DistanceManager.this.tickets.get(long1);
            if (abw4 == null) {
                return Integer.MAX_VALUE;
            }
            if (abw4.isEmpty()) {
                return Integer.MAX_VALUE;
            }
            return abw4.first().getTicketLevel();
        }
        
        @Override
        protected int getLevel(final long long1) {
            if (!DistanceManager.this.isChunkToRemove(long1)) {
                final ChunkHolder wo4 = DistanceManager.this.getChunk(long1);
                if (wo4 != null) {
                    return wo4.getTicketLevel();
                }
            }
            return ChunkMap.MAX_CHUNK_DISTANCE + 1;
        }
        
        @Override
        protected void setLevel(final long long1, final int integer) {
            ChunkHolder wo5 = DistanceManager.this.getChunk(long1);
            final int integer2 = (wo5 == null) ? (ChunkMap.MAX_CHUNK_DISTANCE + 1) : wo5.getTicketLevel();
            if (integer2 == integer) {
                return;
            }
            wo5 = DistanceManager.this.updateChunkScheduling(long1, integer, wo5, integer2);
            if (wo5 != null) {
                DistanceManager.this.chunksToUpdateFutures.add(wo5);
            }
        }
        
        public int runDistnaceUpdates(final int integer) {
            return this.runUpdates(integer);
        }
    }
}
