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

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

import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
import com.hypixel.hytale.server.spawning.world.ChunkEnvironmentSpawnData;
import com.hypixel.hytale.component.RemoveReason;
import com.hypixel.hytale.server.spawning.world.WorldEnvironmentSpawnData;
import java.util.Iterator;
import java.util.Set;
import com.hypixel.hytale.math.iterator.SpiralIterator;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.npc.NPCPlugin;
import com.hypixel.hytale.server.core.asset.type.environment.config.Environment;
import com.hypixel.hytale.server.spawning.world.WorldNPCSpawnStat;
import java.util.logging.Level;
import com.hypixel.hytale.math.util.ChunkUtil;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.AddReason;
import com.hypixel.hytale.component.Ref;
import javax.annotation.Nonnull;
import com.hypixel.hytale.component.query.Query;
import com.hypixel.hytale.server.spawning.world.component.ChunkSpawnedNPCData;
import com.hypixel.hytale.server.spawning.world.component.ChunkSpawnData;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.server.spawning.world.component.WorldSpawnData;
import com.hypixel.hytale.component.ResourceType;
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
import javax.annotation.Nullable;
import com.hypixel.hytale.server.npc.entities.NPCEntity;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.system.RefSystem;

public class WorldSpawnTrackingSystem extends RefSystem<EntityStore>
{
    private static final HytaleLogger LOGGER;
    private static final int COUNT_SPREAD_RADIUS = 3;
    @Nullable
    private final ComponentType<EntityStore, NPCEntity> npcComponentType;
    private final ComponentType<EntityStore, TransformComponent> transformComponentType;
    private final ResourceType<EntityStore, WorldSpawnData> worldSpawnDataResourceType;
    private final ComponentType<ChunkStore, ChunkSpawnData> chunkSpawnDataComponentType;
    private final ComponentType<ChunkStore, ChunkSpawnedNPCData> chunkSpawnedNPCDataComponentType;
    @Nonnull
    private final Query<EntityStore> query;
    
    public WorldSpawnTrackingSystem(@Nonnull final ResourceType<EntityStore, WorldSpawnData> worldSpawnDataResourceType, @Nonnull final ComponentType<ChunkStore, ChunkSpawnData> chunkSpawnDataComponentType, @Nonnull final ComponentType<ChunkStore, ChunkSpawnedNPCData> chunkSpawnedNPCDataComponentType) {
        this.npcComponentType = NPCEntity.getComponentType();
        this.transformComponentType = TransformComponent.getComponentType();
        this.worldSpawnDataResourceType = worldSpawnDataResourceType;
        this.chunkSpawnDataComponentType = chunkSpawnDataComponentType;
        this.chunkSpawnedNPCDataComponentType = chunkSpawnedNPCDataComponentType;
        this.query = (Query<EntityStore>)Query.and(this.npcComponentType, this.transformComponentType);
    }
    
    @Nonnull
    @Override
    public Query<EntityStore> getQuery() {
        return this.query;
    }
    
    @Override
    public void onEntityAdded(@Nonnull final Ref<EntityStore> ref, @Nonnull final AddReason reason, @Nonnull final Store<EntityStore> store, @Nonnull final CommandBuffer<EntityStore> commandBuffer) {
        final NPCEntity npcComponent = store.getComponent(ref, this.npcComponentType);
        assert npcComponent != null;
        final boolean isTracked = npcComponent.updateSpawnTrackingState(true);
        if (isTracked) {
            return;
        }
        final World world = store.getExternalData().getWorld();
        final WorldSpawnData worldSpawnData = store.getResource(this.worldSpawnDataResourceType);
        switch (reason) {
            case SPAWN: {
                final int environmentIndex = npcComponent.getEnvironment();
                if (!trackNPC(environmentIndex, npcComponent.getSpawnRoleIndex(), worldSpawnData, world, commandBuffer)) {
                    return;
                }
                final ChunkStore chunkStore = world.getChunkStore();
                final Store<ChunkStore> chunkComponentStore = chunkStore.getStore();
                final Vector3d position = store.getComponent(ref, this.transformComponentType).getPosition();
                final int originX = ChunkUtil.chunkCoordinate(position.getX());
                final int originZ = ChunkUtil.chunkCoordinate(position.getZ());
                Ref<ChunkStore> chunkRef = chunkStore.getChunkReference(ChunkUtil.indexChunk(originX, originZ));
                double count = trackNewNPC(chunkRef, environmentIndex, 1.0, this.chunkSpawnDataComponentType, this.chunkSpawnedNPCDataComponentType, chunkComponentStore);
                if (count <= 0.0) {
                    return;
                }
                final SpiralIterator spiralIterator = worldSpawnData.getSpiralIterator();
                spiralIterator.init(originX, originZ, 3);
                if (!spiralIterator.hasNext()) {
                    return;
                }
                spiralIterator.next();
                int checkedCount = 0;
                int unloadedCount = 0;
                while (spiralIterator.hasNext() && count > 0.0) {
                    ++checkedCount;
                    final long chunkIndex = spiralIterator.next();
                    chunkRef = chunkStore.getChunkReference(chunkIndex);
                    if (chunkRef == null) {
                        ++unloadedCount;
                    }
                    else {
                        count = trackNewNPC(chunkRef, environmentIndex, count, this.chunkSpawnDataComponentType, this.chunkSpawnedNPCDataComponentType, chunkComponentStore);
                    }
                }
                if (count > 0.0) {
                    final HytaleLogger.Api context = WorldSpawnTrackingSystem.LOGGER.at(Level.FINE);
                    if (context.isEnabled()) {
                        context.log("Failed to spread %s of an NPC spawn to neighbouring chunks. Checked %s chunks, %s not in memory. Centered on chunk (%s, %s), spreading to other chunks with matching environment", count, checkedCount, unloadedCount, originX, originZ);
                    }
                    final Set<Ref<ChunkStore>> chunkOptions = worldSpawnData.getWorldEnvironmentSpawnData(environmentIndex).getChunkRefSet();
                    for (Iterator<Ref<ChunkStore>> iterator = chunkOptions.iterator(); iterator.hasNext() && count > 0.0; count = trackNewNPC(iterator.next(), environmentIndex, count, this.chunkSpawnDataComponentType, this.chunkSpawnedNPCDataComponentType, chunkComponentStore)) {}
                    if (count > 0.0 && context.isEnabled()) {
                        final WorldEnvironmentSpawnData worldEnvironmentSpawnData = worldSpawnData.getWorldEnvironmentSpawnData(environmentIndex);
                        final WorldNPCSpawnStat npcSpawnStat = worldEnvironmentSpawnData.getNpcStatMap().get(npcComponent.getRoleIndex());
                        context.log("Failed to spread %s of an NPC spawn across random chunks with matching environments (%s). NPC Type: %s. World environment exp: %s act: %s. Stat exp: %s act: %s", count, Environment.getAssetMap().getAsset(environmentIndex).getId(), NPCPlugin.get().getName(npcComponent.getRoleIndex()), worldEnvironmentSpawnData.getExpectedNPCs(), worldEnvironmentSpawnData.getActualNPCs(), npcSpawnStat.getExpected(), npcSpawnStat.getActual());
                    }
                }
                spiralIterator.reset();
                break;
            }
            case LOAD: {
                trackNPC(npcComponent.getEnvironment(), npcComponent.getSpawnRoleIndex(), worldSpawnData, world, commandBuffer);
                break;
            }
        }
    }
    
    @Override
    public void onEntityRemove(@Nonnull final Ref<EntityStore> ref, @Nonnull final RemoveReason reason, @Nonnull final Store<EntityStore> store, @Nonnull final CommandBuffer<EntityStore> commandBuffer) {
        final NPCEntity npcComponent = store.getComponent(ref, this.npcComponentType);
        assert npcComponent != null;
        final boolean isTracked = npcComponent.updateSpawnTrackingState(false);
        if (!isTracked) {
            return;
        }
        final WorldSpawnData worldSpawnData = store.getResource(this.worldSpawnDataResourceType);
        switch (reason) {
            case REMOVE: {
                final int environmentIndex = npcComponent.getEnvironment();
                if (!untrackNPC(environmentIndex, npcComponent.getSpawnRoleIndex(), worldSpawnData)) {
                    return;
                }
                final World world = store.getExternalData().getWorld();
                final ChunkStore chunkStore = world.getChunkStore();
                final Store<ChunkStore> chunkComponentStore = chunkStore.getStore();
                final TransformComponent transformComponent = store.getComponent(ref, this.transformComponentType);
                assert transformComponent != null;
                final Vector3d position = transformComponent.getPosition();
                final int originX = ChunkUtil.chunkCoordinate(position.getX());
                final int originZ = ChunkUtil.chunkCoordinate(position.getZ());
                Ref<ChunkStore> chunkRef = chunkStore.getChunkReference(ChunkUtil.indexChunk(originX, originZ));
                double count = untrackRemovedNPC(chunkRef, environmentIndex, 1.0, this.chunkSpawnDataComponentType, this.chunkSpawnedNPCDataComponentType, chunkComponentStore);
                if (count <= 0.0) {
                    return;
                }
                final SpiralIterator spiralIterator = worldSpawnData.getSpiralIterator();
                spiralIterator.init(originX, originZ, 3);
                if (!spiralIterator.hasNext()) {
                    return;
                }
                spiralIterator.next();
                while (spiralIterator.hasNext() && count > 0.0) {
                    final long chunkIndex = spiralIterator.next();
                    chunkRef = chunkStore.getChunkReference(chunkIndex);
                    if (chunkRef == null) {
                        continue;
                    }
                    count = untrackRemovedNPC(chunkRef, environmentIndex, count, this.chunkSpawnDataComponentType, this.chunkSpawnedNPCDataComponentType, chunkComponentStore);
                }
                if (count > 0.0) {
                    final HytaleLogger.Api context = WorldSpawnTrackingSystem.LOGGER.at(Level.FINE);
                    if (context.isEnabled()) {
                        context.log("Failed to remove %s of a spread NPC spawn from neighbouring chunks, spreading to other chunks with matching environment", count);
                    }
                    final Set<Ref<ChunkStore>> chunkOptions = worldSpawnData.getWorldEnvironmentSpawnData(environmentIndex).getChunkRefSet();
                    for (Iterator<Ref<ChunkStore>> iterator = chunkOptions.iterator(); iterator.hasNext() && count > 0.0; count = untrackRemovedNPC(iterator.next(), environmentIndex, count, this.chunkSpawnDataComponentType, this.chunkSpawnedNPCDataComponentType, chunkComponentStore)) {}
                    if (count > 0.0 && context.isEnabled()) {
                        context.log("Failed to remove %s of an NPC spawn from random chunks with matching environments", count);
                    }
                }
                spiralIterator.reset();
                break;
            }
            case UNLOAD: {
                untrackNPC(npcComponent.getEnvironment(), npcComponent.getSpawnRoleIndex(), worldSpawnData);
                break;
            }
        }
    }
    
    private static boolean trackNPC(final int environmentIndex, final int roleIndex, @Nonnull final WorldSpawnData worldSpawnData, @Nonnull final World world, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        if (!worldSpawnData.trackNPC(environmentIndex, roleIndex, 1, world, componentAccessor)) {
            return false;
        }
        final HytaleLogger.Api context = WorldSpawnTrackingSystem.LOGGER.at(Level.FINER);
        if (context.isEnabled()) {
            context.log("Track Spawn env=%s role=%s", getEnvironmentName(environmentIndex), NPCPlugin.get().getName(roleIndex));
        }
        return true;
    }
    
    private static boolean untrackNPC(final int environmentIndex, final int roleIndex, @Nonnull final WorldSpawnData worldSpawnData) {
        if (!worldSpawnData.untrackNPC(environmentIndex, roleIndex, 1)) {
            return false;
        }
        final HytaleLogger.Api context = WorldSpawnTrackingSystem.LOGGER.at(Level.FINER);
        if (context.isEnabled()) {
            context.log("Despawn env=%s role=%s", getEnvironmentName(environmentIndex), NPCPlugin.get().getName(roleIndex));
        }
        return true;
    }
    
    private static double trackNewNPC(@Nonnull final Ref<ChunkStore> ref, final int environmentIndex, double count, @Nonnull final ComponentType<ChunkStore, ChunkSpawnData> chunkSpawnDataComponentType, @Nonnull final ComponentType<ChunkStore, ChunkSpawnedNPCData> chunkSpawnedNPCDataComponentType, @Nonnull final Store<ChunkStore> store) {
        final ChunkSpawnData chunkSpawnData = store.getComponent(ref, chunkSpawnDataComponentType);
        if (chunkSpawnData == null) {
            return count;
        }
        final ChunkEnvironmentSpawnData spawnData = chunkSpawnData.getChunkEnvironmentSpawnDataMap().get(environmentIndex);
        if (spawnData == null) {
            return count;
        }
        final ChunkSpawnedNPCData chunkSpawnedNPCDataComponent = store.getComponent(ref, chunkSpawnedNPCDataComponentType);
        assert chunkSpawnedNPCDataComponent != null;
        final double spawnedNPCs = chunkSpawnedNPCDataComponent.getEnvironmentSpawnCount(environmentIndex);
        if (spawnData.isFullyPopulated(spawnedNPCs)) {
            return count;
        }
        final WorldChunk worldChunkComponent = store.getComponent(ref, WorldChunk.getComponentType());
        assert worldChunkComponent != null;
        worldChunkComponent.markNeedsSaving();
        final double expectedNPCs = spawnData.getExpectedNPCs();
        final double remainingSpace = expectedNPCs - spawnedNPCs;
        if (count > remainingSpace) {
            final HytaleLogger.Api context = WorldSpawnTrackingSystem.LOGGER.at(Level.FINEST);
            if (context.isEnabled()) {
                context.log("Spreading " + remainingSpace + " to chunk " + worldChunkComponent.getIndex() + " with total capacity " + expectedNPCs);
            }
            count -= remainingSpace;
            chunkSpawnedNPCDataComponent.setEnvironmentSpawnCount(environmentIndex, expectedNPCs);
            return count;
        }
        final HytaleLogger.Api context = WorldSpawnTrackingSystem.LOGGER.at(Level.FINEST);
        if (context.isEnabled()) {
            context.log("Spreading " + count + " to chunk " + worldChunkComponent.getIndex() + " with total capacity " + expectedNPCs);
        }
        chunkSpawnedNPCDataComponent.setEnvironmentSpawnCount(environmentIndex, spawnedNPCs + count);
        return 0.0;
    }
    
    private static double untrackRemovedNPC(@Nonnull final Ref<ChunkStore> ref, final int environmentIndex, double count, @Nonnull final ComponentType<ChunkStore, ChunkSpawnData> chunkSpawnDataComponentType, @Nonnull final ComponentType<ChunkStore, ChunkSpawnedNPCData> chunkSpawnedNPCDataComponentType, @Nonnull final Store<ChunkStore> store) {
        final ChunkSpawnData chunkSpawnData = store.getComponent(ref, chunkSpawnDataComponentType);
        if (chunkSpawnData == null) {
            return count;
        }
        final ChunkEnvironmentSpawnData spawnData = chunkSpawnData.getChunkEnvironmentSpawnDataMap().get(environmentIndex);
        if (spawnData == null) {
            return count;
        }
        final ChunkSpawnedNPCData chunkSpawnedNPCDataComponent = store.getComponent(ref, chunkSpawnedNPCDataComponentType);
        assert chunkSpawnedNPCDataComponent != null;
        final double spawnedNPCs = chunkSpawnedNPCDataComponent.getEnvironmentSpawnCount(environmentIndex);
        if (spawnedNPCs <= 0.0) {
            return count;
        }
        final WorldChunk worldChunkComponent = store.getComponent(ref, WorldChunk.getComponentType());
        assert worldChunkComponent != null;
        worldChunkComponent.markNeedsSaving();
        final double expectedNPCs = spawnData.getExpectedNPCs();
        if (spawnedNPCs < count) {
            final HytaleLogger.Api context = WorldSpawnTrackingSystem.LOGGER.at(Level.FINEST);
            if (context.isEnabled()) {
                context.log("Spreading removal of " + spawnedNPCs + " to chunk " + worldChunkComponent.getIndex() + " with total capacity " + expectedNPCs);
            }
            count -= spawnedNPCs;
            chunkSpawnedNPCDataComponent.setEnvironmentSpawnCount(environmentIndex, 0.0);
            return count;
        }
        final HytaleLogger.Api context = WorldSpawnTrackingSystem.LOGGER.at(Level.FINEST);
        if (context.isEnabled()) {
            context.log("Spreading removal of " + count + " to chunk " + worldChunkComponent.getIndex() + " with total capacity " + expectedNPCs);
        }
        chunkSpawnedNPCDataComponent.setEnvironmentSpawnCount(environmentIndex, spawnedNPCs - count);
        return 0.0;
    }
    
    private static String getEnvironmentName(final int id) {
        final Environment env = Environment.getAssetMap().getAsset(id);
        return (env != null) ? env.getId() : ("<" + id);
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
    }
}
