package codechicken.nei.recipe;

import codechicken.lib.inventory.InventoryUtils;
import codechicken.nei.FastTransferManager;
import codechicken.nei.PositionedStack;
import codechicken.nei.api.IOverlayHandler;
import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.entity.player.InventoryPlayer;
import net.minecraft.inventory.Slot;
import net.minecraft.item.ItemStack;

import java.util.*;

public class DefaultOverlayHandler implements IOverlayHandler
{
    public static class DistributedIngred
    {
        public DistributedIngred(ItemStack item)
        {
            stack = InventoryUtils.copyStack(item, 1);
        }
        
        public ItemStack stack;
        public int invAmount;
        public int distributed;
        public int numSlots;
        public int recipeAmount;
    }
    
    public static class IngredientDistribution
    {
        public IngredientDistribution(DistributedIngred distrib, ItemStack permutation)
        {
            this.distrib = distrib;
            this.permutation = permutation;
        }
        
        public DistributedIngred distrib;
        public ItemStack permutation;
        public Slot[] slots;
    }
    
    public DefaultOverlayHandler(int x, int y)
    {
        offsetx = x;
        offsety = y;
    }
    
    public DefaultOverlayHandler()
    {
        this(5, 11);
    }

    public int offsetx;
    public int offsety;
    
    @Override
    public void overlayRecipe(GuiContainer gui, IRecipeHandler recipe, int recipeIndex, boolean shift)
    {
        List<PositionedStack> ingredients = recipe.getIngredientStacks(recipeIndex);
        List<DistributedIngred> ingredStacks = getPermutationIngredients(ingredients);

        if(!clearIngredients(gui, ingredients))
            return;
        
        findInventoryQuantities(gui, ingredStacks);
        
        List<IngredientDistribution> assignedIngredients = assignIngredients(ingredients, ingredStacks);
        if(assignedIngredients == null)
            return;
        
        assignIngredSlots(gui, ingredients, assignedIngredients);
        int quantity = calculateRecipeQuantity(assignedIngredients);
        
        if(quantity != 0)
            moveIngredients(gui, assignedIngredients, quantity);             
    }
    
    @SuppressWarnings("unchecked")
    private boolean clearIngredients(GuiContainer gui, List<PositionedStack> ingreds) {
        for(PositionedStack pstack : ingreds)
            for(Slot slot : (List<Slot>)gui.inventorySlots.inventorySlots)
                if(slot.xDisplayPosition == pstack.relx+offsetx && slot.yDisplayPosition == pstack.rely+offsety) {
                    if(!slot.getHasStack())
                        continue;
                    
                    FastTransferManager.clickSlot(gui, slot.slotNumber, 0, 1);
                    if(slot.getHasStack())
                        return false;
                }
        
        return true;
    }

    @SuppressWarnings("unchecked")
    private void moveIngredients(GuiContainer gui, List<IngredientDistribution> assignedIngredients, int quantity)
    {        
        for(IngredientDistribution distrib : assignedIngredients)
        {
            ItemStack pstack = distrib.permutation;
            int transferCap = quantity*pstack.stackSize;
            int transferred = 0;
            
            int destSlotIndex = 0;
            Slot dest = distrib.slots[0];
            int slotTransferred = 0;
            int slotTransferCap = pstack.getMaxStackSize();
            
            for(Slot slot : (List<Slot>)gui.inventorySlots.inventorySlots)
            {
                if(!slot.getHasStack() || !canMoveFrom(slot, gui))
                    continue;
                
                ItemStack stack = slot.getStack();
                if(!InventoryUtils.canStack(stack, pstack))
                    continue;
                
                FastTransferManager.clickSlot(gui, slot.slotNumber);
                int amount = Math.min(transferCap-transferred, stack.stackSize);
                for(int c = 0; c < amount; c++)
                {
                    FastTransferManager.clickSlot(gui, dest.slotNumber, 1);
                    transferred++;
                    slotTransferred++;
                    if(slotTransferred >= slotTransferCap)
                    {
                        destSlotIndex++;
                        if(destSlotIndex == distrib.slots.length)
                        {
                            dest = null;
                            break;
                        }
                        dest = distrib.slots[destSlotIndex];
                        slotTransferred = 0;
                    }
                }
                FastTransferManager.clickSlot(gui, slot.slotNumber);
                if(transferred >= transferCap || dest == null)
                    break;
            }
        }       
    }

    private int calculateRecipeQuantity(List<IngredientDistribution> assignedIngredients)
    {
        int quantity = Integer.MAX_VALUE;
        for(IngredientDistribution distrib : assignedIngredients)
        {
            DistributedIngred istack = distrib.distrib;
            if(istack.numSlots == 0)
                return 0;
            
            int allSlots = istack.invAmount;
            if(allSlots/istack.numSlots > istack.stack.getMaxStackSize())
                allSlots = istack.numSlots*istack.stack.getMaxStackSize();
            
            quantity = Math.min(quantity, allSlots/istack.distributed);
        }
        
        return quantity;
    }

    private Slot[][] assignIngredSlots(GuiContainer gui, List<PositionedStack> ingredients, List<IngredientDistribution> assignedIngredients)
    {
        Slot[][] recipeSlots = mapIngredSlots(gui, ingredients);//setup the slot map
        
        HashMap<Slot, Integer> distribution = new HashMap<Slot, Integer>();
        for(int i = 0; i < recipeSlots.length; i++)
            for(Slot slot : recipeSlots[i])
                if(!distribution.containsKey(slot))
                    distribution.put(slot, -1);
        
        HashSet<Slot> avaliableSlots = new HashSet<Slot>(distribution.keySet());
        HashSet<Integer> remainingIngreds = new HashSet<Integer>();
        ArrayList<LinkedList<Slot>> assignedSlots = new ArrayList<LinkedList<Slot>>();
        for(int i = 0; i < ingredients.size(); i++)
        {
            remainingIngreds.add(i);
            assignedSlots.add(new LinkedList<Slot>());
        }
                
        while(avaliableSlots.size() > 0 && remainingIngreds.size() > 0)
        {
            for(Iterator<Integer> iterator = remainingIngreds.iterator(); iterator.hasNext();)
            {
                int i = iterator.next();
                boolean assigned = false;
                DistributedIngred istack = assignedIngredients.get(i).distrib;
                
                for(Slot slot : recipeSlots[i])
                {
                    if(avaliableSlots.contains(slot))
                    {
                        avaliableSlots.remove(slot);
                        if(slot.getHasStack())
                            continue;
                        
                        istack.numSlots++;
                        assignedSlots.get(i).add(slot);
                        assigned = true;
                        break;
                    }
                }
                
                if(!assigned || istack.numSlots*istack.stack.getMaxStackSize() >= istack.invAmount)
                    iterator.remove();
            }
        }
        
        for(int i = 0; i < ingredients.size(); i++)
            assignedIngredients.get(i).slots = assignedSlots.get(i).toArray(new Slot[0]);
        return recipeSlots;
    }

    private List<IngredientDistribution> assignIngredients(List<PositionedStack> ingredients, List<DistributedIngred> ingredStacks)
    {
        ArrayList<IngredientDistribution> assignedIngredients = new ArrayList<IngredientDistribution>();
        for(PositionedStack posstack : ingredients)//assign what we need and have
        {
            DistributedIngred biggestIngred = null;
            ItemStack permutation = null;
            int biggestSize = 0;
            for(ItemStack pstack : posstack.items)
            {
                for(int j = 0; j < ingredStacks.size(); j++)
                {
                    DistributedIngred istack = ingredStacks.get(j);
                    if(!InventoryUtils.canStack(pstack, istack.stack) || istack.invAmount-istack.distributed < pstack.stackSize)
                        continue;
                    
                    int relsize = (istack.invAmount-istack.invAmount/istack.recipeAmount*istack.distributed)/pstack.stackSize;
                    if(relsize > biggestSize)
                    {
                        biggestSize = relsize;
                        biggestIngred = istack;
                        permutation = pstack;
                        break;
                    }
                }
            }
            
            if(biggestIngred == null)//not enough ingreds
                return null;

            biggestIngred.distributed+=permutation.stackSize;
            assignedIngredients.add(new IngredientDistribution(biggestIngred, permutation));
        }
        
        return assignedIngredients;
    }

    @SuppressWarnings("unchecked")
    private void findInventoryQuantities(GuiContainer gui, List<DistributedIngred> ingredStacks)
    {
        for(Slot slot : (List<Slot>)gui.inventorySlots.inventorySlots)//work out how much we have to go round
        {
            if(slot.getHasStack() && canMoveFrom(slot, gui))
            {
                ItemStack pstack = slot.getStack();
                DistributedIngred istack = findIngred(ingredStacks, pstack);
                if(istack != null)
                    istack.invAmount+=pstack.stackSize;
            }
        }
    }

    private List<DistributedIngred> getPermutationIngredients(List<PositionedStack> ingredients)
    {
        ArrayList<DistributedIngred> ingredStacks = new ArrayList<DistributedIngred>();
        for(PositionedStack posstack : ingredients)//work out what we need
        {
            for(ItemStack pstack : posstack.items)
            {
                DistributedIngred istack = findIngred(ingredStacks, pstack);
                if(istack == null)
                    ingredStacks.add(istack = new DistributedIngred(pstack));
                istack.recipeAmount+=pstack.stackSize;
            }
        }
        return ingredStacks;
    }

    public boolean canMoveFrom(Slot slot, GuiContainer gui) {
        return slot.inventory instanceof InventoryPlayer;
    }

    @SuppressWarnings("unchecked")
    public Slot[][] mapIngredSlots(GuiContainer gui, List<PositionedStack> ingredients)
    {
        Slot[][] recipeSlotList = new Slot[ingredients.size()][];
        for(int i = 0; i < ingredients.size(); i++)//identify slots
        {
            LinkedList<Slot> recipeSlots = new LinkedList<Slot>();
            PositionedStack pstack = ingredients.get(i);
            for(Slot slot : (List<Slot>)gui.inventorySlots.inventorySlots)
            {
                if(slot.xDisplayPosition == pstack.relx+offsetx && slot.yDisplayPosition == pstack.rely+offsety)
                {
                    recipeSlots.add(slot);
                    break;
                }
            }
            recipeSlotList[i] = recipeSlots.toArray(new Slot[0]);
        }
        return recipeSlotList;
    }

    public DistributedIngred findIngred(List<DistributedIngred> ingredStacks, ItemStack pstack)
    {
        for(DistributedIngred istack : ingredStacks)
            if(InventoryUtils.canStack(pstack, istack.stack))
                return istack;
        return null;
    }
}