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

package com.hypixel.hytale.builtin.crafting.component;

import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.ObjectListIterator;
import com.hypixel.hytale.math.shape.Box;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.server.core.universe.world.meta.BlockStateModule;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.component.spatial.SpatialResource;
import com.hypixel.hytale.server.core.asset.type.blockhitbox.BlockBoundingBoxes;
import java.util.Map;
import com.hypixel.hytale.server.core.universe.world.meta.state.ItemContainerState;
import com.hypixel.hytale.protocol.ItemQuantity;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import com.hypixel.hytale.server.core.inventory.container.filter.FilterType;
import com.hypixel.hytale.server.core.inventory.container.DelegateItemContainer;
import com.hypixel.hytale.server.core.inventory.container.EmptyItemContainer;
import com.hypixel.hytale.server.core.entity.entities.player.windows.MaterialExtraResourcesSection;
import com.hypixel.hytale.server.core.universe.world.SoundUtil;
import com.hypixel.hytale.protocol.SoundCategory;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.bench.BenchUpgradeRequirement;
import com.hypixel.hytale.server.core.inventory.container.CombinedItemContainer;
import com.hypixel.hytale.builtin.crafting.window.BenchWindow;
import com.google.gson.JsonArray;
import com.hypixel.hytale.protocol.ItemResourceType;
import org.bson.BsonDocument;
import java.util.Collections;
import java.util.Iterator;
import com.hypixel.hytale.server.core.inventory.transaction.ListTransaction;
import com.hypixel.hytale.server.core.inventory.MaterialQuantity;
import com.hypixel.hytale.server.core.inventory.transaction.MaterialSlotTransaction;
import com.hypixel.hytale.server.core.inventory.transaction.MaterialTransaction;
import com.hypixel.hytale.server.core.inventory.Inventory;
import com.hypixel.hytale.server.core.inventory.ItemStack;
import java.util.List;
import com.hypixel.hytale.server.core.inventory.container.SimpleItemContainer;
import com.hypixel.hytale.protocol.BenchRequirement;
import com.hypixel.hytale.server.core.universe.world.meta.BlockState;
import com.hypixel.hytale.server.core.entity.entities.player.data.PlayerConfigData;
import com.hypixel.hytale.builtin.crafting.state.BenchState;
import com.hypixel.hytale.builtin.adventure.memories.MemoriesPlugin;
import it.unimi.dsi.fastutil.objects.ObjectList;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.bench.BenchTierLevel;
import com.hypixel.hytale.builtin.crafting.window.CraftingWindow;
import com.hypixel.hytale.server.core.asset.type.item.config.Item;
import com.hypixel.hytale.event.IEventDispatcher;
import com.hypixel.hytale.server.core.universe.world.World;
import java.util.logging.Level;
import com.hypixel.hytale.server.core.util.NotificationUtil;
import com.hypixel.hytale.protocol.packets.interface_.NotificationStyle;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.event.events.player.PlayerCraftEvent;
import com.hypixel.hytale.server.core.HytaleServer;
import com.hypixel.hytale.protocol.GameMode;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.event.events.ecs.CraftRecipeEvent;
import com.hypixel.hytale.server.core.inventory.container.ItemContainer;
import com.hypixel.hytale.server.core.asset.type.item.config.CraftingRecipe;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.bench.Bench;
import com.hypixel.hytale.protocol.BenchType;
import java.util.Objects;
import java.util.Collection;
import java.util.concurrent.LinkedBlockingQueue;
import com.hypixel.hytale.builtin.crafting.CraftingPlugin;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
import javax.annotation.Nullable;
import java.util.concurrent.BlockingQueue;
import javax.annotation.Nonnull;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.Component;

public class CraftingManager implements Component<EntityStore>
{
    @Nonnull
    private static final HytaleLogger LOGGER;
    @Nonnull
    private final BlockingQueue<CraftingJob> queuedCraftingJobs;
    @Nullable
    private BenchUpgradingJob upgradingJob;
    private int x;
    private int y;
    private int z;
    @Nullable
    private BlockType blockType;
    
    @Nonnull
    public static ComponentType<EntityStore, CraftingManager> getComponentType() {
        return CraftingPlugin.get().getCraftingManagerComponentType();
    }
    
    public CraftingManager() {
        this.queuedCraftingJobs = new LinkedBlockingQueue<CraftingJob>();
    }
    
    private CraftingManager(@Nonnull final CraftingManager other) {
        this.queuedCraftingJobs = new LinkedBlockingQueue<CraftingJob>();
        this.x = other.x;
        this.y = other.y;
        this.z = other.z;
        this.blockType = other.blockType;
        this.queuedCraftingJobs.addAll((Collection<?>)other.queuedCraftingJobs);
        this.upgradingJob = other.upgradingJob;
    }
    
    public boolean hasBenchSet() {
        return this.blockType != null;
    }
    
    public void setBench(final int x, final int y, final int z, @Nonnull final BlockType blockType) {
        final Bench bench = blockType.getBench();
        Objects.requireNonNull(bench, "blockType isn't a bench!");
        if (bench.getType() != BenchType.Crafting && bench.getType() != BenchType.DiagramCrafting && bench.getType() != BenchType.StructuralCrafting && bench.getType() != BenchType.Processing) {
            throw new IllegalArgumentException("blockType isn't a crafting bench!");
        }
        if (this.blockType != null) {
            throw new IllegalArgumentException("Bench blockType is already set! Must be cleared (close UI).");
        }
        if (!this.queuedCraftingJobs.isEmpty()) {
            throw new IllegalArgumentException("Queue already has jobs!");
        }
        if (this.upgradingJob != null) {
            throw new IllegalArgumentException("Upgrading job is already set!");
        }
        this.x = x;
        this.y = y;
        this.z = z;
        this.blockType = blockType;
    }
    
    public boolean clearBench(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final boolean result = this.cancelAllCrafting(ref, componentAccessor);
        this.x = 0;
        this.y = 0;
        this.z = 0;
        this.blockType = null;
        this.upgradingJob = null;
        return result;
    }
    
    public boolean craftItem(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor, @Nonnull final CraftingRecipe recipe, final int quantity, @Nonnull final ItemContainer itemContainer) {
        if (this.upgradingJob != null) {
            return false;
        }
        Objects.requireNonNull(recipe, "Recipe can't be null");
        final CraftRecipeEvent.Pre preEvent = new CraftRecipeEvent.Pre(recipe, quantity);
        componentAccessor.invoke(ref, preEvent);
        if (preEvent.isCancelled()) {
            return false;
        }
        if (!this.isValidBenchForRecipe(ref, componentAccessor, recipe)) {
            return false;
        }
        final World world = componentAccessor.getExternalData().getWorld();
        final Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
        assert playerComponent != null;
        if (playerComponent.getGameMode() == GameMode.Creative || removeInputFromInventory(itemContainer, recipe, quantity)) {
            final CraftRecipeEvent.Post postEvent = new CraftRecipeEvent.Post(recipe, quantity);
            componentAccessor.invoke(ref, postEvent);
            if (postEvent.isCancelled()) {
                return true;
            }
            giveOutput(ref, componentAccessor, recipe, quantity);
            final IEventDispatcher<PlayerCraftEvent, PlayerCraftEvent> dispatcher = HytaleServer.get().getEventBus().dispatchFor((Class<? super PlayerCraftEvent>)PlayerCraftEvent.class, world.getName());
            if (dispatcher.hasListener()) {
                dispatcher.dispatch(new PlayerCraftEvent(ref, playerComponent, recipe, quantity));
            }
            return true;
        }
        else {
            final PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
            assert playerRefComponent != null;
            final String translationKey = getRecipeOutputTranslationKey(recipe);
            if (translationKey != null) {
                NotificationUtil.sendNotification(playerRefComponent.getPacketHandler(), Message.translation("server.general.crafting.missingIngredient").param("item", Message.translation(translationKey)), NotificationStyle.Danger);
            }
            CraftingManager.LOGGER.at(Level.FINE).log("Missing items required to craft the item: %s", recipe);
            return false;
        }
    }
    
    @Nullable
    private static String getRecipeOutputTranslationKey(@Nonnull final CraftingRecipe recipe) {
        final String itemId = recipe.getPrimaryOutput().getItemId();
        if (itemId == null) {
            return null;
        }
        final Item itemAsset = Item.getAssetMap().getAsset(itemId);
        return (itemAsset != null) ? itemAsset.getTranslationKey() : null;
    }
    
    public boolean queueCraft(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor, @Nonnull final CraftingWindow window, final int transactionId, @Nonnull final CraftingRecipe recipe, final int quantity, @Nonnull final ItemContainer inputItemContainer, @Nonnull final InputRemovalType inputRemovalType) {
        if (this.upgradingJob != null) {
            return false;
        }
        Objects.requireNonNull(recipe, "Recipe can't be null");
        if (!this.isValidBenchForRecipe(ref, componentAccessor, recipe)) {
            return false;
        }
        float recipeTime = recipe.getTimeSeconds();
        if (recipeTime > 0.0f) {
            final int level = this.getBenchTierLevel(componentAccessor);
            if (level > 1) {
                final BenchTierLevel tierLevelData = this.getBenchTierLevelData(level);
                if (tierLevelData != null) {
                    recipeTime -= recipeTime * tierLevelData.getCraftingTimeReductionModifier();
                }
            }
        }
        this.queuedCraftingJobs.offer(new CraftingJob(window, transactionId, recipe, quantity, recipeTime, inputItemContainer, inputRemovalType));
        return true;
    }
    
    public void tick(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor, float dt) {
        if (this.upgradingJob != null) {
            if (dt > 0.0f) {
                final BenchUpgradingJob upgradingJob = this.upgradingJob;
                upgradingJob.timeSecondsCompleted += dt;
            }
            this.upgradingJob.window.updateBenchUpgradeJob(this.upgradingJob.computeLoadingPercent());
            if (this.upgradingJob.timeSecondsCompleted >= this.upgradingJob.timeSeconds) {
                this.upgradingJob.window.updateBenchTierLevel(this.finishTierUpgrade(ref, componentAccessor));
                this.upgradingJob = null;
            }
            return;
        }
        final Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
        assert playerComponent != null;
        final PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
        assert playerRefComponent != null;
        while (dt > 0.0f && !this.queuedCraftingJobs.isEmpty()) {
            CraftingJob currentJob = this.queuedCraftingJobs.peek();
            final boolean isCreativeMode = playerComponent.getGameMode() == GameMode.Creative;
            if (currentJob != null && currentJob.quantityStarted < currentJob.quantity && currentJob.quantityStarted <= currentJob.quantityCompleted) {
                CraftingManager.LOGGER.at(Level.FINE).log("Removing Items for next quantity: %s", currentJob);
                final int currentItemId = currentJob.quantityStarted++;
                if (!isCreativeMode && !removeInputFromInventory(currentJob, currentItemId)) {
                    final String translationKey = getRecipeOutputTranslationKey(currentJob.recipe);
                    if (translationKey != null) {
                        NotificationUtil.sendNotification(playerRefComponent.getPacketHandler(), Message.translation("server.general.crafting.missingIngredient").param("item", Message.translation(translationKey)), NotificationStyle.Danger);
                    }
                    CraftingManager.LOGGER.at(Level.FINE).log("Missing items required to craft the item: %s", currentJob);
                    currentJob = null;
                    this.queuedCraftingJobs.poll();
                }
                if (!isCreativeMode && currentJob != null && currentJob.quantityStarted < currentJob.quantity && currentJob.quantityStarted <= currentJob.quantityCompleted) {
                    NotificationUtil.sendNotification(playerRefComponent.getPacketHandler(), Message.translation("server.general.crafting.failedTakingCorrectQuantity"), NotificationStyle.Danger);
                    CraftingManager.LOGGER.at(Level.SEVERE).log("Failed to remove the correct quantity of input, removing crafting job %s", currentJob);
                    currentJob = null;
                    this.queuedCraftingJobs.poll();
                }
            }
            if (currentJob != null) {
                final CraftingJob craftingJob = currentJob;
                craftingJob.timeSecondsCompleted += dt;
                float percent = (currentJob.timeSeconds <= 0.0f) ? 1.0f : (currentJob.timeSecondsCompleted / currentJob.timeSeconds);
                if (percent > 1.0f) {
                    percent = 1.0f;
                }
                currentJob.window.updateCraftingJob(percent);
                CraftingManager.LOGGER.at(Level.FINEST).log("Update time: %s", currentJob);
                dt = 0.0f;
                if (currentJob.timeSecondsCompleted < currentJob.timeSeconds) {
                    continue;
                }
                dt = currentJob.timeSecondsCompleted - currentJob.timeSeconds;
                final int currentCompletedItemId = currentJob.quantityCompleted++;
                currentJob.timeSecondsCompleted = 0.0f;
                CraftingManager.LOGGER.at(Level.FINE).log("Crafted 1 Quantity: %s", currentJob);
                if (currentJob.quantityCompleted == currentJob.quantity) {
                    giveOutput(ref, componentAccessor, currentJob, currentCompletedItemId);
                    CraftingManager.LOGGER.at(Level.FINE).log("Crafting Finished: %s", currentJob);
                    this.queuedCraftingJobs.poll();
                }
                else {
                    if (currentJob.quantityCompleted > currentJob.quantity) {
                        this.queuedCraftingJobs.poll();
                        throw new RuntimeException("QuantityCompleted is greater than the Quality! " + String.valueOf(currentJob));
                    }
                    giveOutput(ref, componentAccessor, currentJob, currentCompletedItemId);
                }
                if (!this.queuedCraftingJobs.isEmpty()) {
                    continue;
                }
                currentJob.window.setBlockInteractionState("default", componentAccessor.getExternalData().getWorld());
            }
        }
    }
    
    public boolean cancelAllCrafting(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        CraftingManager.LOGGER.at(Level.FINE).log("Cancel Crafting!");
        final ObjectList<CraftingJob> oldJobs = new ObjectArrayList<CraftingJob>(this.queuedCraftingJobs.size());
        this.queuedCraftingJobs.drainTo(oldJobs);
        if (!oldJobs.isEmpty()) {
            final CraftingJob currentJob = oldJobs.getFirst();
            CraftingManager.LOGGER.at(Level.FINE).log("Refunding Items for: %s", currentJob);
            refundInputToInventory(ref, componentAccessor, currentJob, currentJob.quantityStarted - 1);
            return true;
        }
        return false;
    }
    
    private boolean isValidBenchForRecipe(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor, @Nonnull final CraftingRecipe recipe) {
        final Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
        assert playerComponent != null;
        final PlayerConfigData playerConfigData = playerComponent.getPlayerConfigData();
        final String primaryOutputItemId = (recipe.getPrimaryOutput() != null) ? recipe.getPrimaryOutput().getItemId() : null;
        if (recipe.isKnowledgeRequired() && (primaryOutputItemId == null || !playerConfigData.getKnownRecipes().contains(primaryOutputItemId))) {
            CraftingManager.LOGGER.at(Level.WARNING).log("%s - Attempted to craft %s but doesn't know the recipe!", recipe.getId());
            return false;
        }
        final World world = componentAccessor.getExternalData().getWorld();
        if (recipe.getRequiredMemoriesLevel() > 1 && MemoriesPlugin.get().getMemoriesLevel(world.getGameplayConfig()) < recipe.getRequiredMemoriesLevel()) {
            CraftingManager.LOGGER.at(Level.WARNING).log("Attempted to craft %s but doesn't have the required world memories level!", recipe.getId());
            return false;
        }
        final BenchType benchType = (this.blockType != null) ? this.blockType.getBench().getType() : BenchType.Crafting;
        final String benchName = (this.blockType != null) ? this.blockType.getBench().getId() : "Fieldcraft";
        boolean meetsRequirements = false;
        final BlockState state = world.getState(this.x, this.y, this.z, true);
        final int benchTierLevel = (state instanceof BenchState) ? ((BenchState)state).getTierLevel() : 0;
        final BenchRequirement[] requirements = recipe.getBenchRequirement();
        if (requirements != null) {
            for (final BenchRequirement benchRequirement : requirements) {
                if (benchRequirement.type == benchType && benchName.equals(benchRequirement.id) && benchRequirement.requiredTierLevel <= benchTierLevel) {
                    meetsRequirements = true;
                    break;
                }
            }
        }
        if (!meetsRequirements) {
            CraftingManager.LOGGER.at(Level.WARNING).log("Attempted to craft %s using %s, %s but requires bench %s but a bench is NOT set!", recipe.getId(), benchType, benchName, requirements);
            return false;
        }
        if (benchType == BenchType.Crafting && !"Fieldcraft".equals(benchName)) {
            final CraftingJob craftingJob = this.queuedCraftingJobs.peek();
            return craftingJob == null || craftingJob.recipe.getId().equals(recipe.getId());
        }
        return true;
    }
    
    private static void giveOutput(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor, @Nonnull final CraftingJob job, final int currentItemId) {
        job.removedItems.remove(currentItemId);
        final String recipeId = job.recipe.getId();
        final CraftingRecipe recipeAsset = CraftingRecipe.getAssetMap().getAsset(recipeId);
        if (recipeAsset == null) {
            throw new RuntimeException("A non-existent item ID was provided! " + recipeId);
        }
        giveOutput(ref, componentAccessor, recipeAsset, 1);
    }
    
    private static void giveOutput(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor, @Nonnull final CraftingRecipe craftingRecipe, final int quantity) {
        final Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
        if (playerComponent == null) {
            CraftingManager.LOGGER.at(Level.WARNING).log("Attempted to give output to a non-player entity: %s", ref);
            return;
        }
        final List<ItemStack> itemStacks = getOutputItemStacks(craftingRecipe, quantity);
        final Inventory inventory = playerComponent.getInventory();
        SimpleItemContainer.addOrDropItemStacks(componentAccessor, ref, inventory.getCombinedArmorHotbarStorage(), itemStacks);
    }
    
    private static boolean removeInputFromInventory(@Nonnull final CraftingJob job, final int currentItemId) {
        Objects.requireNonNull(job, "Job can't be null!");
        final CraftingRecipe craftingRecipe = job.recipe;
        Objects.requireNonNull(craftingRecipe, "CraftingRecipe can't be null!");
        final List<MaterialQuantity> materialsToRemove = getInputMaterials(craftingRecipe);
        if (materialsToRemove.isEmpty()) {
            return true;
        }
        CraftingManager.LOGGER.at(Level.FINEST).log("Removing Materials: %s - %s", job, materialsToRemove);
        final ObjectList<ItemStack> itemStackList = new ObjectArrayList<ItemStack>();
        boolean succeeded = false;
        switch (job.inputRemovalType.ordinal()) {
            case 0: {
                final ListTransaction<MaterialTransaction> materialTransactions = job.inputItemContainer.removeMaterials(materialsToRemove, true, true, true);
                for (final MaterialTransaction transaction : materialTransactions.getList()) {
                    for (final MaterialSlotTransaction slotTransaction : transaction.getList()) {
                        if (!ItemStack.isEmpty(slotTransaction.getOutput())) {
                            itemStackList.add(slotTransaction.getOutput());
                        }
                    }
                }
                succeeded = materialTransactions.succeeded();
                break;
            }
            case 1: {
                final ListTransaction<MaterialSlotTransaction> materialTransactions2 = job.inputItemContainer.removeMaterialsOrdered(materialsToRemove, true, true, true);
                for (final MaterialSlotTransaction transaction2 : materialTransactions2.getList()) {
                    if (!ItemStack.isEmpty(transaction2.getOutput())) {
                        itemStackList.add(transaction2.getOutput());
                    }
                }
                succeeded = materialTransactions2.succeeded();
                break;
            }
            default: {
                throw new IllegalArgumentException("Unknown enum: " + String.valueOf(job.inputRemovalType));
            }
        }
        job.removedItems.put(currentItemId, itemStackList);
        job.window.invalidateExtraResources();
        return succeeded;
    }
    
    private static boolean removeInputFromInventory(@Nonnull final ItemContainer itemContainer, @Nonnull final CraftingRecipe craftingRecipe, final int quantity) {
        final List<MaterialQuantity> materialsToRemove = getInputMaterials(craftingRecipe, quantity);
        if (materialsToRemove.isEmpty()) {
            return true;
        }
        CraftingManager.LOGGER.at(Level.FINEST).log("Removing Materials: %s - %s", craftingRecipe, materialsToRemove);
        final ListTransaction<MaterialTransaction> materialTransactions = itemContainer.removeMaterials(materialsToRemove, true, true, true);
        return materialTransactions.succeeded();
    }
    
    private static void refundInputToInventory(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor, @Nonnull final CraftingJob job, final int currentItemId) {
        Objects.requireNonNull(job, "Job can't be null!");
        final List<ItemStack> itemStacks = job.removedItems.get(currentItemId);
        if (itemStacks == null) {
            return;
        }
        final Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
        assert playerComponent != null;
        SimpleItemContainer.addOrDropItemStacks(componentAccessor, ref, playerComponent.getInventory().getCombinedHotbarFirst(), itemStacks);
    }
    
    @Nonnull
    public static List<ItemStack> getOutputItemStacks(@Nonnull final CraftingRecipe recipe) {
        return getOutputItemStacks(recipe, 1);
    }
    
    @Nonnull
    public static List<ItemStack> getOutputItemStacks(@Nonnull final CraftingRecipe recipe, final int quantity) {
        Objects.requireNonNull(recipe);
        final MaterialQuantity[] output = recipe.getOutputs();
        if (output == null) {
            return List.of();
        }
        final ObjectList<ItemStack> outputItemStacks = new ObjectArrayList<ItemStack>();
        for (final MaterialQuantity outputMaterial : output) {
            final ItemStack outputItemStack = getOutputItemStack(outputMaterial, quantity);
            if (outputItemStack != null) {
                outputItemStacks.add(outputItemStack);
            }
        }
        return outputItemStacks;
    }
    
    @Nullable
    public static ItemStack getOutputItemStack(@Nonnull final MaterialQuantity outputMaterial, @Nonnull final String id) {
        return getOutputItemStack(outputMaterial, 1);
    }
    
    @Nullable
    public static ItemStack getOutputItemStack(@Nonnull final MaterialQuantity outputMaterial, final int quantity) {
        final String itemId = outputMaterial.getItemId();
        if (itemId == null) {
            return null;
        }
        final int materialQuantity = (outputMaterial.getQuantity() <= 0) ? 1 : outputMaterial.getQuantity();
        return new ItemStack(itemId, materialQuantity * quantity, outputMaterial.getMetadata());
    }
    
    @Nonnull
    public static List<MaterialQuantity> getInputMaterials(@Nonnull final CraftingRecipe recipe) {
        return getInputMaterials(recipe, 1);
    }
    
    @Nonnull
    private static List<MaterialQuantity> getInputMaterials(@Nonnull final MaterialQuantity[] input) {
        return getInputMaterials(input, 1);
    }
    
    @Nonnull
    public static List<MaterialQuantity> getInputMaterials(@Nonnull final CraftingRecipe recipe, final int quantity) {
        Objects.requireNonNull(recipe);
        if (recipe.getInput() == null) {
            return Collections.emptyList();
        }
        return getInputMaterials(recipe.getInput(), quantity);
    }
    
    @Nonnull
    private static List<MaterialQuantity> getInputMaterials(@Nonnull final MaterialQuantity[] input, final int quantity) {
        final ObjectList<MaterialQuantity> materials = new ObjectArrayList<MaterialQuantity>();
        for (final MaterialQuantity craftingMaterial : input) {
            final String itemId = craftingMaterial.getItemId();
            final String resourceTypeId = craftingMaterial.getResourceTypeId();
            final int materialQuantity = craftingMaterial.getQuantity();
            final BsonDocument metadata = craftingMaterial.getMetadata();
            materials.add(new MaterialQuantity(itemId, resourceTypeId, null, materialQuantity * quantity, metadata));
        }
        return materials;
    }
    
    public static boolean matches(@Nonnull final MaterialQuantity craftingMaterial, @Nonnull final ItemStack itemStack) {
        final String itemId = craftingMaterial.getItemId();
        if (itemId != null) {
            return itemId.equals(itemStack.getItemId());
        }
        final String resourceTypeId = craftingMaterial.getResourceTypeId();
        if (resourceTypeId != null && itemStack.getItem().getResourceTypes() != null) {
            for (final ItemResourceType itemResourceType : itemStack.getItem().getResourceTypes()) {
                if (resourceTypeId.equals(itemResourceType.id)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    @Nonnull
    public static JsonArray generateInventoryHints(@Nonnull final List<CraftingRecipe> recipes, final int inputSlotIndex, @Nonnull final ItemContainer container) {
        final JsonArray inventoryHints = new JsonArray();
        for (short storageSlotIndex = 0, bound = container.getCapacity(); storageSlotIndex < bound; ++storageSlotIndex) {
            final ItemStack itemStack = container.getItemStack(storageSlotIndex);
            if (itemStack != null) {
                if (!itemStack.isEmpty()) {
                    if (matchesAnyRecipe(recipes, inputSlotIndex, itemStack)) {
                        inventoryHints.add(storageSlotIndex);
                    }
                }
            }
        }
        return inventoryHints;
    }
    
    public static boolean matchesAnyRecipe(@Nonnull final List<CraftingRecipe> recipes, final int inputSlotIndex, @Nonnull final ItemStack slotItemStack) {
        for (final CraftingRecipe recipe : recipes) {
            final MaterialQuantity[] input = recipe.getInput();
            if (inputSlotIndex >= input.length) {
                continue;
            }
            final MaterialQuantity slotCraftingMaterial = input[inputSlotIndex];
            if (slotCraftingMaterial.getItemId() != null && slotCraftingMaterial.getItemId().equals(slotItemStack.getItemId())) {
                return true;
            }
            if (slotCraftingMaterial.getResourceTypeId() == null || slotItemStack.getItem().getResourceTypes() == null) {
                continue;
            }
            for (final ItemResourceType itemResourceType : slotItemStack.getItem().getResourceTypes()) {
                if (slotCraftingMaterial.getResourceTypeId().equals(itemResourceType.id)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    public boolean startTierUpgrade(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor, @Nonnull final BenchWindow window) {
        if (this.upgradingJob != null) {
            return false;
        }
        final BenchUpgradeRequirement requirements = this.getBenchUpgradeRequirement(this.getBenchTierLevel(componentAccessor));
        if (requirements == null) {
            return false;
        }
        final List<MaterialQuantity> input = getInputMaterials(requirements.getInput());
        if (input.isEmpty()) {
            return false;
        }
        final Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
        assert playerComponent != null;
        if (playerComponent.getGameMode() != GameMode.Creative) {
            final CombinedItemContainer combined = new CombinedItemContainer(new ItemContainer[] { playerComponent.getInventory().getCombinedBackpackStorageHotbar(), window.getExtraResourcesSection().getItemContainer() });
            if (!combined.canRemoveMaterials(input)) {
                return false;
            }
        }
        this.upgradingJob = new BenchUpgradingJob(window, requirements.getTimeSeconds());
        this.cancelAllCrafting(ref, componentAccessor);
        return true;
    }
    
    private int finishTierUpgrade(@Nonnull final Ref<EntityStore> ref, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        if (this.upgradingJob == null) {
            return 0;
        }
        final World world = componentAccessor.getExternalData().getWorld();
        final BlockState state = world.getState(this.x, this.y, this.z, true);
        final BenchState benchState = (state instanceof BenchState) ? ((BenchState)state) : null;
        if (benchState == null || benchState.getTierLevel() == 0) {
            return 0;
        }
        final BenchUpgradeRequirement requirements = this.getBenchUpgradeRequirement(benchState.getTierLevel());
        if (requirements == null) {
            return benchState.getTierLevel();
        }
        final List<MaterialQuantity> input = getInputMaterials(requirements.getInput());
        if (input.isEmpty()) {
            return benchState.getTierLevel();
        }
        final Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
        assert playerComponent != null;
        boolean canUpgrade = playerComponent.getGameMode() == GameMode.Creative;
        if (!canUpgrade) {
            CombinedItemContainer combined = new CombinedItemContainer(new ItemContainer[] { playerComponent.getInventory().getCombinedBackpackStorageHotbar(), this.upgradingJob.window.getExtraResourcesSection().getItemContainer() });
            combined = new CombinedItemContainer(new ItemContainer[] { combined, this.upgradingJob.window.getExtraResourcesSection().getItemContainer() });
            final ListTransaction<MaterialTransaction> materialTransactions = combined.removeMaterials(input);
            if (materialTransactions.succeeded()) {
                final List<ItemStack> consumed = new ObjectArrayList<ItemStack>();
                for (final MaterialTransaction transaction : materialTransactions.getList()) {
                    for (final MaterialSlotTransaction matSlot : transaction.getList()) {
                        consumed.add(matSlot.getOutput());
                    }
                }
                benchState.addUpgradeItems(consumed);
                canUpgrade = true;
            }
        }
        if (canUpgrade) {
            benchState.setTierLevel(benchState.getTierLevel() + 1);
            if (benchState.getBench().getBenchUpgradeCompletedSoundEventIndex() != 0) {
                SoundUtil.playSoundEvent3d(benchState.getBench().getBenchUpgradeCompletedSoundEventIndex(), SoundCategory.SFX, this.x + 0.5, this.y + 0.5, this.z + 0.5, componentAccessor);
            }
        }
        return benchState.getTierLevel();
    }
    
    @Nullable
    private BenchTierLevel getBenchTierLevelData(final int level) {
        if (this.blockType == null) {
            return null;
        }
        final Bench bench = this.blockType.getBench();
        return (bench == null) ? null : bench.getTierLevel(level);
    }
    
    @Nullable
    private BenchUpgradeRequirement getBenchUpgradeRequirement(final int tierLevel) {
        final BenchTierLevel tierData = this.getBenchTierLevelData(tierLevel);
        return (tierData == null) ? null : tierData.getUpgradeRequirement();
    }
    
    private int getBenchTierLevel(@Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final World world = componentAccessor.getExternalData().getWorld();
        final BlockState state = world.getState(this.x, this.y, this.z, true);
        return (state instanceof BenchState) ? ((BenchState)state).getTierLevel() : 0;
    }
    
    public static int feedExtraResourcesSection(@Nonnull final BenchState benchState, @Nonnull final MaterialExtraResourcesSection extraResourcesSection) {
        final ChestLookupResult result = getContainersAroundBench(benchState);
        final List<ItemContainer> chests = result.containers;
        final List<ItemContainerState> chestStates = result.states;
        ItemContainer itemContainer = EmptyItemContainer.INSTANCE;
        if (!chests.isEmpty()) {
            itemContainer = new CombinedItemContainer((ItemContainer[])chests.stream().map(container -> {
                final DelegateItemContainer<ItemContainer> delegate = new DelegateItemContainer<ItemContainer>(container);
                delegate.setGlobalFilter(FilterType.ALLOW_OUTPUT_ONLY);
                return delegate;
            }).toArray(ItemContainer[]::new));
        }
        final Map<String, ItemQuantity> materials = new Object2ObjectOpenHashMap<String, ItemQuantity>();
        for (final ItemContainer chest : chests) {
            chest.forEach((i, itemStack) -> {
                if (!CraftingPlugin.isValidUpgradeMaterialForBench(benchState, itemStack) && !CraftingPlugin.isValidCraftingMaterialForBench(benchState, itemStack)) {
                    return;
                }
                else {
                    final ItemQuantity itemQuantity = materials.computeIfAbsent(itemStack.getItemId(), k -> new ItemQuantity(itemStack.getItemId(), 0));
                    itemQuantity.quantity += itemStack.getQuantity();
                    return;
                }
            });
        }
        extraResourcesSection.setItemContainer(itemContainer);
        extraResourcesSection.setExtraMaterials(materials.values().toArray(new ItemQuantity[0]));
        extraResourcesSection.setValid(true);
        return chestStates.size();
    }
    
    @Nonnull
    protected static ChestLookupResult getContainersAroundBench(@Nonnull final BenchState benchState) {
        final List<ItemContainer> containers = new ObjectArrayList<ItemContainer>();
        final List<ItemContainerState> states = new ObjectArrayList<ItemContainerState>();
        final List<ItemContainerState> spatialResults = new ObjectArrayList<ItemContainerState>();
        final List<ItemContainerState> filteredOut = new ObjectArrayList<ItemContainerState>();
        final World world = benchState.getChunk().getWorld();
        final Store<ChunkStore> store = world.getChunkStore().getStore();
        final int limit = world.getGameplayConfig().getCraftingConfig().getBenchMaterialChestLimit();
        final double horizontalRadius = world.getGameplayConfig().getCraftingConfig().getBenchMaterialHorizontalChestSearchRadius();
        final double verticalRadius = world.getGameplayConfig().getCraftingConfig().getBenchMaterialVerticalChestSearchRadius();
        final Vector3d blockPos = benchState.getBlockPosition().toVector3d();
        final BlockBoundingBoxes hitboxAsset = BlockBoundingBoxes.getAssetMap().getAsset(benchState.getBlockType().getHitboxTypeIndex());
        final BlockBoundingBoxes.RotatedVariantBoxes rotatedHitbox = hitboxAsset.get(benchState.getRotationIndex());
        final Box boundingBox = rotatedHitbox.getBoundingBox();
        final double benchWidth = boundingBox.width();
        final double benchHeight = boundingBox.height();
        final double benchDepth = boundingBox.depth();
        final double extraSearchRadius = Math.max(benchWidth, Math.max(benchDepth, benchHeight)) - 1.0;
        final SpatialResource<Ref<ChunkStore>, ChunkStore> blockStateSpatialStructure = store.getResource(BlockStateModule.get().getItemContainerSpatialResourceType());
        final ObjectList<Ref<ChunkStore>> results = SpatialResource.getThreadLocalReferenceList();
        blockStateSpatialStructure.getSpatialStructure().ordered3DAxis(blockPos, horizontalRadius + extraSearchRadius, verticalRadius + extraSearchRadius, horizontalRadius + extraSearchRadius, results);
        if (!results.isEmpty()) {
            final int benchMinBlockX = (int)Math.floor(boundingBox.min.x);
            final int benchMinBlockY = (int)Math.floor(boundingBox.min.y);
            final int benchMinBlockZ = (int)Math.floor(boundingBox.min.z);
            final int benchMaxBlockX = (int)Math.ceil(boundingBox.max.x) - 1;
            final int benchMaxBlockY = (int)Math.ceil(boundingBox.max.y) - 1;
            final int benchMaxBlockZ = (int)Math.ceil(boundingBox.max.z) - 1;
            final double minX = blockPos.x + benchMinBlockX - horizontalRadius;
            final double minY = blockPos.y + benchMinBlockY - verticalRadius;
            final double minZ = blockPos.z + benchMinBlockZ - horizontalRadius;
            final double maxX = blockPos.x + benchMaxBlockX + horizontalRadius;
            final double maxY = blockPos.y + benchMaxBlockY + verticalRadius;
            final double maxZ = blockPos.z + benchMaxBlockZ + horizontalRadius;
            for (final Ref<ChunkStore> ref : results) {
                final BlockState state = BlockState.getBlockState(ref, ref.getStore());
                if (state instanceof final ItemContainerState chest) {
                    spatialResults.add(chest);
                }
            }
            for (final ItemContainerState chest2 : spatialResults) {
                final Vector3d chestBlockPos = chest2.getBlockPosition().toVector3d();
                if (chestBlockPos.x >= minX && chestBlockPos.x <= maxX && chestBlockPos.y >= minY && chestBlockPos.y <= maxY && chestBlockPos.z >= minZ && chestBlockPos.z <= maxZ) {
                    containers.add(chest2.getItemContainer());
                    states.add(chest2);
                    if (containers.size() >= limit) {
                        break;
                    }
                    continue;
                }
                else {
                    filteredOut.add(chest2);
                }
            }
        }
        return new ChestLookupResult(containers, states, spatialResults, filteredOut, blockPos);
    }
    
    @Nonnull
    @Override
    public String toString() {
        return "CraftingManager{queuedCraftingJobs=" + String.valueOf(this.queuedCraftingJobs) + ", x=" + this.x + ", y=" + this.y + ", z=" + this.z + ", blockType=" + String.valueOf(this.blockType);
    }
    
    @Nonnull
    @Override
    public Component<EntityStore> clone() {
        return new CraftingManager(this);
    }
    
    static {
        LOGGER = HytaleLogger.forEnclosingClass();
    }
    
    record ChestLookupResult(List<ItemContainer> containers, List<ItemContainerState> states, List<ItemContainerState> spatialResults, List<ItemContainerState> filteredOut, Vector3d benchCenteredPos) {}
    
    private static class CraftingJob
    {
        @Nonnull
        private final CraftingWindow window;
        private final int transactionId;
        @Nonnull
        private final CraftingRecipe recipe;
        private final int quantity;
        private final float timeSeconds;
        @Nonnull
        private final ItemContainer inputItemContainer;
        @Nonnull
        private final InputRemovalType inputRemovalType;
        @Nonnull
        private final Int2ObjectMap<List<ItemStack>> removedItems;
        private int quantityStarted;
        private int quantityCompleted;
        private float timeSecondsCompleted;
        
        public CraftingJob(@Nonnull final CraftingWindow window, final int transactionId, @Nonnull final CraftingRecipe recipe, final int quantity, final float timeSeconds, @Nonnull final ItemContainer inputItemContainer, @Nonnull final InputRemovalType inputRemovalType) {
            this.removedItems = new Int2ObjectOpenHashMap<List<ItemStack>>();
            this.window = window;
            this.transactionId = transactionId;
            this.recipe = recipe;
            this.quantity = quantity;
            this.timeSeconds = timeSeconds;
            this.inputItemContainer = inputItemContainer;
            this.inputRemovalType = inputRemovalType;
        }
        
        @Nonnull
        @Override
        public String toString() {
            return "CraftingJob{window=" + String.valueOf(this.window) + ", transactionId=" + this.transactionId + ", recipe=" + String.valueOf(this.recipe) + ", quantity=" + this.quantity + ", timeSeconds=" + this.timeSeconds + ", inputItemContainer=" + String.valueOf(this.inputItemContainer) + ", inputRemovalType=" + String.valueOf(this.inputRemovalType) + ", removedItems=" + String.valueOf(this.removedItems) + ", quantityStarted=" + this.quantityStarted + ", quantityCompleted=" + this.quantityCompleted + ", timeSecondsCompleted=" + this.timeSecondsCompleted;
        }
    }
    
    private static class BenchUpgradingJob
    {
        @Nonnull
        private final BenchWindow window;
        private final float timeSeconds;
        private float timeSecondsCompleted;
        private float lastSentPercent;
        
        private BenchUpgradingJob(@Nonnull final BenchWindow window, final float timeSeconds) {
            this.window = window;
            this.timeSeconds = timeSeconds;
        }
        
        @Override
        public String toString() {
            return "BenchUpgradingJob{window=" + String.valueOf(this.window) + ", timeSeconds=" + this.timeSeconds;
        }
        
        public float computeLoadingPercent() {
            return (this.timeSeconds <= 0.0f) ? 1.0f : Math.min(this.timeSecondsCompleted / this.timeSeconds, 1.0f);
        }
    }
    
    public enum InputRemovalType
    {
        NORMAL, 
        ORDERED;
    }
}
