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

package com.hypixel.hytale.server.spawning.world.system;

import java.time.Duration;
import javax.annotation.Nullable;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import java.util.Iterator;
import java.util.Set;
import java.util.Collection;
import com.hypixel.hytale.math.random.RandomExtra;
import java.util.concurrent.ThreadLocalRandom;
import com.hypixel.hytale.server.flock.config.FlockAsset;
import com.hypixel.hytale.server.spawning.world.WorldNPCSpawnStat;
import com.hypixel.hytale.server.spawning.wrappers.SpawnWrapper;
import com.hypixel.hytale.server.npc.NPCPlugin;
import com.hypixel.hytale.server.spawning.world.WorldEnvironmentSpawnData;
import com.hypixel.hytale.server.spawning.world.ChunkEnvironmentSpawnData;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.server.core.asset.type.gameplay.GameplayConfig;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.server.spawning.SpawningPlugin;
import com.hypixel.hytale.server.core.asset.type.environment.config.Environment;
import java.util.logging.Level;
import com.hypixel.hytale.server.core.modules.time.WorldTimeResource;
import com.hypixel.hytale.component.Store;
import javax.annotation.Nonnull;
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
import com.hypixel.hytale.server.spawning.world.component.SpawnJobData;
import com.hypixel.hytale.server.spawning.world.component.ChunkSpawnedNPCData;
import com.hypixel.hytale.server.spawning.world.component.ChunkSpawnData;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.server.spawning.world.component.WorldSpawnData;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.ResourceType;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.component.system.tick.TickingSystem;

public class WorldSpawningSystem extends TickingSystem<ChunkStore>
{
    private static final HytaleLogger LOGGER;
    private static final long SPAWN_COOLDOWN_NANOS;
    private final ResourceType<EntityStore, WorldSpawnData> worldSpawnDataResourceType;
    private final ComponentType<ChunkStore, ChunkSpawnData> chunkSpawnDataComponentType;
    private final ComponentType<ChunkStore, ChunkSpawnedNPCData> chunkSpawnedNPCDataComponentType;
    private final ComponentType<ChunkStore, SpawnJobData> spawnJobDataComponentType;
    private final ComponentType<ChunkStore, WorldChunk> worldChunkComponentType;
    static final /* synthetic */ boolean $assertionsDisabled;
    
    public WorldSpawningSystem(@Nonnull final ResourceType<EntityStore, WorldSpawnData> worldSpawnDataResourceType, @Nonnull final ComponentType<ChunkStore, ChunkSpawnData> chunkSpawnDataComponentType, @Nonnull final ComponentType<ChunkStore, ChunkSpawnedNPCData> chunkSpawnedNPCDataComponentType, @Nonnull final ComponentType<ChunkStore, SpawnJobData> spawnJobDataComponentType) {
        this.worldSpawnDataResourceType = worldSpawnDataResourceType;
        this.chunkSpawnDataComponentType = chunkSpawnDataComponentType;
        this.chunkSpawnedNPCDataComponentType = chunkSpawnedNPCDataComponentType;
        this.spawnJobDataComponentType = spawnJobDataComponentType;
        this.worldChunkComponentType = WorldChunk.getComponentType();
    }
    
    @Override
    public void tick(final float dt, final int systemIndex, @Nonnull final Store<ChunkStore> store) {
        final World world = store.getExternalData().getWorld();
        if (!world.getWorldConfig().isSpawningNPC() || world.getPlayerCount() == 0) {
            return;
        }
        final GameplayConfig gameplayConfig = world.getGameplayConfig();
        final Store<EntityStore> entityStore = world.getEntityStore().getStore();
        final WorldSpawnData worldSpawnDataResource = entityStore.getResource(this.worldSpawnDataResourceType);
        if (worldSpawnDataResource.isUnspawnable() || world.getChunkStore().getStore().getEntityCount() == 0 || (gameplayConfig.getMaxEnvironmentalNPCSpawns() > 0 && worldSpawnDataResource.getActualNPCs() >= gameplayConfig.getMaxEnvironmentalNPCSpawns()) || worldSpawnDataResource.getActualNPCs() > worldSpawnDataResource.getExpectedNPCs()) {
            return;
        }
        final WorldTimeResource worldTimeResource = entityStore.getResource(WorldTimeResource.getResourceType());
        if (worldSpawnDataResource.hasUnprocessedUnspawnableChunks()) {
            while (worldSpawnDataResource.hasUnprocessedUnspawnableChunks()) {
                final WorldSpawnData.UnspawnableEntry entry = worldSpawnDataResource.nextUnspawnableChunk();
                final Ref<ChunkStore> chunkRef = world.getChunkStore().getChunkReference(entry.getChunkIndex());
                if (chunkRef == null) {
                    continue;
                }
                final int environmentIndex = entry.getEnvironmentIndex();
                final ChunkSpawnData chunkSpawnDataComponent = store.getComponent(chunkRef, this.chunkSpawnDataComponentType);
                assert chunkSpawnDataComponent != null;
                final ChunkEnvironmentSpawnData environmentSpawnData = chunkSpawnDataComponent.getEnvironmentSpawnData(environmentIndex);
                final int segmentCount = -environmentSpawnData.getSegmentCount();
                worldSpawnDataResource.adjustSegmentCount(segmentCount);
                final WorldEnvironmentSpawnData worldEnvironmentSpawnData = worldSpawnDataResource.getWorldEnvironmentSpawnData(environmentIndex);
                final double expectedNPCs = worldEnvironmentSpawnData.getExpectedNPCs();
                worldEnvironmentSpawnData.adjustSegmentCount(segmentCount);
                worldEnvironmentSpawnData.updateExpectedNPCs(worldTimeResource.getMoonPhase());
                environmentSpawnData.markProcessedAsUnspawnable();
                final HytaleLogger.Api context = WorldSpawningSystem.LOGGER.at(Level.FINEST);
                if (!context.isEnabled()) {
                    continue;
                }
                final Environment environmentAsset = Environment.getAssetMap().getAsset(environmentIndex);
                if (environmentAsset == null) {
                    continue;
                }
                final String environment = environmentAsset.getId();
                context.log("Reducing expected NPC count for %s due to un-spawnable chunk. Was %s, now %s", environment, expectedNPCs, worldEnvironmentSpawnData.getExpectedNPCs());
            }
            worldSpawnDataResource.recalculateWorldCount();
        }
        for (int activeJobs = worldSpawnDataResource.getActiveSpawnJobs(), maxActiveJobs = SpawningPlugin.get().getMaxActiveJobs(); activeJobs < maxActiveJobs && worldSpawnDataResource.getActualNPCs() < MathUtil.floor(worldSpawnDataResource.getExpectedNPCs()) && this.createRandomSpawnJob(worldSpawnDataResource, store, entityStore); activeJobs = worldSpawnDataResource.getActiveSpawnJobs()) {}
    }
    
    private boolean createRandomSpawnJob(@Nonnull final WorldSpawnData worldData, @Nonnull final Store<ChunkStore> chunkStore, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final int[] environmentDataKeySet = worldData.getWorldEnvironmentSpawnDataIndexes();
        WorldNPCSpawnStat npcSpawnStat;
        int environmentIndex;
        WorldEnvironmentSpawnData worldEnvironmentSpawnData;
        do {
            environmentIndex = getAndConsumeNextEnvironmentIndex(worldData, environmentDataKeySet);
            if (environmentIndex == Integer.MIN_VALUE) {
                return false;
            }
            worldEnvironmentSpawnData = worldData.getWorldEnvironmentSpawnData(environmentIndex);
            npcSpawnStat = worldEnvironmentSpawnData.pickRandomSpawnNPCStat(componentAccessor);
        } while (npcSpawnStat == null);
        final int availableSlots = npcSpawnStat.getAvailableSlots();
        if (availableSlots == 0) {
            return false;
        }
        final Ref<ChunkStore> chunkRef = this.pickRandomChunk(worldEnvironmentSpawnData, npcSpawnStat, worldData, chunkStore);
        if (chunkRef == null) {
            return false;
        }
        final Environment environment = Environment.getAssetMap().getAsset(environmentIndex);
        final HytaleLogger.Api context = WorldSpawningSystem.LOGGER.at(Level.FINER);
        if (context.isEnabled()) {
            final WorldChunk worldChunkComponent = chunkStore.getComponent(chunkRef, this.worldChunkComponentType);
            assert worldChunkComponent != null;
            final String roleName = NPCPlugin.get().getName(npcSpawnStat.getRoleIndex());
            context.log("Trying SpawnJob env=%s role=%s chunk=[%s/%s] env(exp/act)=%s/%s npc(exp/act)=%s/%s", environment.getId(), roleName, worldChunkComponent.getX(), worldChunkComponent.getZ(), (int)worldEnvironmentSpawnData.getExpectedNPCs(), worldEnvironmentSpawnData.getActualNPCs(), (int)npcSpawnStat.getExpected(), npcSpawnStat.getActual());
        }
        final SpawnJobData spawnJobDataComponent = chunkStore.addComponent(chunkRef, this.spawnJobDataComponentType);
        final FlockAsset flockDefinition = npcSpawnStat.getSpawnParams().getFlockDefinition();
        int flockSize = (flockDefinition != null) ? flockDefinition.pickFlockSize() : 1;
        final int roleIndex = npcSpawnStat.getRoleIndex();
        if (flockSize > availableSlots) {
            flockSize = availableSlots;
        }
        spawnJobDataComponent.init(roleIndex, environment, environmentIndex, npcSpawnStat.getSpawnWrapper(), flockDefinition, flockSize);
        if (worldEnvironmentSpawnData.isFullyPopulated()) {
            spawnJobDataComponent.setIgnoreFullyPopulated(true);
        }
        final ChunkSpawnData chunkSpawnDataComponent = chunkStore.getComponent(chunkRef, this.chunkSpawnDataComponentType);
        assert chunkSpawnDataComponent != null;
        chunkSpawnDataComponent.getEnvironmentSpawnData(environmentIndex).getRandomChunkColumnIterator().saveIteratorPosition();
        final World world = chunkStore.getExternalData().getWorld();
        final WorldSpawnData worldSpawnData = world.getEntityStore().getStore().getResource(WorldSpawnData.getResourceType());
        worldSpawnData.trackNPC(environmentIndex, roleIndex, flockSize, world, componentAccessor);
        final HytaleLogger.Api finestContext = WorldSpawningSystem.LOGGER.at(Level.FINEST);
        if (finestContext.isEnabled()) {
            final WorldChunk worldChunkComponent2 = chunkStore.getComponent(chunkRef, this.worldChunkComponentType);
            assert worldChunkComponent2 != null;
            finestContext.log("Start Spawnjob id=%s env=%s role=%s chunk=[%s/%s]", spawnJobDataComponent.getJobId(), environment.getId(), NPCPlugin.get().getName(roleIndex), worldChunkComponent2.getX(), worldChunkComponent2.getZ());
        }
        worldData.adjustActiveSpawnJobs(1, flockSize);
        return true;
    }
    
    private static int getAndConsumeNextEnvironmentIndex(@Nonnull final WorldSpawnData worldSpawnData, @Nonnull final int[] environmentKeySet) {
        double weightSum = 0.0;
        for (final int keyReference : environmentKeySet) {
            if (keyReference != Integer.MIN_VALUE) {
                weightSum += worldSpawnData.getWorldEnvironmentSpawnData(keyReference).spawnWeight();
            }
        }
        if (weightSum == 0.0) {
            return Integer.MIN_VALUE;
        }
        weightSum *= ThreadLocalRandom.current().nextDouble();
        for (int i = 0; i < environmentKeySet.length; ++i) {
            final int keyReference2 = environmentKeySet[i];
            if (keyReference2 != Integer.MIN_VALUE) {
                weightSum -= worldSpawnData.getWorldEnvironmentSpawnData(keyReference2).spawnWeight();
                if (weightSum <= 0.0) {
                    environmentKeySet[i] = Integer.MIN_VALUE;
                    return keyReference2;
                }
            }
        }
        return Integer.MIN_VALUE;
    }
    
    @Nullable
    private Ref<ChunkStore> pickRandomChunk(@Nonnull final WorldEnvironmentSpawnData spawnData, @Nonnull final WorldNPCSpawnStat stat, @Nonnull final WorldSpawnData worldSpawnData, @Nonnull final Store<ChunkStore> store) {
        final int roleIndex = stat.getRoleIndex();
        final boolean wasFullyPopulated = spawnData.isFullyPopulated();
        final Set<Ref<ChunkStore>> chunkRefSet = spawnData.getChunkRefSet();
        final int environmentIndex = spawnData.getEnvironmentIndex();
        double weight = 0.0;
        boolean spawnable = false;
        boolean fullyPopulated = true;
        Ref<ChunkStore> chunkRef = null;
        if (wasFullyPopulated) {
            final Iterator<Ref<ChunkStore>> iterator = chunkRefSet.iterator();
            while (iterator.hasNext()) {
                chunkRef = iterator.next();
                final ChunkSpawnData chunkSpawnDataComponent = store.getComponent(chunkRef, this.chunkSpawnDataComponentType);
                assert chunkSpawnDataComponent != null;
                final ChunkSpawnedNPCData chunkSpawnedNPCDataComponent = store.getComponent(chunkRef, this.chunkSpawnedNPCDataComponentType);
                assert chunkSpawnedNPCDataComponent != null;
                final ChunkEnvironmentSpawnData chunkEnvironmentSpawnData = chunkSpawnDataComponent.getEnvironmentSpawnData(environmentIndex);
                fullyPopulated = (fullyPopulated && chunkEnvironmentSpawnData.isFullyPopulated(chunkSpawnedNPCDataComponent.getEnvironmentSpawnCount(environmentIndex)));
                if (!chunkEnvironmentSpawnData.isRoleSpawnable(roleIndex)) {
                    continue;
                }
                spawnable = true;
                weight += ((store.getComponent(chunkRef, this.spawnJobDataComponentType) == null && !getAndUpdateSpawnCooldown(chunkSpawnDataComponent)) ? 1.0 : 0.0);
            }
        }
        else {
            final Iterator<Ref<ChunkStore>> iterator2 = chunkRefSet.iterator();
            while (iterator2.hasNext()) {
                chunkRef = iterator2.next();
                final ChunkSpawnData chunkSpawnDataComponent = store.getComponent(chunkRef, this.chunkSpawnDataComponentType);
                assert chunkSpawnDataComponent != null;
                final ChunkSpawnedNPCData chunkSpawnedNPCDataComponent = store.getComponent(chunkRef, this.chunkSpawnedNPCDataComponentType);
                assert chunkSpawnedNPCDataComponent != null;
                final ChunkEnvironmentSpawnData chunkEnvironmentSpawnData = chunkSpawnDataComponent.getEnvironmentSpawnData(environmentIndex);
                final double spawnCount = chunkSpawnedNPCDataComponent.getEnvironmentSpawnCount(environmentIndex);
                fullyPopulated = (fullyPopulated && chunkEnvironmentSpawnData.isFullyPopulated(spawnCount));
                if (!chunkEnvironmentSpawnData.isRoleSpawnable(roleIndex)) {
                    continue;
                }
                spawnable = true;
                weight += ((store.getComponent(chunkRef, this.spawnJobDataComponentType) == null && !getAndUpdateSpawnCooldown(chunkSpawnDataComponent)) ? chunkEnvironmentSpawnData.getWeight(spawnCount) : 0.0);
            }
        }
        spawnData.setFullyPopulated(fullyPopulated);
        if (!spawnable) {
            stat.setUnspawnable(true);
            boolean unspawnable = true;
            for (final WorldNPCSpawnStat npcStat : spawnData.getNpcStatMap().values()) {
                if (!npcStat.isUnspawnable()) {
                    unspawnable = false;
                    break;
                }
            }
            spawnData.setUnspawnable(unspawnable);
            worldSpawnData.updateSpawnability();
            return null;
        }
        return RandomExtra.randomWeightedElement(chunkRefSet, (chunkRef, index) -> {
            final ChunkSpawnData chunkSpawnDataComponent2 = store.getComponent(chunkRef, this.chunkSpawnDataComponentType);
            if (!WorldSpawningSystem.$assertionsDisabled && chunkSpawnDataComponent2 == null) {
                throw new AssertionError();
            }
            else {
                final ChunkEnvironmentSpawnData chunkEnvironmentSpawnData2 = chunkSpawnDataComponent2.getEnvironmentSpawnData(environmentIndex);
                return chunkEnvironmentSpawnData2.isRoleSpawnable(index);
            }
        }, wasFullyPopulated ? ((chunkRef, index) -> {
            final ChunkSpawnData spawnChunkDataComponent = store.getComponent(chunkRef, this.chunkSpawnDataComponentType);
            if (!WorldSpawningSystem.$assertionsDisabled && spawnChunkDataComponent == null) {
                throw new AssertionError();
            }
            else {
                return (store.getComponent(chunkRef, this.spawnJobDataComponentType) == null && !spawnChunkDataComponent.isOnSpawnCooldown()) ? 1.0 : 0.0;
            }
        }) : ((chunkRef, index) -> {
            final ChunkSpawnData chunkSpawnDataComponent3 = store.getComponent(chunkRef, this.chunkSpawnDataComponentType);
            if (!WorldSpawningSystem.$assertionsDisabled && chunkSpawnDataComponent3 == null) {
                throw new AssertionError();
            }
            else {
                final ChunkSpawnedNPCData chunkSpawnedNPCDataComponent2 = store.getComponent(chunkRef, this.chunkSpawnedNPCDataComponentType);
                if (!WorldSpawningSystem.$assertionsDisabled && chunkSpawnedNPCDataComponent2 == null) {
                    throw new AssertionError();
                }
                else {
                    final ChunkEnvironmentSpawnData chunkEnvironmentSpawnData3 = chunkSpawnDataComponent3.getEnvironmentSpawnData(environmentIndex);
                    if (store.getComponent(chunkRef, this.spawnJobDataComponentType) == null && !chunkSpawnDataComponent3.isOnSpawnCooldown()) {
                        return chunkEnvironmentSpawnData3.getWeight(chunkSpawnedNPCDataComponent2.getEnvironmentSpawnCount(environmentIndex));
                    }
                    else {
                        return 0.0;
                    }
                }
            }
        }), weight, roleIndex);
    }
    
    private static boolean getAndUpdateSpawnCooldown(@Nonnull final ChunkSpawnData chunkSpawnData) {
        boolean onCooldown = chunkSpawnData.isOnSpawnCooldown();
        if (onCooldown && java.lang.System.nanoTime() - chunkSpawnData.getLastSpawn() > WorldSpawningSystem.SPAWN_COOLDOWN_NANOS) {
            chunkSpawnData.setLastSpawn(0L);
            onCooldown = false;
        }
        return onCooldown;
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
        SPAWN_COOLDOWN_NANOS = Duration.ofSeconds(1L).toNanos();
    }
}
