package electroblob.wizardry;

import electroblob.wizardry.constants.Constants;
import electroblob.wizardry.entity.EntityArc;
import electroblob.wizardry.entity.living.EntityEvilWizard;
import electroblob.wizardry.entity.living.ISpellCaster;
import electroblob.wizardry.entity.living.ISummonedCreature;
import electroblob.wizardry.event.DiscoverSpellEvent;
import electroblob.wizardry.event.SpellCastEvent;
import electroblob.wizardry.item.ItemWand;
import electroblob.wizardry.item.ItemWizardArmour;
import electroblob.wizardry.registry.Spells;
import electroblob.wizardry.registry.WizardryAchievements;
import electroblob.wizardry.registry.WizardryEnchantments;
import electroblob.wizardry.registry.WizardryItems;
import electroblob.wizardry.registry.WizardryPotions;
import electroblob.wizardry.registry.WizardrySounds;
import electroblob.wizardry.spell.FreezingWeapon;
import electroblob.wizardry.spell.Spell;
import electroblob.wizardry.util.IElementalDamage;
import electroblob.wizardry.util.MagicDamage;
import electroblob.wizardry.util.MagicDamage.DamageType;
import electroblob.wizardry.util.SpellModifiers;
import electroblob.wizardry.util.WandHelper;
import electroblob.wizardry.util.WizardryParticleType;
import electroblob.wizardry.util.WizardryUtilities;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.monster.IMob;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.entity.projectile.EntityArrow;
import net.minecraft.init.SoundEvents;
import net.minecraft.inventory.ContainerPlayer;
import net.minecraft.inventory.ContainerWorkbench;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.item.ItemSword;
import net.minecraft.potion.PotionEffect;
import net.minecraft.util.EnumHand;
import net.minecraft.util.EnumParticleTypes;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.world.World;
import net.minecraft.world.storage.loot.LootEntry;
import net.minecraft.world.storage.loot.LootEntryTable;
import net.minecraft.world.storage.loot.LootPool;
import net.minecraft.world.storage.loot.RandomValueRange;
import net.minecraft.world.storage.loot.conditions.LootCondition;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.LootTableLoadEvent;
import net.minecraftforge.event.entity.living.LivingAttackEvent;
import net.minecraftforge.event.entity.living.LivingDeathEvent;
import net.minecraftforge.event.entity.living.LivingDropsEvent;
import net.minecraftforge.event.entity.living.LivingEvent.LivingUpdateEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.event.entity.player.EntityItemPickupEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.PlayerEvent.PlayerLoggedInEvent;

/**
 * As of Wizardry 2.1, most of the code in this class has been relocated somewhere sensible, leaving only a few
 * miscellaneous things that don't make much sense anywhere else, or that are better kept together. Previously, this was
 * a gigantic class with about half of the entire mod's logic in it!
 * 
 * @author Electroblob
 * @since Wizardry 1.0
 */
@Mod.EventBusSubscriber
public final class WizardryEventHandler {

	// IDEA: Config option allowing users to specify loot locations
	private static final String[] LOOT_INJECTION_LOCATIONS = {"minecraft:chests/simple_dungeon",
			"minecraft:chests/abandoned_mineshaft", "minecraft:chests/desert_pyramid", "minecraft:chests/jungle_temple",
			"minecraft:chests/stronghold_corridor", "minecraft:chests/stronghold_crossing",
			"minecraft:chests/stronghold_library", "minecraft:chests/igloo_chest", "minecraft:chests/woodland_mansion",
			"minecraft:chests/end_city_treasure"};

	@SubscribeEvent
	public static void onLootTableLoadEvent(LootTableLoadEvent event){
		if(Wizardry.settings.generateLoot){
			for(String location : LOOT_INJECTION_LOCATIONS){
				if(event.getName().toString().matches(location)){
					event.getTable().addPool(getAdditive("wizardry:chests/dungeon_additions"));
				}
			}
		}
	}

	private static LootPool getAdditive(String entryName){
		return new LootPool(new LootEntry[]{getAdditiveEntry(entryName, 1)}, new LootCondition[0],
				new RandomValueRange(1), new RandomValueRange(0, 1), Wizardry.MODID + "_additive_pool");
	}

	private static LootEntryTable getAdditiveEntry(String name, int weight){
		return new LootEntryTable(new ResourceLocation(name), weight, 0, new LootCondition[0],
				Wizardry.MODID + "_additive_entry");
	}

	@SubscribeEvent
	public static void onPlayerLoggedInEvent(PlayerLoggedInEvent event){
		// When a player logs in, they are sent the glyph data and the server's settings.
		if(event.player instanceof EntityPlayerMP){
			SpellGlyphData.get(event.player.world).sync((EntityPlayerMP)event.player);
			Wizardry.settings.sync((EntityPlayerMP)event.player);
		}
	}

	@SubscribeEvent
	public static void onSpellCastPreEvent(SpellCastEvent.Pre event){
		// If a spell is disabled in the config, it will not work.
		if(!event.getSpell().isEnabled()){
			if(!event.getEntityLiving().world.isRemote) event.getEntity().sendMessage(
					new TextComponentTranslation("spell.disabled", event.getSpell().getNameForTranslationFormatted()));
			event.setCanceled(true);
		}
	}

	@SubscribeEvent
	public static void onSpellCastPostEvent(SpellCastEvent.Post event){

		// Spell discovery (only players can discover spells, obviously)
		if(event.getEntity() instanceof EntityPlayer){

			EntityPlayer player = (EntityPlayer)event.getEntity();

			WizardData data = WizardData.get(player);

			if(data != null){
				// Data is updated on both sides (This line was added client-side to fix a bug back in 1.1.3, so now
				// it's in common code, which is nice!)
				// Short-circuiting AND means that discoverSpell is only called if the event isn't cancelled.
				if(!MinecraftForge.EVENT_BUS
						.post(new DiscoverSpellEvent(player, event.getSpell(), DiscoverSpellEvent.Source.CASTING))
						&& data.discoverSpell(event.getSpell())){

					// If the spell wasn't already discovered, other stuff happens:
					if(event.getSource() == SpellCastEvent.Source.COMMAND){
						// If the spell didn't send a packet itself, the extended player needs to be synced so the
						// spell discovery updates on the client.
						if(!event.getSpell().doesSpellRequirePacket()) data.sync();

					}else if(!event.getEntity().world.isRemote && !player.capabilities.isCreativeMode
							&& Wizardry.settings.discoveryMode){
						// Sound and text only happen server-side, in survival, with discovery mode on, and only when
						// the spell wasn't cast using commands.
						WizardryUtilities.playSoundAtPlayer(player, SoundEvents.ENTITY_PLAYER_LEVELUP, 1.25f, 1);
						player.sendMessage(new TextComponentTranslation("spell.discover",
								event.getSpell().getNameForTranslationFormatted()));
					}
				}
			}
		}
	}

	/* There is a subtle but important difference between LivingAttackEvent and LivingHurtEvent - LivingAttackEvent
	 * fires immediately when attackEntityFrom is called, whereas LivingHurtEvent only fires if the attack actually
	 * succeeded, i.e. if the entity in question takes damage (though the event is fired before that so you can cancel
	 * the damage). Things are processed in the following order: * LivingAttackEvent * - Invulnerability -
	 * Already-dead-ness - Fire resistance - Helmets vs. falling things - Hurt resistant time - Invulnerability (again)
	 * * LivingHurtEvent * - Armour - Potions - Health is finally changed Of course, there are no guarantees that other
	 * mods hooking into these two events will be called before or after yours, but you can have some degree of control
	 * by choosing which event to use. EDIT: Actually, there are. Firstly, you can set a priority in the @SubscribeEvent
	 * annotation which defines how early (higher priority) or late (lower priority) the method is called. Methods with
	 * the same priority are sorted alphabetically by mod id (so it's safe to assume wizardry would be fairly late on!).
	 * I wonder if there are any conventions for what sort of things take what priority...? */

	@SubscribeEvent
	public static void onLivingAttackEvent(LivingAttackEvent event){

		// Prevents any damage to allies from magic if friendly fire is disabled
		if(!Wizardry.settings.friendlyFire && event.getSource() != null
				&& event.getSource().getTrueSource() instanceof EntityPlayer && event.getEntity() instanceof EntityPlayer
				&& event.getSource() instanceof IElementalDamage){
			if(WizardryUtilities.isPlayerAlly((EntityPlayer)event.getSource().getTrueSource(),
					(EntityPlayer)event.getEntity())){
				event.setCanceled(true);
				// This needs to be here, since if the event is cancelled nothing else needs to happen.
				return;
			}
		}

		// Retaliatory effects
		// These are better off here because the revenge effects are pretty similar, and I'd rather keep the (lengthy)
		// if statement in one place.
		if(event.getSource() != null && event.getSource().getTrueSource() instanceof EntityLivingBase
				&& !event.getSource().isProjectile() && !(event.getSource() instanceof IElementalDamage
						&& ((IElementalDamage)event.getSource()).isRetaliatory())){

			EntityLivingBase attacker = (EntityLivingBase)event.getSource().getTrueSource();
			World world = event.getEntityLiving().world;

			// Fireskin
			if(event.getEntityLiving().isPotionActive(WizardryPotions.fireskin)
					&& !MagicDamage.isEntityImmune(DamageType.FIRE, event.getEntityLiving()))
				attacker.setFire(5);

			// Ice Shroud
			if(event.getEntityLiving().isPotionActive(WizardryPotions.ice_shroud)
					&& !MagicDamage.isEntityImmune(DamageType.FROST, event.getEntityLiving()))
				attacker.addPotionEffect(new PotionEffect(WizardryPotions.frost, 100, 0));

			// Static Aura
			if(event.getEntityLiving().isPotionActive(WizardryPotions.static_aura)){

				if(!world.isRemote){
					EntityArc arc = new EntityArc(world);
					arc.setEndpointCoords(event.getEntityLiving().posX, event.getEntityLiving().posY + 1,
							event.getEntityLiving().posZ, attacker.posX, attacker.posY + attacker.height / 2,
							attacker.posZ);
					world.spawnEntity(arc);
				}else{
					for(int i = 0; i < 8; i++){
						Wizardry.proxy.spawnParticle(WizardryParticleType.SPARK, world,
								attacker.posX + world.rand.nextFloat() - 0.5, attacker.getEntityBoundingBox().minY
								+ attacker.height / 2 + world.rand.nextFloat() * 2 - 1,
								attacker.posZ + world.rand.nextFloat() - 0.5, 0, 0, 0, 3);
						world.spawnParticle(EnumParticleTypes.SMOKE_LARGE, attacker.posX + world.rand.nextFloat() - 0.5,
								attacker.getEntityBoundingBox().minY + attacker.height / 2 + world.rand.nextFloat() * 2
								- 1,
								attacker.posZ + world.rand.nextFloat() - 0.5, 0, 0, 0);
					}
				}

				attacker.attackEntityFrom(
						MagicDamage.causeDirectMagicDamage(event.getEntityLiving(), DamageType.SHOCK, true), 4.0f);
				attacker.playSound(WizardrySounds.SPELL_SPARK, 1.0F, world.rand.nextFloat() * 0.4F + 1.5F);
			}
		}

	}

	@SubscribeEvent
	public static void onLivingHurtEvent(LivingHurtEvent event){

		// Flaming and freezing swords
		if(event.getSource().getTrueSource() instanceof EntityLivingBase){

			EntityLivingBase attacker = (EntityLivingBase)event.getSource().getTrueSource();

			// Players can only ever attack with their main hand, so this is the right method to use here.
			if(!attacker.getHeldItemMainhand().isEmpty() && attacker.getHeldItemMainhand().getItem() instanceof ItemSword){

				int level = EnchantmentHelper.getEnchantmentLevel(WizardryEnchantments.flaming_weapon,
						attacker.getHeldItemMainhand());

				if(level > 0 && !MagicDamage.isEntityImmune(DamageType.FIRE, event.getEntityLiving()))
					event.getEntityLiving().setFire(level * 4);

				level = EnchantmentHelper.getEnchantmentLevel(WizardryEnchantments.freezing_weapon,
						attacker.getHeldItemMainhand());
				// Frost lasts for longer because it doesn't do any actual damage
				if(level > 0 && !MagicDamage.isEntityImmune(DamageType.FROST, event.getEntityLiving()))
					event.getEntityLiving().addPotionEffect(new PotionEffect(WizardryPotions.frost, level * 200, 0));
			}
		}

		// Freezing bow
		if(event.getSource().getImmediateSource() instanceof EntityArrow
				&& event.getSource().getImmediateSource().getEntityData() != null){

			int level = event.getSource().getImmediateSource().getEntityData()
					.getInteger(FreezingWeapon.FREEZING_ARROW_NBT_KEY);

			if(level > 0 && !MagicDamage.isEntityImmune(DamageType.FROST, event.getEntityLiving()))
				event.getEntityLiving().addPotionEffect(new PotionEffect(WizardryPotions.frost, level * 150, 0));
		}

		// Damage scaling
		if(event.getSource() != null && event.getSource() instanceof IElementalDamage){

			if(event.getSource().getTrueSource() instanceof EntityPlayer){
				event.setAmount((float)(event.getAmount() * Wizardry.settings.playerDamageScale));
			}else{
				event.setAmount((float)(event.getAmount() * Wizardry.settings.npcDamageScale));
			}
		}
	}

	@SubscribeEvent
	public static void onLivingUpdateEvent(LivingUpdateEvent event){

		if(event.getEntityLiving() instanceof EntityPlayer){

			EntityPlayer player = (EntityPlayer)event.getEntityLiving();

			if(player.world.isRemote) hackilyFixContinuousSpellCasting(player);

			// Mana flask crafting
			if(player.openContainer instanceof ContainerWorkbench){
				
				IInventory craftMatrix = ((ContainerWorkbench)player.openContainer).craftMatrix;
				ItemStack output = ((ContainerWorkbench)player.openContainer).craftResult.getStackInSlot(0);
				processManaFlaskCrafting(craftMatrix, output);
				
			}else if(player.openContainer instanceof ContainerPlayer){
				
				IInventory craftMatrix = ((ContainerPlayer)player.openContainer).craftMatrix;
				ItemStack output = ((ContainerPlayer)player.openContainer).craftResult.getStackInSlot(0);
				// Unfortunately I have no choice but to call this method every tick when the player isn't using another
				// inventory, since the only thing tracking whether the player is looking at their inventory is the GUI
				// itself, which is client-side only.
				processManaFlaskCrafting(craftMatrix, output);
			}

		}

		if(event.getEntityLiving().world.isRemote){

			// Client-side continuous spell casting for NPCs

			if(event.getEntity() instanceof ISpellCaster && event.getEntity() instanceof EntityLiving){

				Spell spell = ((ISpellCaster)event.getEntity()).getContinuousSpell();
				SpellModifiers modifiers = ((ISpellCaster)event.getEntity()).getModifiers();

				if(spell != null && spell != Spells.none){

					if(!MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Tick(event.getEntityLiving(), spell, modifiers,
							SpellCastEvent.Source.NPC, 0))){

						spell.cast(event.getEntity().world, (EntityLiving)event.getEntity(), EnumHand.MAIN_HAND, 0,
								// TODO: This implementation of modifiers relies on them being accessible client-side.
								((EntityLiving)event.getEntity()).getAttackTarget(), modifiers);
					}
				}
			}
		}
	}

	@SubscribeEvent
	public static void onLivingDeathEvent(LivingDeathEvent event){

		if(event.getSource().getTrueSource() instanceof EntityPlayer){

			EntityPlayer player = (EntityPlayer)event.getSource().getTrueSource();

			for(ItemStack stack : WizardryUtilities.getPrioritisedHotbarAndOffhand(player)){

				if(stack.getItem() instanceof ItemWand && stack.isItemDamaged()
						&& WandHelper.getUpgradeLevel(stack, WizardryItems.siphon_upgrade) > 0){
					int damage = stack.getItemDamage()
							- Constants.SIPHON_MANA_PER_LEVEL
							* WandHelper.getUpgradeLevel(stack, WizardryItems.siphon_upgrade)
							- player.world.rand.nextInt(Constants.SIPHON_MANA_PER_LEVEL);
					if(damage < 0) damage = 0;
					stack.setItemDamage(damage);
					break;
				}
			}

			if(event.getEntityLiving() == player && event.getSource() instanceof IElementalDamage){
				player.addStat(WizardryAchievements.self_destruct);
			}
		}
	}

	@SubscribeEvent
	public static void onLivingDropsEvent(LivingDropsEvent event){
		// TODO: Really, this should be in a loot table (mob_additions), however I can't seem to find a way of
		// automatically adding it to all subclasses of IMob.
		// Evil wizards drop spell books themselves
		if(event.getEntityLiving() instanceof IMob && !(event.getEntityLiving() instanceof EntityEvilWizard)
				// TODO: Backport when you backport the new summoned creature system.
				&& !(event.getEntityLiving() instanceof ISummonedCreature)
				&& event.getSource().getTrueSource() instanceof EntityPlayer && Wizardry.settings.spellBookDropChance > 0){

			// This does exactly what the entity drop method does, but with a different random number so that the
			// spell book doesn't always drop with other rare drops.
			int rareDropNumber = event.getEntity().world.rand.nextInt(200) - event.getLootingLevel();
			if(rareDropNumber < Wizardry.settings.spellBookDropChance){
				// Drops a spell book
				int id = WizardryUtilities.getStandardWeightedRandomSpellId(event.getEntity().world.rand);

				event.getDrops()
				.add(new EntityItem(event.getEntityLiving().world, event.getEntityLiving().posX,
						event.getEntityLiving().posY, event.getEntityLiving().posZ,
						new ItemStack(WizardryItems.spell_book, 1, id)));
			}
		}
	}

	@SubscribeEvent
	public static void onItemPickupEvent(EntityItemPickupEvent event){
		if(event.getItem().getItem().getItem() == WizardryItems.magic_crystal){
			event.getEntityPlayer().addStat(WizardryAchievements.crystal, 1);
		}
	}

	// Private helper methods
	// ================================================================================================================

	/**
	 * Detects inconsistencies between player.getActiveItemStack and the actual itemstack and forces them to be equal.
	 * Fixes issue #25.
	 * 
	 * @param player
	 */
	private static void hackilyFixContinuousSpellCasting(EntityPlayer player){
		if(player.isHandActive() && player.getHeldItem(player.getActiveHand()).getItem() instanceof ItemWand
				&& WandHelper.getCurrentSpell(player.getHeldItem(player.getActiveHand())).isContinuous){
			if(player.getActiveItemStack() != player.getHeldItem(player.getActiveHand())){
				player.setHeldItem(player.getActiveHand(), player.getActiveItemStack());
			}
		}
	}

	private static void processManaFlaskCrafting(IInventory craftMatrix, ItemStack output){

		// Charges wand using mana flask. It is here rather than in the crafting handler so the result displays
		// the proper damage before it is actually crafted.

		boolean flag = false;
		ItemStack wand = ItemStack.EMPTY;
		ItemStack armour = ItemStack.EMPTY;

		for(int i = 0; i < craftMatrix.getSizeInventory(); i++){
			
			ItemStack itemstack = craftMatrix.getStackInSlot(i);

			if(itemstack.getItem() == WizardryItems.mana_flask){
				flag = true;
			}

			if(itemstack.getItem() instanceof ItemWand){
				wand = itemstack;
			}

			if(itemstack.getItem() instanceof ItemWizardArmour){
				armour = itemstack;
			}
		}

		if(output.getItem() instanceof ItemWand && flag && !wand.isEmpty()){
			output.setTagCompound((wand.getTagCompound()));
			if(wand.getItemDamage() - Constants.MANA_PER_FLASK < 0){
				output.setItemDamage(0);
			}else{
				output.setItemDamage(wand.getItemDamage() - Constants.MANA_PER_FLASK);
			}
		}

		if(output.getItem() instanceof ItemWizardArmour && flag && !armour.isEmpty()){
			output.setTagCompound((armour.getTagCompound()));
			if(armour.getItemDamage() - Constants.MANA_PER_FLASK < 0){
				output.setItemDamage(0);
			}else{
				output.setItemDamage(wand.getItemDamage() - Constants.MANA_PER_FLASK);
			}
		}
	}

}