package codechicken.translocator;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;

import codechicken.core.IGuiPacketSender;
import codechicken.core.ServerUtils;
import codechicken.lib.math.MathHelper;
import codechicken.lib.inventory.InventoryRange;
import codechicken.lib.inventory.InventorySimple;
import codechicken.lib.inventory.InventoryUtils;
import codechicken.lib.packet.PacketCustom;
import codechicken.lib.vec.BlockCoord;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;

public class TileItemTranslocator extends TileTranslocator
{
    public class ItemAttachment extends Attachment
    {
        boolean regulate = false;
        boolean a_powering = false;
        boolean signal = false;
        
        ItemStack[] filters = new ItemStack[9];
        
        public ItemAttachment(int side)
        {
            super(side);
        }

        public void setPowering(boolean b)
        {
            if((signal || !b) && b != a_powering)
            {
                a_powering = b;
                BlockCoord pos = new BlockCoord(TileItemTranslocator.this);
                worldObj.notifyBlocksOfNeighborChange(pos.x, pos.y, pos.z, Blocks.redstone_wire);
                pos.offset(side);
                worldObj.notifyBlocksOfNeighborChange(pos.x, pos.y, pos.z, Blocks.redstone_wire);
                markUpdate();
            }
        }
        
        @Override
        public boolean activate(EntityPlayer player, int subPart)
        {
            ItemStack held = player.inventory.getCurrentItem();
            if(held == null)
            {
                return super.activate(player, subPart);
            }
            else if(held.getItem() == Translocator.itemDiamondNugget && !regulate)
            {
                regulate = true;
                
                if(!player.capabilities.isCreativeMode)
                    held.stackSize--;
                markUpdate();
                return true;
            }
            else if(held.getItem() == Items.iron_ingot && !signal)
            {
                signal = true;
                
                if(!player.capabilities.isCreativeMode)
                    held.stackSize--;
                markUpdate();
                return true;
            }
            else
            {
                return super.activate(player, subPart);
            }
        }
        
        @Override
        public void stripModifiers()
        {
            super.stripModifiers();
            if(regulate)
            {
                regulate = false;
                dropItem(new ItemStack(Translocator.itemDiamondNugget));
            }
            if(signal)
            {
                setPowering(false);
                signal = false;
                dropItem(new ItemStack(Items.iron_ingot));
            }
        }
        
        @Override
        public Collection<ItemStack> getDrops()
        {
            Collection<ItemStack> stuff = super.getDrops();
            if(regulate)
                stuff.add(new ItemStack(Translocator.itemDiamondNugget));
            if(signal)
                stuff.add(new ItemStack(Items.iron_ingot));
            return stuff;
        }
        
        @Override
        public int getIconIndex()
        {
            int i = super.getIconIndex();
            if(regulate)
                i|=8;
            if(signal)
                i|= a_powering? 32 : 16;
            return i;
        }
        
        @Override
        public void openGui(EntityPlayer player)
        {
            openItemGui(player, filters, regulate ? "translocator.regulate" : "translocator.filter");
        }

        private void openItemGui(EntityPlayer player, ItemStack[] filters, final String string)
        {
            ServerUtils.openSMPContainer((EntityPlayerMP) player, 
                new ContainerItemTranslocator(new InventorySimple(filters, filterStackLimit())
            {
                @Override
                public void markDirty()
                {
                    markUpdate();
                }
            }, player.inventory), new IGuiPacketSender()
            {
                @Override
                public void sendPacket(EntityPlayerMP player, int windowId)
                {
                    PacketCustom packet = new PacketCustom(TranslocatorSPH.channel, 4);
                    packet.writeByte(windowId);
                    packet.writeShort(filterStackLimit());
                    packet.writeString(string);
                    
                    packet.sendToPlayer(player);
                }
            });
        }
        
        private int filterStackLimit()
        {
            if(regulate)
                return 65535;
            if(fast)
                return 64;
            return 1;
        }

        @Override
        public void read(NBTTagCompound tag)
        {
            super.read(tag);
            regulate = tag.getBoolean("regulate");
            signal = tag.getBoolean("signal");
            a_powering = tag.getBoolean("powering");
            InventoryUtils.readItemStacksFromTag(filters, tag.getTagList("filters", 10));
        }
        
        @Override
        public NBTTagCompound write(NBTTagCompound tag)
        {
            tag.setBoolean("regulate", regulate);
            tag.setBoolean("signal", signal);
            tag.setBoolean("powering", a_powering);
            tag.setTag("filters", InventoryUtils.writeItemStacksToTag(filters, 65536));
            return super.write(tag);
        }
        
        @Override
        public void read(PacketCustom packet, boolean described)
        {
            super.read(packet, described);
            regulate = packet.readBoolean();
            signal = packet.readBoolean();
            a_powering = packet.readBoolean();
        }
        
        @Override
        public void write(PacketCustom packet)
        {
            super.write(packet);
            packet.writeBoolean(regulate);
            packet.writeBoolean(signal);
            packet.writeBoolean(a_powering);
        }
    }
    
    public class MovingItem
    {
        public int src;
        public int dst;
        public ItemStack stack;
        
        public double a_progress;
        public double b_progress;
        
        public MovingItem(PacketCustom packet)
        {
            int b = packet.readUByte();
            src = b>>4;
            dst = b&0xF;
            stack = packet.readItemStack();
        }
        
        public boolean update()
        {
            if(a_progress >= 1)
                return true;
            
            b_progress = a_progress;
            a_progress = MathHelper.approachLinear(a_progress, 1, 0.2);
            return false;
        }
    }
    
    public LinkedList<MovingItem> movingItems = new LinkedList<MovingItem>();
    
    @Override
    public void readFromNBT(NBTTagCompound tag)
    {
        super.readFromNBT(tag);
        if(tag.hasKey("items"))
            for(Attachment a : attachments)
                if(a != null)
                    InventoryUtils.readItemStacksFromTag(((ItemAttachment)a).filters, tag.getTagList("items", 10));
    }
    
    @Override
    public void createAttachment(int side)
    {
        attachments[side] = new ItemAttachment(side);
    }
    
    @Override
    public void updateEntity()
    {
        super.updateEntity();
        if(worldObj.isRemote)
        {
            for(Iterator<MovingItem> iterator = movingItems.iterator(); iterator.hasNext();)
                if(iterator.next().update())
                    iterator.remove();
        }
        else
        {
            //move those items
            BlockCoord pos = new BlockCoord(this);
            InventoryRange[] attached = new InventoryRange[6];
            
            for(int i = 0; i < 6; i++)
            {
                Attachment a = attachments[i];
                if(a == null)
                    continue;
    
                BlockCoord invpos = pos.copy().offset(i);
                IInventory inv = InventoryUtils.getInventory(worldObj, invpos.x, invpos.y, invpos.z);
                if(inv == null)
                {
                    harvestPart(i, true);
                    continue;
                }
                attached[i] = new InventoryRange(inv, i^1);
            }
            
            for(int i = 0; i < 6; i++)
            {
                ItemAttachment ia = (ItemAttachment) attachments[i];
                if(ia == null || !ia.a_eject)
                    continue;
                
                int largestQuantity = 0;
                int largestSlot = 0;
                InventoryRange access = attached[i];
                for(int slot : access.slots)
                {
                    ItemStack stack = access.inv.getStackInSlot(slot);
                    if(stack == null || !access.canExtractItem(slot, stack) || 
                            stack.stackSize == 0)//stack size 0 hack
                        continue;
                    
                    int quantity = ia.fast ? stack.stackSize : 1;
                    if(quantity <= largestQuantity)
                        continue;
    
                    quantity = Math.min(quantity, extractAmount(stack, ia, access));
                    if(quantity <= largestQuantity)
                        continue;
                    
                    quantity = Math.min(quantity, insertAmount(stack, attached));
                    if(quantity <= largestQuantity)
                        continue;
                    
                    largestSlot = slot;
                    largestQuantity = quantity;
                }
                
                if(largestQuantity > 0)
                {
                    ItemStack move = InventoryUtils.copyStack(access.inv.getStackInSlot(largestSlot), largestQuantity);
                    spreadOutput(move, i, false, attached);
                    spreadOutput(move, i, true, attached);
                    
                    InventoryUtils.decrStackSize(access.inv, largestSlot, largestQuantity-move.stackSize);
                }
            }
            
            boolean allSatisfied = true;
            for(int i = 0; i < 6; i++)
            {
                ItemAttachment ia = (ItemAttachment) attachments[i];
                if(ia != null && !ia.a_eject)
                {
                    boolean b = isSatsified(ia, attached[i]);
                    ia.setPowering(b);
                    if(!b)
                        allSatisfied = false;
                }
            }
            
            for(int i = 0; i < 6; i++)
            {
                ItemAttachment ia = (ItemAttachment) attachments[i];
                if(ia != null && ia.signal && ia.a_eject)
                    ia.setPowering(allSatisfied || !canTransferFilter(ia, attached[i], attached));
            }
        }
    }
    
    
    private boolean matches(ItemStack stack, ItemStack filter)
    {
        return stack.getItem() == filter.getItem() &&
            (!filter.getHasSubtypes() || filter.getItemDamage() == stack.getItemDamage()) && 
            ItemStack.areItemStackTagsEqual(filter, stack);
    }

    private boolean canTransferFilter(ItemAttachment ia, InventoryRange access, InventoryRange[] attached)
    {
        boolean filterSet = false;
        for(ItemStack filter : ia.filters)
            if(filter != null)
            {
                filterSet = true;
                if((!ia.regulate || countMatchingStacks(access, filter, false) > filterCount(ia, filter)) && 
                        insertAmount(filter, attached) > 0)
                    return true;
            }
        
        return !filterSet;
    }

    private boolean isSatsified(ItemAttachment ia, InventoryRange access)
    {
        boolean filterSet = false;
        for(ItemStack filter : ia.filters)
            if(filter != null)
            {
                filterSet = true;
                if(ia.regulate)
                {
                    if(countMatchingStacks(access, filter, !ia.a_eject) < filterCount(ia, filter))
                        return false;
                }
                else
                {
                    if(InventoryUtils.getInsertibleQuantity(access, filter) > 0)
                        return false;
                }
            }
        
        return filterSet || !hasEmptySpace(access);
    }

    private boolean hasEmptySpace(InventoryRange inv)
    {
        for(int slot : inv.slots)
        {
            ItemStack stack = inv.inv.getStackInSlot(slot);
            if(inv.canInsertItem(slot, new ItemStack(Items.diamond)) &&
                    (stack == null || 
                    stack.isStackable() && stack.stackSize < Math.min(stack.getMaxStackSize(), inv.inv.getInventoryStackLimit())))
                return true;
        }
        return false;
    }

    private int filterCount(ItemAttachment ia, ItemStack stack)
    {
        boolean filterSet = false;
        int match = 0;
        for(ItemStack filter : ia.filters)
            if(filter != null)
            {
                filterSet = true;
                if(matches(stack, filter))
                    match += filter.stackSize;
            }
        
        return filterSet ? match : -1;
    }

    private void spreadOutput(ItemStack move, int src, boolean rspass, InventoryRange[] attached)
    {
        if(move.stackSize == 0)
            return;
        
        int outputCount = 0;
        int[] outputQuantities = new int[6];
        for(int i = 0; i < 6; i++)
        {
            ItemAttachment ia = (ItemAttachment) attachments[i];
            if(ia != null && !ia.a_eject && ia.redstone == rspass)
            {
                outputQuantities[i] = insertAmount(move, ia, attached[i]);
                if(outputQuantities[i] > 0)
                    outputCount++;
            }
        }
        
        for(int dst = 0; dst < 6 && move.stackSize > 0; dst++)
        {
            int qty = outputQuantities[dst];
            if(qty <= 0)
                continue;
            
            qty = Math.min(qty, move.stackSize/outputCount + worldObj.rand.nextInt(move.stackSize%outputCount+1));
            outputCount--;
            
            if(qty == 0)
                continue;

            InventoryRange range = attached[dst];
            ItemStack add = InventoryUtils.copyStack(move, qty);
            InventoryUtils.insertItem(range, add, false);
            move.stackSize-=qty;
            
            sendTransferPacket(src, dst, add);
        }
    }

    private int countMatchingStacks(InventoryRange inv, ItemStack filter, boolean insertable)
    {
        int c = 0;
        for(int slot : inv.slots)
        {
            ItemStack stack = inv.inv.getStackInSlot(slot);
            if(stack != null && matches(filter, stack) && 
                    (insertable ? inv.canInsertItem(slot, stack) : inv.canExtractItem(slot, stack)))
                c+=stack.stackSize;
        }
        return c;
    }

    private void sendTransferPacket(int i, int j, ItemStack add)
    {
        PacketCustom packet = new PacketCustom(TranslocatorSPH.channel, 2);
        packet.writeCoord(xCoord, yCoord, zCoord);
        packet.writeByte(i << 4 | j);
        packet.writeItemStack(add);
        packet.sendToChunk(worldObj, xCoord>>4, zCoord>>4);
    }

    private int insertAmount(ItemStack stack, InventoryRange[] attached)
    {
        int insertAmount = 0;
        for(int i = 0; i < 6; i++)
        {
            ItemAttachment ia = (ItemAttachment) attachments[i];
            if(ia == null || ia.a_eject)
                continue;
            
            insertAmount+=insertAmount(stack, ia, attached[i]);
        }
        return insertAmount;
    }

    private int insertAmount(ItemStack stack, ItemAttachment ia, InventoryRange range)
    {
        int filter = filterCount(ia, stack);
        if(filter == 0)
            return 0;
        
        int fit = InventoryUtils.getInsertibleQuantity(range, stack);
        if(fit == 0)
            return 0;

        if(ia.regulate && filter > 0)
            fit = Math.min(fit, filter-countMatchingStacks(range, stack, true));
        
        return fit > 0 ? fit : 0;
    }

    private int extractAmount(ItemStack stack, ItemAttachment ia, InventoryRange range)
    {
        int filter = filterCount(ia, stack);
        if(filter == 0)
            return ia.regulate ? stack.getMaxStackSize() : 0;
        
        int qty = filter < 0 ? stack.getMaxStackSize() : filter;
        
        if(ia.regulate && filter > 0)
            qty = Math.min(qty, countMatchingStacks(range, stack, false)-filter);
        
        return qty > 0 ? qty : 0;
    }
    
    @Override
    public void handleDescriptionPacket(PacketCustom packet)
    {
        if(packet.getType() == 2)
            movingItems.add(new MovingItem(packet));
        else
            super.handleDescriptionPacket(packet);
    }
    
    @Override
    public int strongPowerLevel(int side)
    {
        ItemAttachment ia = (ItemAttachment)attachments[side^1];
        if(ia != null && ia.a_powering)
            return 15;
        return 0;
    }
}