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

package com.hypixel.hytale.builtin.buildertools.prefabeditor;

import java.util.concurrent.CompletionStage;
import com.hypixel.hytale.server.core.prefab.selection.buffer.PrefabBufferUtil;
import java.nio.file.LinkOption;
import java.io.File;
import com.hypixel.hytale.builtin.buildertools.prefabeditor.enums.PrefabRowSplitMode;
import com.hypixel.hytale.server.core.prefab.selection.buffer.impl.PrefabBuffer;
import java.util.Random;
import com.hypixel.hytale.server.core.util.PrefabUtil;
import com.hypixel.hytale.math.util.FastRandom;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.Rotation;
import com.hypixel.hytale.builtin.buildertools.prefabeditor.enums.PrefabAlignment;
import com.hypixel.hytale.builtin.buildertools.prefabeditor.enums.PrefabStackingAxis;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import java.util.List;
import com.hypixel.hytale.server.core.prefab.selection.buffer.impl.IPrefabBuffer;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import com.hypixel.hytale.server.core.command.system.CommandSender;
import com.hypixel.hytale.server.core.modules.time.WorldTimeResource;
import com.hypixel.hytale.server.core.universe.world.worldgen.provider.VoidWorldGenProvider;
import com.hypixel.hytale.server.core.universe.world.worldgen.provider.IWorldGenProvider;
import com.hypixel.hytale.server.core.universe.world.worldgen.provider.FlatWorldGenProvider;
import com.hypixel.hytale.builtin.buildertools.prefabeditor.enums.WorldGenType;
import com.hypixel.hytale.server.core.asset.util.ColorParseUtil;
import com.hypixel.hytale.server.core.Constants;
import com.hypixel.hytale.server.core.asset.type.environment.config.WeatherForecast;
import com.hypixel.hytale.common.map.IWeightedMap;
import com.hypixel.hytale.server.core.asset.type.environment.config.Environment;
import com.hypixel.hytale.math.vector.Vector3i;
import java.util.Collection;
import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport;
import java.util.concurrent.Executor;
import com.hypixel.hytale.server.core.universe.world.spawn.ISpawnProvider;
import com.hypixel.hytale.math.vector.Transform;
import com.hypixel.hytale.server.core.universe.world.spawn.GlobalSpawnProvider;
import java.util.logging.Level;
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.attribute.FileAttribute;
import com.hypixel.hytale.protocol.GameMode;
import com.hypixel.hytale.server.core.universe.world.WorldConfig;
import javax.annotation.Nullable;
import java.util.function.Consumer;
import java.util.Iterator;
import java.util.concurrent.CompletableFuture;
import com.hypixel.hytale.server.core.universe.Universe;
import com.hypixel.hytale.component.Holder;
import com.hypixel.hytale.server.core.inventory.container.ItemContainer;
import com.hypixel.hytale.server.core.inventory.Inventory;
import com.hypixel.hytale.server.core.inventory.ItemStack;
import com.hypixel.hytale.protocol.Packet;
import com.hypixel.hytale.protocol.packets.inventory.SetActiveSlot;
import com.hypixel.hytale.protocol.MovementStates;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.protocol.SavedMovementStates;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.entity.movement.MovementStatesComponent;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.server.core.entity.UUIDComponent;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.event.EventRegistry;
import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
import com.hypixel.hytale.server.core.event.events.player.AddPlayerToWorldEvent;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.UUID;
import java.util.Map;
import com.hypixel.hytale.protocol.Color;
import com.hypixel.hytale.server.core.Message;
import javax.annotation.Nonnull;
import com.hypixel.hytale.logger.HytaleLogger;

public class PrefabEditSessionManager
{
    @Nonnull
    private static final HytaleLogger LOGGER;
    @Nonnull
    private static final Message MESSAGE_COMMANDS_PREFAB_EDIT_SESSION_MANAGER_EXISTING_EDIT_SESSION;
    @Nonnull
    private static final Message MESSAGE_COMMANDS_EDIT_PREFAB_SOMETHING_WENT_WRONG;
    public static final float NOON_TIME = 0.5f;
    public static final String DEFAULT_NEW_WORLD_ZERO_COORDINATE_BLOCK_NAME = "Rock_Stone";
    public static final String DEFAULT_ENVIRONMENT = "Zone1_Sunny";
    private static final String PREFAB_SELECTOR_TOOL_ID = "EditorTool_PrefabEditing_SelectPrefab";
    public static final String DEFAULT_CHUNK_ENVIRONMENT = "Env_Zone1_Plains";
    public static final String PREFAB_EDITING_WORLD_NAME_PREFIX = "prefabEditor-";
    @Nonnull
    public static final Color DEFAULT_TINT;
    private static final long PROGRESS_UPDATE_INTERVAL_NANOS = 100000000L;
    public static final String DEFAULT_GRASS_TINT_HEX = "#5B9E28";
    @Nonnull
    private final Map<UUID, PrefabEditSession> activeEditSessions;
    @Nonnull
    private final HashSet<Path> prefabsBeingEdited;
    @Nonnull
    private final Map<UUID, UUID> inProgressTeleportations;
    @Nonnull
    private final HashSet<UUID> inProgressLoading;
    @Nonnull
    private final HashSet<UUID> cancelledLoading;
    static final /* synthetic */ boolean $assertionsDisabled;
    
    public PrefabEditSessionManager(@Nonnull final JavaPlugin plugin) {
        this.activeEditSessions = new Object2ObjectOpenHashMap<UUID, PrefabEditSession>();
        this.prefabsBeingEdited = new HashSet<Path>();
        this.inProgressTeleportations = new Object2ObjectOpenHashMap<UUID, UUID>();
        this.inProgressLoading = new HashSet<UUID>();
        this.cancelledLoading = new HashSet<UUID>();
        final EventRegistry eventRegistry = plugin.getEventRegistry();
        eventRegistry.registerGlobal(AddPlayerToWorldEvent.class, this::onPlayerAddedToWorld);
        eventRegistry.registerGlobal(PlayerReadyEvent.class, this::onPlayerReady);
    }
    
    private void onPlayerReady(@Nonnull final PlayerReadyEvent event) {
        final Ref<EntityStore> playerRef = event.getPlayer().getReference();
        assert playerRef != null && !playerRef.isValid();
        final Store<EntityStore> store = playerRef.getStore();
        final World world = store.getExternalData().getWorld();
        world.execute(() -> {
            final UUIDComponent uuidComponent = store.getComponent(playerRef, UUIDComponent.getComponentType());
            if (!PrefabEditSessionManager.$assertionsDisabled && uuidComponent == null) {
                throw new AssertionError();
            }
            else {
                final UUID playerUUID = uuidComponent.getUuid();
                if (!(!this.inProgressTeleportations.containsKey(playerUUID))) {
                    this.inProgressTeleportations.remove(playerUUID);
                    final MovementStatesComponent movementStatesComponent = store.getComponent(playerRef, MovementStatesComponent.getComponentType());
                    if (!PrefabEditSessionManager.$assertionsDisabled && movementStatesComponent == null) {
                        throw new AssertionError();
                    }
                    else {
                        final MovementStates movementStates = movementStatesComponent.getMovementStates();
                        final Player playerComponent = store.getComponent(playerRef, Player.getComponentType());
                        if (!PrefabEditSessionManager.$assertionsDisabled && playerComponent == null) {
                            throw new AssertionError();
                        }
                        else {
                            playerComponent.applyMovementStates(playerRef, new SavedMovementStates(true), movementStates, store);
                            final PlayerRef playerRefComponent = store.getComponent(playerRef, PlayerRef.getComponentType());
                            if (playerRefComponent != null) {
                                this.givePrefabSelectorTool(playerComponent, playerRefComponent);
                            }
                        }
                    }
                }
            }
        });
    }
    
    private void givePrefabSelectorTool(@Nonnull final Player playerComponent, @Nonnull final PlayerRef playerRef) {
        final Inventory inventory = playerComponent.getInventory();
        final ItemContainer hotbar = inventory.getHotbar();
        final int hotbarSize = hotbar.getCapacity();
        for (short slot = 0; slot < hotbarSize; ++slot) {
            final ItemStack itemStack = hotbar.getItemStack(slot);
            if (itemStack != null && !itemStack.isEmpty() && "EditorTool_PrefabEditing_SelectPrefab".equals(itemStack.getItemId())) {
                inventory.setActiveHotbarSlot((byte)slot);
                playerRef.getPacketHandler().writeNoCache(new SetActiveSlot(-1, (byte)slot));
                return;
            }
        }
        short emptySlot = -1;
        for (short slot2 = 0; slot2 < hotbarSize; ++slot2) {
            final ItemStack itemStack2 = hotbar.getItemStack(slot2);
            if (itemStack2 == null || itemStack2.isEmpty()) {
                emptySlot = slot2;
                break;
            }
        }
        if (emptySlot == -1) {
            emptySlot = 0;
        }
        hotbar.setItemStackForSlot(emptySlot, new ItemStack("EditorTool_PrefabEditing_SelectPrefab"));
        inventory.setActiveHotbarSlot((byte)emptySlot);
        playerRef.getPacketHandler().writeNoCache(new SetActiveSlot(-1, (byte)emptySlot));
    }
    
    public void onPlayerAddedToWorld(@Nonnull final AddPlayerToWorldEvent event) {
        final World world = event.getWorld();
        if (world.getName().startsWith("prefabEditor-")) {
            world.execute(() -> {
                final Holder<EntityStore> playerHolder = event.getHolder();
                final UUIDComponent uuidComponent = playerHolder.getComponent(UUIDComponent.getComponentType());
                if (!PrefabEditSessionManager.$assertionsDisabled && uuidComponent == null) {
                    throw new AssertionError();
                }
                else {
                    this.inProgressTeleportations.put(uuidComponent.getUuid(), world.getWorldConfig().getUuid());
                }
            });
        }
    }
    
    public void updatePathOfLoadedPrefab(@Nonnull final Path oldPath, @Nonnull final Path newPath) {
        this.prefabsBeingEdited.remove(oldPath);
        this.prefabsBeingEdited.add(newPath);
    }
    
    public boolean isEditingAPrefab(@Nonnull final UUID playerUUID) {
        return this.activeEditSessions.containsKey(playerUUID);
    }
    
    public PrefabEditSession getPrefabEditSession(@Nonnull final UUID playerUUID) {
        return this.activeEditSessions.get(playerUUID);
    }
    
    @Nonnull
    public Map<UUID, PrefabEditSession> getActiveEditSessions() {
        return this.activeEditSessions;
    }
    
    void populateActiveEditSession(@Nonnull final UUID playerUuid, @Nonnull final PrefabEditSession editSession) {
        this.activeEditSessions.put(playerUuid, editSession);
    }
    
    void populatePrefabsBeingEdited(@Nonnull final Path prefabPath) {
        this.prefabsBeingEdited.add(prefabPath);
    }
    
    void scheduleAnchorEntityRecreation(@Nonnull final PrefabEditSession editSession) {
        CompletableFuture.runAsync(() -> {
            final World world = Universe.get().getWorld(editSession.getWorldName());
            if (world != null) {
                world.execute(() -> {
                    for (final PrefabEditingMetadata metadata : editSession.getLoadedPrefabMetadata().values()) {
                        metadata.recreateAnchorEntity(world);
                    }
                });
            }
        });
    }
    
    public boolean hasInProgressLoading(@Nonnull final UUID playerUuid) {
        return this.inProgressLoading.contains(playerUuid);
    }
    
    public void cancelLoading(@Nonnull final UUID playerUuid) {
        this.cancelledLoading.add(playerUuid);
    }
    
    public boolean isLoadingCancelled(@Nonnull final UUID playerUuid) {
        return this.cancelledLoading.contains(playerUuid);
    }
    
    public void clearLoadingState(@Nonnull final UUID playerUuid) {
        this.inProgressLoading.remove(playerUuid);
        this.cancelledLoading.remove(playerUuid);
    }
    
    @Nonnull
    public CompletableFuture<Void> createEditSessionForNewPrefab(@Nonnull final Ref<EntityStore> ref, @Nonnull final Player editor, @Nonnull final PrefabEditorCreationSettings settings, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
        assert playerRefComponent != null;
        final PrefabEditorCreationContext prefabEditorCreationContext = settings.finishProcessing(editor, playerRefComponent, true);
        if (prefabEditorCreationContext == null) {
            playerRefComponent.sendMessage(PrefabEditSessionManager.MESSAGE_COMMANDS_EDIT_PREFAB_SOMETHING_WENT_WRONG);
            return CompletableFuture.completedFuture((Void)null);
        }
        return this.createEditSession(ref, prefabEditorCreationContext, true, componentAccessor);
    }
    
    @Nullable
    public CompletableFuture<Void> loadPrefabAndCreateEditSession(@Nonnull final Ref<EntityStore> ref, @Nonnull final Player editor, @Nonnull final PrefabEditorCreationSettings settings, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        return this.loadPrefabAndCreateEditSession(ref, editor, settings, componentAccessor, null);
    }
    
    @Nullable
    public CompletableFuture<Void> loadPrefabAndCreateEditSession(@Nonnull final Ref<EntityStore> ref, @Nonnull final Player editor, @Nonnull final PrefabEditorCreationSettings settings, @Nonnull final ComponentAccessor<EntityStore> componentAccessor, @Nullable final Consumer<PrefabLoadingState> progressCallback) {
        final PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
        assert playerRefComponent != null;
        final UUID playerUuid = playerRefComponent.getUuid();
        if (this.inProgressLoading.contains(playerUuid)) {
            final PrefabLoadingState loadingState = new PrefabLoadingState();
            loadingState.addError("server.commands.editprefab.error.loadingInProgress");
            this.notifyProgress(progressCallback, loadingState);
            playerRefComponent.sendMessage(Message.translation("server.commands.editprefab.error.loadingInProgress"));
            return null;
        }
        this.inProgressLoading.add(playerUuid);
        this.cancelledLoading.remove(playerUuid);
        final PrefabLoadingState loadingState = new PrefabLoadingState();
        loadingState.setPhase(PrefabLoadingState.Phase.INITIALIZING);
        this.notifyProgress(progressCallback, loadingState);
        final PrefabEditorCreationContext prefabEditorCreationContext = settings.finishProcessing(editor, playerRefComponent, false);
        if (prefabEditorCreationContext == null) {
            loadingState.addError("server.commands.editprefab.error.processingFailed");
            this.notifyProgress(progressCallback, loadingState);
            playerRefComponent.sendMessage(PrefabEditSessionManager.MESSAGE_COMMANDS_EDIT_PREFAB_SOMETHING_WENT_WRONG);
            return null;
        }
        if (prefabEditorCreationContext.getPrefabPaths().isEmpty()) {
            loadingState.addError("server.commands.editprefab.error.noPrefabsFound");
            this.notifyProgress(progressCallback, loadingState);
            playerRefComponent.sendMessage(PrefabEditSessionManager.MESSAGE_COMMANDS_EDIT_PREFAB_SOMETHING_WENT_WRONG);
            return null;
        }
        loadingState.setTotalPrefabs(prefabEditorCreationContext.getPrefabPaths().size());
        this.notifyProgress(progressCallback, loadingState);
        return this.createEditSession(ref, prefabEditorCreationContext, false, componentAccessor, loadingState, progressCallback);
    }
    
    private void notifyProgress(@Nullable final Consumer<PrefabLoadingState> progressCallback, @Nonnull final PrefabLoadingState loadingState) {
        if (progressCallback == null) {
            return;
        }
        final PrefabLoadingState.Phase phase = loadingState.getCurrentPhase();
        if (phase != PrefabLoadingState.Phase.LOADING_PREFABS && phase != PrefabLoadingState.Phase.PASTING_PREFABS) {
            progressCallback.accept(loadingState);
            return;
        }
        final long now = System.nanoTime();
        if (now - loadingState.getLastNotifyTimeNanos() >= 100000000L) {
            loadingState.setLastNotifyTimeNanos(now);
            progressCallback.accept(loadingState);
        }
    }
    
    @Nonnull
    private CompletableFuture<Void> createEditSession(@Nonnull final Ref<EntityStore> ref, @Nonnull final PrefabEditorCreationContext context, final boolean createNewPrefab, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        return this.createEditSession(ref, context, createNewPrefab, componentAccessor, null, null);
    }
    
    @Nonnull
    private CompletableFuture<Void> createEditSession(@Nonnull final Ref<EntityStore> ref, @Nonnull final PrefabEditorCreationContext context, final boolean createNewPrefab, @Nonnull final ComponentAccessor<EntityStore> componentAccessor, @Nullable final PrefabLoadingState loadingState, @Nullable final Consumer<PrefabLoadingState> progressCallback) {
        final World sourceWorld = componentAccessor.getExternalData().getWorld();
        final PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
        assert playerRefComponent != null;
        final UUID playerUUID = playerRefComponent.getUuid();
        if (this.activeEditSessions.containsKey(playerUUID)) {
            if (loadingState != null) {
                loadingState.addError("server.commands.editprefab.error.existingSession");
                this.notifyProgress(progressCallback, loadingState);
            }
            playerRefComponent.sendMessage(PrefabEditSessionManager.MESSAGE_COMMANDS_PREFAB_EDIT_SESSION_MANAGER_EXISTING_EDIT_SESSION);
            return CompletableFuture.completedFuture((Void)null);
        }
        for (final Path prefabPath : context.getPrefabPaths()) {
            if (this.prefabsBeingEdited.contains(prefabPath)) {
                if (loadingState != null) {
                    loadingState.addError("server.commands.editprefab.error.alreadyBeingEdited", prefabPath.toString());
                    this.notifyProgress(progressCallback, loadingState);
                }
                playerRefComponent.sendMessage(Message.translation("server.commands.prefabeditsessionmanager.alreadyBeingEdited").param("path", prefabPath.toString()));
                return CompletableFuture.completedFuture((Void)null);
            }
        }
        if (loadingState != null) {
            loadingState.setPhase(PrefabLoadingState.Phase.CREATING_WORLD);
            this.notifyProgress(progressCallback, loadingState);
        }
        final WorldConfig config = new WorldConfig();
        final boolean enableTicking = context.isWorldTickingEnabled();
        config.setBlockTicking(enableTicking);
        config.setSpawningNPC(false);
        config.setIsSpawnMarkersEnabled(false);
        config.setObjectiveMarkersEnabled(false);
        config.setGameMode(GameMode.Creative);
        config.setDeleteOnRemove(true);
        config.setUuid(UUID.randomUUID());
        config.setGameTimePaused(true);
        config.setIsAllNPCFrozen(true);
        config.setSavingPlayers(true);
        config.setCanSaveChunks(true);
        config.setTicking(enableTicking);
        config.setForcedWeather(this.getWeatherFromEnvironment(context.getEnvironment()));
        final String worldName = this.getWorldName(context);
        try {
            Files.createDirectories(this.getSavePath(context), (FileAttribute<?>[])new FileAttribute[0]);
        }
        catch (final IOException e) {
            if (loadingState != null) {
                loadingState.addError("server.commands.editprefab.error.createDirectoryFailed", e.getMessage());
                this.notifyProgress(progressCallback, loadingState);
            }
            playerRefComponent.sendMessage(Message.translation("server.commands.instances.createDirectory.failed").param("errormsg", e.getMessage()));
            return CompletableFuture.completedFuture((Void)null);
        }
        final TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType());
        assert transformComponent != null;
        final Transform transform = transformComponent.getTransform().clone();
        final PrefabEditSession prefabEditSession = new PrefabEditSession(worldName, playerUUID, sourceWorld.getWorldConfig().getUuid(), transform);
        CompletableFuture<World> future;
        if (createNewPrefab) {
            future = this.getPrefabCreatingCompletableFuture(context, prefabEditSession, config);
        }
        else {
            future = this.getPrefabLoadingCompletableFuture(context, prefabEditSession, config, loadingState, progressCallback, playerUUID);
        }
        if (future == null) {
            if (loadingState != null) {
                loadingState.addError("server.commands.editprefab.error.loadFailed");
                this.notifyProgress(progressCallback, loadingState);
            }
            return CompletableFuture.completedFuture((Void)null);
        }
        return future.exceptionally(throwable -> {
            if (this.isLoadingCancelled(playerUUID)) {
                return null;
            }
            else {
                PrefabEditSessionManager.LOGGER.at(Level.SEVERE).withCause(throwable).log("Error occurred during prefab editor session creation");
                if (loadingState != null) {
                    loadingState.addError("server.commands.editprefab.error.exception", throwable.getMessage());
                    this.notifyProgress(progressCallback, loadingState);
                }
                playerRefComponent.sendMessage(Message.translation("server.commands.editprefab.error.exception").param("details", (throwable.getMessage() != null) ? throwable.getMessage() : "Unknown error"));
                return null;
            }
        }).thenAcceptAsync(targetWorld -> {
            if (!this.isLoadingCancelled(playerUUID)) {
                if (targetWorld != null) {
                    if (loadingState != null) {
                        loadingState.setPhase(PrefabLoadingState.Phase.FINALIZING);
                        this.notifyProgress(progressCallback, loadingState);
                    }
                    final Vector3i spawnPoint = prefabEditSession.getSpawnPoint();
                    targetWorld.getWorldConfig();
                    new GlobalSpawnProvider(new Transform(spawnPoint));
                    final GlobalSpawnProvider spawnProvider;
                    final Object o;
                    ((WorldConfig)o).setSpawnProvider(spawnProvider);
                    CompletableFuture.runAsync(() -> targetWorld.getEntityStore().getStore().replaceResource(PrefabEditSession.getResourceType(), prefabEditSession), targetWorld);
                    CompletableFuture.runAsync(() -> {
                        final Teleport teleportComponent = Teleport.createForPlayer(targetWorld, new Transform(spawnPoint));
                        componentAccessor.putComponent(ref, Teleport.getComponentType(), teleportComponent);
                    }, sourceWorld);
                }
            }
        }).thenRun(() -> {
            if (!this.isLoadingCancelled(playerUUID)) {
                this.prefabsBeingEdited.addAll((Collection<?>)context.getPrefabPaths());
                this.activeEditSessions.put(playerUUID, prefabEditSession);
                if (loadingState != null) {
                    loadingState.markComplete();
                    this.notifyProgress(progressCallback, loadingState);
                }
                playerRefComponent.sendMessage(Message.translation("server.commands.prefabeditsessionmanager.success." + (createNewPrefab ? "new" : "load")));
            }
        }).whenComplete((result, throwable) -> this.inProgressLoading.remove(playerUUID));
    }
    
    @Nonnull
    private CompletableFuture<World> getWorldCreatingFuture(@Nonnull final PrefabEditorCreationContext context, @Nonnull final WorldConfig config) {
        return Universe.get().makeWorld(this.getWorldName(context), this.getSavePath(context), config, true);
    }
    
    @Nonnull
    private String getWorldName(@Nonnull final PrefabEditorCreationContext context) {
        return "prefabEditor-" + context.getEditorRef().getUsername();
    }
    
    @Nonnull
    private String getWeatherFromEnvironment(@Nullable final String environmentId) {
        if (environmentId == null || environmentId.isEmpty()) {
            return "Zone1_Sunny";
        }
        final Environment environment = Environment.getAssetMap().getAsset(environmentId);
        if (environment == null) {
            return "Zone1_Sunny";
        }
        final IWeightedMap<WeatherForecast> forecast = environment.getWeatherForecast(12);
        if (forecast == null || forecast.size() == 0) {
            return "Zone1_Sunny";
        }
        final String[] bestWeatherId = { null };
        final double[] highestWeight = { Double.NEGATIVE_INFINITY };
        forecast.forEachEntry((weatherForecast, weight) -> {
            if (weight > highestWeight[0]) {
                highestWeight[0] = weight;
                bestWeatherId[0] = weatherForecast.getWeatherId();
            }
            return;
        });
        return (bestWeatherId[0] != null) ? bestWeatherId[0] : "Zone1_Sunny";
    }
    
    @Nonnull
    private Path getSavePath(@Nonnull final PrefabEditorCreationContext context) {
        return Constants.UNIVERSE_PATH.resolve("worlds").resolve(this.getWorldName(context));
    }
    
    private void applyWorldGenWorldConfig(@Nonnull final PrefabEditorCreationContext context, final int yLevelToPastePrefabsAt, @Nonnull final WorldConfig worldConfig) {
        final String environment = (context.getEnvironment() != null) ? context.getEnvironment() : "Env_Zone1_Plains";
        Color tint = PrefabEditSessionManager.DEFAULT_TINT;
        if (context.getGrassTint() != null && !context.getGrassTint().isEmpty()) {
            final Color parsed = ColorParseUtil.parseColor(context.getGrassTint());
            if (parsed != null) {
                tint = parsed;
            }
        }
        if (context.getWorldGenType().equals(WorldGenType.FLAT)) {
            final int yLevelForFlatWorldExclusive = Math.max(1, yLevelToPastePrefabsAt - context.getBlocksAboveSurface());
            final FlatWorldGenProvider.Layer topLayer = new FlatWorldGenProvider.Layer();
            topLayer.blockType = "Soil_Grass";
            topLayer.to = yLevelForFlatWorldExclusive;
            topLayer.from = yLevelForFlatWorldExclusive - 1;
            topLayer.environment = environment;
            final FlatWorldGenProvider.Layer airLayer = new FlatWorldGenProvider.Layer();
            airLayer.blockType = "Empty";
            airLayer.to = 320;
            airLayer.from = yLevelForFlatWorldExclusive;
            airLayer.environment = environment;
            if (yLevelForFlatWorldExclusive - 2 >= 0) {
                final FlatWorldGenProvider.Layer bottomLayer = new FlatWorldGenProvider.Layer();
                bottomLayer.blockType = "Soil_Clay";
                bottomLayer.to = yLevelForFlatWorldExclusive - 1;
                bottomLayer.from = 0;
                bottomLayer.environment = environment;
                worldConfig.setWorldGenProvider(new FlatWorldGenProvider(tint, new FlatWorldGenProvider.Layer[] { airLayer, topLayer, bottomLayer }));
            }
            else {
                worldConfig.setWorldGenProvider(new FlatWorldGenProvider(tint, new FlatWorldGenProvider.Layer[] { airLayer, topLayer }));
            }
        }
        else {
            worldConfig.setWorldGenProvider(new VoidWorldGenProvider(tint, environment));
        }
    }
    
    @Nonnull
    private CompletableFuture<World> getPrefabCreatingCompletableFuture(@Nonnull final PrefabEditorCreationContext context, @Nonnull final PrefabEditSession editSession, @Nonnull final WorldConfig worldConfig) {
        this.applyWorldGenWorldConfig(context, context.getPasteLevelGoal() - 1, worldConfig);
        return this.getWorldCreatingFuture(context, worldConfig).thenCompose(world -> CompletableFuture.supplyAsync(() -> {
            final Vector3i pastePosition = new Vector3i(0, context.getPasteLevelGoal(), 0);
            final Vector3i anchorPosition = pastePosition.clone();
            editSession.addPrefab(context.getPrefabPaths().getFirst(), new Vector3i(-1, context.getPasteLevelGoal() - 1, -1), new Vector3i(1, context.getPasteLevelGoal() + 1, 1), anchorPosition, pastePosition);
            final Store<EntityStore> store = world.getEntityStore().getStore();
            final WorldTimeResource worldTimeResource = store.getResource(WorldTimeResource.getResourceType());
            worldTimeResource.setDayTime(0.5, world, store);
            world.setBlock(0, context.getPasteLevelGoal(), 0, "Rock_Stone");
            return world;
        }, world));
    }
    
    @Nullable
    private CompletableFuture<World> getPrefabLoadingCompletableFuture(@Nonnull final PrefabEditorCreationContext context, @Nonnull final PrefabEditSession editSession, @Nonnull final WorldConfig worldConfig, @Nullable final PrefabLoadingState loadingState, @Nullable final Consumer<PrefabLoadingState> progressCallback, @Nonnull final UUID playerUuid) {
        final CompletableFuture[] initializationFutures = new CompletableFuture[context.getPrefabPaths().size()];
        if (loadingState != null) {
            loadingState.setPhase(PrefabLoadingState.Phase.LOADING_PREFABS);
            this.notifyProgress(progressCallback, loadingState);
        }
        for (int i = 0; i < context.getPrefabPaths().size(); ++i) {
            final Path prefabPath = context.getPrefabPaths().get(i);
            final CompletableFuture<IPrefabBuffer> prefabLoadingFuture = this.getPrefabBuffer(context.getEditor(), prefabPath);
            if (prefabLoadingFuture == null) {
                if (loadingState != null) {
                    loadingState.addError("server.commands.editprefab.error.prefabLoadFailed", prefabPath.toString());
                    this.notifyProgress(progressCallback, loadingState);
                }
                return null;
            }
            final Path pathForCallback = prefabPath;
            initializationFutures[i] = prefabLoadingFuture.thenApply(buffer -> {
                if (loadingState != null) {
                    loadingState.onPrefabLoaded(pathForCallback);
                    this.notifyProgress(progressCallback, loadingState);
                }
                return buffer;
            });
        }
        return CompletableFuture.allOf((CompletableFuture<?>[])initializationFutures).thenApply(unused -> {
            if (this.isLoadingCancelled(playerUuid)) {
                return null;
            }
            else {
                final List<IPrefabBuffer> prefabAccessors = new ObjectArrayList<IPrefabBuffer>(initializationFutures.length);
                int heightOfTallestPrefab = 0;
                for (final CompletableFuture initializationFuture : initializationFutures) {
                    final IPrefabBuffer prefabAccessor = initializationFuture.join();
                    prefabAccessors.add(prefabAccessor);
                    if (context.loadChildPrefabs()) {
                        for (final PrefabBuffer.ChildPrefab childPrefab : prefabAccessor.getChildPrefabs()) {}
                    }
                    final int prefabHeight = Math.abs(prefabAccessor.getMaxY() - prefabAccessor.getMinY());
                    if (prefabHeight > heightOfTallestPrefab) {
                        heightOfTallestPrefab = prefabHeight;
                    }
                }
                final int yLevelToPastePrefabsAt = this.getAmountOfBlocksBelowPrefab(heightOfTallestPrefab, context.getPasteLevelGoal());
                this.applyWorldGenWorldConfig(context, yLevelToPastePrefabsAt, worldConfig);
                if (loadingState != null) {
                    loadingState.setPhase(PrefabLoadingState.Phase.PASTING_PREFABS);
                    this.notifyProgress(progressCallback, loadingState);
                }
                if (this.isLoadingCancelled(playerUuid)) {
                    return null;
                }
                else {
                    final String worldName = this.getWorldName(context);
                    if (Universe.get().getWorld(worldName) != null) {
                        PrefabEditSessionManager.LOGGER.at(Level.WARNING).log("Aborting prefab editor creation for %s: world '%s' already exists", playerUuid, worldName);
                        return null;
                    }
                    else {
                        return new Tri(prefabAccessors, yLevelToPastePrefabsAt, this.getWorldCreatingFuture(context, worldConfig).join());
                    }
                }
            }
        }).thenCompose(passedData -> {
            if (passedData == null) {
                return CompletableFuture.completedFuture((Object)null);
            }
            else {
                return (CompletableFuture<Object>)CompletableFuture.supplyAsync(() -> {
                    if (this.isLoadingCancelled(playerUuid)) {
                        return null;
                    }
                    else {
                        final World world = passedData.getRight();
                        final int yLevelToPastePrefabsAt2 = passedData.getMiddle();
                        final List<IPrefabBuffer> prefabAccessors2 = passedData.getLeft();
                        if (world == null || !world.isAlive()) {
                            return null;
                        }
                        else {
                            final Store<EntityStore> store = world.getEntityStore().getStore();
                            final int[] rowGroupIndices = this.calculateRowGroups(context, prefabAccessors2.size());
                            final IntArrayList rowDepths = new IntArrayList();
                            int currentRowGroup = -1;
                            int currentRowIndex = -1;
                            for (int j = 0; j < prefabAccessors2.size(); ++j) {
                                final IPrefabBuffer prefabAccessor2 = prefabAccessors2.get(j);
                                final int rowGroup = rowGroupIndices[j];
                                if (rowGroup != currentRowGroup) {
                                    currentRowGroup = rowGroup;
                                    ++currentRowIndex;
                                    rowDepths.add(0);
                                }
                                int depth;
                                if (context.getStackingAxis().equals(PrefabStackingAxis.X)) {
                                    depth = prefabAccessor2.getMaxZ() - prefabAccessor2.getMinZ();
                                }
                                else {
                                    depth = prefabAccessor2.getMaxX() - prefabAccessor2.getMinX();
                                }
                                rowDepths.set(currentRowIndex, Math.max(rowDepths.getInt(currentRowIndex), depth));
                            }
                            final int[] rowStarts = new int[rowDepths.size()];
                            int cumulativeDepth = 0;
                            for (int r = 0; r < rowDepths.size(); ++r) {
                                rowStarts[r] = cumulativeDepth;
                                cumulativeDepth += rowDepths.getInt(r) + context.getBlocksBetweenEachPrefab() + 1;
                            }
                            int currentRowGroup2 = -1;
                            int currentRowIndex2 = -1;
                            int lineOffset = 0;
                            int k = 0;
                            while (k < prefabAccessors2.size()) {
                                if (this.isLoadingCancelled(playerUuid) || !world.isAlive()) {
                                    return null;
                                }
                                else {
                                    final IPrefabBuffer prefabAccessor3 = prefabAccessors2.get(k);
                                    final Path prefabPath2 = context.getPrefabPaths().get(k);
                                    final int rowGroup2 = rowGroupIndices[k];
                                    if (rowGroup2 != currentRowGroup2) {
                                        currentRowGroup2 = rowGroup2;
                                        ++currentRowIndex2;
                                        lineOffset = 0;
                                    }
                                    final int rowOffset = rowStarts[currentRowIndex2];
                                    final int prefabXSize = prefabAccessor3.getMaxX() - prefabAccessor3.getMinX();
                                    final int prefabZSize = prefabAccessor3.getMaxZ() - prefabAccessor3.getMinZ();
                                    Vector3i pastePosition;
                                    if (context.getAlignment().equals(PrefabAlignment.ZERO)) {
                                        pastePosition = new Vector3i(0, yLevelToPastePrefabsAt2, 0);
                                        pastePosition.subtract(Math.min(prefabAccessor3.getMinX(), 0), prefabAccessor3.getMinY(), Math.min(prefabAccessor3.getMinZ(), 0));
                                        if (context.getStackingAxis().equals(PrefabStackingAxis.X)) {
                                            pastePosition.add(lineOffset, 0, rowOffset);
                                            lineOffset += prefabXSize + context.getBlocksBetweenEachPrefab() + 1;
                                        }
                                        else {
                                            pastePosition.add(rowOffset, 0, lineOffset);
                                            lineOffset += prefabZSize + context.getBlocksBetweenEachPrefab() + 1;
                                        }
                                    }
                                    else {
                                        pastePosition = new Vector3i(0, yLevelToPastePrefabsAt2 - Math.min(prefabAccessor3.getMinY(), 0), 0);
                                        if (context.getStackingAxis().equals(PrefabStackingAxis.X)) {
                                            final int xPos = lineOffset + Math.abs(Math.min(prefabAccessor3.getMinX(), 0));
                                            final int zPos = rowOffset + Math.abs(Math.min(prefabAccessor3.getMinZ(), 0));
                                            pastePosition.add(xPos, 0, zPos);
                                            lineOffset += prefabXSize + context.getBlocksBetweenEachPrefab() + 1;
                                        }
                                        else {
                                            final int xPos2 = rowOffset + Math.abs(Math.min(prefabAccessor3.getMinX(), 0));
                                            final int zPos2 = lineOffset + Math.abs(Math.min(prefabAccessor3.getMinZ(), 0));
                                            pastePosition.add(xPos2, 0, zPos2);
                                            lineOffset += prefabZSize + context.getBlocksBetweenEachPrefab() + 1;
                                        }
                                    }
                                    final Vector3i minPoint = new Vector3i(pastePosition.x + prefabAccessor3.getMinX(), pastePosition.y + prefabAccessor3.getMinY(), pastePosition.z + prefabAccessor3.getMinZ());
                                    final Vector3i maxPoint = new Vector3i(pastePosition.x + prefabAccessor3.getMaxX(), pastePosition.y + prefabAccessor3.getMaxY(), pastePosition.z + prefabAccessor3.getMaxZ());
                                    final Vector3i anchorPosition = new Vector3i(pastePosition.x + prefabAccessor3.getAnchorX(), pastePosition.y + prefabAccessor3.getAnchorY(), pastePosition.z + prefabAccessor3.getAnchorZ());
                                    try {
                                        PrefabUtil.paste(prefabAccessor3, world, pastePosition, Rotation.None, true, new FastRandom(), 0, true, false, context.shouldLoadEntities(), store);
                                        editSession.addPrefab(prefabPath2, minPoint, maxPoint, anchorPosition, pastePosition.clone());
                                        if (loadingState != null) {
                                            loadingState.onPrefabPasted(prefabPath2);
                                            this.notifyProgress(progressCallback, loadingState);
                                        }
                                    }
                                    catch (final Exception e) {
                                        if (this.isLoadingCancelled(playerUuid) || !world.isAlive()) {
                                            return null;
                                        }
                                        else {
                                            PrefabEditSessionManager.LOGGER.at(Level.SEVERE).withCause(e).log("Error pasting prefab: %s", prefabPath2);
                                            if (loadingState != null) {
                                                loadingState.addError("server.commands.editprefab.error.pasteFailed", prefabPath2.getFileName().toString() + ": " + e.getMessage());
                                                this.notifyProgress(progressCallback, loadingState);
                                            }
                                            throw e;
                                        }
                                    }
                                    ++k;
                                }
                            }
                            final WorldTimeResource worldTimeResource = store.getResource(WorldTimeResource.getResourceType());
                            worldTimeResource.setDayTime(0.5, world, store);
                            return world;
                        }
                    }
                }, passedData.getRight());
            }
        });
    }
    
    @Nonnull
    private int[] calculateRowGroups(@Nonnull final PrefabEditorCreationContext context, final int prefabCount) {
        final int[] rowGroups = new int[prefabCount];
        final PrefabRowSplitMode rowSplitMode = context.getRowSplitMode();
        final List<Path> prefabPaths = context.getPrefabPaths();
        if (rowSplitMode == PrefabRowSplitMode.NONE || prefabCount == 0) {
            return rowGroups;
        }
        if (rowSplitMode == PrefabRowSplitMode.BY_SPECIFIED_FOLDER) {
            final List<String> unprocessedPaths = context.getUnprocessedPrefabPaths();
            final Path rootPath = context.getPrefabRootDirectory().getPrefabPath();
            int currentGroup = 0;
            int prefabIndex = 0;
            for (final String unprocessedPath : unprocessedPaths) {
                final Path resolvedPath = rootPath.resolve(unprocessedPath.replace('/', File.separatorChar).replace('\\', File.separatorChar));
                while (prefabIndex < prefabCount) {
                    final Path prefabPath = prefabPaths.get(prefabIndex);
                    if (!prefabPath.startsWith(resolvedPath)) {
                        if (unprocessedPath.endsWith("/") || unprocessedPath.endsWith("\\")) {
                            if (!prefabPath.startsWith(resolvedPath)) {
                                break;
                            }
                        }
                        else if (!prefabPath.equals(resolvedPath)) {
                            break;
                        }
                    }
                    rowGroups[prefabIndex] = currentGroup;
                    ++prefabIndex;
                }
                ++currentGroup;
            }
        }
        else if (rowSplitMode == PrefabRowSplitMode.BY_ALL_SUBFOLDERS) {
            final Object2ObjectOpenHashMap<Path, Integer> parentDirToGroup = new Object2ObjectOpenHashMap<Path, Integer>();
            int nextGroup = 0;
            for (int i = 0; i < prefabCount; ++i) {
                final Path prefabPath2 = prefabPaths.get(i);
                final Path parentDir = prefabPath2.getParent();
                if (parentDir != null) {
                    final Integer group = parentDirToGroup.get(parentDir);
                    if (group == null) {
                        parentDirToGroup.put(parentDir, nextGroup);
                        rowGroups[i] = nextGroup;
                        ++nextGroup;
                    }
                    else {
                        rowGroups[i] = group;
                    }
                }
                else {
                    rowGroups[i] = 0;
                }
            }
        }
        return rowGroups;
    }
    
    private int getAmountOfBlocksBelowPrefab(final int prefabHeight, final int desiredYLevel) {
        if (desiredYLevel < 0) {
            throw new IllegalArgumentException("Cannot have a negative y level for pasting prefabs");
        }
        if (desiredYLevel >= 320) {
            throw new IllegalArgumentException("Cannot paste above or at the world height");
        }
        return Math.min(desiredYLevel, 320 - prefabHeight);
    }
    
    @Nullable
    public CompletableFuture<Void> exitEditSession(@Nonnull final Ref<EntityStore> ref, @Nonnull final World world, @Nonnull final PlayerRef playerRef, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final PrefabEditSession prefabEditSession = this.activeEditSessions.get(playerRef.getUuid());
        if (prefabEditSession == null) {
            return null;
        }
        prefabEditSession.hidePrefabAnchors(playerRef.getPacketHandler());
        World returnWorld = Universe.get().getWorld(prefabEditSession.getWorldArrivedFrom());
        Transform returnLocation = prefabEditSession.getTransformArrivedFrom();
        if (returnWorld == null || returnLocation == null) {
            PrefabEditSessionManager.LOGGER.at(Level.WARNING).log("Prefab editor exit fallback triggered for player %s: returnWorld=%s (worldArrivedFrom=%s), returnLocation=%s. Using default world spawn.", playerRef.getUuid(), (returnWorld != null) ? returnWorld.getName() : "null", prefabEditSession.getWorldArrivedFrom(), returnLocation);
            returnWorld = Universe.get().getDefaultWorld();
            returnLocation = returnWorld.getWorldConfig().getSpawnProvider().getSpawnPoint(ref, componentAccessor);
        }
        final World finalReturnWorld = returnWorld;
        final Transform finalReturnLocation = returnLocation;
        final Teleport teleportComponent = Teleport.createForPlayer(finalReturnWorld, finalReturnLocation);
        return CompletableFuture.runAsync(() -> componentAccessor.putComponent(ref, Teleport.getComponentType(), teleportComponent), world).thenRunAsync(() -> {
            final World worldToRemove = Universe.get().getWorld(prefabEditSession.getWorldName());
            if (worldToRemove != null) {
                Universe.get().removeWorld(prefabEditSession.getWorldName());
            }
            final Collection<PrefabEditingMetadata> prefabsBeingEditedInEditSession = prefabEditSession.getLoadedPrefabMetadata().values();
            for (final PrefabEditingMetadata prefab : prefabsBeingEditedInEditSession) {
                this.prefabsBeingEdited.remove(prefab.getPrefabPath());
            }
            this.activeEditSessions.remove(playerRef.getUuid());
        });
    }
    
    @Nonnull
    public CompletableFuture<Void> cleanupCancelledSession(@Nonnull final UUID playerUuid, @Nonnull final String worldName, @Nullable final Consumer<PrefabLoadingState> progressCallback) {
        this.cancelLoading(playerUuid);
        final PrefabLoadingState loadingState = new PrefabLoadingState();
        loadingState.setPhase(PrefabLoadingState.Phase.CANCELLING);
        this.notifyProgress(progressCallback, loadingState);
        return CompletableFuture.runAsync(() -> {
            final World world = Universe.get().getWorld(worldName);
            if (world != null) {
                loadingState.setPhase(PrefabLoadingState.Phase.SHUTTING_DOWN_WORLD);
                this.notifyProgress(progressCallback, loadingState);
                world.getWorldConfig().setDeleteOnRemove(true);
                loadingState.setPhase(PrefabLoadingState.Phase.DELETING_WORLD);
                this.notifyProgress(progressCallback, loadingState);
                Universe.get().removeWorld(worldName);
            }
            final PrefabEditSession session = this.activeEditSessions.remove(playerUuid);
            if (session != null) {
                final Collection<PrefabEditingMetadata> prefabsInSession = session.getLoadedPrefabMetadata().values();
                for (final PrefabEditingMetadata prefab : prefabsInSession) {
                    this.prefabsBeingEdited.remove(prefab.getPrefabPath());
                }
            }
            this.inProgressLoading.remove(playerUuid);
            loadingState.setPhase(PrefabLoadingState.Phase.SHUTDOWN_COMPLETE);
            this.notifyProgress(progressCallback, loadingState);
        });
    }
    
    @Nonnull
    public CompletableFuture<Void> cleanupCancelledSession(@Nonnull final UUID playerUuid, @Nonnull final String worldName) {
        return this.cleanupCancelledSession(playerUuid, worldName, null);
    }
    
    @Nullable
    private CompletableFuture<IPrefabBuffer> getPrefabBuffer(@Nonnull final CommandSender sender, @Nonnull final Path path) {
        if (!Files.exists(path, new LinkOption[0])) {
            sender.sendMessage(Message.translation("server.commands.editprefab.prefabNotFound").param("name", path.toString()));
            return null;
        }
        return CompletableFuture.supplyAsync(() -> PrefabBufferUtil.getCached(path));
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
        MESSAGE_COMMANDS_PREFAB_EDIT_SESSION_MANAGER_EXISTING_EDIT_SESSION = Message.translation("server.commands.prefabeditsessionmanager.existingEditSession");
        MESSAGE_COMMANDS_EDIT_PREFAB_SOMETHING_WENT_WRONG = Message.translation("server.commands.editprefab.somethingWentWrong");
        DEFAULT_TINT = new Color((byte)91, (byte)(-98), (byte)40);
    }
}
