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

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

import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.server.core.universe.world.worldgen.IWorldGenBenchmark;
import com.hypixel.hytale.metrics.MetricResults;
import com.hypixel.hytale.server.worldgen.zone.Zone;
import com.hypixel.hytale.math.vector.Vector2i;
import java.util.Collection;
import java.util.Arrays;
import java.util.BitSet;
import com.hypixel.hytale.server.worldgen.cave.CaveGenerator;
import com.hypixel.hytale.procedurallib.condition.IHeightThresholdInterpreter;
import it.unimi.dsi.fastutil.ints.IntList;
import com.hypixel.hytale.server.worldgen.container.FadeContainer;
import com.hypixel.hytale.server.worldgen.biome.Biome;
import com.hypixel.hytale.server.worldgen.zone.ZoneGeneratorResult;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.component.Holder;
import com.hypixel.hytale.server.core.universe.world.worldgen.GeneratedEntityChunk;
import com.hypixel.hytale.server.core.universe.world.worldgen.GeneratedBlockStateChunk;
import com.hypixel.hytale.server.core.universe.world.worldgen.GeneratedBlockChunk;
import com.hypixel.hytale.logger.sentry.SkipSentryException;
import java.util.logging.Level;
import com.hypixel.hytale.server.worldgen.util.LogUtil;
import java.util.function.LongPredicate;
import com.hypixel.hytale.server.worldgen.cave.Cave;
import com.hypixel.hytale.server.worldgen.cave.CaveType;
import com.hypixel.hytale.server.worldgen.cache.InterpolatedBiomeCountList;
import com.hypixel.hytale.server.worldgen.zone.ZonePatternGenerator;
import javax.annotation.Nullable;
import com.hypixel.hytale.server.worldgen.cache.CoreDataCacheEntry;
import java.util.Random;
import com.hypixel.hytale.math.vector.Vector3i;
import com.hypixel.hytale.server.worldgen.container.UniquePrefabContainer;
import java.util.concurrent.CompletableFuture;
import com.hypixel.hytale.server.worldgen.util.ArrayUtli;
import com.hypixel.hytale.math.vector.Vector3f;
import com.hypixel.hytale.math.vector.Vector3d;
import java.util.ArrayList;
import com.hypixel.hytale.math.vector.Transform;
import com.hypixel.hytale.server.core.universe.world.worldmap.WorldMapLoadException;
import java.util.concurrent.Executor;
import com.hypixel.hytale.server.worldgen.map.GeneratorChunkWorldMap;
import com.hypixel.hytale.server.core.universe.world.worldmap.IWorldMap;
import com.hypixel.hytale.server.core.universe.world.World;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.BlockingQueue;
import com.hypixel.hytale.server.worldgen.util.ChunkThreadPoolExecutor;
import com.hypixel.hytale.server.worldgen.util.ChunkWorkerThreadFactory;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.nio.file.Path;
import com.hypixel.hytale.server.core.universe.world.worldgen.GeneratedChunk;
import java.util.function.Supplier;
import com.hypixel.hytale.server.worldgen.benchmark.ChunkWorldgenBenchmark;
import com.hypixel.hytale.server.worldgen.cache.UniquePrefabCache;
import com.hypixel.hytale.server.worldgen.prefab.PrefabLoadingCache;
import com.hypixel.hytale.server.worldgen.cache.CaveGeneratorCache;
import com.hypixel.hytale.server.worldgen.cache.ChunkGeneratorCache;
import com.hypixel.hytale.server.worldgen.zone.ZonePatternGeneratorCache;
import com.hypixel.hytale.server.worldgen.zone.ZonePatternProvider;
import com.hypixel.hytale.server.core.universe.world.worldgen.WorldGenTimingsCollector;
import javax.annotation.Nonnull;
import java.util.concurrent.ThreadPoolExecutor;
import com.hypixel.hytale.server.worldgen.ChunkGeneratorResource;
import com.hypixel.hytale.server.core.universe.world.worldmap.provider.IWorldMapProvider;
import com.hypixel.hytale.metrics.MetricProvider;
import com.hypixel.hytale.server.core.universe.world.worldgen.ValidatableWorldGen;
import com.hypixel.hytale.server.core.universe.world.worldgen.IBenchmarkableWorldGen;

public class ChunkGenerator implements IBenchmarkableWorldGen, ValidatableWorldGen, MetricProvider, IWorldMapProvider
{
    public static final int TINT_INTERPOLATION_RADIUS = 4;
    private static final ThreadLocal<ChunkGeneratorResource> THREAD_LOCAL;
    public static final int POOL_SIZE;
    @Nonnull
    private final ThreadPoolExecutor executor;
    @Nonnull
    private final WorldGenTimingsCollector timings;
    private final ZonePatternProvider zonePatternProvider;
    private final ZonePatternGeneratorCache zonePatternGeneratorCache;
    @Nonnull
    private final ChunkGeneratorCache generatorCache;
    @Nonnull
    private final CaveGeneratorCache caveGeneratorCache;
    @Nonnull
    private final PrefabLoadingCache prefabLoadingCache;
    @Nonnull
    private final UniquePrefabCache uniquePrefabCache;
    @Nonnull
    private final ChunkWorldgenBenchmark benchmark;
    @Nonnull
    private final Supplier<GeneratedChunk> generatedChunkSupplier;
    private final Path dataFolder;
    
    public ChunkGenerator(final ZonePatternProvider zonePatternProvider, final Path dataFolder) {
        this.dataFolder = dataFolder;
        (this.executor = new ChunkThreadPoolExecutor(ChunkGenerator.POOL_SIZE, ChunkGenerator.POOL_SIZE, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new ChunkWorkerThreadFactory(this, "ChunkGenerator-%d-Worker-%d"), this::onExecutorShutdown)).allowCoreThreadTimeOut(true);
        this.timings = new WorldGenTimingsCollector(this.executor);
        this.zonePatternProvider = zonePatternProvider;
        this.zonePatternGeneratorCache = new ZonePatternGeneratorCache(zonePatternProvider);
        this.generatorCache = new ChunkGeneratorCache(this::generateZoneBiomeResultAt, this::generateInterpolatedBiomeCountAt, this::generateHeight, this::generateInterpolatedHeightNoise, 50000, 20L);
        this.caveGeneratorCache = new CaveGeneratorCache(this::generateCave, 5000, 30L);
        this.uniquePrefabCache = new UniquePrefabCache(this::generateUniquePrefabs, 50, 300L);
        this.prefabLoadingCache = new PrefabLoadingCache();
        this.generatedChunkSupplier = GeneratedChunk::new;
        this.benchmark = new ChunkWorldgenBenchmark();
    }
    
    public ZonePatternProvider getZonePatternProvider() {
        return this.zonePatternProvider;
    }
    
    @Override
    public WorldGenTimingsCollector getTimings() {
        return this.timings;
    }
    
    @Nonnull
    @Override
    public IWorldMap getGenerator(final World world) throws WorldMapLoadException {
        return new GeneratorChunkWorldMap(this, this.executor);
    }
    
    @Override
    public Transform[] getSpawnPoints(final int seed) {
        return CompletableFuture.supplyAsync(() -> {
            final ArrayList list = new ArrayList<Transform>();
            for (final UniquePrefabContainer.UniquePrefabEntry entry : this.getUniquePrefabs(seed)) {
                if (entry.isSpawnLocation()) {
                    final Vector3i position = entry.getPosition();
                    final Vector3d spawnPosition = new Vector3d(entry.getSpawnOffset());
                    final Vector3f spawnRotation = new Vector3f(Vector3f.ZERO);
                    entry.getRotation().rotate(spawnPosition);
                    spawnRotation.addYaw(-entry.getRotation().getYaw());
                    list.add(new Transform(spawnPosition.add(position).add(0.5, 0.0, 0.5), spawnRotation));
                }
            }
            if (list.isEmpty()) {
                list.add(new Transform(16.5, -1.0, 16.5));
            }
            final Transform[] array = (Transform[])list.toArray(Transform[]::new);
            final Random random = getResource().random;
            random.setSeed(seed * 1494360372L);
            ArrayUtli.shuffleArray(array, random);
            return array;
        }, this.executor).join();
    }
    
    @Nonnull
    @Override
    public ChunkWorldgenBenchmark getBenchmark() {
        return this.benchmark;
    }
    
    public Path getDataFolder() {
        return this.dataFolder;
    }
    
    @Nullable
    public CoreDataCacheEntry getCoreData(final int seed, final int x, final int z) {
        return this.generatorCache.get(seed, x, z);
    }
    
    @Nonnull
    public ZonePatternGenerator getZonePatternGenerator(final int seed) {
        return this.zonePatternGeneratorCache.get(seed);
    }
    
    public ZoneBiomeResult getZoneBiomeResultAt(final int seed, final int x, final int z) {
        return this.generatorCache.getZoneBiomeResult(seed, x, z);
    }
    
    public int getHeight(final int seed, final int x, final int z) {
        return this.generatorCache.getHeight(seed, x, z);
    }
    
    public void putHeight(final int seed, final int x, final int z, final int y) {
        this.generatorCache.putHeight(seed, x, z, y);
    }
    
    @Nullable
    public InterpolatedBiomeCountList getInterpolatedBiomeCountAt(final int seed, final int x, final int z) {
        return this.generatorCache.getBiomeCountResult(seed, x, z);
    }
    
    @Nullable
    public Cave getCave(@Nonnull final CaveType caveType, final int seed, final int x, final int z) {
        return this.caveGeneratorCache.get(caveType, seed, x, z);
    }
    
    @Nonnull
    public PrefabLoadingCache getPrefabLoadingCache() {
        return this.prefabLoadingCache;
    }
    
    @Nullable
    public UniquePrefabContainer.UniquePrefabEntry[] getUniquePrefabs(final int seed) {
        return this.uniquePrefabCache.get(seed);
    }
    
    @Nonnull
    @Override
    public CompletableFuture<GeneratedChunk> generate(final int seed, final long index, final int x, final int z, @Nullable final LongPredicate stillNeeded) {
        return CompletableFuture.supplyAsync(() -> {
            if (stillNeeded == null || stillNeeded.test(index)) {
                final long start = -System.nanoTime();
                final GeneratedChunk generatedChunk = this.generatedChunkSupplier.get();
                final GeneratedBlockChunk blockChunk = generatedChunk.getBlockChunk();
                blockChunk.setCoordinates(index, x, z);
                final GeneratedBlockStateChunk blockStateChunk = generatedChunk.getBlockStateChunk();
                final GeneratedEntityChunk entityChunk = generatedChunk.getEntityChunk();
                final Holder<ChunkStore>[] sections = generatedChunk.getSections();
                new ChunkGeneratorExecution(seed, this, blockChunk, blockStateChunk, entityChunk, sections).execute(seed);
                final long end = System.nanoTime();
                final double time = (end + start) / 1.0E9;
                final double avg = this.timings.reportChunk(end + start);
                if (avg != this.timings.getWarmupValue()) {
                    LogUtil.getLogger().at(Level.FINE).log("Time taken: %s (avg: %s) (%s)", time, avg, this.timings);
                }
                else {
                    LogUtil.getLogger().at(Level.FINE).log("Time taken: %s (warming up)", time);
                }
                return generatedChunk;
            }
            else {
                return null;
            }
        }, this.executor).exceptionally(t -> {
            throw new SkipSentryException(t);
        });
    }
    
    @Override
    public void shutdown() {
        this.executor.shutdown();
    }
    
    @Nonnull
    public ZoneBiomeResult generateZoneBiomeResultAt(final int seed, final int x, final int z) {
        return this.generateZoneBiomeResultAt(seed, x, z, new ZoneBiomeResult());
    }
    
    @Nonnull
    public ZoneBiomeResult generateZoneBiomeResultAt(final int seed, final int x, final int z, @Nonnull final ZoneBiomeResult result) {
        final long time = -System.nanoTime();
        final ZonePatternGenerator zonePatternGenerator = this.getZonePatternGenerator(seed);
        final ZoneGeneratorResult tempZoneResult = result.getZoneResult();
        final ZoneGeneratorResult zoneResult = zonePatternGenerator.generate(seed, x, z, (tempZoneResult != null) ? tempZoneResult : new ZoneGeneratorResult());
        final Biome biome = zoneResult.getZone().biomePatternGenerator().generateBiomeAt(zoneResult, seed, x, z);
        final double heightThresholdContext = biome.getHeightmapInterpreter().getContext(seed, x, z);
        double heightmapNoise = biome.getHeightmapNoise().get(seed, x, z);
        final FadeContainer fadeContainer = biome.getFadeContainer();
        if (fadeContainer.shouldFade()) {
            final double factor = fadeContainer.getTerrainFactor(zoneResult);
            heightmapNoise = heightmapNoise * (1.0 - factor) + fadeContainer.getFadeHeightmap() * factor;
        }
        result.setZoneResult(zoneResult);
        result.setBiome(biome);
        result.setHeightThresholdContext(heightThresholdContext);
        result.setHeightmapNoise(heightmapNoise);
        this.timings.reportZoneBiomeResult(time + System.nanoTime());
        return result;
    }
    
    public void generateInterpolatedBiomeCountAt(final int seed, final int x, final int z, @Nonnull final InterpolatedBiomeCountList biomeCountList) {
        final ZoneBiomeResult center = this.getZoneBiomeResultAt(seed, x, z);
        biomeCountList.setCenter(center);
        final int radius = center.getBiome().getInterpolation().getRadius();
        final int radius2 = radius * radius;
        for (int ix = -radius; ix <= radius; ++ix) {
            for (int iz = -radius; iz <= radius; ++iz) {
                if (ix != 0 || iz != 0) {
                    final int distance2 = ix * ix + iz * iz;
                    if (distance2 <= radius2) {
                        final ZoneBiomeResult biomeResult = this.getZoneBiomeResultAt(seed, x + ix, z + iz);
                        biomeCountList.add(biomeResult, distance2);
                    }
                }
            }
        }
        if (biomeCountList.getBiomeIds().size() == 1) {
            final InterpolatedBiomeCountList.BiomeCountResult result = biomeCountList.get(center.getBiome());
            result.heightNoise = center.heightmapNoise;
            result.count = 1;
        }
    }
    
    public int generateLowestThresholdDependent(@Nonnull final InterpolatedBiomeCountList biomeCounts) {
        int lowestNonOne = 320;
        final IntList biomes = biomeCounts.getBiomeIds();
        for (int i = 0, size = biomes.size(); i < size; ++i) {
            final int id = biomes.getInt(i);
            final int v;
            if ((v = biomeCounts.get(id).biome.getHeightmapInterpreter().getLowestNonOne()) < lowestNonOne) {
                lowestNonOne = v;
            }
        }
        return lowestNonOne;
    }
    
    public int generateHighestThresholdDependent(@Nonnull final InterpolatedBiomeCountList biomeCounts) {
        int highestNonZero = -1;
        final IntList biomes = biomeCounts.getBiomeIds();
        for (int i = 0, size = biomes.size(); i < size; ++i) {
            final int id = biomes.getInt(i);
            final int v = biomeCounts.get(id).biome.getHeightmapInterpreter().getHighestNonZero();
            if (v > highestNonZero) {
                highestNonZero = v;
            }
        }
        return highestNonZero;
    }
    
    public static float generateInterpolatedThreshold(final int seed, final int x, final int z, final int y, @Nonnull final InterpolatedBiomeCountList biomeCounts) {
        float threshold = 0.0f;
        int counter = 0;
        final IntList biomes = biomeCounts.getBiomeIds();
        for (int i = 0, size = biomes.size(); i < size; ++i) {
            final InterpolatedBiomeCountList.BiomeCountResult r = biomeCounts.get(biomes.getInt(i));
            threshold += r.biome.getHeightmapInterpreter().getThreshold(seed, x, z, y, r.heightThresholdContext) * r.count;
            counter += r.count;
        }
        return threshold / counter;
    }
    
    public double generateInterpolatedHeightNoise(@Nonnull final InterpolatedBiomeCountList biomeCounts) {
        double n = 0.0;
        int counter = 0;
        final IntList biomes = biomeCounts.getBiomeIds();
        for (int i = 0, size = biomes.size(); i < size; ++i) {
            final InterpolatedBiomeCountList.BiomeCountResult r = biomeCounts.get(biomes.getInt(i));
            n += r.heightNoise * r.count;
            counter += r.count;
        }
        n /= counter;
        return n;
    }
    
    public int generateHeight(final int seed, final int x, final int z) {
        final CoreDataCacheEntry entry = this.getCoreData(seed, x, z);
        this.generatorCache.ensureHeightNoise(seed, x, z, entry);
        final InterpolatedBiomeCountList biomeCounts = entry.biomeCountList;
        final double heightNoise = entry.heightNoise;
        for (int y = this.generateHighestThresholdDependent(biomeCounts); y > 0; --y) {
            final float threshold = generateInterpolatedThreshold(seed, x, z, y, biomeCounts);
            if (threshold > heightNoise || threshold == 1.0) {
                return y;
            }
        }
        return 0;
    }
    
    public int generateHeightBetween(final int seed, final int x, final int z, @Nonnull final IHeightThresholdInterpreter interpreter) {
        final CoreDataCacheEntry entry = this.getCoreData(seed, x, z);
        this.generatorCache.ensureHeightNoise(seed, x, z, entry);
        final InterpolatedBiomeCountList biomeCounts = entry.biomeCountList;
        final double heightNoise = entry.heightNoise;
        for (int y = this.generateHighestThresholdDependent(biomeCounts); y > 0; --y) {
            if (interpreter.isSpawnable(y)) {
                final float threshold = generateInterpolatedThreshold(seed, x, z, y, biomeCounts);
                if (threshold > heightNoise || threshold == 1.0) {
                    return y;
                }
            }
        }
        return 0;
    }
    
    @Nullable
    public Cave generateCave(@Nonnull final CaveType caveType, final int seed, final int x, final int z) {
        final ZoneBiomeResult zoneBiomeResult = this.getZoneBiomeResultAt(seed, x, z);
        final CaveGenerator caveGenerator = zoneBiomeResult.zoneResult.getZone().caveGenerator();
        if (caveGenerator == null) {
            return null;
        }
        final int height = this.getHeight(seed, x, z);
        return caveGenerator.generate(seed, this, caveType, x, height, z);
    }
    
    @Nonnull
    public UniquePrefabContainer.UniquePrefabEntry[] generateUniquePrefabs(final int seed) {
        final ZonePatternGenerator zonePatternGenerator = this.getZonePatternGenerator(seed);
        final ArrayList<UniquePrefabContainer.UniquePrefabEntry> entries = new ArrayList<UniquePrefabContainer.UniquePrefabEntry>();
        final BitSet visited = new BitSet(zonePatternGenerator.getZones().length);
        for (final Zone.Unique uniqueZone : zonePatternGenerator.getUniqueZones()) {
            final Vector2i position = uniqueZone.getPosition();
            final UniquePrefabContainer.UniquePrefabEntry[] zoneEntries = uniqueZone.zone().uniquePrefabContainer().generate(seed, position, this);
            entries.addAll(Arrays.asList(zoneEntries));
            visited.set(uniqueZone.zone().id());
        }
        for (final Zone zone : zonePatternGenerator.getZones()) {
            if (!visited.get(zone.id())) {
                final UniquePrefabContainer.UniquePrefabEntry[] zoneEntries2 = zone.uniquePrefabContainer().generate(seed, null, this);
                entries.addAll(Arrays.asList(zoneEntries2));
            }
        }
        return entries.toArray(UniquePrefabContainer.UniquePrefabEntry[]::new);
    }
    
    protected final void onExecutorShutdown() {
        this.prefabLoadingCache.clear();
    }
    
    public static ChunkGeneratorResource getResource() {
        return ChunkGenerator.THREAD_LOCAL.get();
    }
    
    @Override
    public boolean validate() {
        return !ValidationUtil.isInvalid(this.zonePatternProvider, this.executor);
    }
    
    @Nonnull
    @Override
    public MetricResults toMetricResults() {
        return WorldGenTimingsCollector.METRICS_REGISTRY.toMetricResults(this.timings);
    }
    
    @Nonnull
    public String toString(final boolean timings, final boolean zonePatternGenerator) {
        return "ChunkGenerator{timings=" + String.valueOf(timings ? this.timings : "-hidden-") + ", zonePatternProvider=" + String.valueOf(zonePatternGenerator ? this.zonePatternProvider : "-hidden-") + ", generatorCache=" + String.valueOf(this.generatorCache) + ", caveGeneratorCache=" + String.valueOf(this.caveGeneratorCache) + ", uniquePrefabCache=" + String.valueOf(this.uniquePrefabCache);
    }
    
    @Nonnull
    @Override
    public String toString() {
        return this.toString(true, true);
    }
    
    static {
        THREAD_LOCAL = ThreadLocal.withInitial((Supplier<? extends ChunkGeneratorResource>)ChunkGeneratorResource::new);
        POOL_SIZE = Math.max(2, MathUtil.fastCeil(Runtime.getRuntime().availableProcessors() * 0.75f));
    }
}
