package electroblob.wizardry.entity;

import electroblob.wizardry.Wizardry;
import electroblob.wizardry.registry.Spells;
import electroblob.wizardry.spell.Spell;
import electroblob.wizardry.util.AllyDesignationSystem;
import electroblob.wizardry.util.MagicDamage;
import electroblob.wizardry.util.NBTExtras;
import electroblob.wizardry.util.WizardryUtilities;
import io.netty.buffer.ByteBuf;
import net.minecraft.block.Block;
import net.minecraft.block.BlockFalling;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.MoverType;
import net.minecraft.entity.item.EntityFallingBlock;
import net.minecraft.init.Blocks;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;
import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
import net.minecraftforge.fml.common.registry.IEntityAdditionalSpawnData;

import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.List;
import java.util.UUID;

/** Custom extended version of {@link EntityFallingBlock} for use with the greater telekinesis spell. */
public class EntityLevitatingBlock extends EntityFallingBlock implements IEntityAdditionalSpawnData {

	private static final Field fallTile;

	static {
		fallTile = ObfuscationReflectionHelper.findField(EntityFallingBlock.class, "field_175132_d");
		fallTile.setAccessible(true);
	}

	/** The entity that created this levitating block */
	private WeakReference<EntityLivingBase> caster;

	/**
	 * The UUID of the caster. Note that this is only for loading purposes; during normal updates the actual entity
	 * instance is stored (so that getEntityByUUID is not called constantly), so this will not always be synced (this is
	 * why it is private).
	 */
	private UUID casterUUID;

	/** The damage multiplier for this levitating block, determined by the wand with which it was cast. */
	public float damageMultiplier = 1.0f;

	private int suspendTimer = 5;

	public EntityLevitatingBlock(World world){
		super(world);
		// EntityFallingBlock never uses this constructor so doesn't bother setting this, but we need to
		this.setSize(0.98F, 0.98F);
	}

	public EntityLevitatingBlock(World world, double x, double y, double z, IBlockState state){
		super(world, x, y, z, state);
	}

	/** Resets the suspension timer to 5 ticks, during which this block will not re-attach itself to the ground. */
	public void suspend(){
		suspendTimer = 5;
	}

	@Override
	public void onUpdate(){

		if(suspendTimer > 0){
			suspendTimer--;
		}

		if(this.getCaster() == null && this.casterUUID != null){
			Entity entity = WizardryUtilities.getEntityByUUID(world, casterUUID);
			if(entity instanceof EntityLivingBase){
				this.caster = new WeakReference<>((EntityLivingBase)entity);
			}
		}

		if(getBlock() != null){

			// === Copied from super ===

			Block block = getBlock().getBlock();

			if(getBlock().getMaterial() == Material.AIR){
				this.setDead();

			}else{

				this.prevPosX = this.posX;
				this.prevPosY = this.posY;
				this.prevPosZ = this.posZ;

				if(this.fallTime++ == 0){

					BlockPos blockpos = new BlockPos(this);

					if(this.world.getBlockState(blockpos).getBlock() == block){
						this.world.setBlockToAir(blockpos);
					}else if(!this.world.isRemote){
						this.setDead();
						return;
					}
				}

				if(!this.hasNoGravity()){
					this.motionY -= 0.03999999910593033D;
				}

				this.move(MoverType.SELF, this.motionX, this.motionY, this.motionZ);

				if(!this.world.isRemote){

					BlockPos blockpos1 = new BlockPos(this);
					boolean isConcrete = getBlock().getBlock() == Blocks.CONCRETE_POWDER;
					boolean isConcreteInWater = isConcrete && this.world.getBlockState(blockpos1).getMaterial() == Material.WATER;
					double d0 = this.motionX * this.motionX + this.motionY * this.motionY + this.motionZ * this.motionZ;

					if(isConcrete && d0 > 1.0D){

						RayTraceResult raytraceresult = this.world.rayTraceBlocks(new Vec3d(this.prevPosX, this.prevPosY, this.prevPosZ), new Vec3d(this.posX, this.posY, this.posZ), true);

						if(raytraceresult != null && this.world.getBlockState(raytraceresult.getBlockPos()).getMaterial() == Material.WATER){
							blockpos1 = raytraceresult.getBlockPos();
							isConcreteInWater = true;
						}
					}

					if(!this.onGround && !isConcreteInWater){

						if(this.fallTime > 100 && !this.world.isRemote && (blockpos1.getY() < 1 || blockpos1.getY() > 256) || this.fallTime > 600){
							this.setDead();
						}

					}else{

						IBlockState iblockstate = this.world.getBlockState(blockpos1);

						if(this.world.isAirBlock(new BlockPos(this.posX, this.posY - 0.009999999776482582D, this.posZ))){
							if(!isConcreteInWater && BlockFalling.canFallThrough(this.world.getBlockState(new BlockPos(this.posX, this.posY - 0.009999999776482582D, this.posZ)))){
								this.onGround = false;
								return;
							}
						}

						this.motionX *= 0.699999988079071D;
						this.motionZ *= 0.699999988079071D;
						this.motionY *= -0.5D;

						if(iblockstate.getBlock() != Blocks.PISTON_EXTENSION){

							if(suspendTimer == 0){

								this.setDead(); // Moved inside the above if statement

								if(this.world.mayPlace(block, blockpos1, true, EnumFacing.UP, null)
										&& (isConcreteInWater || !BlockFalling.canFallThrough(this.world.getBlockState(blockpos1.down())))
										&& this.world.setBlockState(blockpos1, getBlock(), 3)){

									if(block instanceof BlockFalling){
										((BlockFalling)block).onEndFalling(this.world, blockpos1, getBlock(), iblockstate);
									}

									if(this.tileEntityData != null && block.hasTileEntity(getBlock())){

										TileEntity tileentity = this.world.getTileEntity(blockpos1);

										if(tileentity != null){

											NBTTagCompound nbttagcompound = tileentity.writeToNBT(new NBTTagCompound());

											for(String s : this.tileEntityData.getKeySet()){
												NBTBase nbtbase = this.tileEntityData.getTag(s);

												if(!"x".equals(s) && !"y".equals(s) && !"z".equals(s)){
													NBTExtras.storeTagSafely(nbttagcompound, s, nbtbase.copy());
												}
											}

											tileentity.readFromNBT(nbttagcompound);
											tileentity.markDirty();
										}
									}

								}else{
									// Never drops the block, instead if it can't reattach to the world it breaks
									world.playEvent(2001, this.getPosition(), Block.getStateId(getBlock()));
								}
							}
						}
					}
				}

				this.motionX *= 0.9800000190734863D;
				this.motionY *= 0.9800000190734863D;
				this.motionZ *= 0.9800000190734863D;
			}

			// === End super copy ===
		}

		double velocitySquared = motionX * motionX + motionY * motionY + motionZ * motionZ;

		if(velocitySquared >= 0.2){

			List<Entity> list = this.world.getEntitiesWithinAABBExcludingEntity(this, this.getEntityBoundingBox());

			for(Entity entity : list){

				if(entity instanceof EntityLivingBase && isValidTarget(entity)){

					float damage = Spells.greater_telekinesis.getProperty(Spell.DAMAGE).floatValue() * damageMultiplier;
					damage *= Math.min(1, velocitySquared/0.4); // Reduce damage at low speeds

					entity.attackEntityFrom(MagicDamage.causeIndirectMagicDamage(this, getCaster(),
							MagicDamage.DamageType.FORCE), damage);

					double dx = -this.motionX;
					double dz;
					for(dz = -this.motionZ; dx * dx + dz * dz < 1.0E-4D; dz = (Math.random() - Math.random()) * 0.01D){
						dx = (Math.random() - Math.random()) * 0.01D;
					}
					((EntityLivingBase)entity).knockBack(this, 0.6f, dx, dz);
				}
			}
		}

	}

	/**
	 * Returns the EntityLivingBase that created this construct, or null if it no longer exists. Cases where the entity
	 * may no longer exist are: entity died or was deleted, mob despawned, player logged out, entity teleported to
	 * another dimension, or this construct simply had no caster in the first place.
	 */
	public EntityLivingBase getCaster(){
		return caster == null ? null : caster.get();
	}

	public void setCaster(EntityLivingBase caster){
		if(getCaster() != caster) this.caster = new WeakReference<>(caster);
	}

	/**
	 * Shorthand for {@link AllyDesignationSystem#isValidTarget(Entity, Entity)}, with the owner of this construct as the
	 * attacker. Also allows subclasses to override it if they wish to do so.
	 */
	public boolean isValidTarget(Entity target){
		return AllyDesignationSystem.isValidTarget(this.getCaster(), target);
	}

	@Override
	protected void readEntityFromNBT(NBTTagCompound nbttagcompound){
		super.readEntityFromNBT(nbttagcompound);
		casterUUID = nbttagcompound.getUniqueId("casterUUID");
		damageMultiplier = nbttagcompound.getFloat("damageMultiplier");
	}

	@Override
	protected void writeEntityToNBT(NBTTagCompound nbttagcompound){
		super.writeEntityToNBT(nbttagcompound);
		if(this.getCaster() != null){
			nbttagcompound.setUniqueId("casterUUID", this.getCaster().getUniqueID());
		}
		nbttagcompound.setFloat("damageMultiplier", damageMultiplier);
	}

	@Override
	public void readSpawnData(ByteBuf buf){
		if(buf.isReadable()){
			Block block = Block.REGISTRY.getObjectById(buf.readInt());
			try{
				fallTile.set(this, block.getStateFromMeta(buf.readInt()));
			}catch(IllegalAccessException e){
				Wizardry.logger.error("Error reading levitating block data from packet: ", e);
			}
		}
	}

	@Override
	public void writeSpawnData(ByteBuf buf){
		if(getBlock() != null){
			buf.writeInt(Block.REGISTRY.getIDForObject(getBlock().getBlock()));
			buf.writeInt(getBlock().getBlock().getMetaFromState(getBlock()));
		}
	}
}
