package net.minecraft.world.level;

import java.util.function.Consumer;
import it.unimi.dsi.fastutil.objects.Object2IntMaps;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import java.util.stream.Stream;
import org.apache.logging.log4j.LogManager;
import net.minecraft.world.level.pathfinder.PathComputationType;
import net.minecraft.core.Direction;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.level.block.Block;
import net.minecraft.tags.Tag;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.feature.StructureFeature;
import net.minecraft.world.level.block.Blocks;
import java.util.List;
import net.minecraft.util.WeighedRandom;
import java.util.Random;
import javax.annotation.Nullable;
import net.minecraft.core.Registry;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.SpawnPlacements;
import java.util.Objects;
import net.minecraft.core.Position;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.SpawnGroupData;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.entity.MobSpawnType;
import net.minecraft.util.Mth;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.biome.NearestNeighborBiomeZoomer;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.core.BlockPos;
import java.util.Iterator;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.entity.Mob;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.MobCategory;
import org.apache.logging.log4j.Logger;

public final class NaturalSpawner {
    private static final Logger LOGGER;
    private static final int MAGIC_NUMBER;
    private static final MobCategory[] SPAWNING_CATEGORIES;
    
    public static SpawnState createState(final int integer, final Iterable<Entity> iterable, final ChunkGetter b) {
        final PotentialCalculator bqn4 = new PotentialCalculator();
        final Object2IntOpenHashMap<MobCategory> object2IntOpenHashMap5 = (Object2IntOpenHashMap<MobCategory>)new Object2IntOpenHashMap();
        for (final Entity aom7 : iterable) {
            if (aom7 instanceof Mob) {
                final Mob aoz8 = (Mob)aom7;
                if (aoz8.isPersistenceRequired()) {
                    continue;
                }
                if (aoz8.requiresCustomPersistence()) {
                    continue;
                }
            }
            final MobCategory apa8 = aom7.getType().getCategory();
            if (apa8 == MobCategory.MISC) {
                continue;
            }
            final BlockPos fu9 = aom7.blockPosition();
            final long long10 = ChunkPos.asLong(fu9.getX() >> 4, fu9.getZ() >> 4);
            final BlockPos fu10;
            final Biome bre7;
            final Entity entity;
            final Biome.MobSpawnCost e8;
            final PotentialCalculator potentialCalculator;
            final Object2IntOpenHashMap object2IntOpenHashMap6;
            final Object o;
            b.query(long10, chj -> {
                bre7 = getRoughBiome(fu10, chj);
                e8 = bre7.getMobSpawnCost(entity.getType());
                if (e8 != null) {
                    potentialCalculator.addCharge(entity.blockPosition(), e8.getCharge());
                }
                object2IntOpenHashMap6.addTo(o, 1);
                return;
            });
        }
        return new SpawnState(integer, (Object2IntOpenHashMap)object2IntOpenHashMap5, bqn4);
    }
    
    private static Biome getRoughBiome(final BlockPos fu, final ChunkAccess cgy) {
        return NearestNeighborBiomeZoomer.INSTANCE.getBiome(0L, fu.getX(), fu.getY(), fu.getZ(), cgy.getBiomes());
    }
    
    public static void spawnForChunk(final ServerLevel zd, final LevelChunk chj, final SpawnState d, final boolean boolean4, final boolean boolean5, final boolean boolean6) {
        zd.getProfiler().push("spawner");
        for (final MobCategory apa10 : NaturalSpawner.SPAWNING_CATEGORIES) {
            if ((boolean4 || !apa10.isFriendly()) && (boolean5 || apa10.isFriendly()) && (boolean6 || !apa10.isPersistent()) && d.canSpawnForCategory(apa10)) {
                spawnCategoryForChunk(apa10, zd, chj, (aoq, fu, cgy) -> d.canSpawn(aoq, fu, cgy), (aoz, cgy) -> d.afterSpawn(aoz, cgy));
            }
        }
        zd.getProfiler().pop();
    }
    
    public static void spawnCategoryForChunk(final MobCategory apa, final ServerLevel zd, final LevelChunk chj, final SpawnPredicate c, final AfterSpawnCallback a) {
        final BlockPos fu6 = getRandomPosWithin(zd, chj);
        if (fu6.getY() < 1) {
            return;
        }
        spawnCategoryForPosition(apa, zd, chj, fu6, c, a);
    }
    
    public static void spawnCategoryForPosition(final MobCategory apa, final ServerLevel zd, final ChunkAccess cgy, final BlockPos fu, final SpawnPredicate c, final AfterSpawnCallback a) {
        final StructureFeatureManager bqq7 = zd.structureFeatureManager();
        final ChunkGenerator cha8 = zd.getChunkSource().getGenerator();
        final int integer9 = fu.getY();
        final BlockState cfj10 = cgy.getBlockState(fu);
        if (cfj10.isRedstoneConductor(cgy, fu)) {
            return;
        }
        final BlockPos.MutableBlockPos a2 = new BlockPos.MutableBlockPos();
        int integer10 = 0;
        for (int integer11 = 0; integer11 < 3; ++integer11) {
            int integer12 = fu.getX();
            int integer13 = fu.getZ();
            final int integer14 = 6;
            Biome.SpawnerData g17 = null;
            SpawnGroupData apo18 = null;
            int integer15 = Mth.ceil(zd.random.nextFloat() * 4.0f);
            int integer16 = 0;
            for (int integer17 = 0; integer17 < integer15; ++integer17) {
                integer12 += zd.random.nextInt(6) - zd.random.nextInt(6);
                integer13 += zd.random.nextInt(6) - zd.random.nextInt(6);
                a2.set(integer12, integer9, integer13);
                final double double22 = integer12 + 0.5;
                final double double23 = integer13 + 0.5;
                final Player bec26 = zd.getNearestPlayer(double22, integer9, double23, -1.0, false);
                if (bec26 != null) {
                    final double double24 = bec26.distanceToSqr(double22, integer9, double23);
                    if (isRightDistanceToPlayerAndSpawnPoint(zd, cgy, a2, double24)) {
                        if (g17 == null) {
                            g17 = getRandomSpawnMobAt(zd, bqq7, cha8, apa, zd.random, a2);
                            if (g17 == null) {
                                break;
                            }
                            integer15 = g17.minCount + zd.random.nextInt(1 + g17.maxCount - g17.minCount);
                        }
                        if (isValidSpawnPostitionForType(zd, apa, bqq7, cha8, g17, a2, double24)) {
                            if (c.test(g17.type, a2, cgy)) {
                                final Mob aoz29 = getMobForSpawn(zd, g17.type);
                                if (aoz29 == null) {
                                    return;
                                }
                                aoz29.moveTo(double22, integer9, double23, zd.random.nextFloat() * 360.0f, 0.0f);
                                if (isValidPositionForMob(zd, aoz29, double24)) {
                                    apo18 = aoz29.finalizeSpawn(zd, zd.getCurrentDifficultyAt(aoz29.blockPosition()), MobSpawnType.NATURAL, apo18, null);
                                    ++integer10;
                                    ++integer16;
                                    zd.addFreshEntity(aoz29);
                                    a.run(aoz29, cgy);
                                    if (integer10 >= aoz29.getMaxSpawnClusterSize()) {
                                        return;
                                    }
                                    if (aoz29.isMaxGroupSizeReached(integer16)) {
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    
    private static boolean isRightDistanceToPlayerAndSpawnPoint(final ServerLevel zd, final ChunkAccess cgy, final BlockPos.MutableBlockPos a, final double double4) {
        if (double4 <= 576.0) {
            return false;
        }
        if (zd.getSharedSpawnPos().closerThan(new Vec3(a.getX() + 0.5, a.getY(), a.getZ() + 0.5), 24.0)) {
            return false;
        }
        final ChunkPos bph6 = new ChunkPos(a);
        return Objects.equals(bph6, cgy.getPos()) || zd.getChunkSource().isEntityTickingChunk(bph6);
    }
    
    private static boolean isValidSpawnPostitionForType(final ServerLevel zd, final MobCategory apa, final StructureFeatureManager bqq, final ChunkGenerator cha, final Biome.SpawnerData g, final BlockPos.MutableBlockPos a, final double double7) {
        final EntityType<?> aoq9 = g.type;
        if (aoq9.getCategory() == MobCategory.MISC) {
            return false;
        }
        if (!aoq9.canSpawnFarFromPlayer() && double7 > aoq9.getCategory().getDespawnDistance() * aoq9.getCategory().getDespawnDistance()) {
            return false;
        }
        if (!aoq9.canSummon() || !canSpawnMobAt(zd, bqq, cha, apa, g, a)) {
            return false;
        }
        final SpawnPlacements.Type c10 = SpawnPlacements.getPlacementType(aoq9);
        return isSpawnPositionOk(c10, zd, a, aoq9) && SpawnPlacements.checkSpawnRules(aoq9, zd, MobSpawnType.NATURAL, a, zd.random) && zd.noCollision(aoq9.getAABB(a.getX() + 0.5, a.getY(), a.getZ() + 0.5));
    }
    
    @Nullable
    private static Mob getMobForSpawn(final ServerLevel zd, final EntityType<?> aoq) {
        Mob aoz3;
        try {
            final Entity aom4 = (Entity)aoq.create(zd);
            if (!(aom4 instanceof Mob)) {
                throw new IllegalStateException("Trying to spawn a non-mob: " + Registry.ENTITY_TYPE.getKey(aoq));
            }
            aoz3 = (Mob)aom4;
        }
        catch (Exception exception4) {
            NaturalSpawner.LOGGER.warn("Failed to create mob", (Throwable)exception4);
            return null;
        }
        return aoz3;
    }
    
    private static boolean isValidPositionForMob(final ServerLevel zd, final Mob aoz, final double double3) {
        return (double3 <= aoz.getType().getCategory().getDespawnDistance() * aoz.getType().getCategory().getDespawnDistance() || !aoz.removeWhenFarAway(double3)) && aoz.checkSpawnRules(zd, MobSpawnType.NATURAL) && aoz.checkSpawnObstruction(zd);
    }
    
    @Nullable
    private static Biome.SpawnerData getRandomSpawnMobAt(final ServerLevel zd, final StructureFeatureManager bqq, final ChunkGenerator cha, final MobCategory apa, final Random random, final BlockPos fu) {
        final Biome bre7 = zd.getBiome(fu);
        if (apa == MobCategory.WATER_AMBIENT && bre7.getBiomeCategory() == Biome.BiomeCategory.RIVER && random.nextFloat() < 0.98f) {
            return null;
        }
        final List<Biome.SpawnerData> list8 = mobsAt(zd, bqq, cha, apa, fu, bre7);
        if (list8.isEmpty()) {
            return null;
        }
        return WeighedRandom.<Biome.SpawnerData>getRandomItem(random, list8);
    }
    
    private static boolean canSpawnMobAt(final ServerLevel zd, final StructureFeatureManager bqq, final ChunkGenerator cha, final MobCategory apa, final Biome.SpawnerData g, final BlockPos fu) {
        return mobsAt(zd, bqq, cha, apa, fu, null).contains(g);
    }
    
    private static List<Biome.SpawnerData> mobsAt(final ServerLevel zd, final StructureFeatureManager bqq, final ChunkGenerator cha, final MobCategory apa, final BlockPos fu, @Nullable final Biome bre) {
        if (apa == MobCategory.MONSTER && zd.getBlockState(fu.below()).getBlock() == Blocks.NETHER_BRICKS && bqq.getStructureAt(fu, false, StructureFeature.NETHER_BRIDGE).isValid()) {
            return StructureFeature.NETHER_BRIDGE.getSpecialEnemies();
        }
        return cha.getMobsAt((bre != null) ? bre : zd.getBiome(fu), bqq, apa, fu);
    }
    
    private static BlockPos getRandomPosWithin(final Level bqb, final LevelChunk chj) {
        final ChunkPos bph3 = chj.getPos();
        final int integer4 = bph3.getMinBlockX() + bqb.random.nextInt(16);
        final int integer5 = bph3.getMinBlockZ() + bqb.random.nextInt(16);
        final int integer6 = chj.getHeight(Heightmap.Types.WORLD_SURFACE, integer4, integer5) + 1;
        final int integer7 = bqb.random.nextInt(integer6 + 1);
        return new BlockPos(integer4, integer7, integer5);
    }
    
    public static boolean isValidEmptySpawnBlock(final BlockGetter bpg, final BlockPos fu, final BlockState cfj, final FluidState cxa, final EntityType aoq) {
        return !cfj.isCollisionShapeFullBlock(bpg, fu) && !cfj.isSignalSource() && cxa.isEmpty() && !cfj.is(BlockTags.PREVENT_MOB_SPAWNING_INSIDE) && !aoq.isBlockDangerous(cfj);
    }
    
    public static boolean isSpawnPositionOk(final SpawnPlacements.Type c, final LevelReader bqd, final BlockPos fu, @Nullable final EntityType<?> aoq) {
        if (c == SpawnPlacements.Type.NO_RESTRICTIONS) {
            return true;
        }
        if (aoq == null || !bqd.getWorldBorder().isWithinBounds(fu)) {
            return false;
        }
        final BlockState cfj5 = bqd.getBlockState(fu);
        final FluidState cxa6 = bqd.getFluidState(fu);
        final BlockPos fu2 = fu.above();
        final BlockPos fu3 = fu.below();
        switch (c) {
            case IN_WATER: {
                return cxa6.is(FluidTags.WATER) && bqd.getFluidState(fu3).is(FluidTags.WATER) && !bqd.getBlockState(fu2).isRedstoneConductor(bqd, fu2);
            }
            case IN_LAVA: {
                return cxa6.is(FluidTags.LAVA);
            }
            default: {
                final BlockState cfj6 = bqd.getBlockState(fu3);
                return cfj6.isValidSpawn(bqd, fu3, aoq) && isValidEmptySpawnBlock(bqd, fu, cfj5, cxa6, aoq) && isValidEmptySpawnBlock(bqd, fu2, bqd.getBlockState(fu2), bqd.getFluidState(fu2), aoq);
            }
        }
    }
    
    public static void spawnMobsForChunkGeneration(final LevelAccessor bqc, final Biome bre, final int integer3, final int integer4, final Random random) {
        final List<Biome.SpawnerData> list6 = bre.getMobs(MobCategory.CREATURE);
        if (list6.isEmpty()) {
            return;
        }
        final int integer5 = integer3 << 4;
        final int integer6 = integer4 << 4;
        while (random.nextFloat() < bre.getCreatureProbability()) {
            final Biome.SpawnerData g9 = WeighedRandom.<Biome.SpawnerData>getRandomItem(random, list6);
            final int integer7 = g9.minCount + random.nextInt(1 + g9.maxCount - g9.minCount);
            SpawnGroupData apo11 = null;
            int integer8 = integer5 + random.nextInt(16);
            int integer9 = integer6 + random.nextInt(16);
            final int integer10 = integer8;
            final int integer11 = integer9;
            for (int integer12 = 0; integer12 < integer7; ++integer12) {
                boolean boolean17 = false;
                for (int integer13 = 0; !boolean17 && integer13 < 4; ++integer13) {
                    final BlockPos fu19 = getTopNonCollidingPos(bqc, g9.type, integer8, integer9);
                    if (g9.type.canSummon() && isSpawnPositionOk(SpawnPlacements.getPlacementType(g9.type), bqc, fu19, g9.type)) {
                        final float float20 = g9.type.getWidth();
                        final double double21 = Mth.clamp(integer8, integer5 + (double)float20, integer5 + 16.0 - float20);
                        final double double22 = Mth.clamp(integer9, integer6 + (double)float20, integer6 + 16.0 - float20);
                        if (!bqc.noCollision(g9.type.getAABB(double21, fu19.getY(), double22))) {
                            continue;
                        }
                        if (!SpawnPlacements.<Entity>checkSpawnRules(g9.type, bqc, MobSpawnType.CHUNK_GENERATION, new BlockPos(double21, fu19.getY(), double22), bqc.getRandom())) {
                            continue;
                        }
                        Entity aom25;
                        try {
                            aom25 = (Entity)g9.type.create(bqc.getLevel());
                        }
                        catch (Exception exception26) {
                            NaturalSpawner.LOGGER.warn("Failed to create mob", (Throwable)exception26);
                            continue;
                        }
                        aom25.moveTo(double21, fu19.getY(), double22, random.nextFloat() * 360.0f, 0.0f);
                        if (aom25 instanceof Mob) {
                            final Mob aoz26 = (Mob)aom25;
                            if (aoz26.checkSpawnRules(bqc, MobSpawnType.CHUNK_GENERATION) && aoz26.checkSpawnObstruction(bqc)) {
                                apo11 = aoz26.finalizeSpawn(bqc, bqc.getCurrentDifficultyAt(aoz26.blockPosition()), MobSpawnType.CHUNK_GENERATION, apo11, null);
                                bqc.addFreshEntity(aoz26);
                                boolean17 = true;
                            }
                        }
                    }
                    for (integer8 += random.nextInt(5) - random.nextInt(5), integer9 += random.nextInt(5) - random.nextInt(5); integer8 < integer5 || integer8 >= integer5 + 16 || integer9 < integer6 || integer9 >= integer6 + 16; integer8 = integer10 + random.nextInt(5) - random.nextInt(5), integer9 = integer11 + random.nextInt(5) - random.nextInt(5)) {}
                }
            }
        }
    }
    
    private static BlockPos getTopNonCollidingPos(final LevelReader bqd, final EntityType<?> aoq, final int integer3, final int integer4) {
        final int integer5 = bqd.getHeight(SpawnPlacements.getHeightmapType(aoq), integer3, integer4);
        final BlockPos.MutableBlockPos a6 = new BlockPos.MutableBlockPos(integer3, integer5, integer4);
        if (bqd.dimensionType().hasCeiling()) {
            do {
                a6.move(Direction.DOWN);
            } while (!bqd.getBlockState(a6).isAir());
            do {
                a6.move(Direction.DOWN);
            } while (bqd.getBlockState(a6).isAir() && a6.getY() > 0);
        }
        if (SpawnPlacements.getPlacementType(aoq) == SpawnPlacements.Type.ON_GROUND) {
            final BlockPos fu7 = a6.below();
            if (bqd.getBlockState(fu7).isPathfindable(bqd, fu7, PathComputationType.LAND)) {
                return fu7;
            }
        }
        return a6.immutable();
    }
    
    static {
        LOGGER = LogManager.getLogger();
        MAGIC_NUMBER = (int)Math.pow(17.0, 2.0);
        SPAWNING_CATEGORIES = Stream.<MobCategory>of(MobCategory.values()).filter(apa -> apa != MobCategory.MISC).<MobCategory>toArray(MobCategory[]::new);
    }
    
    public static class SpawnState {
        private final int spawnableChunkCount;
        private final Object2IntOpenHashMap<MobCategory> mobCategoryCounts;
        private final PotentialCalculator spawnPotential;
        private final Object2IntMap<MobCategory> unmodifiableMobCategoryCounts;
        @Nullable
        private BlockPos lastCheckedPos;
        @Nullable
        private EntityType<?> lastCheckedType;
        private double lastCharge;
        
        private SpawnState(final int integer, final Object2IntOpenHashMap<MobCategory> object2IntOpenHashMap, final PotentialCalculator bqn) {
            this.spawnableChunkCount = integer;
            this.mobCategoryCounts = object2IntOpenHashMap;
            this.spawnPotential = bqn;
            this.unmodifiableMobCategoryCounts = (Object2IntMap<MobCategory>)Object2IntMaps.unmodifiable((Object2IntMap)object2IntOpenHashMap);
        }
        
        private boolean canSpawn(final EntityType<?> aoq, final BlockPos fu, final ChunkAccess cgy) {
            this.lastCheckedPos = fu;
            this.lastCheckedType = aoq;
            final Biome bre5 = getRoughBiome(fu, cgy);
            final Biome.MobSpawnCost e6 = bre5.getMobSpawnCost(aoq);
            if (e6 == null) {
                this.lastCharge = 0.0;
                return true;
            }
            final double double7 = e6.getCharge();
            this.lastCharge = double7;
            final double double8 = this.spawnPotential.getPotentialEnergyChange(fu, double7);
            return double8 <= e6.getEnergyBudget();
        }
        
        private void afterSpawn(final Mob aoz, final ChunkAccess cgy) {
            final EntityType<?> aoq4 = aoz.getType();
            final BlockPos fu7 = aoz.blockPosition();
            double double5;
            if (fu7.equals(this.lastCheckedPos) && aoq4 == this.lastCheckedType) {
                double5 = this.lastCharge;
            }
            else {
                final Biome bre8 = getRoughBiome(fu7, cgy);
                final Biome.MobSpawnCost e9 = bre8.getMobSpawnCost(aoq4);
                if (e9 != null) {
                    double5 = e9.getCharge();
                }
                else {
                    double5 = 0.0;
                }
            }
            this.spawnPotential.addCharge(fu7, double5);
            this.mobCategoryCounts.addTo(aoq4.getCategory(), 1);
        }
        
        public int getSpawnableChunkCount() {
            return this.spawnableChunkCount;
        }
        
        public Object2IntMap<MobCategory> getMobCategoryCounts() {
            return this.unmodifiableMobCategoryCounts;
        }
        
        private boolean canSpawnForCategory(final MobCategory apa) {
            final int integer3 = apa.getMaxInstancesPerChunk() * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER;
            return this.mobCategoryCounts.getInt(apa) < integer3;
        }
    }
    
    @FunctionalInterface
    public interface ChunkGetter {
        void query(final long long1, final Consumer<LevelChunk> consumer);
    }
    
    @FunctionalInterface
    public interface AfterSpawnCallback {
        void run(final Mob aoz, final ChunkAccess cgy);
    }
    
    @FunctionalInterface
    public interface SpawnPredicate {
        boolean test(final EntityType<?> aoq, final BlockPos fu, final ChunkAccess cgy);
    }
}
