package com.hbm.items.tool;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import org.lwjgl.opengl.GL11;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.hbm.blocks.ModBlocks;
import com.hbm.config.ClientConfig;
import com.hbm.handler.HbmKeybinds.EnumKeybind;
import com.hbm.handler.ability.AvailableAbilities;
import com.hbm.handler.ability.IBaseAbility;
import com.hbm.handler.ability.IToolAreaAbility;
import com.hbm.handler.ability.IToolHarvestAbility;
import com.hbm.handler.ability.ToolPreset;
import com.hbm.interfaces.IItemHUD;
import com.hbm.inventory.gui.GUIScreenToolAbility;
import com.hbm.items.IItemControlReceiver;
import com.hbm.items.IKeybindReceiver;
import com.hbm.main.MainRegistry;
import com.hbm.packet.PacketDispatcher;
import com.hbm.packet.toclient.PlayerInformPacket;
import com.hbm.tileentity.IGUIProvider;
import com.hbm.util.Tuple.Pair;

import api.hbm.item.IDepthRockTool;
import cpw.mods.fml.relauncher.ReflectionHelper;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.gui.GuiIngame;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.SharedMonsterAttributes;
import net.minecraft.entity.ai.attributes.AttributeModifier;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.init.Blocks;
import net.minecraft.inventory.Container;
import net.minecraft.item.EnumRarity;
import net.minecraft.item.ItemStack;
import net.minecraft.item.ItemTool;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.network.play.server.S23PacketBlockChange;
import net.minecraft.stats.StatList;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.world.World;
import net.minecraftforge.client.event.RenderGameOverlayEvent.ElementType;
import net.minecraftforge.client.event.RenderGameOverlayEvent.Pre;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.common.IShearable;
import net.minecraftforge.event.world.BlockEvent;

public class ItemToolAbility extends ItemTool implements IDepthRockTool, IGUIProvider, IItemControlReceiver, IKeybindReceiver, IItemHUD {

	protected boolean isShears = false;
	protected EnumToolType toolType;
	protected EnumRarity rarity = EnumRarity.common;
	//was there a reason for this to be private?
	protected float damage;
	protected double movement;
	protected AvailableAbilities availableAbilities = new AvailableAbilities().addToolAbilities();
	protected boolean rockBreaker = false;

	public static enum EnumToolType {

		PICKAXE(
				Sets.newHashSet(new Material[] { Material.iron, Material.anvil, Material.rock, Material.glass }),
				Sets.newHashSet(new Block[] { Blocks.cobblestone, Blocks.double_stone_slab, Blocks.stone_slab, Blocks.stone, Blocks.sandstone, Blocks.mossy_cobblestone, Blocks.iron_ore, Blocks.iron_block, Blocks.coal_ore, Blocks.gold_block, Blocks.gold_ore, Blocks.diamond_ore, Blocks.diamond_block, Blocks.ice, Blocks.netherrack, Blocks.lapis_ore, Blocks.lapis_block, Blocks.redstone_ore, Blocks.lit_redstone_ore, Blocks.rail, Blocks.detector_rail, Blocks.golden_rail, Blocks.activator_rail })
		),
		AXE(
				Sets.newHashSet(new Material[] { Material.wood, Material.plants, Material.vine }),
				Sets.newHashSet(new Block[] { Blocks.planks, Blocks.bookshelf, Blocks.log, Blocks.log2, Blocks.chest, Blocks.pumpkin, Blocks.lit_pumpkin })
		),
		SHOVEL(
				Sets.newHashSet(new Material[] { Material.clay, Material.sand, Material.ground, Material.snow, Material.craftedSnow }),
				Sets.newHashSet(new Block[] { Blocks.grass, Blocks.dirt, Blocks.sand, Blocks.gravel, Blocks.snow_layer, Blocks.snow, Blocks.clay, Blocks.farmland, Blocks.soul_sand, Blocks.mycelium })
		),
		MINER(
				Sets.newHashSet(new Material[] { Material.grass, Material.iron, Material.anvil, Material.glass, Material.rock, Material.clay, Material.sand, Material.ground, Material.snow, Material.craftedSnow })
		);

		private EnumToolType(Set<Material> materials) {
			this.materials = materials;
		}

		private EnumToolType(Set<Material> materials, Set<Block> blocks) {
			this.materials = materials;
			this.blocks = blocks;
		}

		public Set<Material> materials = new HashSet();
		public Set<Block> blocks = new HashSet();
	}

	public ItemToolAbility setShears() {
		this.isShears = true;
		return this;
	}

	public ItemToolAbility(float damage, double movement, ToolMaterial material, EnumToolType type) {
		super(0, material, type.blocks);
		this.damage = damage;
		this.movement = movement;
		this.toolType = type;

		// hacky workaround, might be good to rethink this entire system
		if(type == EnumToolType.MINER) {
			this.setHarvestLevel("pickaxe", material.getHarvestLevel());
			this.setHarvestLevel("shovel", material.getHarvestLevel());
		} else {
			this.setHarvestLevel(type.toString().toLowerCase(Locale.US), material.getHarvestLevel());
		}
	}

	public ItemToolAbility addAbility(IBaseAbility ability, int level) {
		this.availableAbilities.addAbility(ability, level);

		return this;
	}

	public ItemToolAbility setDepthRockBreaker() {
		this.rockBreaker = true;
		return this;
	}

	// <insert obvious Rarity joke here>
	public ItemToolAbility setRarity(EnumRarity rarity) {
		this.rarity = rarity;
		return this;
	}

	public EnumRarity getRarity(ItemStack stack) {
		return this.rarity != EnumRarity.common ? this.rarity : super.getRarity(stack);
	}

	public boolean hitEntity(ItemStack stack, EntityLivingBase victim, EntityLivingBase attacker) {

		if(!attacker.worldObj.isRemote && attacker instanceof EntityPlayer && canOperate(stack)) {

			this.availableAbilities.getWeaponAbilities().forEach((ability, level) -> {
				ability.onHit(level, attacker.worldObj, (EntityPlayer) attacker, victim, this);
			});
		}

		stack.damageItem(1, attacker);

		return true;
	}

	// Should be safe, considering the AoE ability does a similar trick already.
	// If not, wrap this in a ThreadLocal or something...
	public static int dropX, dropY, dropZ;

	@Override
	public boolean onBlockStartBreak(ItemStack stack, int x, int y, int z, EntityPlayer player) {

		World world = player.worldObj;
		Block block = world.getBlock(x, y, z);

		/*
		 * The original implementation of this always returned FALSE which uses the vanilla block break code.
		 * This one now returns TRUE when an ability applies and instead relies on breakExtraBlock, which has the minor
		 * issue of only running on the sever, while the client uses the vanilla implementation. breakExtraBlock was only
		 * meant to be used for AoE or vein miner and not for the block that's being mined, hence break EXTRA block.
		 * The consequence was that the server would fail to break keyholes since breakExtraBlock is supposed to exclude
		 * them, while the client happily removes the block, causing a desync.
		 *
		 * Since keyholes aren't processable and exempt from silk touch anyway, we just default to the vanilla implementation in every case.
		 */
		if(block == ModBlocks.stone_keyhole || block == ModBlocks.stone_keyhole_meta) return false;

		if(!world.isRemote && (canHarvestBlock(block, stack) || canShearBlock(block, stack, world, x, y, z)) && canOperate(stack)) {
			Configuration config = getConfiguration(stack);
			ToolPreset preset = config.getActivePreset();

			dropX = x;
			dropY = y;
			dropZ = z;

			preset.harvestAbility.preHarvestAll(preset.harvestAbilityLevel, world, player);

			boolean skipRef = preset.areaAbility.onDig(preset.areaAbilityLevel, world, x, y, z, player, this);

			if(!skipRef) {
				breakExtraBlock(world, x, y, z, player, x, y, z);
			}

			preset.harvestAbility.postHarvestAll(preset.harvestAbilityLevel, world, player);

			return true;
		}

		return false;
	}

	@Override
	public boolean onBlockDestroyed(ItemStack stack, World world, Block block, int x, int y, int z, EntityLivingBase player) {
		return super.onBlockDestroyed(stack, world, block, x, y, z, player);
	}

	@Override
	public float getDigSpeed(ItemStack stack, Block block, int meta) {

		if(!canOperate(stack))
			return 1;

		if(toolType == null)
			return super.getDigSpeed(stack, block, meta);

		if(toolType.blocks.contains(block) || toolType.materials.contains(block.getMaterial()))
			return this.efficiencyOnProperMaterial;

		return super.getDigSpeed(stack, block, meta);
	}

	public boolean canOperate(ItemStack stack) {
		return true;
	}

	@Override
	public boolean canHarvestBlock(Block block, ItemStack stack) {

		if(!canOperate(stack))
			return false;

		if(this.getConfiguration(stack).getActivePreset().harvestAbility == IToolHarvestAbility.SILK)
			return true;

		return getDigSpeed(stack, block, 0) > 1;
	}

	@Override
	public boolean canBreakRock(World world, EntityPlayer player, ItemStack tool, Block block, int x, int y, int z) {
		return canOperate(tool) && this.rockBreaker;
	}

	public boolean canShearBlock(Block block, ItemStack stack, World world, int x, int y, int z) {
		return this.isShears(stack) && block instanceof IShearable && ((IShearable) block).isShearable(stack, world, x, y, z);
	}

	public boolean isShears(ItemStack stack) {
		return this.isShears;
	}

	@Override
	public Multimap getItemAttributeModifiers() {

		Multimap multimap = HashMultimap.create();
		multimap.put(SharedMonsterAttributes.attackDamage.getAttributeUnlocalizedName(), new AttributeModifier(field_111210_e, "Tool modifier", (double) this.damage, 0));
		multimap.put(SharedMonsterAttributes.movementSpeed.getAttributeUnlocalizedName(), new AttributeModifier(field_111210_e, "Tool modifier", movement, 1));
		return multimap;
	}

	@SideOnly(Side.CLIENT)
	public boolean hasEffect(ItemStack stack) {
		return stack.isItemEnchanted() || !getConfiguration(stack).getActivePreset().isNone();
	}

	@SideOnly(Side.CLIENT)
	public void addInformation(ItemStack stack, EntityPlayer player, List list, boolean ext) {

		availableAbilities.addInformation(list);

		if(this.rockBreaker) {
			list.add("");
			list.add(EnumChatFormatting.RED + "Can break depth rock!");
		}
	}

	public void breakExtraBlock(World world, int x, int y, int z, EntityPlayer playerEntity, int refX, int refY, int refZ) {

		if(world.isAirBlock(x, y, z))
			return;

		if(!(playerEntity instanceof EntityPlayerMP))
			return;

		EntityPlayerMP player = (EntityPlayerMP) playerEntity;
		ItemStack stack = player.getHeldItem();

		if(stack == null) {
			return;
		}

		Block block = world.getBlock(x, y, z);
		int meta = world.getBlockMetadata(x, y, z);

		if(!(canHarvestBlock(block, stack) ||
				canShearBlock(block, stack, world, x, y, z)) ||
				(block.getBlockHardness(world, x, y, z) == -1.0F && block.getPlayerRelativeBlockHardness(player, world, x, y, z) == 0.0F) ||
				block == ModBlocks.stone_keyhole) return;

		Block refBlock = world.getBlock(refX, refY, refZ);
		float refStrength = ForgeHooks.blockStrength(refBlock, player, world, refX, refY, refZ);
		float strength = ForgeHooks.blockStrength(block, player, world, x, y, z);

		if(
			!ForgeHooks.canHarvestBlock(block, player, meta) ||
			refStrength / strength > 10f ||
			refBlock.getPlayerRelativeBlockHardness(player, world, refX, refY, refZ) < 0
		)
			return;

		BlockEvent.BreakEvent event = ForgeHooks.onBlockBreakEvent(world, player.theItemInWorldManager.getGameType(), player, x, y, z);
		if(event.isCanceled())
			return;

		Configuration config = getConfiguration(stack);
		ToolPreset preset = config.getActivePreset();

		preset.harvestAbility.onHarvestBlock(preset.harvestAbilityLevel, world, x, y, z, player, block, meta);
	}

	/** Assumes a canShearBlock check has passed, will most likely crash otherwise! */
	public static void shearBlock(World world, int x, int y, int z, Block block, EntityPlayer player) {

		ItemStack held = player.getHeldItem();

		IShearable target = (IShearable) block;
		if(target.isShearable(held, player.worldObj, x, y, z)) {
			ArrayList<ItemStack> drops = target.onSheared(held, player.worldObj, x, y, z, EnchantmentHelper.getEnchantmentLevel(Enchantment.fortune.effectId, held));
			Random rand = new Random();

			for(ItemStack stack : drops) {
				float f = 0.7F;
				double d = (double) (rand.nextFloat() * f) + (double) (1.0F - f) * 0.5D;
				double d1 = (double) (rand.nextFloat() * f) + (double) (1.0F - f) * 0.5D;
				double d2 = (double) (rand.nextFloat() * f) + (double) (1.0F - f) * 0.5D;
				EntityItem entityitem = new EntityItem(player.worldObj, (double) dropX + d, (double) dropY + d1, (double) dropZ + d2, stack);
				entityitem.delayBeforeCanPickup = 10;
				player.worldObj.spawnEntityInWorld(entityitem);
			}

			held.damageItem(1, player);
			player.addStat(StatList.mineBlockStatArray[Block.getIdFromBlock(block)], 1);
		}
	}

	// Since it's added by forge, access transformers don't affect it (even wildcards), so we do it the old-fashioned way
	private static Method blockCaptureDrops = ReflectionHelper.findMethod(Block.class, null, new String[] { "captureDrops" }, new Class[] { boolean.class });

	public static void standardDigPost(World world, int x, int y, int z, EntityPlayerMP player) {

		Block block = world.getBlock(x, y, z);
		int l = world.getBlockMetadata(x, y, z);
		world.playAuxSFXAtEntity(player, 2001, x, y, z, Block.getIdFromBlock(block) + (world.getBlockMetadata(x, y, z) << 12));
		boolean removedByPlayer = false;

		if(player.capabilities.isCreativeMode) {
			removedByPlayer = removeBlock(world, x, y, z, false, player);
			player.playerNetServerHandler.sendPacket(new S23PacketBlockChange(x, y, z, world));
		} else {
			ItemStack itemstack = player.getCurrentEquippedItem();
			boolean canHarvest = block.canHarvestBlock(player, l);

			removedByPlayer = removeBlock(world, x, y, z, canHarvest, player);

			if(itemstack != null) {
				itemstack.func_150999_a(world, block, x, y, z, player);

				if(itemstack.stackSize == 0) {
					player.destroyCurrentEquippedItem();
				}
			}

			if(removedByPlayer && canHarvest) {
				try {
					
					blockCaptureDrops.invoke(block, true);
					block.harvestBlock(world, player, x, y, z, l);
					List<ItemStack> drops = (List)blockCaptureDrops.invoke(block, false);
					for (ItemStack stack : drops) {
						block.dropBlockAsItem(world, dropX, dropY, dropZ, stack);
					}
				} catch (IllegalAccessException e) {
					// Shouldn't be possible with ReflectionHelper
					MainRegistry.logger.error("Failed to capture drops for block " + block, e);
				} catch (InvocationTargetException e) {
					// Might be possible? Not in practice, though
					MainRegistry.logger.error("Failed to capture drops for block " + block, e);
				}
			}
		}

		// Why was this commented out?
		// Drop experience
		// if (!player.capabilities.isCreativeMode && flag && event != null) {
		// 	block.dropXpOnBlockBreak(world, x, y, z, event.getExpToDrop());
		// }
	}

	public static boolean removeBlock(World world, int x, int y, int z, boolean canHarvest, EntityPlayerMP player) {
		Block block = world.getBlock(x, y, z);
		int l = world.getBlockMetadata(x, y, z);
		block.onBlockHarvested(world, x, y, z, l, player);
		boolean flag = block.removedByPlayer(world, player, x, y, z, canHarvest);

		if(flag) {
			block.onBlockDestroyedByPlayer(world, x, y, z, l);
		}

		return flag;
	}

	public static class Configuration {
		public List<ToolPreset> presets;
		public int currentPreset;

		public Configuration() {
			this.presets = null;
			this.currentPreset = 0;
		}

		public Configuration(List<ToolPreset> presets, int currentPreset) {
			this.presets = presets;
			this.currentPreset = currentPreset;
		}

		public void writeToNBT(NBTTagCompound nbt) {
			nbt.setInteger("ability", currentPreset);

			NBTTagList nbtPresets = new NBTTagList();

			for(ToolPreset preset : presets) {
				NBTTagCompound nbtPreset = new NBTTagCompound();
				preset.writeToNBT(nbtPreset);
				nbtPresets.appendTag(nbtPreset);
			}

			nbt.setTag("abilityPresets", nbtPresets);
		}

		public void readFromNBT(NBTTagCompound nbt) {
			currentPreset = nbt.getInteger("ability");

			NBTTagList nbtPresets = nbt.getTagList("abilityPresets", 10);
			int numPresets = Math.min(nbtPresets.tagCount(), 99);

			presets = new ArrayList<ToolPreset>(numPresets);

			for(int i = 0; i < numPresets; i++) {
				NBTTagCompound nbtPreset = nbtPresets.getCompoundTagAt(i);
				ToolPreset preset = new ToolPreset();
				preset.readFromNBT(nbtPreset);
				presets.add(preset);
			}

			currentPreset = Math.max(0, Math.min(currentPreset, presets.size() - 1));
		}

		public void reset(AvailableAbilities availableAbilities) {
			currentPreset = 0;

			presets = new ArrayList<ToolPreset>(availableAbilities.size());
			presets.add(new ToolPreset());

			availableAbilities.getToolAreaAbilities().forEach((ability, level) -> {
				if (ability == IToolAreaAbility.NONE)
					return;
				presets.add(new ToolPreset(ability, level, IToolHarvestAbility.NONE, 0));
			});

			availableAbilities.getToolHarvestAbilities().forEach((ability, level) -> {
				if (ability == IToolHarvestAbility.NONE)
					return;
				presets.add(new ToolPreset(IToolAreaAbility.NONE, 0, ability, level));
			});

			presets.sort(
				Comparator
					.comparing((ToolPreset p) -> p.harvestAbility)
					.thenComparingInt(p -> p.harvestAbilityLevel)
					.thenComparing(p -> p.areaAbility)
					.thenComparingInt(p -> p.areaAbilityLevel)
			);
		}

		public void restrictTo(AvailableAbilities availableAbilities) {
			for (ToolPreset preset : presets) {
				preset.restrictTo(availableAbilities);
			}
		}

		public ToolPreset getActivePreset() {
			return presets.get(currentPreset);
		}
	}

	public Configuration getConfiguration(ItemStack stack) {
		Configuration config = new Configuration();

		if(stack == null || !stack.hasTagCompound() || !stack.stackTagCompound.hasKey("ability") || !stack.stackTagCompound.hasKey("abilityPresets")) {
			config.reset(availableAbilities);
			return config;
		}

		config.readFromNBT(stack.stackTagCompound);
		config.restrictTo(availableAbilities);
		return config;
	}

	public void setConfiguration(ItemStack stack, Configuration config) {
		if (stack == null) {
			return;
		}

		if (!stack.hasTagCompound()) {
			stack.stackTagCompound = new NBTTagCompound();
		}

		config.writeToNBT(stack.stackTagCompound);
	}

	@Override
	public void receiveControl(ItemStack stack, NBTTagCompound data) {
		Configuration config = new Configuration();
		config.readFromNBT(data);
		config.restrictTo(availableAbilities);
		setConfiguration(stack, config);
	}

	@Override
	public Container provideContainer(int ID, EntityPlayer player, World world, int x, int y, int z) {
		return null;
	}

	@Override
	@SideOnly(Side.CLIENT)
	public Object provideGUI(int ID, EntityPlayer player, World world, int x, int y, int z) {
		return new GUIScreenToolAbility(this.availableAbilities);
	}

	@Override
	public boolean canHandleKeybind(EntityPlayer player, ItemStack stack, EnumKeybind keybind) {
		if(player.worldObj.isRemote) return keybind == EnumKeybind.ABILITY_ALT;
		return keybind == EnumKeybind.ABILITY_CYCLE;
	}

	@Override
	public void handleKeybind(EntityPlayer player, ItemStack stack, EnumKeybind keybind, boolean state) {

		if(keybind == EnumKeybind.ABILITY_CYCLE && state) {

			World world = player.worldObj;
			if(!canOperate(stack)) return;

			Configuration config = getConfiguration(stack);
			if(config.presets.size() < 2 || world.isRemote) return;

			if(player.isSneaking()) {
				config.currentPreset = 0;
			} else {
				config.currentPreset = (config.currentPreset + 1) % config.presets.size();
			}

			setConfiguration(stack, config);
			PacketDispatcher.wrapper.sendTo(new PlayerInformPacket(config.getActivePreset().getMessage(), MainRegistry.proxy.ID_TOOLABILITY), (EntityPlayerMP) player);
			world.playSoundAtEntity(player, "random.orb", 0.25F, config.getActivePreset().isNone() ? 0.75F : 1.25F);
		}
	}

	@Override
	public void handleKeybindClient(EntityPlayer player, ItemStack stack, EnumKeybind keybind, boolean state) {
		if(state) player.openGui(MainRegistry.instance, 0, player.worldObj, 0, 0, 0);
	}

	private static final Map<IBaseAbility, Pair<Integer, Integer>> abilityGui = new HashMap<>();

	static {
		abilityGui.put(IToolAreaAbility.RECURSION, new Pair<Integer,Integer>(0, 138));
		abilityGui.put(IToolAreaAbility.HAMMER, new Pair<Integer,Integer>(16, 138));
		abilityGui.put(IToolAreaAbility.HAMMER_FLAT, new Pair<Integer,Integer>(32, 138));
		abilityGui.put(IToolAreaAbility.EXPLOSION, new Pair<Integer,Integer>(48, 138));
	}

	@Override
	public void renderHUD(Pre event, ElementType type, EntityPlayer player, ItemStack stack) {
		if(type != ElementType.CROSSHAIRS) return;

		Configuration config = getConfiguration(stack);
		ToolPreset preset = config.getActivePreset();
		Pair<Integer, Integer> uv = abilityGui.get(preset.areaAbility);

		if(uv == null) return;

		GuiIngame gui = Minecraft.getMinecraft().ingameGUI;
		int size = 16;
		int ox = ClientConfig.TOOL_HUD_INDICATOR_X.get();
		int oy = ClientConfig.TOOL_HUD_INDICATOR_Y.get();

		GL11.glPushMatrix();
		Minecraft.getMinecraft().renderEngine.bindTexture(GUIScreenToolAbility.texture);
		GL11.glEnable(GL11.GL_BLEND);
		OpenGlHelper.glBlendFunc(GL11.GL_ONE_MINUS_DST_COLOR, GL11.GL_ONE_MINUS_SRC_COLOR, 1, 0);
		gui.drawTexturedModalRect(event.resolution.getScaledWidth() / 2 - size - 8 + ox, event.resolution.getScaledHeight() / 2 + 8 + oy, uv.key, uv.value, size, size);
		OpenGlHelper.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, 1, 0);
		GL11.glDisable(GL11.GL_BLEND);
		GL11.glPopMatrix();
		Minecraft.getMinecraft().renderEngine.bindTexture(Gui.icons);
	}

}
