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

package com.hypixel.hytale.server.worldgen.chunk.populator;

import com.hypixel.hytale.server.worldgen.container.CoverContainer;
import com.hypixel.hytale.procedurallib.condition.IBlockFluidCondition;
import com.hypixel.hytale.server.worldgen.util.BlockFluidEntry;
import com.hypixel.hytale.procedurallib.condition.ConstantIntCondition;
import com.hypixel.hytale.server.worldgen.util.bounds.IChunkBounds;
import com.hypixel.hytale.server.worldgen.prefab.PrefabPasteUtil;
import com.hypixel.hytale.server.worldgen.container.WaterContainer;
import com.hypixel.hytale.server.worldgen.prefab.PrefabCategory;
import com.hypixel.hytale.server.core.prefab.selection.buffer.impl.IPrefabBuffer;
import com.hypixel.hytale.common.map.IWeightedMap;
import com.hypixel.hytale.server.worldgen.chunk.ZoneBiomeResult;
import com.hypixel.hytale.server.worldgen.prefab.PrefabPatternGenerator;
import java.util.Random;
import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.math.vector.Vector3i;
import com.hypixel.hytale.procedurallib.condition.DefaultCoordinateRndCondition;
import com.hypixel.hytale.procedurallib.condition.ICoordinateRndCondition;
import com.hypixel.hytale.server.worldgen.util.condition.BlockMaskCondition;
import com.hypixel.hytale.server.worldgen.loader.WorldGenPrefabSupplier;
import com.hypixel.hytale.server.core.prefab.PrefabRotation;
import com.hypixel.hytale.math.util.HashUtil;
import com.hypixel.hytale.server.worldgen.biome.CustomBiome;
import com.hypixel.hytale.server.worldgen.biome.TileBiome;
import com.hypixel.hytale.server.worldgen.biome.BiomePatternGenerator;
import com.hypixel.hytale.server.worldgen.zone.Zone;
import com.hypixel.hytale.server.worldgen.zone.ZonePatternGenerator;
import com.hypixel.hytale.server.worldgen.zone.ZoneGeneratorResult;
import com.hypixel.hytale.server.worldgen.cache.CoreDataCacheEntry;
import com.hypixel.hytale.math.util.ChunkUtil;
import java.util.Objects;
import com.hypixel.hytale.server.worldgen.chunk.ChunkGenerator;
import java.util.BitSet;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import com.hypixel.hytale.math.util.FastRandom;
import javax.annotation.Nonnull;
import com.hypixel.hytale.server.worldgen.chunk.ChunkGeneratorExecution;
import com.hypixel.hytale.server.worldgen.container.PrefabContainer;
import javax.annotation.Nullable;
import com.hypixel.hytale.server.worldgen.biome.Biome;
import com.hypixel.hytale.server.worldgen.container.UniquePrefabContainer;

public class PrefabPopulator
{
    private static final UniquePrefabContainer.UniquePrefabEntry[] EMPTY_UNIQUE_PREFABS;
    private static final int BIOME_SAMPLE_STEP_SIZE = 8;
    private int worldSeed;
    private long prefabSeed;
    private int minPriority;
    @Nullable
    private Biome biome;
    @Nullable
    private PrefabContainer.PrefabContainerEntry entry;
    @Nullable
    private ChunkGeneratorExecution execution;
    @Nonnull
    private UniquePrefabContainer.UniquePrefabEntry[] uniquePrefabs;
    private final FastRandom random;
    private final ObjectArrayList<Biome> biomes;
    private final ObjectArrayList<Candidate> prefabs;
    private final BitSet conflicts;
    
    public PrefabPopulator() {
        this.minPriority = Integer.MAX_VALUE;
        this.uniquePrefabs = PrefabPopulator.EMPTY_UNIQUE_PREFABS;
        this.random = new FastRandom(0L);
        this.biomes = new ObjectArrayList<Biome>();
        this.prefabs = new ObjectArrayList<Candidate>();
        this.conflicts = new BitSet();
    }
    
    public static void populate(final int seed, @Nonnull final ChunkGeneratorExecution execution) {
        ChunkGenerator.getResource().prefabPopulator.run(seed, execution);
    }
    
    public void run(final int seed, @Nonnull final ChunkGeneratorExecution execution) {
        this.worldSeed = seed;
        this.minPriority = Integer.MAX_VALUE;
        this.uniquePrefabs = Objects.requireNonNullElse(execution.getChunkGenerator().getUniquePrefabs(seed), PrefabPopulator.EMPTY_UNIQUE_PREFABS);
        this.collectBiomes(seed, execution);
        this.collectPrefabs(seed, execution);
        this.collectConflicts();
        this.generatePrefabs(seed, execution);
        this.generateUniquePrefabs(seed, execution);
        this.biome = null;
        this.entry = null;
        this.execution = null;
        this.uniquePrefabs = PrefabPopulator.EMPTY_UNIQUE_PREFABS;
        this.biomes.clear();
        this.prefabs.clear();
        this.conflicts.clear();
    }
    
    private void collectBiomes(final int seed, final ChunkGeneratorExecution execution) {
        for (final CoreDataCacheEntry entry : execution.getCoreDataEntries()) {
            final Biome biome = entry.zoneBiomeResult.getBiome();
            if (biome.getPrefabContainer() != null) {
                this.collectBiome(biome);
            }
        }
        final int chunkX = execution.getX();
        final int chunkZ = execution.getZ();
        final int chunkMinX = ChunkUtil.minBlock(chunkX) - 5;
        final int chunkMinZ = ChunkUtil.minBlock(chunkZ) - 5;
        final int chunkMaxX = ChunkUtil.maxBlock(chunkX) + 5;
        final int chunkMaxZ = ChunkUtil.maxBlock(chunkZ) + 5;
        final int extents = execution.getChunkGenerator().getZonePatternProvider().getMaxExtent();
        final int regionMinX = chunkMinX - extents;
        final int regionMinZ = chunkMinZ - extents;
        final int regionMaxX = chunkMaxX + extents;
        final int regionMaxZ = chunkMaxZ + extents;
        final ZoneGeneratorResult zoneResult = ChunkGenerator.getResource().zoneBiomeResult.zoneResult;
        final ZonePatternGenerator zoneGenerator = execution.getChunkGenerator().getZonePatternGenerator(seed);
        for (int z = regionMinZ; z <= regionMaxZ; z += 8) {
            for (int x = regionMinX; x <= regionMaxX; x += 8) {
                if (x < chunkMinX || z < chunkMinZ || x > chunkMaxX || z > chunkMaxZ) {
                    final Zone zone = zoneGenerator.generate(seed, x, z, zoneResult).getZone();
                    final BiomePatternGenerator biomeGenerator = zone.biomePatternGenerator();
                    final int minX = chunkMinX - biomeGenerator.getExtents();
                    final int minZ = chunkMinZ - biomeGenerator.getExtents();
                    final int maxX = chunkMaxX + biomeGenerator.getExtents();
                    final int maxZ = chunkMaxZ + biomeGenerator.getExtents();
                    if (x >= minX && z >= minZ && x <= maxX) {
                        if (z <= maxZ) {
                            final TileBiome biome2 = biomeGenerator.getBiome(seed, x, z);
                            if (biome2 != null) {
                                if (biome2.getPrefabContainer() != null) {
                                    this.collectBiome(biome2);
                                }
                                for (final CustomBiome customBiome : biomeGenerator.getCustomBiomes()) {
                                    if (customBiome.getPrefabContainer() != null && customBiome.getCustomBiomeGenerator().isValidParentBiome(biome2.getId())) {
                                        this.collectBiome(customBiome);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    
    private void collectPrefabs(final int seed, final ChunkGeneratorExecution execution) {
        for (int i = 0; i < this.biomes.size(); ++i) {
            final Biome biome = this.biomes.get(i);
            final PrefabContainer container = biome.getPrefabContainer();
            if (container != null) {
                int id = 0;
                for (final PrefabContainer.PrefabContainerEntry entry : container.getEntries()) {
                    this.biome = biome;
                    this.entry = entry;
                    this.execution = execution;
                    this.prefabSeed = HashUtil.hash(seed, biome.getId(), ++id);
                    entry.getPrefabPatternGenerator().getGridGenerator().collect(seed, ChunkUtil.minBlock(execution.getX()) - entry.getExtents(), ChunkUtil.minBlock(execution.getZ()) - entry.getExtents(), ChunkUtil.maxBlock(execution.getX()) + entry.getExtents(), ChunkUtil.maxBlock(execution.getZ()) + entry.getExtents(), this::collectPrefab);
                }
            }
        }
    }
    
    private void generatePrefabs(final int seed, @Nonnull final ChunkGeneratorExecution execution) {
        for (int i = 0; i < this.prefabs.size(); ++i) {
            if (!this.conflicts.get(i)) {
                final Candidate prefab = this.prefabs.get(i);
                final int x = prefab.x;
                final int y = prefab.y;
                final int z = prefab.z;
                final PrefabRotation rotation = prefab.rotation;
                final WorldGenPrefabSupplier supplier = prefab.supplier;
                if (isMatchingChunkBounds(x, z, execution, rotation, supplier.getBounds(prefab.buffer))) {
                    final BlockMaskCondition config = prefab.generator.getPrefabPlacementConfiguration();
                    final ICoordinateRndCondition heightCondition = prefab.generator.getHeightCondition();
                    final int environment = prefab.entry.getEnvironmentId();
                    final boolean fitHeightmap = prefab.generator.isFitHeightmap();
                    final boolean submerge = prefab.generator.isSubmerge();
                    generatePrefabAt(seed, x, z, y, execution, supplier, config, rotation, heightCondition, environment, fitHeightmap, submerge);
                }
            }
        }
    }
    
    private void generateUniquePrefabs(final int seed, @Nonnull final ChunkGeneratorExecution execution) {
        for (final UniquePrefabContainer.UniquePrefabEntry entry : this.uniquePrefabs) {
            if (isMatchingChunkBounds(execution, entry.getLowBoundX(), entry.getLowBoundZ(), entry.getHighBoundX(), entry.getHighBoundZ())) {
                final Vector3i v = entry.getPosition();
                generatePrefabAt(seed, v.getX(), v.getZ(), v.getY(), execution, entry.getPrefabSupplier(), entry.getConfiguration(), entry.getRotation(), DefaultCoordinateRndCondition.DEFAULT_TRUE, entry.getEnvironmentId(), entry.isFitHeightmap(), entry.isSubmerge());
            }
        }
    }
    
    private void collectBiome(@Nonnull final Biome biome) {
        Biome insert = biome;
        for (int i = 0; i < this.biomes.size(); ++i) {
            final int id = this.biomes.get(i).getId();
            if (insert.getId() == id) {
                return;
            }
            if (insert.getId() < id) {
                insert = this.biomes.set(i, insert);
            }
        }
        this.biomes.add(insert);
    }
    
    private void collectPrefab(final double px, final double pz) {
        Objects.requireNonNull(this.biome);
        Objects.requireNonNull(this.entry);
        Objects.requireNonNull(this.execution);
        final int x = (int)MathUtil.fastFloor(px);
        final int z = (int)MathUtil.fastFloor(pz);
        this.random.setSeed(HashUtil.hash(this.prefabSeed, x, z) * 1609272495L);
        final PrefabPatternGenerator patternGenerator = this.entry.getPrefabPatternGenerator();
        if (!isMatchingNoiseDensity(this.worldSeed, x, z, patternGenerator)) {
            return;
        }
        if (isWithinUniquePrefabExclusionRange(x, z, patternGenerator, this.uniquePrefabs)) {
            return;
        }
        final ZoneBiomeResult result = this.execution.getChunkGenerator().getZoneBiomeResultAt(this.worldSeed, x, z);
        if (!isMatchingBiome(this.biome, result)) {
            return;
        }
        final IWeightedMap<WorldGenPrefabSupplier> prefabs = this.entry.getPrefabs();
        final WorldGenPrefabSupplier supplier = prefabs.get(this.random);
        if (supplier == null) {
            return;
        }
        final IPrefabBuffer prefab = supplier.get();
        if (prefab == null) {
            return;
        }
        final PrefabRotation rotation = generateRotation(x, z, this.random, patternGenerator);
        final int y = getHeight(this.worldSeed, x, z, this.execution, result.getBiome(), patternGenerator, this.random);
        if (!isMatchingHeight(this.worldSeed, x, z, y, this.random, patternGenerator)) {
            return;
        }
        if (!isMatchingParentBlock(this.worldSeed, x, z, y, this.random, result, this.entry)) {
            return;
        }
        final PrefabCategory category = patternGenerator.getCategory();
        this.prefabs.add(new Candidate(x, y, z, category.priority(), rotation, prefab, supplier, this.entry, patternGenerator));
        this.minPriority = Math.min(this.minPriority, category.priority());
    }
    
    private void collectConflicts() {
        for (int i = 0; i < this.prefabs.size(); ++i) {
            final Candidate candidate = this.prefabs.get(i);
            final int minY = candidate.y + candidate.buffer.getMinY();
            final int maxY = candidate.y + candidate.buffer.getMaxY();
            final int minX = candidate.x + candidate.buffer.getMinX(candidate.rotation);
            final int minZ = candidate.z + candidate.buffer.getMinZ(candidate.rotation);
            final int maxX = candidate.x + candidate.buffer.getMaxX(candidate.rotation);
            final int maxZ = candidate.z + candidate.buffer.getMaxZ(candidate.rotation);
            if (candidate.priority > this.minPriority) {
                if (!this.conflicts.get(i)) {
                    for (int j = 0; j < this.prefabs.size(); ++j) {
                        final Candidate other = this.prefabs.get(j);
                        if (candidate.priority > other.priority) {
                            if (intersects(minX, minY, minZ, maxX, maxY, maxZ, other.x + other.buffer.getMinX(other.rotation), other.y + other.buffer.getMinY(), other.z + other.buffer.getMinZ(other.rotation), other.x + other.buffer.getMaxX(other.rotation), other.y + other.buffer.getMaxY(), other.z + other.buffer.getMaxZ(other.rotation))) {
                                this.conflicts.set(j);
                            }
                        }
                    }
                }
            }
        }
    }
    
    private static boolean intersects(final int minX1, final int minY1, final int minZ1, final int maxX1, final int maxY1, final int maxZ1, final int minX2, final int minY2, final int minZ2, final int maxX2, final int maxY2, final int maxZ2) {
        return maxX1 >= minX2 && minX1 <= maxX2 && maxY1 >= minY2 && minY1 <= maxY2 && maxZ1 >= minZ2 && minZ1 <= maxZ2;
    }
    
    private static boolean isWithinUniquePrefabExclusionRange(final int x, final int z, @Nonnull final PrefabPatternGenerator generator, @Nonnull final UniquePrefabContainer.UniquePrefabEntry[] uniquePrefabs) {
        final long radius = generator.getExclusionRadius();
        if (radius <= 0L) {
            return false;
        }
        final long radius2 = radius * radius;
        final int priority = generator.getCategory().priority();
        for (final UniquePrefabContainer.UniquePrefabEntry unique : uniquePrefabs) {
            if (priority < unique.getCategory().priority()) {
                final long dx = x - unique.getPosition().getX();
                final long dz = z - unique.getPosition().getZ();
                if (dx * dx + dz * dz <= radius2) {
                    return true;
                }
            }
        }
        return false;
    }
    
    private static int getHeight(final int seed, final int x, final int z, @Nonnull final ChunkGeneratorExecution execution, @Nonnull final Biome biome, @Nonnull final PrefabPatternGenerator prefabPatternGenerator, final Random random) {
        int height;
        if (prefabPatternGenerator.isOnWater() && prefabPatternGenerator.isDeepSearch()) {
            height = Integer.MIN_VALUE;
            for (final WaterContainer.Entry waterContainer : biome.getWaterContainer().getEntries()) {
                final int max = waterContainer.getMax(seed, x, z);
                if (max != Integer.MIN_VALUE && prefabPatternGenerator.getHeightCondition().eval(seed, x, z, max, random)) {
                    height = max;
                    break;
                }
            }
        }
        else if (prefabPatternGenerator.isOnWater()) {
            height = biome.getWaterContainer().getMaxHeight(seed, x, z);
        }
        else if (prefabPatternGenerator.isDeepSearch()) {
            height = execution.getChunkGenerator().generateHeightBetween(seed, x, z, prefabPatternGenerator.getHeightThresholdInterpreter());
        }
        else {
            height = execution.getChunkGenerator().getHeight(seed, x, z);
        }
        height += prefabPatternGenerator.getDisplacement(seed, x, z);
        return height;
    }
    
    private static PrefabRotation generateRotation(final int x, final int z, @Nonnull final Random random, @Nonnull final PrefabPatternGenerator patternGenerator) {
        PrefabRotation[] prefabRotations = patternGenerator.getRotations();
        if (prefabRotations == null) {
            prefabRotations = PrefabRotation.VALUES;
        }
        return prefabRotations[random.nextInt(prefabRotations.length)];
    }
    
    private static void generatePrefabAt(final int seed, final int x, final int z, final int height, @Nonnull final ChunkGeneratorExecution execution, @Nonnull final WorldGenPrefabSupplier supplier, final BlockMaskCondition configuration, final PrefabRotation rotation, final ICoordinateRndCondition heightCondition, final int environmentId, final boolean fitHeightmap, final boolean submerge) {
        final int cx = x - ChunkUtil.minBlock(execution.getX());
        final int cz = z - ChunkUtil.minBlock(execution.getZ());
        final long externalSeed = HashUtil.hash(x, z) * -1058827062L;
        final PrefabPasteUtil.PrefabPasteBuffer buffer = ChunkGenerator.getResource().prefabBuffer;
        buffer.setSeed(seed, externalSeed);
        buffer.execution = execution;
        buffer.blockMask = configuration;
        buffer.environmentId = environmentId;
        buffer.fitHeightmap = fitHeightmap;
        buffer.priority = (byte)(submerge ? 41 : 9);
        buffer.spawnCondition = heightCondition;
        if (execution.getChunkGenerator().getBenchmark().isEnabled() && ChunkUtil.isInsideChunkRelative(cx, cz)) {
            final ZoneBiomeResult zb = execution.zoneBiomeResult(cx, cz);
            final String zoneName = zb.zoneResult.getZone().name();
            final String biomeName = zb.biome.getName();
            execution.getChunkGenerator().getBenchmark().registerPrefab(zoneName + "\t" + biomeName + "\t" + supplier.getName());
        }
        PrefabPasteUtil.generate(buffer, rotation, supplier, x, height, z, cx, cz);
    }
    
    private static boolean isMatchingBiome(final Biome biome, @Nonnull final ZoneBiomeResult zoneAndBiomeResult) {
        return zoneAndBiomeResult.getBiome() == biome;
    }
    
    private static boolean isMatchingChunkBounds(final int x, final int z, @Nonnull final ChunkGeneratorExecution execution, @Nonnull final PrefabRotation rotation, @Nonnull final IChunkBounds bounds) {
        final int minX = x + bounds.getLowBoundX(rotation);
        final int minZ = z + bounds.getLowBoundZ(rotation);
        final int maxX = x + bounds.getHighBoundX(rotation);
        final int maxZ = z + bounds.getHighBoundZ(rotation);
        return isMatchingChunkBounds(execution, minX, minZ, maxX, maxZ);
    }
    
    private static boolean isMatchingChunkBounds(@Nonnull final ChunkGeneratorExecution execution, final int lowBoundX, final int lowBoundZ, final int highBoundX, final int highBoundZ) {
        return ChunkUtil.maxBlock(execution.getX()) >= lowBoundX && ChunkUtil.minBlock(execution.getX()) <= highBoundX && ChunkUtil.maxBlock(execution.getZ()) >= lowBoundZ && ChunkUtil.minBlock(execution.getZ()) <= highBoundZ;
    }
    
    private static boolean isMatchingHeight(final int seed, final int x, final int z, final int y, final Random random, @Nonnull final PrefabPatternGenerator prefabPatternGenerator) {
        return prefabPatternGenerator.getHeightCondition().eval(seed, x, z, y, random);
    }
    
    private static boolean isMatchingNoiseDensity(final int seed, final int x, final int z, @Nonnull final PrefabPatternGenerator prefabPatternGenerator) {
        return prefabPatternGenerator.getMapCondition().eval(seed, x, z);
    }
    
    private static boolean isMatchingParentBlock(final int seed, final int x, final int z, final int y, @Nonnull final Random random, @Nonnull final ZoneBiomeResult zoneAndBiomeResult, @Nonnull final PrefabContainer.PrefabContainerEntry containerEntry) {
        final IBlockFluidCondition parentCondition = containerEntry.getPrefabPatternGenerator().getParentCondition();
        if (parentCondition == ConstantIntCondition.DEFAULT_TRUE) {
            return true;
        }
        if (parentCondition == ConstantIntCondition.DEFAULT_FALSE) {
            return false;
        }
        final BlockFluidEntry groundCover = getCoverInGroundAt(seed, x, z, y, random, zoneAndBiomeResult.getBiome());
        if (!groundCover.equals(BlockFluidEntry.EMPTY) && !parentCondition.eval(groundCover.blockId(), groundCover.fluidId())) {
            return false;
        }
        final BlockFluidEntry topBlock = zoneAndBiomeResult.getBiome().getLayerContainer().getTopBlockAt(seed, x, z);
        return parentCondition.eval(topBlock.blockId(), topBlock.fluidId());
    }
    
    private static BlockFluidEntry getCoverInGroundAt(final int seed, final int x, final int z, final int y, @Nonnull final Random random, @Nonnull final Biome biome) {
        for (final CoverContainer.CoverContainerEntry coverContainerEntry : biome.getCoverContainer().getEntries()) {
            if (y < 320 && isMatchingCover(seed, x, z, y, random, coverContainerEntry)) {
                final CoverContainer.CoverContainerEntry.CoverContainerEntryPart coverEntry = coverContainerEntry.get(random);
                if (coverEntry != null && coverEntry.getOffset() == -1) {
                    return coverEntry.getEntry();
                }
            }
        }
        return BlockFluidEntry.EMPTY;
    }
    
    private static boolean isMatchingCover(final int seed, final int x, final int z, final int y, @Nonnull final Random random, @Nonnull final CoverContainer.CoverContainerEntry coverContainerEntry) {
        return random.nextDouble() < coverContainerEntry.getCoverDensity() && coverContainerEntry.getMapCondition().eval(seed, x, z) && coverContainerEntry.getHeightCondition().eval(seed, x, z, y, random);
    }
    
    static {
        EMPTY_UNIQUE_PREFABS = new UniquePrefabContainer.UniquePrefabEntry[0];
    }
    
    record Candidate(int x, int y, int z, int priority, PrefabRotation rotation, IPrefabBuffer buffer, WorldGenPrefabSupplier supplier, PrefabContainer.PrefabContainerEntry entry, PrefabPatternGenerator generator) {}
}
