package org.ultramine.mods.hawkeye.util;

import gnu.trove.iterator.TIntObjectIterator;
import gnu.trove.map.TIntObjectMap;
import gnu.trove.map.hash.TIntObjectHashMap;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;

import net.minecraft.init.Blocks;
import net.minecraft.inventory.IInventory;
import net.minecraft.inventory.InventoryLargeChest;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.tileentity.TileEntityChest;
import net.minecraft.util.ChatComponentText;
import net.minecraft.util.ChatStyle;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.IChatComponent;
import net.minecraft.world.World;

public class HawkInventoryUtil
{
	private static boolean isNBTEquals(ItemStack is1, ItemStack is2)
	{
		return is1.stackTagCompound == null && is2.stackTagCompound == null || is1.stackTagCompound != null && is1.stackTagCompound.equals(is2.stackTagCompound);
	}
	
	private static String toString(ItemStack is, int size)
	{
		return Item.getIdFromItem(is.getItem()) + (is.getItemDamage() != 0 ? (":" + is.getItemDamage()) : "") + "," + size;
	}
	
	private static NBTTagCompound toNBT(ItemStack is, int size)
	{
		NBTTagCompound nbt = new NBTTagCompound();
		is.writeToNBT(nbt);
		nbt.setInteger("count2", size);
		return nbt;
	}
	
	public static ItemStack loadFromNBT(NBTTagCompound nbt)
	{
		ItemStack is = ItemStack.loadItemStackFromNBT(nbt);
		int newSize = nbt.getInteger("count2");
		if(newSize != 0)
			is.stackSize = newSize;
		return is;
	}
	
	public static IInventory getInvContents(IInventory inv)
	{
		if(inv instanceof TileEntityChest)
		{
			TileEntityChest te = (TileEntityChest)inv;
			World world = te.getWorldObj();
			int x = te.xCoord;
			int y = te.yCoord;
			int z = te.zCoord;
			if (world.getBlock(x - 1, y, z) == Blocks.chest)
				inv = new InventoryLargeChest("container.chestDouble", (TileEntityChest)world.getTileEntity(x - 1, y, z), inv);
			else if (world.getBlock(x + 1, y, z) == Blocks.chest)
				inv = new InventoryLargeChest("container.chestDouble", inv, (TileEntityChest)world.getTileEntity(x + 1, y, z));
			else if (world.getBlock(x, y, z - 1) == Blocks.chest)
				inv = new InventoryLargeChest("container.chestDouble", (TileEntityChest)world.getTileEntity(x, y, z - 1), inv);
			else if (world.getBlock(x, y, z + 1) == Blocks.chest)
				inv = new InventoryLargeChest("container.chestDouble", inv, (TileEntityChest)world.getTileEntity(x, y, z + 1));
		}
		
		return inv;
	}

	public static TIntObjectMap<List<ItemStack>> compressInventory(IInventory inv)
	{
		inv = getInvContents(inv);
		TIntObjectMap<List<ItemStack>> items = new TIntObjectHashMap<List<ItemStack>>();
		for(int i = 0; i < inv.getSizeInventory(); i++)
		{
			ItemStack item = inv.getStackInSlot(i);
			if(item == null)
				continue;
			int key = Item.getIdFromItem(item.getItem()) | (item.getItemDamage() << 16);
			List<ItemStack> list = items.get(key);
			if(list != null)
			{
				boolean found = false;
				for(ItemStack is : list)
				{
					if(isNBTEquals(is, item))
					{
						is.stackSize += item.stackSize;
						found = true;
						break;
					}
				}
				
				if(!found)
				{
					list.add(item.copy());
				}
			}
			else
			{
				list = new ArrayList<ItemStack>(1);
				list.add(item.copy());
				items.put(key, list);
			}
		}
		return items;
	}

	/**
	 * Uncompress an inventory back into proper ItemStacks
	 * 
	 * @param comp
	 *            Compressed HashMap inventory
	 * @return ItemStack array
	 */
	public static ItemStack[] uncompressInventory(HashMap<String, Integer> comp)
	{
		List<ItemStack> inv = new ArrayList<ItemStack>();
		for(Entry<String, Integer> item : comp.entrySet())
		{
			int i = item.getValue();
			while(i > 0)
			{
				if(i < 64)
					inv.add(HawkBlockUtil.itemStringToStack(item.getKey(), i));
				else
					inv.add(HawkBlockUtil.itemStringToStack(item.getKey(), 64));
				i = i - 64;
			}
		}
		return inv.toArray(new ItemStack[0]);
	}

	public static NBTTagCompound createDifferenceNBT(TIntObjectMap<List<ItemStack>> before, TIntObjectMap<List<ItemStack>> after)
	{
		NBTTagList add = new NBTTagList();
		NBTTagList sub = new NBTTagList();
		for(TIntObjectIterator<List<ItemStack>> it = before.iterator(); it.hasNext();)
		{
			it.advance();
			int key = it.key();
			List<ItemStack> list = after.get(key);
			for(ItemStack is1 : it.value())
			{
				if(list == null)
				{
					sub.appendTag(toNBT(is1, is1.stackSize));
				}
				else
				{
					boolean found = false;
					for(ItemStack is2 : list)
					{
						if(isNBTEquals(is1, is2))
						{
							if(is1.stackSize > is2.stackSize)
								sub.appendTag(toNBT(is1, is1.stackSize - is2.stackSize));
							else if(is1.stackSize < is2.stackSize)
								add.appendTag(toNBT(is1, is2.stackSize - is1.stackSize));
							found = true;
							break;
						}
					}
					if(!found)
					{
						NBTTagCompound nbt = new NBTTagCompound();
						is1.writeToNBT(nbt);
						sub.appendTag(nbt);
					}
				}
			}
		}
		for(TIntObjectIterator<List<ItemStack>> it = after.iterator(); it.hasNext(); )
		{
			it.advance();
			int key = it.key();
			for(ItemStack item : it.value())
			{
				List<ItemStack> list = before.get(key);
				if(list == null)
				{
					add.appendTag(toNBT(item, item.stackSize));
				}
				else
				{
					boolean found = false;
					for(ItemStack is2 : list)
					{
						if(isNBTEquals(item, is2))
						{
							found = true;
							break;
						}
					}
					if(!found)
					{
						add.appendTag(toNBT(item, item.stackSize));
					}
				}
			}
		}
		
		if(add.tagCount() == 0 && sub.tagCount() == 0)
			return null;
		
		NBTTagCompound nbt = new NBTTagCompound();
		nbt.setTag("add", add);
		nbt.setTag("sub", sub);
		return nbt;
	}


	/**
	 * Takes two compressed inventories and returns a string representation of
	 * the difference
	 * 
	 * @param before
	 * @param after
	 * @return String in the form
	 *         item:data,amount&item:data,amount@item:data,amount
	 *         &item:data,amount where the first part is additions and second is
	 *         subtractions
	 */
	public static String createDifferenceString(TIntObjectMap<List<ItemStack>> before, TIntObjectMap<List<ItemStack>> after)
	{
		List<String> add = new ArrayList<String>();
		List<String> sub = new ArrayList<String>();
		for(TIntObjectIterator<List<ItemStack>> it = before.iterator(); it.hasNext(); )
		{
			it.advance();
			int key = it.key();
			List<ItemStack> list = after.get(key);
			ItemStack is1 = it.value().get(0);
			int size = 0;
			for(ItemStack is : it.value())
				size += is.stackSize;
			
			if(list == null)
			{
				sub.add(toString(is1, size));
			}
			else
			{
				int size2 = 0;
				for(ItemStack is : list)
					size2 += is.stackSize;
				if(size > size2)
					sub.add(toString(is1, size - size2));
				else if(size < size2)
					add.add(toString(is1, size2 - size));
			}
		}
		for(TIntObjectIterator<List<ItemStack>> it = after.iterator(); it.hasNext(); )
		{
			it.advance();
			int key = it.key();
			if(!before.containsKey(key))
			{
				ItemStack is1 = it.value().get(0);
				int size = 0;
				for(ItemStack is : it.value())
					size += is.stackSize;
				add.add(toString(is1, size));
			}
		}
		return HawkUtil.join(add, "&") + "@" + HawkUtil.join(sub, "&");
	}
	
	public static String createDifferenceString2(HashMap<String, Integer> before, HashMap<String, Integer> after)
	{
		List<String> add = new ArrayList<String>();
		List<String> sub = new ArrayList<String>();
		for(Entry<String, Integer> item : before.entrySet())
		{
			//If the item does not appear after changes
			if(!after.containsKey(item.getKey()))
				sub.add(item.getKey() + "," + item.getValue());
			//If the item is smaller after changes
			else if(item.getValue() > after.get(item.getKey()))
				sub.add(item.getKey() + "," + (item.getValue() - after.get(item.getKey())));
			//If the item is larger after changes
			else if(item.getValue() < after.get(item.getKey()))
				add.add(item.getKey() + "," + (after.get(item.getKey()) - item.getValue()));
		}
		for(Entry<String, Integer> item : after.entrySet())
		{
			//If the item does not appear before changes
			if(!before.containsKey(item.getKey()))
				add.add(item.getKey() + "," + item.getValue());
		}
		return HawkUtil.join(add, "&") + "@" + HawkUtil.join(sub, "&");
	}

	/**
	 * Takes an inventory difference string and forms two HashMaps containing
	 * the compressed inventory forms of the additions and subtractions
	 * 
	 * @param diff
	 *            The difference string to be processed
	 * @return a List of two HashMaps containing the additions and subtractions.
	 *         First list element is adds, second is subs.
	 */
	public static List<HashMap<String, Integer>> interpretDifferenceString(String diff)
	{
		List<HashMap<String, Integer>> ops = new ArrayList<HashMap<String, Integer>>();
		for(String changes : diff.split("@"))
		{
			HashMap<String, Integer> op = new HashMap<String, Integer>();
			for(String change : changes.split("&"))
			{
				if(change.length() == 0)
					continue;
				String[] item = change.split(",");
				op.put(item[0], Integer.parseInt(item[1]));
			}
			ops.add(op);
		}
		if(ops.size() == 1)
			ops.add(new HashMap<String, Integer>());
		return ops;
	}

	/**
	 * Creates a readable string representing the changes of a difference string
	 * 
	 * @param ops
	 *            additions and subtractions as supplied by
	 *            interpretDifferenceString
	 * @return
	 */
	public static IChatComponent createChangeComponent(List<HashMap<String, Integer>> ops)
	{
		ChatComponentText changeComp = new ChatComponentText("");
		if(ops.size() == 0)
			return changeComp;

		//Loop through ops
		List<String> add = new ArrayList<String>();
		for(Entry<String, Integer> item : ops.get(0).entrySet())
			add.add(item.getValue() + "x " + item.getKey());
		List<String> sub = new ArrayList<String>();
		for(Entry<String, Integer> item : ops.get(1).entrySet())
			sub.add(item.getValue() + "x " + item.getKey());

		//Build string
		if(add.size() > 0)
			changeComp.appendSibling(new ChatComponentText("+(" + HawkUtil.join(add, ", ") + ")").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.GREEN)));
		if(sub.size() > 0)
			changeComp.appendSibling(new ChatComponentText("-(" + HawkUtil.join(sub, ", ") + ")").setChatStyle(new ChatStyle().setColor(EnumChatFormatting.DARK_RED)));

		return changeComp;

	}

	public static boolean contains(IInventory inv, ItemStack item)
	{
		for(int i = 0; i < inv.getSizeInventory(); i++)
		{
			ItemStack is = inv.getStackInSlot(i);
			if(is != null && is.getItem() == item.getItem() && is.getItemDamage() == is.getItemDamage())
				return true;
		}

		return false;
	}

	public static int first(IInventory inv, ItemStack item)
	{
		for(int i = 0; i < inv.getSizeInventory(); i++)
		{
			ItemStack is = inv.getStackInSlot(i);
			if(is != null && is.getItem() == item.getItem() && is.getItemDamage() == is.getItemDamage())
				return i;
		}

		return -1;
	}

	public static int firstEmpty(IInventory inv)
	{
		for(int i = 0; i < inv.getSizeInventory(); i++)
		{
			if(inv.getStackInSlot(i) == null)
				return i;
		}

		return -1;
	}

	private static int firstPartial(IInventory inv, ItemStack filteredItem)
	{
		if(filteredItem == null)
		{
			return -1;
		}
		for(int i = 0; i < inv.getSizeInventory(); i++)
		{
			ItemStack cItem = inv.getStackInSlot(i);
			if(cItem != null && cItem.stackSize < cItem.getMaxStackSize() && cItem.getItem() == filteredItem.getItem() && cItem.getItemDamage() == filteredItem.getItemDamage())
			{
				return i;
			}
		}
		return -1;
	}

	public static HashMap<Integer, ItemStack> removeItem(IInventory inv, ItemStack... items)
	{
		HashMap<Integer, ItemStack> leftover = new HashMap<Integer, ItemStack>();

		// TODO: optimization

		for(int i = 0; i < items.length; i++)
		{
			ItemStack item = items[i];
			int toDelete = item.stackSize;

			while(true)
			{
				int first = first(inv, item);

				// Drat! we don't have this type in the inventory
				if(first == -1)
				{
					item.stackSize = toDelete;
					leftover.put(i, item);
					break;
				}
				else
				{
					ItemStack itemStack = inv.getStackInSlot(first);
					int amount = itemStack.stackSize;

					if(amount <= toDelete)
					{
						toDelete -= amount;
						// clear the slot, all used up
						inv.setInventorySlotContents(first, null);
					}
					else
					{
						// split the stack and store
						itemStack.stackSize = amount - toDelete;
						inv.setInventorySlotContents(first, itemStack);
						toDelete = 0;
					}
				}

				// Bail when done
				if(toDelete <= 0)
				{
					break;
				}
			}
		}
		return leftover;
	}

	public static HashMap<Integer, ItemStack> addItem(IInventory inv, ItemStack... items)
	{
		HashMap<Integer, ItemStack> leftover = new HashMap<Integer, ItemStack>();

		/* TODO: some optimization
		 *  - Create a 'firstPartial' with a 'fromIndex'
		 *  - Record the lastPartial per Material
		 *  - Cache firstEmpty result
		 */

		for(int i = 0; i < items.length; i++)
		{
			ItemStack item = items[i];
			while(true)
			{
				// Do we already have a stack of it?
				int firstPartial = firstPartial(inv, item);

				// Drat! no partial stack
				if(firstPartial == -1)
				{
					// Find a free spot!
					int firstFree = firstEmpty(inv);

					if(firstFree == -1)
					{
						// No space at all!
						leftover.put(i, item);
						break;
					}
					else
					{
						// More than a single stack!
						if(item.stackSize > inv.getInventoryStackLimit())
						{
							ItemStack stack = item.copy();
							stack.stackSize = inv.getInventoryStackLimit();
							inv.setInventorySlotContents(firstFree, stack);
							item.stackSize = item.stackSize - inv.getInventoryStackLimit();
						}
						else
						{
							// Just store it
							inv.setInventorySlotContents(firstFree, item);
							break;
						}
					}
				}
				else
				{
					// So, apparently it might only partially fit, well lets do just that
					ItemStack partialItem = inv.getStackInSlot(firstPartial);

					int amount = item.stackSize;
					int partialAmount = partialItem.stackSize;
					int maxAmount = partialItem.getMaxStackSize();

					// Check if it fully fits
					if(amount + partialAmount <= maxAmount)
					{
						partialItem.stackSize = (amount + partialAmount);
						break;
					}

					// It fits partially
					partialItem.stackSize = maxAmount;
					item.stackSize = (amount + partialAmount - maxAmount);
				}
			}
		}
		return leftover;
	}
}
