package flaxbeard.cyberware.api;

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

import javax.annotation.Nullable;

import net.minecraft.client.Minecraft;
import net.minecraft.entity.Entity;
import net.minecraft.entity.ai.attributes.IAttribute;
import net.minecraft.entity.ai.attributes.RangedAttribute;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.EnumFacing;
import net.minecraft.world.WorldServer;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.CapabilityInject;
import net.minecraftforge.fml.common.network.simpleimpl.SimpleNetworkWrapper;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import net.minecraftforge.oredict.OreDictionary;
import flaxbeard.cyberware.api.hud.UpdateHudColorPacket;
import flaxbeard.cyberware.api.item.ICyberware;
import flaxbeard.cyberware.api.item.ICyberware.Quality;
import flaxbeard.cyberware.api.item.IDeconstructable;
import flaxbeard.cyberware.api.item.IMenuItem;
import flaxbeard.cyberware.common.CyberwareConfig;
import flaxbeard.cyberware.common.network.CyberwareSyncPacket;

public final class CyberwareAPI
{
	/**
	 * Store any functional data of your Cyberware in NBT under this tag, which will be cleared when new items are added or removed
	 * to ensure stacking and such works
	 */
	public static final String DATA_TAG = "cyberwareFunctionData";
	
	public static final String QUALITY_TAG = "cyberwareQuality";

	/**
	 * Quality for Cyberware scavenged from mobs
	 */
	public static final Quality QUALITY_SCAVENGED = new Quality("cyberware.quality.scavenged", "cyberware.quality.scavenged.nameModifier", "scavenged");
	
	/**
	 * Quality for Cyberware built at the Engineering Table
	 */
	public static final Quality QUALITY_MANUFACTURED = new Quality("cyberware.quality.manufactured");

	@CapabilityInject(ICyberwareUserData.class)
	public static final Capability<ICyberwareUserData> CYBERWARE_CAPABILITY = null;
	
	/**
	 * Maximum Tolerance, per-player
	 */
	public static final IAttribute TOLERANCE_ATTR = new RangedAttribute(null, "cyberware.tolerance", CyberwareConfig.ESSENCE, 0.0f, Double.MAX_VALUE).setDescription("Tolerance").setShouldWatch(true);
	
	public static Map<ItemStack, ICyberware> linkedWare = new HashMap<ItemStack, ICyberware>();
	
	
	private static float[] mainColor = new float[] { 0F, 1F, 1F };
	
	public static SimpleNetworkWrapper PACKET_HANDLER;
	
	/**
	 * Sets the HUD color for the Hudjack, radial menu, and other AR HUD elements
	 * 
	 * @param color	A float representation of the desired color
	 */
	@SideOnly(Side.CLIENT)
	public static void setHUDColor(float[] color)
	{
		EntityPlayer p = Minecraft.getMinecraft().thePlayer;
		if (CyberwareAPI.hasCapability(p))
		{
			CyberwareAPI.getCapability(p).setHudColor(color);
		}
	}
	
	public static void syncHUDColor()
	{
		PACKET_HANDLER.sendToServer(new UpdateHudColorPacket(getHUDColorHex()));
	}
	
	/**
	 * Sets the HUD color for the Hudjack, radial menu, and other AR HUD elements
	 * 
	 * @param color	A hexadecimal representation of the desired color
	 */
	@SideOnly(Side.CLIENT)
	public static void setHUDColor(int hexVal)
	{
		EntityPlayer p = Minecraft.getMinecraft().thePlayer;
		if (CyberwareAPI.hasCapability(p))
		{
			CyberwareAPI.getCapability(p).setHudColor(hexVal);
		}
	}
	
	@SideOnly(Side.CLIENT)
	public static void setHUDColor(float r, float g, float b)
	{
		setHUDColor(new float[] { r, g, b });
	}
	
	@SideOnly(Side.CLIENT)
	public static int getHUDColorHex()
	{
		EntityPlayer p = Minecraft.getMinecraft().thePlayer;
		if (CyberwareAPI.hasCapability(p))
		{
			return CyberwareAPI.getCapability(p).getHudColorHex();
		}
		return 0;
	}
	
	@SideOnly(Side.CLIENT)
	public static float[] getHUDColor()
	{
		EntityPlayer p = Minecraft.getMinecraft().thePlayer;
		if (CyberwareAPI.hasCapability(p))
		{
			return CyberwareAPI.getCapability(p).getHudColor();
		}
		return new float[] { 0F, 0F, 0F };
	}
	
	/**
	 * Can be used by your ICyberware implementation's setQuality function. Helper method that
	 * writes a quality to an easily accessible NBT tag. See the partner function, readQualityTag
	 * 
	 * @param stack	The stack to write to
	 * @return		The modified stack
	 */
	public static ItemStack writeQualityTag(ItemStack stack, Quality quality)
	{
		if (stack == null) return stack;
		if (!stack.hasTagCompound())
		{
			stack.setTagCompound(new NBTTagCompound());
		}
		stack.getTagCompound().setString(QUALITY_TAG, quality.getUnlocalizedName());
		return stack;
	}
	
	public static Quality getQualityTag(ItemStack stack)
	{
		if (stack == null) return null;
		if (!stack.hasTagCompound() || !stack.getTagCompound().hasKey(QUALITY_TAG))
		{
			return null;
		}
		return Quality.getQualityFromString(stack.getTagCompound().getString(QUALITY_TAG));
	}
	
	/**
	 * Clears all NBT data from Cyberware related to its function, things like power storage or oxygen storage
	 * This ensures that removed Cyberware will stack. This should only be called on Cyberware that is being removed
	 * from the body or otherwise reset - otherwise it may interrupt functionality.
	 * 
	 * @param stack	The ItemStack to sanitize
	 * @return		A sanitized version of the stack
	 */
	public static ItemStack sanitize(ItemStack stack)
	{
		if (stack != null)
		{
			if (stack.hasTagCompound() && stack.getTagCompound().hasKey(DATA_TAG))
			{
				stack.getTagCompound().removeTag(DATA_TAG);
			}
			if (stack.hasTagCompound() && stack.getTagCompound().hasNoTags())
			{
				stack.setTagCompound(null);
			}
		}
		
		return stack;
	}
	
	/**
	 * Gets the NBT data for Cyberware related to its function. This data is removed when a piece of Cyberware
	 * is removed, and is not counted when determining whether Cyberware stacks are the same for purposes of merging
	 * and such. This function will create a data tag if one does not exist.
	 * 
	 * @param stack	The ItemStack for which you want the data
	 * @return		The data, in the form of an NBTTagCompound
	 */
	public static NBTTagCompound getCyberwareNBT(ItemStack stack)
	{
		if (!stack.hasTagCompound())
		{
			stack.setTagCompound(new NBTTagCompound());
		}
		if (!stack.getTagCompound().hasKey(DATA_TAG))
		{
			stack.getTagCompound().setTag(DATA_TAG, new NBTTagCompound());
		}
		
		return stack.getTagCompound().getCompoundTag(DATA_TAG);
	}
	
	public static boolean areCyberwareStacksEqual(ItemStack stack1, ItemStack stack2)
	{
		if (stack1 == null || stack2 == null) return false;
		
		ItemStack sanitized1 = sanitize(ItemStack.copyItemStack(stack1));
		ItemStack sanitized2 = sanitize(ItemStack.copyItemStack(stack2));
		return sanitized1.getItem() == sanitized2.getItem() && sanitized1.getItemDamage() == sanitized2.getItemDamage() && ItemStack.areItemStackTagsEqual(stack1, stack2);
	}
	
	/**
	 * Links an ItemStack to an instance of ICyberware. This option is generally worse than
	 * implementing ICyberware in your Item, but if you don't have access to the Item it's the
	 * best option. This version of the method links a specific meta value.
	 * 
	 * @param stack	The ItemStack to link
	 * @param link	An instance of ICyberware to link it to
	 */
	public static void linkCyberware(ItemStack stack, ICyberware link)
	{
		if (stack == null) return;
		
		ItemStack key = new ItemStack(stack.getItem(), 1, stack.getItemDamage());
		linkedWare.put(key, link);
	}
	
	/**
	 * Links an Item to an instance of ICyberware. This option is generally worse than
	 * implementing ICyberware in your Item, but if you don't have access to the Item it's the
	 * best option. This version of the method links all meta values.
	 * 
	 * @param item	The Item to link
	 * @param link	An instance of ICyberware to link it to
	 */
	public static void linkCyberware(Item item, ICyberware link)
	{
		if (item == null) return;
		
		ItemStack key = new ItemStack(item, 1, OreDictionary.WILDCARD_VALUE);
		linkedWare.put(key, link);
	}
	
	/**
	 * Determines if the inputted item stack is Cyberware. This means its item either
	 * implements ICyberware or is linked to one (in the case of vanilla items)
	 * 
	 * @param stack	The ItemStack to test
	 * @return		If the stack is valid Cyberware
	 */
	public static boolean isCyberware(ItemStack stack)
	{
		return stack != null && (stack.getItem() instanceof ICyberware || getLinkedWare(stack) != null);
	}
	
	/**
	 * Returns an instance of ICyberware linked with an itemstack, usually
	 * the item which extends ICyberware, though it may be a standalone
	 * ICyberware-implementing object
	 * 
	 * @param stack	The ItemStack, from which the linked ICyberware is found
	 * @return		The linked instance of ICyberware
	 */
	public static ICyberware getCyberware(ItemStack stack)
	{
		if (stack != null)
		{
			if (stack.getItem() instanceof ICyberware)
			{
				return (ICyberware) stack.getItem();
			}
			else if (getLinkedWare(stack) != null)
			{
				return getLinkedWare(stack);
			}
		}
		
		throw new RuntimeException("Cannot call getCyberware on a non-cyberware item!");
	}
	
	/**
	 * Determines if the inputted item stack can be destroyed in the Engineering Table,
	 * meaning it implements IDeconstructable.
	 * 
	 * @param stack	The ItemStack to test
	 * @return		If the stack can be deconstructed.
	 */
	public static boolean canDeconstruct(ItemStack stack)
	{
		return stack != null && (stack.getItem() instanceof IDeconstructable) && ((IDeconstructable) stack.getItem()).canDestroy(stack);
	}

	/**
	 * Returns a list of ItemStacks containing the components of a destructable
	 * item.
	 * 
	 * @param stack	The ItemStack to test
	 * @return		The components of the item
	 */
	public static ItemStack[] getComponents(ItemStack stack)
	{
		if (stack != null)
		{
			if (stack.getItem() instanceof IDeconstructable)
			{
				return ((IDeconstructable) stack.getItem()).getComponents(stack).clone();
			}
		}
		
		throw new RuntimeException("Cannot call getComponents on a non-cyberware item!");
	}
	
	private static ICyberware getLinkedWare(ItemStack stack)
	{
		if (stack == null) return null;
		
		ItemStack test = new ItemStack(stack.getItem(), 1, stack.getItemDamage());
		ICyberware result = getWareFromKey(test);
		if (result != null)
		{
			return result;
		}
		
		ItemStack testGeneric = new ItemStack(stack.getItem(), 1, OreDictionary.WILDCARD_VALUE);
		result = getWareFromKey(testGeneric);
		if (result != null)
		{
			return result;
		}
		
		return null;
	}
	
	private static ICyberware getWareFromKey(ItemStack key)
	{
		for (Entry<ItemStack, ICyberware> entry : linkedWare.entrySet())
		{
			ItemStack entryKey = entry.getKey();
			if (key.getItem() == entryKey.getItem() && key.getItemDamage() == entryKey.getItemDamage())
			{
				return entry.getValue();
			}
		}
		return null;
	}
	
	/**
	 * A shortcut method to determine if the entity that is inputted
	 * has ICyberwareUserData. Works with null entites.
	 * 
	 * @param targetEntity	The entity to test
	 * @return				If the entity has ICyberwareUserData
	 */
	public static boolean hasCapability(@Nullable Entity targetEntity)
	{
		if (targetEntity == null) return false;
		return targetEntity.hasCapability(CYBERWARE_CAPABILITY, EnumFacing.EAST);
	}
	
	/**
	 * Assistant method to hasCapability. A shortcut to get you the ICyberwareUserData
	 * of a specific entity. Note that you must verify if it has the capability first.
	 * 
	 * @param targetEntity	The entity whose ICyberwareUserData you want
	 * @return				The ICyberwareUserData associated with the entity
	 */
	public static ICyberwareUserData getCapability(Entity targetEntity)
	{
		return targetEntity.getCapability(CYBERWARE_CAPABILITY, EnumFacing.EAST);
	}
	
	/**
	 * A shortcut method for event handlers and the like to quickly tell if an entity
	 * has a piece of Cyberware installed. Can handle null entites and entities without
	 * ICyberwareUserData.
	 * 
	 * @param targetEntity	The entity you want to check
	 * @param stack			The Cyberware you want to check for
	 * @return				If the entity has the Cyberware
	 */
	public static boolean isCyberwareInstalled(@Nullable Entity targetEntity, ItemStack stack)
	{
		if (!hasCapability(targetEntity)) return false;
		
		ICyberwareUserData cyberware = getCapability(targetEntity);
		return cyberware.isCyberwareInstalled(stack);
	}
	
	/**
	 * A shortcut method for event handlers and the like to quickly determine what level of
	 * Cyberware is installed. Returns 0 if none. Can handle null entites and entities without
	 * ICyberwareUserData.
	 * 
	 * @param targetEntity	The entity you want to check
	 * @param stack			The Cyberware you want to check for
	 * @return				If the entity has the Cyberware, the level, or 0 if not
	 */
	public static int getCyberwareRank(@Nullable Entity targetEntity, ItemStack stack)
	{
		if (!hasCapability(targetEntity)) return 0;
		
		ICyberwareUserData cyberware = getCapability(targetEntity);
		return cyberware.getCyberwareRank(stack);
	}
	
	/**
	 * A shortcut method for event handlers and the like to get the itemstack for a piece
	 * of cyberware. Useful for NBT data. Can handle null entites and entities without
	 * ICyberwareUserData.
	 * 
	 * @param targetEntity	The entity you want to check
	 * @param stack			The Cyberware you want to check for
	 * @return				The ItemStack found, or null if none
	 */
	public static ItemStack getCyberware(@Nullable Entity targetEntity, ItemStack stack)
	{
		if (!hasCapability(targetEntity)) return null;
		
		ICyberwareUserData cyberware = getCapability(targetEntity);
		return cyberware.getCyberware(stack);
	}
	
	public static void updateData(Entity targetEntity)
	{
		if (!targetEntity.worldObj.isRemote)
		{
			WorldServer world = (WorldServer) targetEntity.worldObj;
			
			NBTTagCompound nbt = CyberwareAPI.getCapability(targetEntity).serializeNBT();
			
			if (targetEntity instanceof EntityPlayer)
			{
				PACKET_HANDLER.sendTo(new CyberwareSyncPacket(nbt, targetEntity.getEntityId()), (EntityPlayerMP) targetEntity);
				//System.out.println("Sent data for player " + ((EntityPlayer) targetEntity).getName() + " to that player's client");
			}

			for (EntityPlayer trackingPlayer : world.getEntityTracker().getTrackingPlayers(targetEntity))
			{
				PACKET_HANDLER.sendTo(new CyberwareSyncPacket(nbt, targetEntity.getEntityId()), (EntityPlayerMP) trackingPlayer);
				
				if (targetEntity instanceof EntityPlayer)
				{
					//System.out.println("Sent data for player " + ((EntityPlayer) targetEntity).getName() + " to player " + trackingPlayer.getName());
				}
			}
		}

	}
	
	public static void useActiveItem(Entity entity, ItemStack stack)
	{
		((IMenuItem) stack.getItem()).use(entity, stack);
	}

}