package fi.dy.masa.enderutilities.tileentity;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import javax.annotation.Nullable;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.SoundEvents;
import net.minecraft.inventory.Container;
import net.minecraft.item.ItemStack;
import net.minecraft.item.crafting.FurnaceRecipes;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ITickable;
import net.minecraft.util.NonNullList;
import net.minecraft.util.SoundCategory;
import net.minecraft.util.math.MathHelper;
import net.minecraftforge.items.CapabilityItemHandler;
import net.minecraftforge.items.IItemHandler;
import net.minecraftforge.items.IItemHandlerModifiable;
import net.minecraftforge.items.wrapper.CombinedInvWrapper;
import net.minecraftforge.items.wrapper.InvWrapper;
import net.minecraftforge.items.wrapper.PlayerOffhandInvWrapper;
import fi.dy.masa.enderutilities.gui.client.GuiCreationStation;
import fi.dy.masa.enderutilities.inventory.IModularInventoryHolder;
import fi.dy.masa.enderutilities.inventory.ItemStackHandlerTileEntity;
import fi.dy.masa.enderutilities.inventory.container.ContainerCreationStation;
import fi.dy.masa.enderutilities.inventory.container.base.SlotRange;
import fi.dy.masa.enderutilities.inventory.item.InventoryItemCallback;
import fi.dy.masa.enderutilities.inventory.wrapper.InventoryCraftingPermissions;
import fi.dy.masa.enderutilities.inventory.wrapper.ItemHandlerCraftResult;
import fi.dy.masa.enderutilities.inventory.wrapper.ItemHandlerWrapperPermissions;
import fi.dy.masa.enderutilities.inventory.wrapper.ItemHandlerWrapperSelectiveModifiable;
import fi.dy.masa.enderutilities.reference.ReferenceNames;
import fi.dy.masa.enderutilities.util.InventoryUtils;
import fi.dy.masa.enderutilities.util.InventoryUtils.InvResult;
import fi.dy.masa.enderutilities.util.ItemType;
import fi.dy.masa.enderutilities.util.nbt.NBTUtils;

public class TileEntityCreationStation extends TileEntityEnderUtilitiesInventory implements IModularInventoryHolder, ITickable
{
    public static final int GUI_ACTION_SELECT_MODULE       = 0;
    public static final int GUI_ACTION_MOVE_ITEMS          = 1;
    public static final int GUI_ACTION_SET_QUICK_ACTION    = 2;
    public static final int GUI_ACTION_CLEAR_CRAFTING_GRID = 3;
    public static final int GUI_ACTION_RECIPE_LOAD         = 4;
    public static final int GUI_ACTION_RECIPE_STORE        = 5;
    public static final int GUI_ACTION_RECIPE_CLEAR        = 6;
    public static final int GUI_ACTION_TOGGLE_MODE         = 7;
    public static final int GUI_ACTION_SORT_ITEMS          = 8;

    public static final int INV_SIZE_ITEMS = 27;

    public static final int INV_ID_MODULES        = 1;
    public static final int INV_ID_CRAFTING_LEFT  = 2;
    public static final int INV_ID_CRAFTING_RIGHT = 3;
    public static final int INV_ID_FURNACE        = 4;

    public static final int COOKTIME_INC_SLOW = 12; // Slow/eco mode: 5 seconds per item
    public static final int COOKTIME_INC_FAST = 30; // Fast mode: 2 second per item (2.5x as fast)
    public static final int COOKTIME_DEFAULT = 1200; // Base cooktime per item: 5 seconds on slow

    public static final int BURNTIME_USAGE_SLOW = 20; // Slow/eco mode base usage
    public static final int BURNTIME_USAGE_FAST = 120; // Fast mode: use fuel 6x faster over time

    // The crafting mode button bits are in continuous order for easier checking in the gui
    public static final int MODE_BIT_LEFT_CRAFTING_OREDICT  = 0x0001;
    public static final int MODE_BIT_LEFT_CRAFTING_KEEPONE  = 0x0002;
    public static final int MODE_BIT_LEFT_CRAFTING_AUTOUSE  = 0x0004;
    public static final int MODE_BIT_RIGHT_CRAFTING_OREDICT = 0x0008;
    public static final int MODE_BIT_RIGHT_CRAFTING_KEEPONE = 0x0010;
    public static final int MODE_BIT_RIGHT_CRAFTING_AUTOUSE = 0x0020;
    public static final int MODE_BIT_LEFT_FAST              = 0x0040;
    public static final int MODE_BIT_RIGHT_FAST             = 0x0080;
    // Note: The selected recipe index is stored in bits 0x3F00 for right and left side (3 bits for each)
    public static final int MODE_BIT_SHOW_RECIPE_LEFT       = 0x4000;
    public static final int MODE_BIT_SHOW_RECIPE_RIGHT      = 0x8000;

    private InventoryItemCallback itemInventory;
    private ItemHandlerWrapperPermissions wrappedInventory;
    private final IItemHandler itemHandlerMemoryCards;
    private final ItemStackHandlerTileEntity furnaceInventory;
    private final IItemHandler furnaceInventoryWrapper;

    private final InventoryItemCallback[] craftingInventories;
    private final List<NonNullList<ItemStack>> craftingGridTemplates;
    private final ItemHandlerCraftResult[] craftResults;
    private final NonNullList<ItemStack> recipeItems0;
    private final NonNullList<ItemStack> recipeItems1;
    private int selectedModule;
    private int actionMode;
    private Map<UUID, Long> clickTimes;

    private ItemStack[] smeltingResultCache;
    private int[] burnTimeRemaining;   // Remaining burn time from the currently burning fuel
    private int[] burnTimeFresh;       // The time the currently burning fuel will burn in total
    private int[] cookTime;            // The time the currently cooking item has been cooking for
    private boolean[] inputDirty;
    private int modeMask;
    private int recipeLoadClickCount;
    public int lastInteractedCraftingGrid;

    public TileEntityCreationStation()
    {
        super(ReferenceNames.NAME_TILE_ENTITY_CREATION_STATION);

        this.itemHandlerBase = new ItemStackHandlerTileEntity(INV_ID_MODULES, 4, 1, false, "Items", this);
        this.itemHandlerMemoryCards = new TileEntityHandyChest.ItemHandlerWrapperMemoryCards(this.getBaseItemHandler());

        this.craftingInventories = new InventoryItemCallback[2];
        this.craftingGridTemplates = new ArrayList<NonNullList<ItemStack>>();
        this.craftingGridTemplates.add(null);
        this.craftingGridTemplates.add(null);
        this.recipeItems0 = NonNullList.withSize(10, ItemStack.EMPTY);
        this.recipeItems1 = NonNullList.withSize(10, ItemStack.EMPTY);
        this.craftResults = new ItemHandlerCraftResult[] { new ItemHandlerCraftResult(), new ItemHandlerCraftResult() };

        this.furnaceInventory = new ItemStackHandlerTileEntity(INV_ID_FURNACE, 6, 1024, true, "FurnaceItems", this);
        this.furnaceInventoryWrapper = new ItemHandlerWrapperFurnace(this.furnaceInventory);

        this.clickTimes = new HashMap<UUID, Long>();

        this.smeltingResultCache = new ItemStack[] { ItemStack.EMPTY, ItemStack.EMPTY };
        this.burnTimeRemaining = new int[2];
        this.burnTimeFresh = new int[2];
        this.cookTime = new int[2];
        this.inputDirty = new boolean[] { true, true };
        this.modeMask = 0;
    }

    @Override
    public void onLoad()
    {
        super.onLoad();

        this.initStorage(this.getWorld().isRemote);
    }

    private void initStorage(boolean isRemote)
    {
        this.itemInventory = new InventoryItemCallback(null, INV_SIZE_ITEMS, true, isRemote, this);
        this.wrappedInventory = new ItemHandlerWrapperPermissions(this.itemInventory, null);
        this.itemHandlerExternal = this.wrappedInventory;

        this.craftingInventories[0] = new InventoryItemCallback(null, 9, 64, false, isRemote, this, "CraftItems_0");
        this.craftingInventories[1] = new InventoryItemCallback(null, 9, 64, false, isRemote, this, "CraftItems_1");

        if (isRemote == false)
        {
            ItemStack containerStack = this.getContainerStack();
            this.itemInventory.setContainerItemStack(containerStack);
            this.craftingInventories[0].setContainerItemStack(containerStack);
            this.craftingInventories[1].setContainerItemStack(containerStack);
        }

        this.readModeMaskFromModule();
        this.loadRecipe(0, this.getRecipeId(0));
        this.loadRecipe(1, this.getRecipeId(1));
    }

    public ItemHandlerWrapperPermissions getItemInventory(EntityPlayer player)
    {
        return new ItemHandlerWrapperPermissions(this.itemInventory, player);
    }

    @Override
    public IItemHandler getWrappedInventoryForContainer(EntityPlayer player)
    {
        return this.getItemInventory(player);
    }

    public InventoryCraftingPermissions getCraftingInventory(int invId, EntityPlayer player, Container container)
    {
        ItemHandlerWrapperPermissions invPerm = new ItemHandlerWrapperPermissions(this.craftingInventories[invId], player);
        return new InventoryCraftingPermissions(3, 3, invPerm, this.craftResults[invId], player, container);
    }

    public ItemHandlerCraftResult getCraftResultInventory(int invId)
    {
        return this.craftResults[invId];
    }

    public IItemHandler getMemoryCardInventory()
    {
        return this.itemHandlerMemoryCards;
    }

    public IItemHandler getFurnaceInventory()
    {
        return this.furnaceInventoryWrapper;
    }

    /**
     * Gets a wrapped **InventoryCraftingPermissions** instance.
     * This must be used by anything that wants to modify the crafting grid contents,
     * so that the recipe and output slot will get updated properly!
     * @param gridId
     * @param player
     * @return
     */
    @Nullable
    private IItemHandler getWrappedCraftingInventoryFromContainer(int gridId, EntityPlayer player)
    {
        if (player.openContainer instanceof ContainerCreationStation)
        {
            return new InvWrapper(((ContainerCreationStation) player.openContainer).getCraftingInventory(gridId));
        }

        return null;
    }

    private boolean canAccessCraftingGrid(int gridId, EntityPlayer player)
    {
        if (player.openContainer instanceof ContainerCreationStation)
        {
            return ((ContainerCreationStation) player.openContainer).getCraftingInventory(gridId)
                    .getBaseInventory().isAccessibleByPlayer(player);
        }

        return false;
    }

    private void updateCraftingResults(EntityPlayer player)
    {
        if (player.openContainer instanceof ContainerCreationStation)
        {
            ((ContainerCreationStation) player.openContainer).getCraftingInventory(0).markDirty();
            ((ContainerCreationStation) player.openContainer).getCraftingInventory(1).markDirty();
        }
    }

    @Override
    public void readFromNBTCustom(NBTTagCompound nbt)
    {
        this.setSelectedModuleSlot(nbt.getByte("SelModule"));
        this.actionMode = nbt.getByte("QuickMode");
        this.modeMask = nbt.getByte("FurnaceMode");

        for (int i = 0; i < 2; i++)
        {
            this.burnTimeRemaining[i]  = nbt.getInteger("BurnTimeRemaining" + i);
            this.burnTimeFresh[i]      = nbt.getInteger("BurnTimeFresh" + i);
            this.cookTime[i]           = nbt.getInteger("CookTime" + i);
        }

        super.readFromNBTCustom(nbt);
    }

    @Override
    protected void readItemsFromNBT(NBTTagCompound nbt)
    {
        // This will read the Memory Cards themselves into the Memory Card inventory
        super.readItemsFromNBT(nbt);

        this.furnaceInventory.deserializeNBT(nbt);
    }

    @Override
    public NBTTagCompound writeToNBT(NBTTagCompound nbt)
    {
        nbt.setByte("QuickMode", (byte) this.actionMode);
        nbt.setByte("SelModule", (byte) this.selectedModule);
        nbt.setByte("FurnaceMode", (byte) (this.modeMask & (MODE_BIT_LEFT_FAST | MODE_BIT_RIGHT_FAST)));

        for (int i = 0; i < 2; i++)
        {
            nbt.setInteger("BurnTimeRemaining" + i, this.burnTimeRemaining[i]);
            nbt.setInteger("BurnTimeFresh" + i, this.burnTimeFresh[i]);
            nbt.setInteger("CookTime" + i, this.cookTime[i]);
        }

        super.writeToNBT(nbt);

        return nbt;
    }

    @Override
    public void writeItemsToNBT(NBTTagCompound nbt)
    {
        super.writeItemsToNBT(nbt);

        nbt.merge(this.furnaceInventory.serializeNBT());
    }

    @Override
    public NBTTagCompound getUpdatePacketTag(NBTTagCompound nbt)
    {
        nbt = super.getUpdatePacketTag(nbt);

        nbt.setByte("msel", (byte)this.selectedModule);

        return nbt;
    }

    @Override
    public void handleUpdateTag(NBTTagCompound tag)
    {
        this.selectedModule = tag.getByte("msel");

        super.handleUpdateTag(tag);
    }

    public int getQuickMode()
    {
        return this.actionMode;
    }

    public void setModeMask(int mask)
    {
        this.modeMask = mask;
    }

    public void setQuickMode(int mode)
    {
        this.actionMode = mode;
    }

    public int getSelectedModuleSlot()
    {
        return this.selectedModule;
    }

    public void setSelectedModuleSlot(int index)
    {
        this.selectedModule = MathHelper.clamp(index, 0, this.itemHandlerMemoryCards.getSlots() - 1);
    }

    public int getModeMask()
    {
        return this.modeMask;
    }

    protected int readModeMaskFromModule()
    {
        this.modeMask &= (MODE_BIT_LEFT_FAST | MODE_BIT_RIGHT_FAST);

        ItemStack containerStack = this.getContainerStack();

        // Furnace modes are stored in the TileEntity itself, other modes are on the modules
        if (containerStack.isEmpty() == false)
        {
            NBTTagCompound tag = NBTUtils.getCompoundTag(containerStack, null, "CreationStation", false);

            if (tag != null)
            {
                this.modeMask |= tag.getShort("ConfigMask");
            }
        }

        return this.modeMask;
    }

    protected void writeModeMaskToModule()
    {
        // Cache the value locally, because taking out the memory card will trigger an update that will overwrite the
        // value stored in the field in the TE.
        int modeMask = this.modeMask;
        ItemStack stack = this.itemHandlerMemoryCards.extractItem(this.selectedModule, 1, false);

        if (stack.isEmpty() == false)
        {
            // Furnace modes are stored in the TileEntity itself, other modes are on the modules
            NBTTagCompound tag = NBTUtils.getCompoundTag(stack, null, "CreationStation", true);
            tag.setShort("ConfigMask", (short) (modeMask & ~(MODE_BIT_LEFT_FAST | MODE_BIT_RIGHT_FAST)));
            this.itemHandlerMemoryCards.insertItem(this.selectedModule, stack, false);
        }
    }

    protected int getRecipeId(int invId)
    {
        int s = (invId == 1) ? 11 : 8;
        return (this.modeMask >> s) & 0x7;
    }

    protected void setRecipeId(int invId, int recipeId)
    {
        int shift = (invId == 1) ? 11 : 8;
        int mask = (invId == 1) ? 0x3800 : 0x0700;
        this.modeMask = (this.modeMask & ~mask) | ((recipeId & 0x7) << shift);
    }

    public boolean getShowRecipe(int invId)
    {
        return invId == 1 ? (this.modeMask & MODE_BIT_SHOW_RECIPE_RIGHT) != 0 : (this.modeMask & MODE_BIT_SHOW_RECIPE_LEFT) != 0;
    }

    protected void setShowRecipe(int invId, boolean show)
    {
        int mask = (invId == 1) ? MODE_BIT_SHOW_RECIPE_RIGHT : MODE_BIT_SHOW_RECIPE_LEFT;

        if (show)
        {
            this.modeMask |= mask;
        }
        else
        {
            this.modeMask &= ~mask;
        }
    }

    /**
     * Returns the recipeItems list of ItemStacks for the currently selected recipe
     * @param invId
     */
    public NonNullList<ItemStack> getRecipeItems(int invId)
    {
        return invId == 1 ? this.recipeItems1 : this.recipeItems0;
    }

    protected NBTTagCompound getRecipeTag(int invId, int recipeId, boolean create)
    {
        ItemStack containerStack = this.getContainerStack();

        if (containerStack.isEmpty() == false)
        {
            return NBTUtils.getCompoundTag(containerStack, "CreationStation", "Recipes_" + invId, create);
        }

        return null;
    }

    protected void loadRecipe(int invId, int recipeId)
    {
        NBTTagCompound tag = this.getRecipeTag(invId, recipeId, false);

        if (tag != null)
        {
            this.clearLoadedRecipe(invId);
            NBTUtils.readStoredItemsFromTag(tag, this.getRecipeItems(invId), "Recipe_" + recipeId);
        }
        else
        {
            this.removeRecipe(invId, recipeId);
        }
    }

    protected void storeRecipe(IItemHandler invCrafting, int invId, int recipeId)
    {
        invId = MathHelper.clamp(invId, 0, 1);
        NBTTagCompound tag = this.getRecipeTag(invId, recipeId, true);

        if (tag != null)
        {
            int invSize = invCrafting.getSlots();
            NonNullList<ItemStack> items = this.getRecipeItems(invId);

            for (int i = 0; i < invSize; i++)
            {
                ItemStack stack = invCrafting.getStackInSlot(i);

                if (stack.isEmpty() == false)
                {
                    stack = stack.copy();
                    stack.setCount(1);
                }

                items.set(i, stack);
            }

            // Store the recipe output item in the last slot, it will be used for GUI stuff
            ItemStack stack = this.craftResults[invId].getStackInSlot(0);
            items.set(items.size() - 1, stack.isEmpty() ? ItemStack.EMPTY : stack.copy());

            NBTUtils.writeItemsToTag(tag, items, "Recipe_" + recipeId, true);
        }
    }

    protected void clearLoadedRecipe(int invId)
    {
        this.getRecipeItems(invId).clear();
    }

    protected void removeRecipe(int invId, int recipeId)
    {
        NBTTagCompound tag = this.getRecipeTag(invId, recipeId, false);

        if (tag != null)
        {
            tag.removeTag("Recipe_" + recipeId);
        }

        this.clearLoadedRecipe(invId);
    }

    /**
     * Adds one more of each item in the recipe into the crafting grid, if possible
     * @param invId
     * @param recipeId
     */
    protected boolean addOneSetOfRecipeItemsIntoGrid(IItemHandler invCrafting, int invId, int recipeId, EntityPlayer player)
    {
        invId = MathHelper.clamp(invId, 0, 1);
        int maskOreDict = invId == 1 ? MODE_BIT_RIGHT_CRAFTING_OREDICT : MODE_BIT_LEFT_CRAFTING_OREDICT;
        boolean useOreDict = (this.modeMask & maskOreDict) != 0;

        IItemHandlerModifiable playerInv = (IItemHandlerModifiable) player.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.UP);
        IItemHandler invWrapper = new CombinedInvWrapper(this.itemInventory, playerInv);

        NonNullList<ItemStack> template = this.getRecipeItems(invId);
        InventoryUtils.clearInventoryToMatchTemplate(invCrafting, invWrapper, template);

        return InventoryUtils.restockInventoryBasedOnTemplate(invCrafting, invWrapper, template, 1, true, useOreDict);
    }

    protected void fillCraftingGrid(IItemHandler invCrafting, int invId, int recipeId, EntityPlayer player)
    {
        invId = MathHelper.clamp(invId, 0, 1);
        int largestStack = InventoryUtils.getLargestExistingStackSize(invCrafting);

        // If all stacks only have one item, then try to fill them all the way to maxStackSize
        if (largestStack == 1)
        {
            largestStack = 64;
        }

        NonNullList<ItemStack> template = InventoryUtils.createInventorySnapshot(invCrafting);
        int maskOreDict = invId == 1 ? MODE_BIT_RIGHT_CRAFTING_OREDICT : MODE_BIT_LEFT_CRAFTING_OREDICT;
        boolean useOreDict = (this.modeMask & maskOreDict) != 0;

        Map<ItemType, Integer> slotCounts = InventoryUtils.getSlotCountPerItem(invCrafting);

        // Clear old contents and then fill all the slots back up
        if (InventoryUtils.tryMoveAllItems(invCrafting, this.itemInventory) == InvResult.MOVED_ALL)
        {
            IItemHandlerModifiable playerInv = (IItemHandlerModifiable) player.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.UP);
            IItemHandler invWrapper = new CombinedInvWrapper(this.itemInventory, playerInv);

            // Next we find out how many items we have available for each item type on the crafting grid
            // and we cap the max stack size to that value, so the stacks will be balanced
            Iterator<Entry<ItemType, Integer>> iter = slotCounts.entrySet().iterator();

            while (iter.hasNext())
            {
                Entry<ItemType, Integer> entry = iter.next();
                ItemType item = entry.getKey();

                if (item.getStack().getMaxStackSize() == 1)
                {
                    continue;
                }

                int numFound = InventoryUtils.getNumberOfMatchingItemsInInventory(invWrapper, item.getStack(), useOreDict);
                int numSlots = entry.getValue();
                int maxSize = numFound / numSlots;

                if (maxSize < largestStack)
                {
                    largestStack = maxSize;
                }
            }

            InventoryUtils.restockInventoryBasedOnTemplate(invCrafting, invWrapper, template, largestStack, false, useOreDict);
        }
    }

    protected InvResult clearCraftingGrid(IItemHandler invCrafting, int invId, EntityPlayer player)
    {
        if (InventoryUtils.tryMoveAllItems(invCrafting, this.itemInventory) != InvResult.MOVED_ALL)
        {
            return InventoryUtils.tryMoveAllItems(invCrafting, player.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.UP));
        }

        return InvResult.MOVED_ALL;
    }

    /**
     * Check if there are enough items on the crafting grid to craft once, and try to add more items
     * if necessary and the auto-use feature is enabled.
     * @param invId
     * @return
     */
    public boolean canCraftItems(IItemHandler invCrafting, int invId)
    {
        if (invCrafting == null)
        {
            return false;
        }

        invId = MathHelper.clamp(invId, 0, 1);
        int maskKeepOne = invId == 1 ? MODE_BIT_RIGHT_CRAFTING_KEEPONE : MODE_BIT_LEFT_CRAFTING_KEEPONE;
        int maskAutoUse = invId == 1 ? MODE_BIT_RIGHT_CRAFTING_AUTOUSE : MODE_BIT_LEFT_CRAFTING_AUTOUSE;

        this.craftingGridTemplates.set(invId, null);

        // Auto-use-items feature enabled, create a snapshot of the current state of the crafting grid
        if ((this.modeMask & maskAutoUse) != 0)
        {
            //if (invCrafting != null && InventoryUtils.checkInventoryHasAllItems(this.itemInventory, invCrafting, 1))
            this.craftingGridTemplates.set(invId, InventoryUtils.createInventorySnapshot(invCrafting));
        }

        // No requirement to keep one item on the grid
        if ((this.modeMask & maskKeepOne) == 0)
        {
            return true;
        }
        // Need to keep one item on the grid; if auto-use is disabled and there is only one item left, then we can't craft anymore
        else if ((this.modeMask & maskAutoUse) == 0 && InventoryUtils.getMinNonEmptyStackSize(invCrafting) <= 1)
        {
            return false;
        }

        // We are required to keep one item on the grid after crafting.
        // So we must check that either there are more than one item left in each slot,
        // or that the auto-use feature is enabled and the inventory has all the required items
        // to re-stock the crafting grid afterwards.

        int maskOreDict = invId == 1 ? MODE_BIT_RIGHT_CRAFTING_OREDICT : MODE_BIT_LEFT_CRAFTING_OREDICT;
        boolean useOreDict = (this.modeMask & maskOreDict) != 0;

        // More than one item left in each slot
        if (InventoryUtils.getMinNonEmptyStackSize(invCrafting) > 1 ||
            InventoryUtils.checkInventoryHasAllItems(this.itemInventory, invCrafting, 1, useOreDict))
        {
            return true;
        }

        return false;
    }

    public void restockCraftingGrid(IItemHandler invCrafting, int invId)
    {
        invId = MathHelper.clamp(invId, 0, 1);
        NonNullList<ItemStack> template = this.craftingGridTemplates.get(invId);

        if (invCrafting == null || template == null)
        {
            return;
        }

        this.recipeLoadClickCount = 0;
        int maskAutoUse = invId == 1 ? MODE_BIT_RIGHT_CRAFTING_AUTOUSE : MODE_BIT_LEFT_CRAFTING_AUTOUSE;

        // Auto-use feature not enabled
        if ((this.modeMask & maskAutoUse) == 0)
        {
            return;
        }

        int maskOreDict = invId == 1 ? MODE_BIT_RIGHT_CRAFTING_OREDICT : MODE_BIT_LEFT_CRAFTING_OREDICT;
        boolean useOreDict = (this.modeMask & maskOreDict) != 0;

        InventoryUtils.clearInventoryToMatchTemplate(invCrafting, this.itemInventory, template);
        InventoryUtils.restockInventoryBasedOnTemplate(invCrafting, this.itemInventory, template, 1, true, useOreDict);

        this.craftingGridTemplates.set(invId, null);
    }

    @Override
    public ItemStack getContainerStack()
    {
        return this.itemHandlerMemoryCards.getStackInSlot(this.selectedModule);
    }

    @Override
    public void inventoryChanged(int inventoryId, int slot)
    {
        if (this.getWorld().isRemote)
        {
            return;
        }

        if (inventoryId == INV_ID_FURNACE)
        {
            // This gets called from the furnace inventory's markDirty
            this.inputDirty[0] = this.inputDirty[1] = true;
            return;
        }

        ItemStack containerStack = this.getContainerStack();
        this.itemInventory.setContainerItemStack(containerStack);
        this.craftingInventories[0].setContainerItemStack(containerStack);
        this.craftingInventories[1].setContainerItemStack(containerStack);

        this.readModeMaskFromModule();
        this.loadRecipe(0, this.getRecipeId(0));
        this.loadRecipe(1, this.getRecipeId(1));
    }

    public boolean isInventoryAccessible(EntityPlayer player)
    {
        return this.wrappedInventory.isAccessibleByPlayer(player);
    }

    @Override
    public void onLeftClickBlock(EntityPlayer player)
    {
        if (this.getWorld().isRemote)
        {
            return;
        }

        Long last = this.clickTimes.get(player.getUniqueID());

        if (last != null && this.getWorld().getTotalWorldTime() - last < 5)
        {
            // Double left clicked fast enough (< 5 ticks) - do the selected item moving action
            this.performGuiAction(player, GUI_ACTION_MOVE_ITEMS, this.actionMode);
            this.getWorld().playSound(null, this.getPos(), SoundEvents.ENTITY_ENDERMEN_TELEPORT, SoundCategory.BLOCKS, 0.2f, 1.8f);
            this.clickTimes.remove(player.getUniqueID());
        }
        else
        {
            this.clickTimes.put(player.getUniqueID(), this.getWorld().getTotalWorldTime());
        }
    }

    @Override
    public void performGuiAction(EntityPlayer player, int action, int element)
    {
        if (action == GUI_ACTION_SELECT_MODULE && element >= 0 && element < 4)
        {
            this.setSelectedModuleSlot(element);
            this.inventoryChanged(INV_ID_MODULES, element);
            this.markDirty();

            // This updates the crafting output slots, since they normally only update
            // when the grid contents are changed via the InventoryCrafting* inventory wrapper
            this.updateCraftingResults(player);
        }
        else if (action == GUI_ACTION_MOVE_ITEMS && element >= 0 && element < 6)
        {
            ItemHandlerWrapperPermissions inventory = new ItemHandlerWrapperPermissions(this.itemInventory, player);

            if (inventory.isAccessibleByPlayer(player) == false)
            {
                return;
            }

            IItemHandlerModifiable playerMainInv = (IItemHandlerModifiable) player.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.UP);
            IItemHandlerModifiable offhandInv = new PlayerOffhandInvWrapper(player.inventory);
            IItemHandler playerInv = new CombinedInvWrapper(playerMainInv, offhandInv);

            switch (element)
            {
                case 0: // Move all items to Chest
                    InventoryUtils.tryMoveAllItemsWithinSlotRange(playerInv, inventory, new SlotRange(9, 27), new SlotRange(inventory));
                    break;
                case 1: // Move matching items to Chest
                    InventoryUtils.tryMoveMatchingItemsWithinSlotRange(playerInv, inventory, new SlotRange(9, 27), new SlotRange(inventory));
                    break;
                case 2: // Leave one stack of each item type and fill that stack
                    InventoryUtils.leaveOneFullStackOfEveryItem(playerInv, inventory, true);
                    break;
                case 3: // Fill stacks in player inventory from Chest
                    InventoryUtils.fillStacksOfMatchingItems(inventory, playerInv);
                    break;
                case 4: // Move matching items to player inventory
                    InventoryUtils.tryMoveMatchingItems(inventory, playerInv);
                    break;
                case 5: // Move all items to player inventory
                    InventoryUtils.tryMoveAllItems(inventory, playerInv);
                    break;
            }
        }
        else if (action == GUI_ACTION_SET_QUICK_ACTION && element >= 0 && element < 6)
        {
            this.actionMode = element;
        }
        else if (action == GUI_ACTION_CLEAR_CRAFTING_GRID && element >= 0 && element <= 1)
        {
            int gridId = element;

            if (this.canAccessCraftingGrid(gridId, player) == false)
            {
                return;
            }

            IItemHandler inv = this.getWrappedCraftingInventoryFromContainer(gridId, player);

            // Already empty crafting grid, set the "show recipe" mode to disabled
            if (InventoryUtils.isInventoryEmpty(inv))
            {
                this.setShowRecipe(gridId, false);
                this.clearLoadedRecipe(gridId);
                this.writeModeMaskToModule();
            }
            // Items in grid, clear the grid
            else
            {
                this.clearCraftingGrid(inv, gridId, player);
            }

            this.recipeLoadClickCount = 0;
            this.lastInteractedCraftingGrid = gridId;
        }
        else if (action == GUI_ACTION_RECIPE_LOAD && element >= 0 && element < 10)
        {
            int gridId = element / 5;
            int recipeId = element % 5;

            if (this.canAccessCraftingGrid(gridId, player) == false)
            {
                return;
            }

            IItemHandler inv = this.getWrappedCraftingInventoryFromContainer(gridId, player);

            // Clicked again on a recipe button that is already currently selected => load items into crafting grid
            if (this.getRecipeId(gridId) == recipeId && this.getShowRecipe(gridId))
            {
                // First click after loading the recipe itself: load one item to each slot
                if (this.recipeLoadClickCount == 0)
                {
                    // First clear away the old contents
                    //if (this.clearCraftingGrid(inv, invId, player) == InvResult.MOVED_ALL)
                    {
                        if (this.addOneSetOfRecipeItemsIntoGrid(inv, gridId, recipeId, player))
                        {
                            this.recipeLoadClickCount += 1;
                        }
                    }
                }
                // Subsequent click will load the crafting grid with items up to either the largest stack size,
                // or the max stack size if the largest existing stack size is 1
                else
                {
                    this.fillCraftingGrid(inv, gridId, recipeId, player);
                }
            }
            // Clicked on a different recipe button, or the recipe was hidden => load the recipe
            // and show it, but don't load the items into the grid
            else
            {
                this.loadRecipe(gridId, recipeId);
                this.setRecipeId(gridId, recipeId);
                this.recipeLoadClickCount = 0;
            }

            this.setShowRecipe(gridId, true);
            this.writeModeMaskToModule();
            this.lastInteractedCraftingGrid = gridId;
        }
        else if (action == GUI_ACTION_RECIPE_STORE && element >= 0 && element < 10)
        {
            int gridId = element / 5;
            int recipeId = element % 5;

            if (this.canAccessCraftingGrid(gridId, player) == false)
            {
                return;
            }

            IItemHandler inv = this.getWrappedCraftingInventoryFromContainer(gridId, player);

            /*IItemHandler inv = this.craftingInventories[gridId];
            if (InventoryUtils.isInventoryEmpty(inv))
            {
                this.setShowRecipe(gridId, false);
            }
            else
            {
                this.storeRecipe(gridId, recipeId);
                this.setShowRecipe(gridId, true);
            }*/

            this.storeRecipe(inv, gridId, recipeId);
            this.setShowRecipe(gridId, true);
            this.setRecipeId(gridId, recipeId);
            this.writeModeMaskToModule();
            this.lastInteractedCraftingGrid = gridId;
        }
        else if (action == GUI_ACTION_RECIPE_CLEAR && element >= 0 && element < 10)
        {
            int gridId = element / 5;
            int recipeId = element % 5;

            if (this.canAccessCraftingGrid(gridId, player) == false)
            {
                return;
            }

            //if (this.getRecipeId(invId) == recipeId)
            {
                this.removeRecipe(gridId, recipeId);
                this.setShowRecipe(gridId, false);
                //this.setRecipeId(invId, recipeId);
                this.writeModeMaskToModule();
            }

            this.recipeLoadClickCount = 0;
            this.lastInteractedCraftingGrid = gridId;
        }
        else if (action == GUI_ACTION_TOGGLE_MODE && element >= 0 && element < 8)
        {
            switch (element)
            {
                case 0: this.modeMask ^= MODE_BIT_LEFT_CRAFTING_OREDICT; break;
                case 1: this.modeMask ^= MODE_BIT_LEFT_CRAFTING_KEEPONE; break;
                case 2: this.modeMask ^= MODE_BIT_LEFT_CRAFTING_AUTOUSE; break;
                case 3: this.modeMask ^= MODE_BIT_RIGHT_CRAFTING_AUTOUSE; break;
                case 4: this.modeMask ^= MODE_BIT_RIGHT_CRAFTING_KEEPONE; break;
                case 5: this.modeMask ^= MODE_BIT_RIGHT_CRAFTING_OREDICT; break;
                case 6: this.modeMask ^= MODE_BIT_LEFT_FAST; break;
                case 7: this.modeMask ^= MODE_BIT_RIGHT_FAST; break;
                default:
            }

            if (element <= 5)
            {
                this.lastInteractedCraftingGrid = element / 3;
            }

            this.writeModeMaskToModule();
        }
        else if (action == GUI_ACTION_SORT_ITEMS && element >= 0 && element <= 1)
        {
            // Station's item inventory
            if (element == 0)
            {
                ItemHandlerWrapperPermissions inventory = new ItemHandlerWrapperPermissions(this.itemInventory, player);

                if (inventory.isAccessibleByPlayer(player) == false)
                {
                    return;
                }

                InventoryUtils.sortInventoryWithinRange(this.itemInventory, new SlotRange(this.itemInventory));
            }
            // Player inventory (don't sort the hotbar)
            else
            {
                IItemHandlerModifiable inv = (IItemHandlerModifiable) player.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.UP);
                InventoryUtils.sortInventoryWithinRange(inv, new SlotRange(9, 27));
            }
        }
    }

    /**
     * Updates the cached smelting result for the current input item, if the input has changed since last caching the result.
     */
    private void updateSmeltingResult(int id)
    {
        if (this.inputDirty[id])
        {
            ItemStack inputStack = this.furnaceInventory.getStackInSlot(id * 3);

            if (inputStack.isEmpty() == false)
            {
                this.smeltingResultCache[id] = FurnaceRecipes.instance().getSmeltingResult(inputStack);
            }
            else
            {
                this.smeltingResultCache[id] = ItemStack.EMPTY;
            }

            this.inputDirty[id] = false;
        }
    }

    /**
     * Checks if there is a valid fuel item in the fuel slot.
     * @return true if the fuel slot has an item that can be used as fuel
     */
    private boolean hasFuelAvailable(int id)
    {
        return TileEntityEnderFurnace.consumeFuelItem(this.furnaceInventory, id * 3 + 1, true) > 0;
    }

    /**
     * Consumes one fuel item or one dose of fluid fuel. Sets the burnTimeFresh field to the amount of burn time gained.
     * @return returns the amount of furnace burn time that was gained from the fuel
     */
    private int consumeFuelItem(int id)
    {
        int fuelSlot = id * 3 + 1;
        int burnTime = TileEntityEnderFurnace.consumeFuelItem(this.furnaceInventory, fuelSlot, false);

        if (burnTime > 0)
        {
            this.burnTimeFresh[id] = burnTime;
        }

        return burnTime;
    }

    /**
     * Returns true if the furnace can smelt an item. Checks the input slot for valid smeltable items and the output buffer
     * for an equal item and free space or empty buffer. Does not check the fuel.
     * @return true if input and output item stacks allow the current item to be smelted
     */
    private boolean canSmelt(int id)
    {
        ItemStack inputStack = this.furnaceInventory.getStackInSlot(id * 3);

        if (inputStack.isEmpty() || this.smeltingResultCache[id].isEmpty())
        {
            return false;
        }

        int amount = 0;
        ItemStack outputStack = this.furnaceInventory.getStackInSlot(id * 3 + 2);

        if (outputStack.isEmpty() == false)
        {
            if (InventoryUtils.areItemStacksEqual(this.smeltingResultCache[id], outputStack) == false)
            {
                return false;
            }

            amount = outputStack.getCount();
        }

        if ((this.furnaceInventory.getInventoryStackLimit() - amount) < this.smeltingResultCache[id].getCount())
        {
            return false;
        }

        return true;
    }

    /**
     * Turn one item from the furnace input slot into a smelted item in the furnace output buffer.
     */
    private void smeltItem(int id)
    {
        if (this.canSmelt(id))
        {
            this.furnaceInventory.insertItem(id * 3 + 2, this.smeltingResultCache[id], false);
            this.furnaceInventory.extractItem(id * 3, 1, false);

            if (this.furnaceInventory.getStackInSlot(id * 3).isEmpty())
            {
                this.inputDirty[id] = true;
            }
        }
    }

    private void smeltingLogic(int id)
    {
        this.updateSmeltingResult(id);

        boolean dirty = false;
        boolean hasFuel = this.hasFuelAvailable(id);
        boolean isFastMode = id == 0 ? (this.modeMask & MODE_BIT_LEFT_FAST) != 0 : (this.modeMask & MODE_BIT_RIGHT_FAST) != 0;

        int cookTimeIncrement = COOKTIME_INC_SLOW;

        if (this.burnTimeRemaining[id] == 0 && hasFuel == false)
        {
            return;
        }
        else if (isFastMode)
        {
            cookTimeIncrement = COOKTIME_INC_FAST;
        }

        boolean canSmelt = this.canSmelt(id);

        // The furnace is currently burning fuel
        if (this.burnTimeRemaining[id] > 0)
        {
            int btUse = (isFastMode ? BURNTIME_USAGE_FAST : BURNTIME_USAGE_SLOW);

            // Not enough fuel burn time remaining for the elapsed tick
            if (btUse > this.burnTimeRemaining[id])
            {
                if (hasFuel && canSmelt)
                {
                    this.burnTimeRemaining[id] += this.consumeFuelItem(id);
                    hasFuel = this.hasFuelAvailable(id);
                }
                // Running out of fuel, scale the cook progress according to the elapsed burn time
                else
                {
                    cookTimeIncrement = (this.burnTimeRemaining[id] * cookTimeIncrement) / btUse;
                    btUse = this.burnTimeRemaining[id];
                }
            }

            this.burnTimeRemaining[id] -= btUse;
            dirty = true;
        }
        // Furnace wasn't burning, but it now has fuel and smeltable items, start burning/smelting
        else if (canSmelt && hasFuel)
        {
            this.burnTimeRemaining[id] += this.consumeFuelItem(id);
            hasFuel = this.hasFuelAvailable(id);
            dirty = true;
        }

        // Valid items to smelt, room in output
        if (canSmelt)
        {
            this.cookTime[id] += cookTimeIncrement;

            // One item done smelting
            if (this.cookTime[id] >= COOKTIME_DEFAULT)
            {
                this.smeltItem(id);
                canSmelt = this.canSmelt(id);

                // We can smelt the next item and we "overcooked" the last one, carry over the extra progress
                if (canSmelt && this.cookTime[id] > COOKTIME_DEFAULT)
                {
                    this.cookTime[id] -= COOKTIME_DEFAULT;
                }
                else // No more items to smelt or didn't overcook
                {
                    this.cookTime[id] = 0;
                }
            }

            // If the current fuel ran out and we still have items to cook, consume the next fuel item
            if (this.burnTimeRemaining[id] == 0 && hasFuel && canSmelt)
            {
                this.burnTimeRemaining[id] += this.consumeFuelItem(id);
            }

            dirty = true;
        }
        // Can't smelt anything at the moment, rewind the cooking progress at half the speed of normal cooking
        else if (this.cookTime[id] > 0)
        {
            this.cookTime[id] -= Math.min(this.cookTime[id], COOKTIME_INC_SLOW / 2);
            dirty = true;
        }

        if (dirty)
        {
            this.markDirty();
        }
    }

    @Override
    public void update()
    {
        if (this.getWorld().isRemote == false)
        {
            this.smeltingLogic(0);
            this.smeltingLogic(1);
        }
    }

    /**
     * Returns an integer between 0 and the passed value representing how close the current item is to being completely cooked
     */
    public int getSmeltProgressScaled(int id, int i)
    {
        return this.cookTime[id] * i / COOKTIME_DEFAULT;
    }

    /**
     * Returns an integer between 0 and the passed value representing how much burn time is left on the current fuel
     * item, where 0 means that the item is exhausted and the passed value means that the item is fresh
     */
    public int getBurnTimeRemainingScaled(int id, int i)
    {
        if (this.burnTimeFresh[id] == 0)
        {
            return 0;
        }

        return this.burnTimeRemaining[id] * i / this.burnTimeFresh[id];
    }

    private class ItemHandlerWrapperFurnace extends ItemHandlerWrapperSelectiveModifiable
    {
        public ItemHandlerWrapperFurnace(IItemHandlerModifiable baseHandler)
        {
            super(baseHandler);
        }

        @Override
        public boolean isItemValidForSlot(int slot, ItemStack stack)
        {
            if (stack.isEmpty())
            {
                return false;
            }

            if (slot == 0 || slot == 3)
            {
                return FurnaceRecipes.instance().getSmeltingResult(stack).isEmpty() == false;
            }

            return (slot == 1 || slot == 4) && TileEntityEnderFurnace.isItemFuel(stack);
        }

        /*
        @Override
        protected boolean canExtractFromSlot(int slot)
        {
            // 2 & 5: output slots; 1 & 4: fuel slots => allow pulling out from output slots, and non-fuel items (like empty buckets) from fuel slots
            if (  slot == 2 || slot == 5 ||
                ((slot == 1 || slot == 4) && TileEntityEnderFurnace.isItemFuel(this.getStackInSlot(slot)) == false))
            {
                return true;
            }

            return false;
        }
        */
    }

    @Override
    public ContainerCreationStation getContainer(EntityPlayer player)
    {
        return new ContainerCreationStation(player, this);
    }

    @Override
    public Object getGui(EntityPlayer player)
    {
        return new GuiCreationStation(this.getContainer(player), this);
    }
}
