package thebetweenlands.common.entity.mobs;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import javax.annotation.Nullable;

import com.google.common.base.Optional;

import gnu.trove.map.TObjectIntMap;
import gnu.trove.map.hash.TObjectIntHashMap;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.Minecraft;
import net.minecraft.client.audio.ISound;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.IEntityLivingData;
import net.minecraft.entity.MultiPartEntityPart;
import net.minecraft.entity.SharedMonsterAttributes;
import net.minecraft.entity.ai.EntityAIBase;
import net.minecraft.entity.ai.EntityAINearestAttackableTarget;
import net.minecraft.entity.item.EntityXPOrb;
import net.minecraft.entity.monster.IMob;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.datasync.DataParameter;
import net.minecraft.network.datasync.DataSerializers;
import net.minecraft.network.datasync.EntityDataManager;
import net.minecraft.util.DamageSource;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumHand;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.SoundCategory;
import net.minecraft.util.SoundEvent;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.BossInfo;
import net.minecraft.world.BossInfoServer;
import net.minecraft.world.DifficultyInstance;
import net.minecraft.world.World;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import thebetweenlands.api.entity.IBLBoss;
import thebetweenlands.api.entity.IEntityMusic;
import thebetweenlands.api.entity.IEntityPreventUnmount;
import thebetweenlands.api.entity.IEntityScreenShake;
import thebetweenlands.api.entity.spawning.IWeightProvider;
import thebetweenlands.client.audio.EntityMusicLayers;
import thebetweenlands.client.audio.EntitySound;
import thebetweenlands.client.render.particle.BLParticles;
import thebetweenlands.client.render.particle.BatchedParticleRenderer;
import thebetweenlands.client.render.particle.DefaultParticleBatches;
import thebetweenlands.client.render.particle.ParticleFactory;
import thebetweenlands.client.render.particle.entity.ParticleGasCloud;
import thebetweenlands.common.entity.EntityTinyWormEggSac;
import thebetweenlands.common.entity.ai.EntityAIHurtByTargetImproved;
import thebetweenlands.common.entity.projectiles.EntitySludgeBall;
import thebetweenlands.common.registries.LootTableRegistry;
import thebetweenlands.common.registries.SoundRegistry;
import thebetweenlands.common.sound.BLSoundEvent;
import thebetweenlands.common.world.storage.BetweenlandsWorldStorage;
import thebetweenlands.common.world.storage.location.EnumLocationType;
import thebetweenlands.common.world.storage.location.LocationSludgeWormDungeon;
import thebetweenlands.common.world.storage.location.LocationStorage;
import thebetweenlands.util.WeightedList;

public class EntitySludgeMenace extends EntityWallLivingRoot implements IEntityScreenShake, IBLBoss, IEntityMusic, IEntityPreventUnmount {
	protected static final byte EVENT_START_ACTION = 90;
	protected static final byte EVENT_SLAM_HIT = 91;

	protected static final byte EVENT_ADD_LEECH_BULGE = 92;
	protected static final byte EVENT_ADD_EGG_SAC_BULGE = 93;
	protected static final byte EVENT_ADD_LARGE_SLUDGE_WORM_BULGE = 94;
	protected static final byte EVENT_ADD_SLUDGE_BALL_SERIES_BULGE = 95;
	protected static final byte EVENT_ADD_SLUDGE_BALL_BULGE = 96;


	public int renderedFrame = -1;

	protected static final DataParameter<Optional<UUID>> BOSSINFO_ID = EntityDataManager.createKey(EntitySludgeMenace.class, DataSerializers.OPTIONAL_UNIQUE_ID);
	protected static final DataParameter<Integer> ACTION_STATE = EntityDataManager.createKey(EntitySludgeMenace.class, DataSerializers.VARINT);

	public static enum ActionState {
		IDLE, SLAM, POKE, SWING, SPIT_MOBS, STUNNED, DEATH;
	}

	private DummyPart[] dummies;

	private AxisAlignedBB renderBoundingBox = new AxisAlignedBB(0, 0, 0, 0, 0, 0);

	protected int actionTimer = 0;
	protected ActionState actionState = ActionState.IDLE;

	protected Vec3d actionTargetPos = null;

	protected int screenShakeTimer = 0;

	protected int hitCounter = 0;
	protected float damageCounter = 0.0f;

	public static enum BulgeType implements IWeightProvider {
		LEECH(EntityLeech.class, 2, 25, 1, 0.01f, 0.8f, 1.0f, EVENT_ADD_LEECH_BULGE),
		EGG_SAC(EntityTinyWormEggSac.class, 3, 15, 1, 0.01f, 0.9f, 1.0f, EVENT_ADD_EGG_SAC_BULGE),
		LARGE_SLUDGE_WORM(EntityLargeSludgeWorm.class, 1, 10, 1, 0.01f, 1.2f, 1.6f, EVENT_ADD_LARGE_SLUDGE_WORM_BULGE),
		SLUDGE_BALL_SERIES(EntitySludgeBall.class, 6, 50, 12, 0.07f, 0.7f, 1.0f, EVENT_ADD_SLUDGE_BALL_SERIES_BULGE),
		SLUDGE_BALL(EntitySludgeBall.class, 6, 30, 1, 0.01f, 0.7f, 1.0f, EVENT_ADD_SLUDGE_BALL_BULGE);

		public final Class<? extends Entity> entityType;
		public final int maxEntityCount;
		public final short spawnWeight;
		public final int spawnSeries;
		public final float speed;
		public final float size;
		public final float length;
		public final byte eventId;

		private BulgeType(Class<? extends Entity> entityType, int maxEntityCount, int spawnWeight, int spawnSeries, float speed, float size, float length, byte eventId) {
			this.entityType = entityType;
			this.maxEntityCount = maxEntityCount;
			this.spawnWeight = (short) spawnWeight;
			this.spawnSeries = spawnSeries;
			this.speed = speed;
			this.size = size;
			this.length = length;
			this.eventId = eventId;
		}

		public static BulgeType byEventId(byte eventId) {
			for(BulgeType type : BulgeType.values()) {
				if(type.eventId == eventId) {
					return type;
				}
			}
			return null;
		}

		@Override
		public short getWeight() {
			return this.spawnWeight;
		}
	}

	public static class Bulge {
		public float renderPosition;
		public float renderSize;

		public BulgeType type;
		protected float prevSize;
		protected float size;
		protected float prevPosition;
		protected float position;
	}

	protected List<Bulge> bulges = new ArrayList<>();
	protected int bulgeSpawnCooldown = 0;
	protected int spawnSeriesCount = 0;
	protected BulgeType spawnSeriesType = null;

	public static class DummyPart extends EntityMultipartDummy implements IEntityPreventUnmount {
		public DummyPart(World world) {
			super(world);
		}

		public DummyPart(World world, MultiPartEntityPart parent) {
			super(world, parent);
		}

		@Override
		public double getMountedYOffset() {
			return -0.4D;
		}

		@Override
		public boolean shouldRiderSit() {
			return false;
		}

		@Override
		public String getName() {
			return getParent() != null ? getParent().getName(): super.getName();
		}

		@Override
		public boolean isUnmountBlocked(EntityPlayer rider) {
			return this.getParent() != null && this.getParent().parent instanceof IEntityPreventUnmount ? ((IEntityPreventUnmount) this.getParent().parent).isUnmountBlocked(rider) : false;
		}
	}

	private final BossInfoServer bossInfo = (BossInfoServer)(new BossInfoServer(this.getDisplayName(), BossInfo.Color.RED, BossInfo.Overlay.PROGRESS)).setDarkenSky(false);

	private int deathTicks = 0;
	private Vec3d deathSpazzMotion = Vec3d.ZERO;

	@SideOnly(Side.CLIENT)
	private ISound livingSound;

	public EntitySludgeMenace(World world) {
		super(world);
		this.dummies = new DummyPart[this.getParts().length];
	}

	@Override
	public IEntityLivingData onInitialSpawn(DifficultyInstance difficulty, IEntityLivingData livingdata) {
		this.setPositionToAnchor(new BlockPos(this).down(), EnumFacing.UP, EnumFacing.NORTH);
		return super.onInitialSpawn(difficulty, livingdata);
	}

	@Override
	protected void entityInit() {
		super.entityInit();
		this.dataManager.register(ACTION_STATE, ActionState.IDLE.ordinal());
		this.dataManager.register(BOSSINFO_ID, Optional.absent());
	}

	@Override
	protected void initEntityAI() {
		this.targetTasks.addTask(0, new EntityAIHurtByTargetImproved(this, true) {
			@Override
			protected double getTargetDistance() {
				return 8.0D;
			}
		});
		this.targetTasks.addTask(1, new EntityAINearestAttackableTarget<>(this, EntityPlayer.class, 0, true, false, null).setUnseenMemoryTicks(120));

		this.tasks.addTask(1, new AISludgeMenaceArmAttack(this));
		this.tasks.addTask(2, new AIAction(this, 10, 40));
	}

	@Override
	protected void applyEntityAttributes() {
		super.applyEntityAttributes();

		this.getEntityAttribute(MAX_ARM_LENGTH).setBaseValue(9);
		this.getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(600.0D);
		this.getEntityAttribute(SharedMonsterAttributes.ATTACK_DAMAGE).setBaseValue(6);
	}

	@Override
	public void readEntityFromNBT(NBTTagCompound nbt) {
		super.readEntityFromNBT(nbt);

		if(this.hasCustomName()) {
			this.bossInfo.setName(this.getDisplayName());
		}

		this.deathTicks = nbt.getInteger("deathTicks");
	}

	@Override
	public void writeEntityToNBT(NBTTagCompound nbt) {
		super.writeEntityToNBT(nbt);

		nbt.setInteger("deathTicks", this.deathTicks);
	}

	@Override
	public void setCustomNameTag(String name) {
		super.setCustomNameTag(name);
		this.bossInfo.setName(this.getDisplayName());
	}

	@Override
	public void addTrackingPlayer(EntityPlayerMP player) {
		super.addTrackingPlayer(player);
		this.bossInfo.addPlayer(player);
	}

	@Override
	public void removeTrackingPlayer(EntityPlayerMP player) {
		super.removeTrackingPlayer(player);
		this.bossInfo.removePlayer(player);
	}

	@Override
	protected void updateAITasks() {
		super.updateAITasks();
		this.bossInfo.setPercent(this.getHealth() / this.getMaxHealth());
	}

	@Override
	protected float[][] getArmCrossSection() {
		float width = this.getFullArmWidth();
		return new float[][] {
			{-width, width},
			{-width, -width},
			{width, -width},
			{width, width}
		};
	}

	@Override
	protected float getNodeSize(int node) {
		return 0.2f + node / (float)this.getNumSegments() * 0.75f;
	}

	@Override
	protected int getNumSegments() {
		return 16;
	}

	@Override
	protected float getFullArmWidth() {
		return 0.55F;
	}

	@Override
	protected float getArmLengthSlack() {
		return 0.25f;
	}

	@Override
	public void playLivingSound() {
		if(this.world.isRemote) {
			this.playLivingSoundLoop();
		}
	}

	@SideOnly(Side.CLIENT)
	private void playLivingSoundLoop() {
		if(this.livingSound != null && !Minecraft.getMinecraft().getSoundHandler().isSoundPlaying(this.livingSound)) {
			this.livingSound = null;
		}

		if(this.livingSound == null) {
			this.livingSound = new EntitySound<EntitySludgeMenace>(SoundRegistry.SLUDGE_MENACE_LIVING, SoundCategory.HOSTILE, this, e -> e.isEntityAlive());
		}

		if(!Minecraft.getMinecraft().getSoundHandler().isSoundPlaying(this.livingSound)) {
			Minecraft.getMinecraft().getSoundHandler().playSound(this.livingSound);
		}
	}

	@Override
	protected SoundEvent getHurtSound(DamageSource damageSource) {
		return SoundRegistry.SLUDGE_MENACE_HURT;
	}

	@Override
	protected SoundEvent getDeathSound() {
		return SoundRegistry.SLUDGE_MENACE_DEATH;
	}

	@Override
	protected void playHurtSound(DamageSource source) {
		this.livingSoundTime = -this.getTalkInterval();

		SoundEvent soundevent = this.getHurtSound(source);

		if (soundevent != null) {
			this.playSound(soundevent, this.getSoundVolume(), this.getSoundPitch());
		}
	}

	@Override
	protected ResourceLocation getLootTable() {
		return LootTableRegistry.SLUDGE_MENACE;
	}

	@Override
	protected boolean isValidBlockForMovement(BlockPos pos, IBlockState state) {
		return state.isOpaqueCube() && state.isNormalCube() && state.isFullCube() && state.getBlockHardness(this.world, pos) > 0;
	}

	@Override
	protected boolean isTravelBlocked() {
		//Don't travel unless obstructed and not in sludgeon
		return this.isMovementBlocked() || this.checkAnchorHere(AnchorChecks.BLOCKS) == 0 ||
				!BetweenlandsWorldStorage.forWorld(this.world).getLocalStorageHandler().getLocalStorages(LocationSludgeWormDungeon.class, this.getEntityBoundingBox(), loc -> loc.isInside(this)).isEmpty();
	}

	@Override
	public float getArmSize(float partialTicks) {
		if(this.ticksExisted < 10) {
			return (float) Math.pow((this.ticksExisted + partialTicks) / 10.0f, 6.0f);
		} else if(!this.isEntityAlive()) {
			return (float) Math.max(0, 1 - Math.pow(this.deathTicks / 130.0f, 8.0f));
		}
		return super.getArmSize(partialTicks);
	}

	@Override
	protected boolean canDespawn() {
		return false;
	}

	@Override
	public boolean isNonBoss() {
		return false;
	}

	@Override
	public void onLivingUpdate() {
		super.onLivingUpdate();

		if (!this.world.isRemote) {
			this.dataManager.set(BOSSINFO_ID, Optional.of(this.bossInfo.getUniqueId()));
		}
	}

	@Override
	public void onUpdate() {
		if(this.world.isRemote) {
			this.actionState = ActionState.values()[this.dataManager.get(ACTION_STATE)];
		}

		if(this.actionState != ActionState.IDLE) {
			this.actionTimer++;
		} else {
			this.actionTimer = 0;
		}

		Vec3d armGravity = new Vec3d(0, 0.5f, 0);

		if(this.actionState == ActionState.SLAM) {
			armGravity = Vec3d.ZERO;
		} else if(this.actionState == ActionState.POKE) {
			armGravity = new Vec3d(0, 0.8f, 0);
		} else if(this.actionState == ActionState.STUNNED) {
			float str = 0.2f;
			armGravity = new Vec3d((this.world.rand.nextFloat() - 0.5f) * str, (this.world.rand.nextFloat() - 0.5f) * str, (this.world.rand.nextFloat() - 0.5f) * str);
		}

		int segmentIndex = 0;
		for(ArmSegment segment : this.armSegments) {
			segment.motion = armGravity.scale(1 - segmentIndex / (float)this.armSegments.size());
			segmentIndex++;
		}

		if(this.screenShakeTimer > 0) {
			this.screenShakeTimer--;
		}

		//Spawn animation
		if(this.ticksExisted < 10) {
			Vec3d straightPos = this.getPositionVector().add(new Vec3d(this.getFacing().getXOffset(), this.getFacing().getYOffset(), this.getFacing().getZOffset()).scale(this.getArmSize(1) * (this.getEntityAttribute(MAX_ARM_LENGTH).getAttributeValue() + 1)));
			this.rootTip.setPosition(straightPos.x, straightPos.y, straightPos.z);
		}

		super.onUpdate();

		Entity[] parts = this.getParts();

		this.updateBulges(parts);

		if(this.world.isRemote) {
			for(int i = 0; i < parts.length; i++) {
				if(i == 0) {
					this.renderBoundingBox = parts[i].getEntityBoundingBox();
				} else {
					this.renderBoundingBox = this.renderBoundingBox.union(parts[i].getEntityBoundingBox());
				}
			}

			this.spawnTipParticles();
		}

		if(!this.world.isRemote && this.isEntityAlive()) {
			//Heal when no player is nearby
			if(this.ticksExisted % 12 == 0 && this.getHealth() < this.getMaxHealth()) {
				final double rangeXZ = 14;
				final double rangeY = 13;

				List<EntityLivingBase> nearbyPlayers = this.world.getEntitiesWithinAABB(EntityPlayer.class, this.getEntityBoundingBox().grow(rangeXZ, rangeXZ, rangeXZ));

				Iterator<EntityLivingBase> it = nearbyPlayers.iterator();
				while(it.hasNext()) {
					EntityLivingBase living = it.next();
					if(living.getDistance(this.posX, living.posY, this.posZ) > rangeXZ || Math.abs(living.posY - this.posY) > rangeY)
						it.remove();
				}

				if(nearbyPlayers.isEmpty()) {
					this.heal(1);
				}
			}

			for(int i = 0; i < this.dummies.length; i++) {
				DummyPart dummy = this.dummies[i];

				if(dummy == null || !dummy.isEntityAlive()) {
					Entity multipart = parts[i];

					if(multipart instanceof MultiPartEntityPart) {
						this.dummies[i] = dummy = new DummyPart(this.world, (MultiPartEntityPart) multipart);
						this.world.spawnEntity(dummy);
					}
				} else {
					dummy.updatePositioning();
				}
			}
		}
	}

	protected void updateBulges(Entity[] parts) {
		Iterator<Bulge> it = this.bulges.iterator();
		while(it.hasNext()) {
			Bulge bulge = it.next();

			bulge.prevPosition = bulge.position;
			bulge.prevSize = bulge.size;

			bulge.position += bulge.type.speed;

			if(bulge.position >= 0.95f) {
				bulge.size -= 0.1f;

				if(bulge.size < 0.0f) {
					bulge.size = 0.0f;
				}
			}

			if(bulge.position >= 1.0f) {
				bulge.position = 1.0f;

				it.remove();

				if(!this.world.isRemote && parts.length >= 2) {
					Entity mob;

					switch(bulge.type) {
					default:
					case LEECH:
						mob = new EntityLeech(this.world);
						break;
					case EGG_SAC:
						mob = new EntityTinyWormEggSac(this.world);
						break;
					case LARGE_SLUDGE_WORM:
						mob = new EntityLargeSludgeWorm(this.world);
						break;
					case SLUDGE_BALL:
					case SLUDGE_BALL_SERIES:
						mob = new EntitySludgeBall(this.world, this, false);
						break;
					}

					Vec3d dir = parts[parts.length - 1].getPositionVector().subtract(parts[parts.length - 2].getPositionVector()).normalize();

					float spawnOffset = -0.5f;

					mob.setPosition(this.rootTip.posX + dir.x * spawnOffset, this.rootTip.posY + dir.y * spawnOffset, this.rootTip.posZ + dir.z * spawnOffset);

					float speed = 0.85f;

					mob.motionX = dir.x * speed;
					mob.motionY = dir.y * speed;
					mob.motionZ = dir.z * speed;

					if(mob instanceof EntityLeech) {
						EntityLivingBase target = this.getAttackTarget();

						if(target != null) {
							((EntityLeech) mob).setAttackTarget(target);
						}
					}

					this.world.spawnEntity(mob);

					this.playSound(SoundRegistry.SLUDGE_MENACE_SPIT, 1, 1);
				}
			}
		}
	}

	@Override
	protected void onDeathUpdate() {
		this.bossInfo.setPercent(0);

		++this.deathTicks;

		if(!this.world.isRemote) {
			if(this.actionState != ActionState.DEATH) {
				this.startAction(ActionState.DEATH);
			}

			if (this.deathTicks > 100 && this.deathTicks % 5 == 0) {
				int xp = 800;
				while (xp > 0) {
					int dropXP = EntityXPOrb.getXPSplit(xp);
					xp -= dropXP;
					this.world.spawnEntity(new EntityXPOrb(this.world, this.posX, this.posY + this.height / 2.0D, this.posZ, dropXP));
				}
			}

			if(this.deathTicks > 130) {
				int xp = 3000;
				while (xp > 0) {
					int dropXP = EntityXPOrb.getXPSplit(xp);
					xp -= dropXP;
					this.world.spawnEntity(new EntityXPOrb(this.world, this.posX, this.posY + this.height / 2.0D, this.posZ, dropXP));
				}

				List<LocationStorage> locations = LocationStorage.getLocations(this.world, this.getPositionVector().add(0, 2, 0));
				for(LocationStorage location : locations) {
					if(location.getType() == EnumLocationType.SLUDGE_WORM_DUNGEON) {
						//Remove block guard
						if(location.getGuard() != null) {
							location.getGuard().clear(this.world);
							location.setDirty(true);
						}

						//Set location to defeated to remove ground fog and mob spawns
						if(location instanceof LocationSludgeWormDungeon) {
							((LocationSludgeWormDungeon) location).setDefeated(true);
						}
					}
				}

				this.setDead();
			}
		}
	}

	@SideOnly(Side.CLIENT)
	protected void spawnTipParticles() {
		Entity[] parts = this.getParts();

		if(parts.length > 2) {
			Entity lastPart = parts[parts.length - 1];
			Entity secondLastPart = parts[parts.length - 2];

			Vec3d dir = lastPart.getPositionVector().subtract(secondLastPart.getPositionVector()).normalize().add((this.rand.nextFloat() - 0.5f) * 0.5f, 0, (this.rand.nextFloat() - 0.5f) * 0.5f).scale(0.1D);

			double x = lastPart.posX;
			double y = lastPart.posY + lastPart.height / 2;
			double z = lastPart.posZ;

			double mx = dir.x;
			double my = dir.y;
			double mz = dir.z;

			int[] color = {100, 70, 0, 255};

			ParticleGasCloud hazeParticle = (ParticleGasCloud) BLParticles.GAS_CLOUD
					.create(this.world, x, y, z, ParticleFactory.ParticleArgs.get()
							.withData(null)
							.withMotion(mx, my, mz)
							.withColor(color[0] / 255.0F, color[1] / 255.0F, color[2] / 255.0F, color[3] / 255.0F)
							.withScale(3.5f));

			BatchedParticleRenderer.INSTANCE.addParticle(DefaultParticleBatches.GAS_CLOUDS_HEAT_HAZE, hazeParticle);

			ParticleGasCloud particle = (ParticleGasCloud) BLParticles.GAS_CLOUD
					.create(this.world, x, y, z, ParticleFactory.ParticleArgs.get()
							.withData(null)
							.withMotion(mx, my, mz)
							.withColor(color[0] / 255.0F, color[1] / 255.0F, color[2] / 255.0F, color[3] / 255.0F)
							.withScale(2.5f));

			BatchedParticleRenderer.INSTANCE.addParticle(DefaultParticleBatches.GAS_CLOUDS_TEXTURED, particle);
		}
	}

	@Override
	public boolean attackEntityAsMob(Entity target) {
		boolean damaged = super.attackEntityAsMob(target);

		this.playSound(SoundRegistry.SLUDGE_MENACE_ATTACK, 1, 1);

		if(this.actionState == ActionState.POKE) {
			if(target == this.getAttackTarget() && this.actionTimer >= 40) {
				DummyPart endDummy = this.dummies[this.dummies.length - 1];

				if(endDummy != null && endDummy.isEntityAlive()) {
					List<Entity> passengers = endDummy.getPassengers();

					if(!passengers.isEmpty() && passengers.get(0) != this.getAttackTarget()) {
						endDummy.removePassengers();
					}

					if(target.getRidingEntity() != endDummy) {
						target.dismountRidingEntity();
						target.startRiding(endDummy, true);
					}
				}

				if(damaged) {
					this.heal((float) this.getEntityAttribute(SharedMonsterAttributes.ATTACK_DAMAGE).getBaseValue() * 1.5f);
				}
			}
		} else if(this.actionState == ActionState.SWING) {
			if(!damaged && target instanceof EntityLivingBase && ((EntityLivingBase) target).isActiveItemStackBlocking() && this.world.rand.nextInt(3) == 0) {
				this.startAction(ActionState.STUNNED);
			} else if(damaged && target instanceof EntityLivingBase) {
				float dx = (float)(this.rootTip.posX - this.posX);
				float dz = (float)(this.rootTip.posZ - this.posZ);

				float len = MathHelper.sqrt(dx*dx + dz*dz);

				dx /= len;
				dz /= len;

				((EntityLivingBase) target).knockBack(this, 1.75f, dz, -dx);
			}
		}

		return damaged;
	}

	@Override
	public boolean attackEntityFrom(DamageSource source, float amount) {
		if(source.isProjectile() || source.isExplosion() || source.isFireDamage()) {
			return false;
		}

		if(super.attackEntityFrom(source, amount)) {
			if(!this.world.isRemote && this.actionState == ActionState.POKE) {
				this.damageCounter += amount;
				this.hitCounter++;

				if(this.damageCounter >= 15 || this.hitCounter >= 6) {
					DummyPart endDummy = this.dummies[this.dummies.length - 1];

					if(endDummy != null && !endDummy.getPassengers().isEmpty()) {
						this.startAction(ActionState.IDLE);
						endDummy.removePassengers();
					}
				}
			}

			return true;
		}

		return false;
	}

	@Override
	protected Vec3d updateTargetTipPos(Vec3d armStartWorld, float maxArmLength, Vec3d dirFwd, Vec3d dirUp) {
		switch(this.actionState) {
		default:
		case DEATH:
			return this.updateDeathTipPos(armStartWorld, maxArmLength, dirFwd, dirUp);
		case IDLE:
			return this.updateIdleTargetTipPos(armStartWorld, maxArmLength, dirFwd, dirUp);
		case SLAM:
			return this.updateSlamTargetTipPos(armStartWorld, maxArmLength, dirFwd, dirUp);
		case POKE:
			return this.updatePokeTargetTipPos(armStartWorld, maxArmLength, dirFwd, dirUp);
		case SWING:
			return this.updateSwingTargetTipPos(armStartWorld, maxArmLength, dirFwd, dirUp);
		case SPIT_MOBS:
			return this.updateSpitMobsTipPos(armStartWorld, maxArmLength, dirFwd, dirUp);
		case STUNNED:
			return this.updateStunnedTipPos(armStartWorld, maxArmLength, dirFwd, dirUp);
		}
	}

	protected Vec3d updateDeathTipPos(Vec3d armStartWorld, float maxArmLength, Vec3d dirFwd, Vec3d dirUp) {
		if(this.world.rand.nextInt(2) == 0) {
			float vel = 2.0f + this.world.rand.nextFloat() * 4.0f;
			this.deathSpazzMotion = new Vec3d((this.world.rand.nextFloat() - 0.5f) * vel, (this.world.rand.nextFloat() - 0.5f) * vel + 1.25f, (this.world.rand.nextFloat() - 0.5f) * vel);
		}

		return this.rootTip.getPositionVector().add(this.deathSpazzMotion);
	}

	protected Vec3d updateStunnedTipPos(Vec3d armStartWorld, float maxArmLength, Vec3d dirFwd, Vec3d dirUp) {
		if(this.actionTimer >= 150) {
			this.actionState = ActionState.IDLE;
		}

		Vec3d moveDir = this.getPositionVector().subtract(this.rootTip.getPositionVector()).normalize().scale(0.02f);

		float str = 0.2f;

		return this.rootTip.getPositionVector().add((this.world.rand.nextFloat() - 0.5f) * str + moveDir.x, (this.world.rand.nextFloat() - 0.5f) * str + 0.01f, (this.world.rand.nextFloat() - 0.5f) * str + moveDir.z);
	}

	protected Vec3d updateSpitMobsTipPos(Vec3d armStartWorld, float maxArmLength, Vec3d dirFwd, Vec3d dirUp) {
		if(this.actionTimer < 200 && this.bulgeSpawnCooldown-- <= 0) {
			if(this.spawnSeriesType == null) {
				Set<BulgeType> availableTypes = new HashSet<>();

				AxisAlignedBB checkAabb = this.getEntityBoundingBox().grow(12, 1, 12).expand(0, 11, 0);

				TObjectIntMap<BulgeType> bulgeCounts = new TObjectIntHashMap<>();
				for(Bulge bulge : this.bulges) {
					bulgeCounts.increment(bulge.type);
				}

				for(BulgeType bulgeType : BulgeType.values()) {
					if(this.world.getEntitiesWithinAABB(bulgeType.entityType, checkAabb).size() + bulgeCounts.get(bulgeType) < bulgeType.maxEntityCount && (bulgeType.spawnSeries <= 1 || this.bulges.isEmpty())) {
						availableTypes.add(bulgeType);
					}
				}

				if(!availableTypes.isEmpty()) {
					WeightedList<BulgeType> list = new WeightedList<>();

					list.addAll(availableTypes);
					list.recalculateWeight();

					BulgeType bulgeType = list.getRandomItem(this.rand);

					this.addBulge(bulgeType);

					if(bulgeType.spawnSeries > 1) {
						this.spawnSeriesCount = bulgeType.spawnSeries;
						this.spawnSeriesType = bulgeType;
					}
				}
			} else {
				this.addBulge(this.spawnSeriesType);

				this.spawnSeriesCount--;

				if(this.spawnSeriesCount <= 0) {
					this.spawnSeriesCount = 0;
					this.spawnSeriesType = null;
				}
			}

			if(this.spawnSeriesType == null) {
				this.bulgeSpawnCooldown = 15 + this.world.rand.nextInt(15);
			} else {
				this.bulgeSpawnCooldown = 4;
			}
		}

		if(this.actionTimer >= 200 && this.bulges.isEmpty()) {
			this.startAction(ActionState.IDLE);
		}

		float furthestPos = 0.0f;
		Bulge nextBulge = null;

		for(Bulge bulge : this.bulges) {
			if(bulge.position > furthestPos) {
				furthestPos = bulge.position;
				nextBulge = bulge;
			}
		}

		this.armMovementTicks++;

		Vec3d targetTipPos = armStartWorld.add(dirFwd.scale(maxArmLength + 1.0f));

		Entity target = this.getAttackTarget();

		if(nextBulge != null && nextBulge.type == BulgeType.LARGE_SLUDGE_WORM) {
			float rx = (float) Math.cos(this.ticksExisted * 0.05f);
			float rz = (float) Math.sin(this.ticksExisted * 0.05f);

			targetTipPos = armStartWorld.add(dirFwd.scale(maxArmLength / 2)).add(rx * 2, 0, rz * 2);
		} else if(nextBulge != null && nextBulge.type == BulgeType.LEECH && target != null) {
			Vec3d bendPos = armStartWorld.add(dirFwd.scale(maxArmLength));

			Vec3d targetDir = target.getPositionVector().subtract(bendPos).normalize().scale(5);

			targetTipPos = bendPos.add(targetDir);
		} else {
			float idleX = MathHelper.cos(this.armMovementTicks / 9.0f) * 0.75F;
			float idleY = MathHelper.sin(this.armMovementTicks / 7.0f) * 0.75F;
			float idleZ = (MathHelper.cos(this.armMovementTicks / 15.0f) + 1) * 0.25f;

			float forwardPos = (float) dirFwd.dotProduct(targetTipPos.subtract(armStartWorld));
			float offsetZ = 0.0f;
			if(forwardPos < 1.0F) {
				offsetZ = 1.0F - forwardPos;
			}

			//Idle movement
			targetTipPos = targetTipPos.add(dirUp.scale(idleY)).add(dirFwd.crossProduct(dirUp).scale(idleX)).add(dirFwd.scale(offsetZ - idleZ));
		}

		Vec3d tipPos = this.rootTip.getPositionVector();

		Vec3d diff = targetTipPos.subtract(tipPos);

		targetTipPos = tipPos.add(diff.normalize().scale(Math.min(diff.length(), 0.5D)));

		return targetTipPos;
	}

	protected Vec3d updateSwingTargetTipPos(Vec3d armStartWorld, float maxArmLength, Vec3d dirFwd, Vec3d dirUp) {
		float prevDrive = (this.actionTimer - 1) * 0.07f * (1.0f + (this.actionTimer - 1) / 300.0f * 2.0f);
		float drive = this.actionTimer * 0.07f * (1.0f + this.actionTimer / 300.0f * 2.0f);

		float prevDriveMod = prevDrive % ((float)Math.PI * 2);
		float driveMod = drive % ((float)Math.PI * 2);

		if(prevDriveMod <= 1f && driveMod > 1f) {
			this.playSound(SoundRegistry.SLUDGE_MENACE_ATTACK, 1, 1);
		}

		if(this.actionTimer >= 300) {
			this.startAction(ActionState.IDLE);
		}

		float swingLength = maxArmLength + 0.2f;

		Vec3d targetTipPos = armStartWorld.add(Math.cos(drive) * swingLength, 1.8F + (Math.sin(drive * 0.28f) + 1) * 2.2f, Math.sin(drive) * swingLength);

		Vec3d tipPos = this.rootTip.getPositionVector();

		Vec3d diff = targetTipPos.subtract(tipPos);

		targetTipPos = tipPos.add(diff.normalize().scale(Math.min(diff.length(), 0.5D + this.actionTimer / 300.0f * 2.5D)));

		return targetTipPos;
	}

	protected Vec3d updatePokeTargetTipPos(Vec3d armStartWorld, float maxArmLength, Vec3d dirFwd, Vec3d dirUp) {
		boolean isSucking = false;

		DummyPart endDummy = this.dummies[this.dummies.length - 1];

		if(endDummy != null) {
			List<Entity> passengers = endDummy.getPassengers();

			if(passengers.isEmpty() || passengers.get(0) != this.getAttackTarget()) {
				if(this.actionTimer >= 70) {
					this.startAction(ActionState.IDLE);
				}
			} else if(!passengers.isEmpty() && passengers.get(0) == this.getAttackTarget()) {
				isSucking = true;
			}
		} else if(this.actionTimer >= 70) {
			this.startAction(ActionState.IDLE);
		}

		if(isSucking) {
			if(endDummy != null && endDummy.getPassengers().get(0) == this.getAttackTarget() && this.getAttackTarget().getHealth() <= 4.0f) {
				this.startAction(ActionState.IDLE);
				endDummy.removePassengers();
			}

			this.armMovementTicks++;

			float idleX = MathHelper.cos(this.armMovementTicks / 4.0f) * 0.75F;
			float idleY = MathHelper.sin(this.armMovementTicks / 3.0f) * 0.75F;
			float idleZ = (MathHelper.cos(this.armMovementTicks / 8.0f) + 1) * 0.25f;

			return this.actionTargetPos.add(idleX, idleY + 1.5f, idleZ);
		} else {
			this.damageCounter = 0.0f;
			this.hitCounter = 0;

			float drive = this.actionTimer;

			if(drive < 43 && this.getAttackTarget() != null) {
				this.actionTargetPos = this.getAttackTarget().getPositionVector();
			}

			if(drive < 40) {
				Vec3d bendPos = armStartWorld.add(dirFwd.scale(maxArmLength));

				Vec3d targetDir = new Vec3d(3, 0, 0);

				if(this.getAttackTarget() != null) {
					targetDir = this.getAttackTarget().getPositionVector().subtract(bendPos).normalize().scale(5);
				}

				return bendPos.add(targetDir);
			} else if(drive < 48) {
				float speed = (drive - 40) / 8.0f * 4.0f;

				Vec3d targetTipPos = armStartWorld.add(dirFwd.scale(maxArmLength));

				if(this.actionTargetPos != null) {
					targetTipPos = this.actionTargetPos.add(0, this.getAttackTarget() != null ? this.getAttackTarget().height / 2 : 0, 0);
				}

				Vec3d tipPos = this.rootTip.getPositionVector();

				Vec3d tipDiff = targetTipPos.subtract(tipPos);
				targetTipPos = tipPos.add(tipDiff.normalize().scale(Math.min(tipDiff.length(), speed)));

				return targetTipPos;
			}
		}

		return this.rootTip.getPositionVector();
	}

	protected Vec3d updateSlamTargetTipPos(Vec3d armStartWorld, float maxArmLength, Vec3d dirFwd, Vec3d dirUp) {
		float drive = this.actionTimer;

		if(drive < 58 && this.getAttackTarget() != null) {
			this.actionTargetPos = this.getAttackTarget().getPositionVector();
		}

		float rot;
		if(drive < 20) {
			rot = (float)Math.PI / 2;
		} else if(drive < 60) {
			float slap = (drive - 20) / 40.0f;
			float s = 6;
			float a = 6;
			rot = (float)Math.pow(slap, a + slap * s) * (float)(Math.PI / 2) + (float)Math.PI / 2;
		} else {
			rot = (float)Math.PI;
		}

		Vec3d targetDir = new Vec3d(maxArmLength + 1, 0, 0);

		if(this.actionTargetPos != null) {
			targetDir = armStartWorld.subtract(this.actionTargetPos).normalize().scale(maxArmLength + 1.0F);
		}

		if(this.actionTimer >= 90) {
			this.startAction(ActionState.IDLE);
		}

		if(!this.world.isRemote && this.actionTimer == 63) {
			this.startSlamHit();

			for(Entity part : this.getParts()) {
				BlockPos pos = new BlockPos(part).down();

				BlockPos spawnPos = null;

				if(!this.world.isAirBlock(pos)) {
					spawnPos = pos;
				} else {
					for(int i = 0; i < 5; i++) {
						pos = pos.up();
						if(this.world.isAirBlock(pos)) {
							spawnPos = pos.down();
							break;
						}
					}
				}

				if(spawnPos != null) {
					EntitySludgeJet jet = new EntitySludgeJet(this.world);
					jet.setLocationAndAngles(part.posX + this.rand.nextFloat() - 0.5f, spawnPos.getY() + 0.5D, part.posZ + this.rand.nextFloat() - 0.5f, 0, 0);
					this.world.spawnEntity(jet);
				}
			}
		}

		Vec3d targetTipPos = armStartWorld.add(targetDir.x * Math.cos(rot), Math.sin(rot) * targetDir.length(), targetDir.z * Math.cos(rot));

		Vec3d tipPos = this.rootTip.getPositionVector();

		Vec3d diff = targetTipPos.subtract(tipPos);

		targetTipPos = tipPos.add(diff.normalize().scale(Math.min(diff.length(), 0.5D + this.actionTimer / 90.0f * 5.0D)));

		return targetTipPos;
	}

	protected Vec3d updateIdleTargetTipPos(Vec3d armStartWorld, float maxArmLength, Vec3d dirFwd, Vec3d dirUp) {
		float flailingStrength = this.isSwingInProgress ? (1 - this.swingProgress) : this.hurtTime > 0 ? (this.hurtTime / (float)this.maxHurtTime) * 0.5F : 0.0f;

		this.armMovementTicks += 1 + (int)(flailingStrength * 10);

		float idleX = MathHelper.cos(this.armMovementTicks / 9.0f) * 1.25F;
		float idleY = MathHelper.sin(this.armMovementTicks / 7.0f) * 0.25F;
		float idleZ = (MathHelper.cos(this.armMovementTicks / 15.0f) + 1) * 0.75f;

		Vec3d targetTipPos = armStartWorld.add(dirFwd.scale(maxArmLength));

		EntityLivingBase target = this.getAttackTarget();
		if(target != null) {
			targetTipPos = target.getPositionVector().add(0, target.height / 2, 0);
		}

		float forwardPos = (float) dirFwd.dotProduct(targetTipPos.subtract(armStartWorld));
		float offsetZ = 0.0f;
		if(forwardPos < 1.0F) {
			offsetZ = 1.0F - forwardPos;
		}

		//Idle movement
		targetTipPos = targetTipPos.add(dirUp.scale(idleY)).add(dirFwd.crossProduct(dirUp).scale(idleX)).add(dirFwd.scale(offsetZ - idleZ));

		Vec3d tipPos = this.rootTip.getPositionVector();

		Vec3d tipDiff = targetTipPos.subtract(tipPos);
		targetTipPos = tipPos.add(tipDiff.normalize().scale(Math.min(tipDiff.length(), 0.125D + flailingStrength * 0.8D)));

		return targetTipPos;
	}

	@Override
	public AxisAlignedBB getRenderBoundingBox() {
		return this.renderBoundingBox;
	}

	@Override
	public float getShakeIntensity(Entity viewer, float partialTicks) {
		if(this.screenShakeTimer > 0) {
			float dstMul = MathHelper.clamp(1.0f - this.getDistance(viewer) / 24.0f, 0, 1);
			return dstMul * (this.screenShakeTimer - partialTicks) / 10.0f * 0.2f;
		}
		return 0;
	}

	@Override
	public void handleStatusUpdate(byte id) {
		super.handleStatusUpdate(id);

		BulgeType bulgeType = BulgeType.byEventId(id);
		if(bulgeType != null) {
			this.addBulge(bulgeType);
		} else if(id == EVENT_START_ACTION) {
			this.startAction(ActionState.values()[this.dataManager.get(ACTION_STATE)]);
		} else if(id == EVENT_SLAM_HIT) {
			this.startSlamHit();
		}
	}

	protected void startAction(ActionState action) {
		this.actionState = action;
		this.actionTimer = 0;

		if(!this.world.isRemote) {
			this.world.setEntityState(this, EVENT_START_ACTION);
			this.dataManager.set(ACTION_STATE, this.actionState.ordinal());
		}
	}

	protected void startSlamHit() {
		this.screenShakeTimer = 10;

		this.playSound(SoundRegistry.WALL_SLAM, 2, 1);
		this.playSound(SoundRegistry.SLUDGE_MENACE_ATTACK, 1, 1);

		if(!this.world.isRemote) {
			this.world.setEntityState(this, EVENT_SLAM_HIT);
		}
	}

	protected void addBulge(BulgeType type) {
		Bulge bulge = new Bulge();

		bulge.size = bulge.renderSize = type.size;
		bulge.type = type;

		this.bulges.add(bulge);

		if(!this.world.isRemote) {
			this.world.setEntityState(this, type.eventId);
		}
	}

	@Nullable
	public List<Bulge> getBulges(float partialTicks) {
		for(Bulge bulge : this.bulges) {
			bulge.renderPosition = bulge.prevPosition + (bulge.position - bulge.prevPosition) * partialTicks;
			bulge.renderSize = bulge.prevSize + (bulge.size - bulge.prevSize) * partialTicks;
		}
		return this.bulges;
	}

	protected static class AISludgeMenaceArmAttack extends AIArmAttack {
		protected final EntitySludgeMenace menace;

		protected AISludgeMenaceArmAttack(EntitySludgeMenace entity) {
			super(entity);
			this.menace = entity;
		}

		@Override
		public void updateTask() {
			Entity target = this.entity.getAttackTarget();

			if(this.attackTicks > 0) {
				this.attackTicks--;
			} else if(target != null && this.menace.isEntityAlive()) {
				AxisAlignedBB targetAabb = target.getEntityBoundingBox();

				boolean attacked = false;

				Set<Entity> attackedSet = new HashSet<>();

				for(Entity part : this.menace.getParts()) {
					if(!attackedSet.contains(target) && targetAabb.intersects(part.getEntityBoundingBox())) {
						attacked = true;

						if(this.entity.attackEntityAsMob(target)) {
							attackedSet.add(target);
						}
					}

					List<EntityLivingBase> collidingList = this.menace.getWorld().getEntitiesWithinAABB(EntityLivingBase.class, part.getEntityBoundingBox(), e -> e instanceof IMob == false && e != target && !attackedSet.contains(e));

					for(EntityLivingBase colliding : collidingList) {
						attacked = true;

						if(this.entity.attackEntityAsMob(colliding)) {
							attackedSet.add(colliding);
						}
					}
				}

				if(attacked) {
					this.entity.swingArm(EnumHand.MAIN_HAND);
					this.attackTicks = 20;
				}
			}
		}
	}

	protected static class AIAction extends EntityAIBase {
		protected final EntitySludgeMenace menace;

		protected int cooldown;
		protected int minCooldown, maxCooldown;

		protected AIAction(EntitySludgeMenace entity, int minCooldown, int maxCooldown) {
			this.menace = entity;
			this.minCooldown = minCooldown;
			this.maxCooldown = maxCooldown;
			this.resetTask();
		}

		@Override
		public boolean shouldExecute() {
			if(this.menace.actionState == ActionState.IDLE && this.menace.getAttackTarget() != null && this.menace.isEntityAlive()) {
				return this.cooldown-- <= 0;
			}
			return false;
		}

		@Override
		public boolean shouldContinueExecuting() {
			return false;
		}

		@Override
		public void startExecuting() {
			int nr = this.menace.rand.nextInt(100);

			if(nr <= 17) {
				this.menace.startAction(ActionState.SPIT_MOBS);
			} else if(nr <= 34) {
				this.menace.startAction(ActionState.SWING);
			} else if(nr <= 67) {
				this.menace.startAction(ActionState.POKE);
			} else {
				this.menace.startAction(ActionState.SLAM);
			}
		}

		@Override
		public void resetTask() {
			this.cooldown = this.minCooldown + this.menace.rand.nextInt(this.maxCooldown - this.minCooldown);
		}
	}

	@Override
	public UUID getBossInfoUuid() {
		return this.dataManager.get(BOSSINFO_ID).or(new UUID(0, 0));
	}

	@Override
	public BLSoundEvent getMusicFile(EntityPlayer listener) {
		return SoundRegistry.PIT_OF_DECAY_LOOP;
	}

	@Override
	public double getMusicRange(EntityPlayer listener) {
		return 32.0D;
	}

	@Override
	public boolean isMusicActive(EntityPlayer listener) {
		return this.isEntityAlive();
	}

	@Override
	public int getMusicLayer(EntityPlayer listener) {
		return EntityMusicLayers.BOSS;
	}

	@Override
	public boolean isUnmountBlocked(EntityPlayer rider) {
		//Prevent target from dismounting while sucking out target

		if(!this.world.isRemote && this.actionState == ActionState.POKE) {
			DummyPart endDummy = this.dummies[this.dummies.length - 1];

			if(endDummy != null && endDummy.isEntityAlive()) {
				List<Entity> passengers = endDummy.getPassengers();

				if(!passengers.isEmpty() && passengers.get(0) == rider) {
					return true;
				}
			}
		}

		return false;
	}
}
