// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.spawning.util;

import java.util.function.Supplier;
import com.hypixel.hytale.server.core.asset.type.fluid.Fluid;
import java.util.Random;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntLists;
import com.hypixel.hytale.server.npc.role.Role;
import com.hypixel.hytale.server.npc.asset.builder.Builder;
import com.hypixel.fastutil.longs.Long2ObjectConcurrentHashMap;
import com.hypixel.hytale.server.spawning.ISpawnableWithModel;
import com.hypixel.hytale.protocol.BlockMaterial;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
import java.util.logging.Level;
import com.hypixel.hytale.server.core.universe.world.accessor.LocalCachedChunkAccessor;
import com.hypixel.hytale.math.vector.Vector3i;
import com.hypixel.hytale.server.spawning.SpawnTestResult;
import com.hypixel.hytale.server.spawning.suppression.component.ChunkSuppressionEntry;
import com.hypixel.hytale.math.util.ChunkUtil;
import com.hypixel.hytale.server.spawning.suppression.component.SpawnSuppressionController;
import java.util.concurrent.ThreadLocalRandom;
import it.unimi.dsi.fastutil.objects.ObjectArrays;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.server.spawning.assets.spawns.config.RoleSpawnParameters;
import com.hypixel.hytale.server.npc.NPCPlugin;
import com.hypixel.hytale.server.spawning.assets.spawns.config.BeaconNPCSpawn;
import java.util.Arrays;
import com.hypixel.hytale.math.util.MathUtil;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import java.util.ArrayDeque;
import com.hypixel.hytale.server.spawning.SpawningPlugin;
import com.hypixel.hytale.component.ComponentType;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.ints.IntSet;
import com.hypixel.hytale.server.spawning.SpawningContext;
import java.util.Deque;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import java.util.BitSet;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import javax.annotation.Nonnull;
import com.hypixel.hytale.server.spawning.wrappers.BeaconSpawnWrapper;
import javax.annotation.Nullable;
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
import com.hypixel.hytale.server.core.universe.world.accessor.ChunkAccessor;
import com.hypixel.hytale.server.core.universe.world.World;
import java.util.Comparator;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.Component;

public class FloodFillPositionSelector implements Component<EntityStore>
{
    private static final int MAX_SPAWN_POSITIONS_HINT = 30;
    private static final double SPAWN_POSITION_DENSITY = 0.1;
    private static final int CONCURRENT_POSITION_OPTION_MULTIPLIER = 3;
    private static final double MAX_FAILED_SPAWN_POSITION_RATIO = 0.25;
    private static final double IRREGULAR_MIN_SPAWNS_MULTIPLIER = 0.3;
    private static final double IRREGULAR_MAX_SPAWNS_MULTIPLIER = 5.0;
    private static final int NOT_CHECKED = -1;
    private static final int BLOCKED = -2;
    private static final int TOO_HIGH = Integer.MAX_VALUE;
    private static final int TOO_LOW = Integer.MIN_VALUE;
    private static final int MAX_RESOLUTION_DIVISOR = 8;
    private static final ThreadLocal<SortBufferProvider> sortBufferProvider;
    private static final Comparator<Object> WEIGHTED_POSITION_COMPARATOR;
    private final World world;
    @Nullable
    private ChunkAccessor<WorldChunk> chunkAccessor;
    private final int size;
    private int minY;
    private int maxY;
    @Nonnull
    private final BeaconSpawnWrapper spawnWrapper;
    @Nonnull
    private final int[] roleIndexes;
    @Nonnull
    private final int[] heightGrid;
    @Nonnull
    private final Int2ObjectMap<BitSet> resolutionMaps;
    private final BitSet fullResolutionMap;
    private int desiredPositionCount;
    private final IntArrayList highResolutionOptions;
    private final Deque<int[]> floodFillQueue;
    private final SpawningContext spawningContext;
    @Nullable
    private WorldChunk chunk;
    private final IntSet positionIndexes;
    private final Int2ObjectMap<ObjectArrayList<WeightedPosition>> positionsByRole;
    private final Int2IntMap failedSpawnsByRole;
    private boolean hasRun;
    private Debug debug;
    private boolean irregularCase;
    private IntSet failedPositionTestIndexes;
    private double calculatePositionsAfter;
    
    public static ComponentType<EntityStore, FloodFillPositionSelector> getComponentType() {
        return SpawningPlugin.get().getFloodFillPositionSelectorComponentType();
    }
    
    public FloodFillPositionSelector(final World world, @Nonnull final BeaconSpawnWrapper spawnWrapper) {
        this.highResolutionOptions = new IntArrayList(4);
        this.floodFillQueue = new ArrayDeque<int[]>();
        this.spawningContext = new SpawningContext();
        this.positionIndexes = new IntOpenHashSet();
        this.positionsByRole = new Int2ObjectOpenHashMap<ObjectArrayList<WeightedPosition>>();
        this.failedSpawnsByRole = new Int2IntOpenHashMap();
        this.world = world;
        final int baseSize = MathUtil.ceil(spawnWrapper.getSpawnRadius()) * 2;
        this.size = (baseSize + 8 - 1) / 8 * 8;
        Arrays.fill(this.heightGrid = new int[this.size * this.size], -1);
        this.resolutionMaps = new Int2ObjectOpenHashMap<BitSet>();
        for (int i = 1; i <= 8; i *= 2) {
            this.resolutionMaps.put(i, new BitSet(this.heightGrid.length / i));
        }
        this.fullResolutionMap = this.resolutionMaps.get(1);
        this.spawnWrapper = spawnWrapper;
        final RoleSpawnParameters[] roleSpawnParameters = spawnWrapper.getSpawn().getNPCs();
        this.roleIndexes = new int[roleSpawnParameters.length];
        for (int j = 0; j < this.roleIndexes.length; ++j) {
            final int roleIndex = NPCPlugin.get().getIndex(roleSpawnParameters[j].getId());
            this.roleIndexes[j] = roleIndex;
            this.positionsByRole.put(roleIndex, new ObjectArrayList<WeightedPosition>());
        }
        this.debug = spawnWrapper.getSpawn().getDebug();
        if (this.debug != Debug.DISABLED) {
            this.failedPositionTestIndexes = new IntOpenHashSet();
        }
    }
    
    public void setCalculatePositionsAfter(final double calculatePositionsAfter) {
        this.calculatePositionsAfter = calculatePositionsAfter;
    }
    
    public boolean tickCalculatePositionsAfter(final float dt) {
        final double calculatePositionsAfter = this.calculatePositionsAfter - dt;
        this.calculatePositionsAfter = calculatePositionsAfter;
        return calculatePositionsAfter <= 0.0;
    }
    
    public boolean hasPositionsForRole(final int roleIndex) {
        return !this.positionsByRole.get(roleIndex).isEmpty();
    }
    
    public boolean prepareSpawnContext(@Nonnull final Vector3d playerPosition, final int spawnsThisRound, final int roleIndex, @Nonnull final SpawningContext spawningContext, @Nonnull final BeaconSpawnWrapper spawnWrapper) {
        final ObjectArrayList<WeightedPosition> positions = this.positionsByRole.get(roleIndex);
        if (this.positionsByRole.isEmpty()) {
            return false;
        }
        final double minDistanceFromPlayerSquared = spawnWrapper.getMinDistanceFromPlayerSquared();
        final double targetDistanceFromPlayerSquared = spawnWrapper.getTargetDistanceFromPlayerSquared();
        for (int i = 0; i < positions.size(); ++i) {
            final WeightedPosition entry = positions.get(i);
            final double distance = playerPosition.distanceSquaredTo(entry.position);
            entry.weight = ((distance < minDistanceFromPlayerSquared) ? 0.0 : Math.max(0.0, targetDistanceFromPlayerSquared - Math.abs(distance - targetDistanceFromPlayerSquared)));
        }
        final int targetNumberOfOptions = spawnsThisRound * 3;
        final int size = positions.size();
        final WeightedPosition[] sortBuffer = FloodFillPositionSelector.sortBufferProvider.get().getBuffer(size);
        System.arraycopy(positions.elements(), 0, sortBuffer, 0, size);
        ObjectArrays.mergeSort(positions.elements(), 0, size, FloodFillPositionSelector.WEIGHTED_POSITION_COMPARATOR, sortBuffer);
        double sum = 0.0;
        for (int j = 0; j < targetNumberOfOptions && j < positions.size(); ++j) {
            final WeightedPosition entry2 = positions.get(j);
            if (entry2.weight == 0.0) {
                break;
            }
            sum += entry2.weight;
        }
        if (sum == 0.0) {
            return false;
        }
        sum = ThreadLocalRandom.current().nextDouble(sum);
        int selectedIndex = -1;
        WeightedPosition entry2 = null;
        for (int k = 0; k < targetNumberOfOptions && k < positions.size(); ++k) {
            entry2 = positions.get(k);
            selectedIndex = k;
            sum -= entry2.weight;
            if (sum < 0.0) {
                break;
            }
        }
        if (entry2 == null) {
            return false;
        }
        final Vector3i position = entry2.position;
        final SpawnSuppressionController suppressionController = this.world.getEntityStore().getStore().getResource(SpawnSuppressionController.getResourceType());
        final long indexChunk = ChunkUtil.indexChunk(ChunkUtil.chunkCoordinate(position.x), ChunkUtil.chunkCoordinate(position.z));
        final ChunkSuppressionEntry suppressionEntry = suppressionController.getChunkSuppressionMap().get(indexChunk);
        if ((suppressionEntry != null && suppressionEntry.isSuppressingRoleAt(roleIndex, position.y)) || !spawningContext.set(this.world, position.x, position.y, position.z) || spawningContext.canSpawn() != SpawnTestResult.TEST_OK) {
            positions.remove(selectedIndex);
            final int totalFailed = this.failedSpawnsByRole.mergeInt(roleIndex, 1, Integer::sum);
            if (totalFailed > positions.size() * 0.25) {
                this.hasRun = false;
            }
            return false;
        }
        return true;
    }
    
    public boolean shouldRebuildCache() {
        return !this.hasRun;
    }
    
    public void forceRebuildCache() {
        this.hasRun = false;
    }
    
    public void init() {
        Arrays.fill(this.heightGrid, -1);
        for (int i = 1; i <= 8; i *= 2) {
            this.resolutionMaps.get(i).clear();
        }
        for (final int role : this.roleIndexes) {
            this.positionsByRole.get(role).clear();
            this.failedSpawnsByRole.put(role, 0);
        }
        this.irregularCase = false;
    }
    
    public void buildPositionCache(@Nonnull final Vector3d origin, @Nonnull final FloodFillEntryPoolSimple pool) {
        final int sizeHalf = this.size / 2;
        final int worldX = MathUtil.floor(origin.getX());
        final int worldY = MathUtil.floor(origin.getY());
        final int worldZ = MathUtil.floor(origin.getZ());
        this.chunkAccessor = LocalCachedChunkAccessor.atWorldCoords(this.world, worldX, worldZ, sizeHalf);
        this.chunk = this.chunkAccessor.getChunkIfInMemory(ChunkUtil.indexChunkFromBlock(worldX, worldZ));
        if (this.chunk == null) {
            return;
        }
        final int[] yRange = this.spawnWrapper.getSpawn().getYRange();
        this.minY = Math.max(0, worldY + yRange[0]);
        this.maxY = Math.min(319, worldY + yRange[1]);
        this.floodFill(worldX, worldY, worldZ, sizeHalf, sizeHalf, pool);
        this.desiredPositionCount = Math.min(MathUtil.ceil(this.fullResolutionMap.cardinality() * 0.1), 30);
        if (this.desiredPositionCount > 0) {
            this.findPositions(worldX, worldZ);
        }
        else {
            SpawningPlugin.get().getLogger().at(Level.WARNING).log("Spawn beacon at: " + String.valueOf(origin) + " unable to find any suitable positions to check");
        }
        if (this.debug != Debug.DISABLED && (this.irregularCase || this.debug == Debug.ALL)) {
            for (int i = 2; i <= 8; i *= 2) {
                final BitSet map = this.resolutionMaps.get(i);
                if (map.cardinality() > 0) {
                    SpawningPlugin.get().getLogger().at(Level.WARNING).log(this.debugDumpLowResolutionMap(map, this.size / i));
                }
            }
            SpawningPlugin.get().getLogger().at(Level.WARNING).log("Spawn beacon at: " + String.valueOf(origin) + (this.irregularCase ? " is an irregular case" : ""));
        }
        this.chunkAccessor = null;
        this.chunk = null;
        this.spawningContext.releaseFull();
        this.hasRun = true;
    }
    
    private void floodFill(int worldX, int worldY, int worldZ, int setX, int setZ, @Nonnull final FloodFillEntryPoolSimple pool) {
        if (this.chunk == null) {
            return;
        }
        this.floodFillQueue.clear();
        final int[] initialEntry = pool.allocate();
        initialEntry[0] = worldX;
        initialEntry[1] = worldY;
        initialEntry[2] = worldZ;
        initialEntry[3] = setX;
        initialEntry[4] = setZ;
        this.floodFillQueue.add(initialEntry);
        int chunkX = this.chunk.getX();
        int chunkZ = this.chunk.getZ();
    Label_0081:
        while (!this.floodFillQueue.isEmpty()) {
            final int[] state = this.floodFillQueue.poll();
            worldX = state[0];
            worldY = state[1];
            worldZ = state[2];
            setX = state[3];
            setZ = state[4];
            if (setX < 0 || setX >= this.size || setZ < 0 || setZ >= this.size) {
                pool.deallocate(state);
            }
            else {
                final int index = getPositionIndex(setX, setZ, this.size);
                if (this.heightGrid[index] != -1) {
                    pool.deallocate(state);
                }
                else {
                    if (!ChunkUtil.isInsideChunk(chunkX, chunkZ, worldX, worldZ)) {
                        final WorldChunk newChunk = this.chunkAccessor.getChunkIfInMemory(ChunkUtil.indexChunkFromBlock(worldX, worldZ));
                        if (newChunk == null) {
                            this.heightGrid[index] = -2;
                            pool.deallocate(state);
                            continue;
                        }
                        this.chunk = newChunk;
                        chunkX = this.chunk.getX();
                        chunkZ = this.chunk.getZ();
                    }
                    int block = this.chunk.getBlock(worldX, worldY, worldZ);
                    if (block == 0 || BlockType.getAssetMap().getAsset(block).getMaterial() != BlockMaterial.Solid) {
                        while (block == 0) {
                            --worldY;
                            block = this.chunk.getBlock(worldX, worldY, worldZ);
                            if (worldY < this.minY) {
                                this.heightGrid[index] = Integer.MIN_VALUE;
                                pool.deallocate(state);
                                continue Label_0081;
                            }
                        }
                        ++worldY;
                    }
                    else {
                        while (block != 0) {
                            ++worldY;
                            block = this.chunk.getBlock(worldX, worldY, worldZ);
                            if (worldY > this.maxY) {
                                this.heightGrid[index] = Integer.MAX_VALUE;
                                pool.deallocate(state);
                                continue Label_0081;
                            }
                        }
                    }
                    this.heightGrid[index] = worldY;
                    this.fullResolutionMap.set(index);
                    state[0] = worldX + 1;
                    state[1] = worldY;
                    state[2] = worldZ;
                    state[3] = setX + 1;
                    state[4] = setZ;
                    this.floodFillQueue.add(state);
                    final int[] entry2 = pool.allocate();
                    entry2[0] = worldX - 1;
                    entry2[1] = worldY;
                    entry2[2] = worldZ;
                    entry2[3] = setX - 1;
                    entry2[4] = setZ;
                    this.floodFillQueue.add(entry2);
                    final int[] entry3 = pool.allocate();
                    entry3[0] = worldX;
                    entry3[1] = worldY;
                    entry3[2] = worldZ + 1;
                    entry3[3] = setX;
                    entry3[4] = setZ + 1;
                    this.floodFillQueue.add(entry3);
                    final int[] entry4 = pool.allocate();
                    entry4[0] = worldX;
                    entry4[1] = worldY;
                    entry4[2] = worldZ - 1;
                    entry4[3] = setX;
                    entry4[4] = setZ - 1;
                    this.floodFillQueue.add(entry4);
                }
            }
        }
    }
    
    private void findPositions(final int originX, final int originZ) {
        int resolution = 1;
        int segments;
        do {
            resolution *= 2;
            segments = this.buildLowerResolutionMap(this.resolutionMaps.get(resolution), this.size / resolution, this.resolutionMaps.get(resolution / 2), this.size / (resolution / 2));
        } while (resolution < 8 && segments / this.desiredPositionCount > 1);
        BitSet lowestResolutionMap = this.resolutionMaps.get(resolution);
        final int openSpots = lowestResolutionMap.cardinality();
        if (openSpots < this.desiredPositionCount) {
            resolution /= 2;
            lowestResolutionMap = this.resolutionMaps.get(resolution);
        }
        final int sizeHalf = this.size / 2;
        final int offsetOriginX = originX - sizeHalf;
        final int offsetOriginZ = originZ - sizeHalf;
        final SpawnSuppressionController suppressionController = this.world.getEntityStore().getStore().getResource(SpawnSuppressionController.getResourceType());
        long chunkIndex = ChunkUtil.NOT_FOUND;
        ChunkSuppressionEntry suppressionEntry = null;
        final Long2ObjectConcurrentHashMap<ChunkSuppressionEntry> chunkSuppressionMap = suppressionController.getChunkSuppressionMap();
        final int[] roleIndexes = this.roleIndexes;
        for (int length = roleIndexes.length, j = 0; j < length; ++j) {
            final int roleIndex = roleIndexes[j];
            final Builder<Role> spawnable = NPCPlugin.get().tryGetCachedValidRole(roleIndex);
            if (spawnable != null && spawnable.isSpawnable() && spawnable instanceof ISpawnableWithModel) {
                if (this.spawningContext.setSpawnable((ISpawnableWithModel)spawnable, true)) {
                    final ObjectArrayList<WeightedPosition> positionList = this.positionsByRole.get(roleIndex);
                    for (int i = lowestResolutionMap.nextSetBit(0); i >= 0; i = lowestResolutionMap.nextSetBit(i + 1)) {
                        int chosenIndex = i;
                        for (int currentResolution = resolution; currentResolution > 1; currentResolution /= 2) {
                            final int nextResolution = currentResolution / 2;
                            chosenIndex = this.pickOpenSegment(chosenIndex, this.size / currentResolution, this.resolutionMaps.get(nextResolution), this.size / nextResolution);
                        }
                        int x = offsetOriginX + xFromIndex(chosenIndex, this.size);
                        int y = this.heightGrid[chosenIndex];
                        int z = offsetOriginZ + zFromIndex(chosenIndex, this.size);
                        long newChunkIndex = ChunkUtil.indexChunk(ChunkUtil.chunkCoordinate(x), ChunkUtil.chunkCoordinate(z));
                        if (chunkIndex != newChunkIndex) {
                            suppressionEntry = chunkSuppressionMap.get(newChunkIndex);
                            chunkIndex = newChunkIndex;
                        }
                        if (!this.canSpawn(x, y, z, roleIndex, suppressionEntry)) {
                            if (this.debug != Debug.DISABLED) {
                                this.failedPositionTestIndexes.add(chosenIndex);
                            }
                            final int originalIndex = chosenIndex;
                            chosenIndex = this.shiftIndexAwayFromWall(chosenIndex);
                            if (chosenIndex == originalIndex) {
                                continue;
                            }
                            x = offsetOriginX + xFromIndex(chosenIndex, this.size);
                            y = this.heightGrid[chosenIndex];
                            z = offsetOriginZ + zFromIndex(chosenIndex, this.size);
                            newChunkIndex = ChunkUtil.indexChunk(ChunkUtil.chunkCoordinate(x), ChunkUtil.chunkCoordinate(z));
                            if (chunkIndex != newChunkIndex) {
                                suppressionEntry = chunkSuppressionMap.get(newChunkIndex);
                                chunkIndex = newChunkIndex;
                            }
                            if (!this.canSpawn(x, y, z, roleIndex, suppressionEntry)) {
                                if (this.debug != Debug.DISABLED) {
                                    this.failedPositionTestIndexes.add(chosenIndex);
                                }
                                continue;
                            }
                        }
                        if (this.positionIndexes.add(chosenIndex)) {
                            positionList.add(new WeightedPosition(x, y, z));
                        }
                        if (i == Integer.MAX_VALUE) {
                            break;
                        }
                    }
                    final int positionCount = this.positionIndexes.size();
                    if (this.debug != Debug.DISABLED && (positionCount < this.desiredPositionCount * 0.3 || positionCount > this.desiredPositionCount * 5.0)) {
                        this.irregularCase = true;
                    }
                    if (this.irregularCase || this.debug == Debug.ALL) {
                        SpawningPlugin.get().getLogger().at(Level.WARNING).log("Role: " + NPCPlugin.get().getName(roleIndex));
                        SpawningPlugin.get().getLogger().at(Level.WARNING).log(this.debugDumpBaseFloodFill());
                        this.failedPositionTestIndexes.clear();
                    }
                    this.positionIndexes.clear();
                }
            }
        }
    }
    
    private int buildLowerResolutionMap(@Nonnull final BitSet targetMap, final int mapSize, @Nonnull final BitSet parentMap, final int parentMapSize) {
        for (int x = 0; x < mapSize; ++x) {
            for (int z = 0; z < mapSize; ++z) {
                final int parentX = x * 2;
                final int parentZ = z * 2;
                final int index = getPositionIndex(parentX, parentZ, parentMapSize);
                if (parentMap.get(index) || parentMap.get(index + 1) || parentMap.get(index + parentMapSize) || parentMap.get(index + parentMapSize + 1)) {
                    targetMap.set(getPositionIndex(x, z, mapSize));
                }
            }
        }
        return targetMap.cardinality();
    }
    
    private int pickOpenSegment(final int lowResolutionIndex, final int lowResolutionMapSize, @Nonnull final BitSet higherResolutionMap, final int highResolutionMapSize) {
        final int x = xFromIndex(lowResolutionIndex, lowResolutionMapSize);
        final int z = zFromIndex(lowResolutionIndex, lowResolutionMapSize);
        final int parentX = x * 2;
        final int parentZ = z * 2;
        final int originIndex = getPositionIndex(parentX, parentZ, highResolutionMapSize);
        if (higherResolutionMap.get(originIndex)) {
            this.highResolutionOptions.add(originIndex);
        }
        int index = originIndex + 1;
        if (higherResolutionMap.get(index)) {
            this.highResolutionOptions.add(index);
        }
        index = originIndex + highResolutionMapSize;
        if (higherResolutionMap.get(index)) {
            this.highResolutionOptions.add(index);
        }
        index = originIndex + highResolutionMapSize + 1;
        if (higherResolutionMap.get(index)) {
            this.highResolutionOptions.add(index);
        }
        if (this.highResolutionOptions.size() > 1) {
            IntLists.shuffle(this.highResolutionOptions, ThreadLocalRandom.current());
        }
        index = this.highResolutionOptions.getInt(0);
        this.highResolutionOptions.clear();
        return index;
    }
    
    private int shiftIndexAwayFromWall(final int index) {
        int newIndex = index;
        int checkIndex = index - 1;
        if (checkIndex < 0 || !this.fullResolutionMap.get(checkIndex)) {
            ++newIndex;
        }
        checkIndex = index + 1;
        if (checkIndex >= this.fullResolutionMap.size() || !this.fullResolutionMap.get(checkIndex)) {
            --newIndex;
        }
        checkIndex = index - this.size;
        if (checkIndex < 0 || !this.fullResolutionMap.get(checkIndex)) {
            newIndex += this.size;
        }
        checkIndex = index + this.size;
        if (checkIndex >= this.fullResolutionMap.size() || !this.fullResolutionMap.get(checkIndex)) {
            newIndex -= this.size;
        }
        return this.fullResolutionMap.get(newIndex) ? newIndex : index;
    }
    
    private boolean canSpawn(final int x, final int y, final int z, final int roleIndex, @Nullable final ChunkSuppressionEntry suppressionEntry) {
        if (!this.spawnWrapper.getSpawn().isOverrideSpawnSuppressors() && suppressionEntry != null && suppressionEntry.isSuppressingRoleAt(roleIndex, y)) {
            return false;
        }
        if (!this.spawningContext.set(this.world, x, y, z) || this.spawningContext.canSpawn() != SpawnTestResult.TEST_OK || !this.spawnWrapper.withinLightRange(this.spawningContext)) {
            return false;
        }
        if (this.spawningContext.ySpawn > this.maxY) {
            return false;
        }
        final IntSet spawnBlockSet = this.spawnWrapper.getSpawnBlockSet(roleIndex);
        final int spawnFluidTag = this.spawnWrapper.getSpawnFluidTag(roleIndex);
        return (spawnBlockSet == null && spawnFluidTag == Integer.MIN_VALUE) || (spawnBlockSet != null && spawnBlockSet.contains(this.spawningContext.groundBlockId)) || (spawnFluidTag != Integer.MIN_VALUE && Fluid.getAssetMap().getIndexesForTag(spawnFluidTag).contains(this.spawningContext.groundFluidId));
    }
    
    @Nonnull
    private String debugDumpBaseFloodFill() {
        final StringBuilder sb = new StringBuilder();
        final int sizeHalf = this.size / 2;
        final int centre = getPositionIndex(sizeHalf, sizeHalf, this.size);
        for (int z = 0; z < this.size; ++z) {
            for (int x = 0; x < this.size; ++x) {
                final int index = getPositionIndex(x, z, this.size);
                if (index == centre) {
                    sb.append('B');
                }
                else if (this.positionIndexes.contains(index)) {
                    sb.append('S');
                }
                else if (this.failedPositionTestIndexes.contains(index)) {
                    sb.append('F');
                }
                else {
                    switch (this.heightGrid[index]) {
                        case Integer.MAX_VALUE: {
                            sb.append('^');
                            break;
                        }
                        case Integer.MIN_VALUE: {
                            sb.append('v');
                            break;
                        }
                        case -2:
                        case -1: {
                            sb.append('X');
                            break;
                        }
                        default: {
                            sb.append('.');
                            break;
                        }
                    }
                }
                sb.append(' ');
            }
            sb.append('\n');
        }
        return sb.toString();
    }
    
    @Nonnull
    private String debugDumpLowResolutionMap(@Nonnull final BitSet map, final int size) {
        final StringBuilder sb = new StringBuilder();
        for (int z = 0; z < size; ++z) {
            for (int x = 0; x < size; ++x) {
                sb.append(map.get(getPositionIndex(x, z, size)) ? '.' : 'X').append(' ');
            }
            sb.append('\n');
        }
        return sb.toString();
    }
    
    public static int getPositionIndex(final int x, final int z, final int size) {
        return x * size + z;
    }
    
    public static int xFromIndex(final int index, final int size) {
        return index / size;
    }
    
    public static int zFromIndex(final int index, final int size) {
        return index % size;
    }
    
    @Nonnull
    @Override
    public Component<EntityStore> clone() {
        final FloodFillPositionSelector selector = new FloodFillPositionSelector(this.world, this.spawnWrapper);
        selector.init();
        return selector;
    }
    
    static {
        sortBufferProvider = ThreadLocal.withInitial((Supplier<? extends SortBufferProvider>)SortBufferProvider::new);
        WEIGHTED_POSITION_COMPARATOR = ((entry1, entry2) -> Double.compare(((WeightedPosition)entry2).getWeight(), ((WeightedPosition)entry1).getWeight()));
    }
    
    public enum Debug
    {
        DISABLED, 
        IRREGULARITIES, 
        ALL;
    }
    
    private static class WeightedPosition
    {
        @Nonnull
        private final Vector3i position;
        private double weight;
        
        private WeightedPosition(final int x, final int y, final int z) {
            this.position = new Vector3i(x, y, z);
        }
        
        public double getWeight() {
            return this.weight;
        }
    }
    
    public static class SortBufferProvider
    {
        protected WeightedPosition[] buffer;
        
        public SortBufferProvider() {
            this.buffer = new WeightedPosition[10];
        }
        
        public WeightedPosition[] getBuffer(final int size) {
            if (size <= this.buffer.length) {
                return this.buffer;
            }
            return this.buffer = ObjectArrays.grow(this.buffer, size);
        }
    }
}
