package pneumaticCraft.common.entity.living;

import io.netty.buffer.ByteBuf;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID;

import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.SharedMonsterAttributes;
import net.minecraft.entity.ai.EntityAIBase;
import net.minecraft.entity.ai.EntityAITasks;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.item.EntityXPOrb;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.entity.player.InventoryPlayer;
import net.minecraft.init.Blocks;
import net.minecraft.inventory.IInventory;
import net.minecraft.inventory.InventoryBasic;
import net.minecraft.item.ItemDye;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.network.NetHandlerPlayServer;
import net.minecraft.network.NetworkManager;
import net.minecraft.network.play.client.C15PacketClientSettings;
import net.minecraft.pathfinding.PathEntity;
import net.minecraft.pathfinding.PathPoint;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.management.ItemInWorldManager;
import net.minecraft.stats.StatBase;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.util.ChunkCoordinates;
import net.minecraft.util.DamageSource;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.IChatComponent;
import net.minecraft.util.Vec3;
import net.minecraft.world.ChunkPosition;
import net.minecraft.world.World;
import net.minecraft.world.WorldServer;
import net.minecraftforge.common.IExtendedEntityProperties;
import net.minecraftforge.common.util.ForgeDirection;
import net.minecraftforge.fluids.FluidTank;
import net.minecraftforge.fluids.IFluidTank;

import org.lwjgl.opengl.GL11;

import pneumaticCraft.api.block.IPneumaticWrenchable;
import pneumaticCraft.api.client.pneumaticHelmet.IHackableEntity;
import pneumaticCraft.api.drone.IPathNavigator;
import pneumaticCraft.api.drone.IPathfindHandler;
import pneumaticCraft.api.tileentity.IManoMeasurable;
import pneumaticCraft.client.render.RenderProgressingLine;
import pneumaticCraft.common.NBTUtil;
import pneumaticCraft.common.PneumaticCraftAPIHandler;
import pneumaticCraft.common.ai.DroneAIManager;
import pneumaticCraft.common.ai.DroneAIManager.EntityAITaskEntry;
import pneumaticCraft.common.ai.DroneGoToChargingStation;
import pneumaticCraft.common.ai.DroneGoToOwner;
import pneumaticCraft.common.ai.DroneMoveHelper;
import pneumaticCraft.common.ai.EntityPathNavigateDrone;
import pneumaticCraft.common.ai.FakePlayerItemInWorldManager;
import pneumaticCraft.common.ai.IDroneBase;
import pneumaticCraft.common.block.Blockss;
import pneumaticCraft.common.config.Config;
import pneumaticCraft.common.item.ItemGPSTool;
import pneumaticCraft.common.item.ItemMachineUpgrade;
import pneumaticCraft.common.item.ItemProgrammingPuzzle;
import pneumaticCraft.common.item.Itemss;
import pneumaticCraft.common.minigun.Minigun;
import pneumaticCraft.common.network.NetworkHandler;
import pneumaticCraft.common.network.PacketSendDroneDebugEntry;
import pneumaticCraft.common.network.PacketShowWireframe;
import pneumaticCraft.common.network.PacketSyncDroneEntityProgWidgets;
import pneumaticCraft.common.progwidgets.IProgWidget;
import pneumaticCraft.common.progwidgets.ProgWidgetGoToLocation;
import pneumaticCraft.common.recipes.AmadronOffer;
import pneumaticCraft.common.recipes.AmadronOfferCustom;
import pneumaticCraft.common.tileentity.TileEntityPlasticMixer;
import pneumaticCraft.common.tileentity.TileEntityProgrammer;
import pneumaticCraft.common.util.PneumaticCraftUtils;
import pneumaticCraft.lib.NBTKeys;
import pneumaticCraft.lib.PneumaticValues;

import com.mojang.authlib.GameProfile;

import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.network.ByteBufUtils;
import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData;
import cpw.mods.fml.relauncher.ReflectionHelper;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;

public class EntityDrone extends EntityDroneBase implements IManoMeasurable, IInventoryHolder, IPneumaticWrenchable,
        IEntityAdditionalSpawnData, IHackableEntity, IDroneBase{

    private static final HashMap<String, Integer> colorMap = new HashMap<String, Integer>();

    static {
        colorMap.put("aureylian", 0xff69b4);
        colorMap.put("loneztar", 0x00a0a0);
        colorMap.put("jadedcat", 0xa020f0);
    }

    public boolean isChangingCurrentStack;//used when syncing up the stacks of the drone with the fake player. Without it they'll keep syncing resulting in a stackoverflow.
    private IInventory inventory = new InventoryDrone("Drone Inventory", true, 0);
    private final FluidTank tank = new FluidTank(Integer.MAX_VALUE);
    private ItemStack[] upgradeInventory = new ItemStack[9];
    private final int[] emittingRedstoneValues = new int[6];
    private float propSpeed;
    private static final float LASER_EXTEND_SPEED = 0.05F;

    protected float currentAir; //the current held energy of the Drone;
    private float volume;
    private RenderProgressingLine targetLine;
    private RenderProgressingLine oldTargetLine;
    public List<IProgWidget> progWidgets = new ArrayList<IProgWidget>();

    private DroneFakePlayer fakePlayer;
    public String playerName = "Drone";
    private String playerUUID;

    public DroneGoToChargingStation chargeAI;
    public DroneGoToOwner gotoOwnerAI;
    private final DroneAIManager aiManager = new DroneAIManager(this);

    private boolean firstTick = true;
    public boolean naturallySpawned = true;//determines if it should drop a drone when it dies.
    public boolean hasLiquidImmunity;
    private double speed;
    private int lifeUpgrades;
    private int suffocationCounter = 40;//Drones are invincible for suffocation for this time.
    private boolean isSuffocating;
    private boolean disabledByHacking;
    private boolean standby;//If true, the drone's propellors stop, the drone will fall down, and won't use pressure.
    private Minigun minigun;

    private AmadronOffer handlingOffer;
    private int offerTimes;
    private ItemStack usedTablet;//Tablet used to place the order.
    private String buyingPlayer;
    private final SortedSet<DebugEntry> debugEntries = new TreeSet<DebugEntry>();
    private final Set<EntityPlayerMP> syncedPlayers = new HashSet<EntityPlayerMP>();

    public EntityDrone(World world){
        super(world);
        setSize(0.7F, 0.35F);
        ReflectionHelper.setPrivateValue(EntityLiving.class, this, new EntityPathNavigateDrone(this, world), "navigator", "field_70699_by");
        ReflectionHelper.setPrivateValue(EntityLiving.class, this, new DroneMoveHelper(this), "moveHelper", "field_70765_h");
        tasks.addTask(1, chargeAI = new DroneGoToChargingStation(this));
    }

    public EntityDrone(World world, EntityPlayer player){
        this(world);
        playerUUID = player.getGameProfile().getId().toString();
        playerName = player.getCommandSenderName();
    }

    private void initializeFakePlayer(){
        fakePlayer = new DroneFakePlayer((WorldServer)worldObj, new GameProfile(playerUUID != null ? UUID.fromString(playerUUID) : null, playerName), new FakePlayerItemInWorldManager(worldObj, fakePlayer, this), this);
        fakePlayer.playerNetServerHandler = new NetHandlerPlayServer(MinecraftServer.getServer(), new NetworkManager(false), fakePlayer);
        fakePlayer.inventory = new InventoryFakePlayer(fakePlayer);
    }

    @Override
    protected void entityInit(){
        super.entityInit();
        dataWatcher.addObject(12, 0F);
        dataWatcher.addObject(13, (byte)0);
        dataWatcher.addObject(14, 0);
        dataWatcher.addObject(15, 0);
        dataWatcher.addObject(16, 0);
        dataWatcher.addObject(17, "");
        dataWatcher.addObject(18, 0);
        dataWatcher.addObject(19, 0);
        dataWatcher.addObject(20, 0);
        dataWatcher.addObject(21, (byte)0);
        dataWatcher.addObject(22, 0);
        dataWatcher.addObject(23, (byte)0);
        dataWatcher.addObject(24, (byte)0);
        dataWatcher.addObjectByDataType(25, 5);
        dataWatcher.addObject(26, "Main");
        dataWatcher.addObject(27, 0);
    }

    @Override
    protected void applyEntityAttributes(){
        super.applyEntityAttributes();
        getAttributeMap().registerAttribute(SharedMonsterAttributes.attackDamage).setBaseValue(3.0D);
        getEntityAttribute(SharedMonsterAttributes.maxHealth).setBaseValue(40F);
        getEntityAttribute(SharedMonsterAttributes.followRange).setBaseValue(getRange());
    }

    @Override
    public void writeSpawnData(ByteBuf data){
        ByteBufUtils.writeUTF8String(data, getFakePlayer().getCommandSenderName());
    }

    @Override
    public void readSpawnData(ByteBuf data){
        playerName = ByteBufUtils.readUTF8String(data);
    }

    /**
     * Determines if an entity can be despawned, used on idle far away entities
     */
    @Override
    protected boolean canDespawn(){
        return false;
    }

    @Override
    protected float getSoundVolume(){
        return 0.2F;
    }

    @Override
    protected String getHurtSound(){
        return "pneumaticcraft:drone.hurt";
    }

    /**
     * Returns the sound this mob makes on death.
     */
    @Override
    protected String getDeathSound(){
        return "pneumaticcraft:drone.death";
    }

    @Override
    public void onUpdate(){
        if(firstTick) {
            firstTick = false;
            volume = PneumaticValues.DRONE_VOLUME + getUpgrades(ItemMachineUpgrade.UPGRADE_VOLUME_DAMAGE) * PneumaticValues.VOLUME_VOLUME_UPGRADE;
            hasLiquidImmunity = getUpgrades(ItemMachineUpgrade.UPGRADE_SECURITY) > 0;
            if(hasLiquidImmunity) {
                ((EntityPathNavigateDrone)getPathNavigator()).pathThroughLiquid = true;
            }
            speed = 0.1 + Math.min(10, getUpgrades(ItemMachineUpgrade.UPGRADE_SPEED_DAMAGE)) * 0.01;
            lifeUpgrades = getUpgrades(ItemMachineUpgrade.UPGRADE_ITEM_LIFE);
            if(!worldObj.isRemote) setHasMinigun(getUpgrades(ItemMachineUpgrade.UPGRADE_ENTITY_TRACKER) > 0);
            aiManager.setWidgets(progWidgets);
        }
        boolean enabled = !disabledByHacking && getPressure(null) > 0.01F;
        if(!worldObj.isRemote) {
            setAccelerating(!standby && enabled);
            if(isAccelerating()) {
                fallDistance = 0;
            }
            if(lifeUpgrades > 0) {
                int interval = 10 / lifeUpgrades;
                if(interval == 0 || ticksExisted % interval == 0) {
                    heal(1);
                }
            }
            if(!isSuffocating) {
                suffocationCounter = 40;
            }
            isSuffocating = false;
            PathEntity path = getNavigator().getPath();
            if(path != null) {
                PathPoint target = path.getFinalPathPoint();
                if(target != null) {
                    setTargetedBlock(target.xCoord, target.yCoord, target.zCoord);
                } else {
                    setTargetedBlock(0, 0, 0);
                }
            } else {
                setTargetedBlock(0, 0, 0);
            }
            if(worldObj.getTotalWorldTime() % 20 == 0) {
                updateSyncedPlayers();
            }
        } else {
            if(digLaser != null) digLaser.update();
            oldLaserExtension = laserExtension;
            if(getActiveProgramKey().equals("dig")) {
                laserExtension = Math.min(1, laserExtension + LASER_EXTEND_SPEED);
            } else {
                laserExtension = Math.max(0, laserExtension - LASER_EXTEND_SPEED);
            }

            if(isAccelerating()) {
                int x = (int)Math.floor(posX);
                int y = (int)Math.floor(posY - 1);
                int z = (int)Math.floor(posZ);
                Block block = null;
                for(int i = 0; i < 3; i++) {
                    block = worldObj.getBlock(x, y, z);
                    if(block.getMaterial() != Material.air) break;
                    y--;
                }

                if(block.getMaterial() != Material.air) {
                    Vec3 vec = Vec3.createVectorHelper(posY - y, 0, 0);
                    vec.rotateAroundY((float)(rand.nextFloat() * Math.PI * 2));
                    worldObj.spawnParticle("blockcrack_" + Block.getIdFromBlock(block) + "_" + worldObj.getBlockMetadata(x, y, z), posX + vec.xCoord, y + 1, posZ + vec.zCoord, vec.xCoord, 0, vec.zCoord);
                }
            }
        }
        if(hasLiquidImmunity) {
            for(int x = (int)posX - 1; x <= (int)(posX + width); x++) {
                for(int y = (int)posY - 1; y <= (int)(posY + height + 1); y++) {
                    for(int z = (int)posZ - 2; z <= (int)(posZ + width); z++) {
                        if(PneumaticCraftUtils.isBlockLiquid(worldObj.getBlock(x, y, z))) {
                            worldObj.setBlock(x, y, z, Blocks.air, 0, 2);
                        }
                    }
                }
            }
        }
        if(isAccelerating()) {
            motionX *= 0.3D;
            motionY *= 0.3D;
            motionZ *= 0.3D;
            propSpeed = Math.min(1, propSpeed + 0.04F);
            addAir(null, -1);
        } else {
            propSpeed = Math.max(0, propSpeed - 0.04F);
        }
        oldPropRotation = propRotation;
        propRotation += propSpeed;

        if(!worldObj.isRemote && isEntityAlive()/*((FakePlayerItemInWorldManager)fakePlayer.theItemInWorldManager).isDigging()*/) {
            for(int i = 0; i < 4; i++) {
                getFakePlayer().theItemInWorldManager.updateBlockRemoving();
            }
        }
        super.onUpdate();
        if(hasMinigun()) getMinigun().setAttackTarget(getAttackTarget()).update(posX, posY, posZ);
        if(!worldObj.isRemote && isEntityAlive()) {
            if(enabled) aiManager.onUpdateTasks();
            for(ForgeDirection d : ForgeDirection.VALID_DIRECTIONS) {
                if(getEmittingRedstone(d) > 0) {
                    if(worldObj.isAirBlock((int)Math.floor(posX + width / 2), (int)Math.floor(posY), (int)Math.floor(posZ + width / 2))) {
                        worldObj.setBlock((int)Math.floor(posX + width / 2), (int)Math.floor(posY), (int)Math.floor(posZ + width / 2), Blockss.droneRedstoneEmitter);
                    }
                    break;
                }
            }
        }
    }

    public ChunkPosition getTargetedBlock(){
        int x = dataWatcher.getWatchableObjectInt(14);
        int y = dataWatcher.getWatchableObjectInt(15);
        int z = dataWatcher.getWatchableObjectInt(16);
        return x != 0 || y != 0 || z != 0 ? new ChunkPosition(x, y, z) : null;
    }

    private void setTargetedBlock(int x, int y, int z){
        dataWatcher.updateObject(14, x);
        dataWatcher.updateObject(15, y);
        dataWatcher.updateObject(16, z);
    }

    @Override
    public int getLaserColor(){
        if(colorMap.containsKey(getCustomNameTag().toLowerCase())) {
            return colorMap.get(getCustomNameTag().toLowerCase());
        } else if(colorMap.containsKey(playerName.toLowerCase())) {
            return colorMap.get(playerName.toLowerCase());
        }
        return super.getLaserColor();
    }

    @Override
    protected ChunkPosition getDugBlock(){
        int x = dataWatcher.getWatchableObjectInt(18);
        int y = dataWatcher.getWatchableObjectInt(19);
        int z = dataWatcher.getWatchableObjectInt(20);
        return x != 0 || y != 0 || z != 0 ? new ChunkPosition(x, y, z) : null;
    }

    @Override
    public void setDugBlock(int x, int y, int z){
        dataWatcher.updateObject(18, x);
        dataWatcher.updateObject(19, y);
        dataWatcher.updateObject(20, z);
    }

    public List<EntityAITaskEntry> getRunningTasks(){
        return aiManager.getRunningTasks();
    }

    public EntityAIBase getRunningTargetAI(){
        return aiManager.getTargetAI();
    }

    public void setVariable(String varName, ChunkPosition pos){
        aiManager.setCoordinate(varName, pos);
    }

    public ChunkPosition getVariable(String varName){
        return aiManager.getCoordinate(varName);
    }

    public ItemStack getActiveProgram(){
        String key = getActiveProgramKey();
        if(key.equals("")) {
            return null;
        } else {
            return ItemProgrammingPuzzle.getStackForWidgetKey(key);
        }
    }

    private String getActiveProgramKey(){
        return dataWatcher.getWatchableObjectString(17);
    }

    /**
     * Can only be called when the drone is being debugged, so the client has a synced progWidgets array.
     * @return
     */
    public IProgWidget getActiveWidget(){
        int index = getActiveWidgetIndex();
        if(index >= 0 && index < progWidgets.size()) {
            return progWidgets.get(index);
        } else {
            return null;
        }
    }

    private int getActiveWidgetIndex(){
        return dataWatcher.getWatchableObjectInt(27);
    }

    @Override
    public void setActiveProgram(IProgWidget widget){
        dataWatcher.updateObject(17, widget.getWidgetString());
        dataWatcher.updateObject(27, progWidgets.indexOf(widget));
    }

    private void setAccelerating(boolean accelerating){
        dataWatcher.updateObject(13, (byte)(accelerating ? 1 : 0));
    }

    @Override
    public boolean isAccelerating(){
        return dataWatcher.getWatchableObjectByte(13) == 1;
    }

    private void setDroneColor(int color){
        dataWatcher.updateObject(22, color);
    }

    @Override
    public int getDroneColor(){
        return dataWatcher.getWatchableObjectInt(22);
    }

    public void setMinigunActivated(boolean activated){
        dataWatcher.updateObject(23, (byte)(activated ? 1 : 0));
    }

    public boolean isMinigunActivated(){
        return dataWatcher.getWatchableObjectByte(23) == 1;
    }

    public void setHasMinigun(boolean hasMinigun){
        dataWatcher.updateObject(24, (byte)(hasMinigun ? 1 : 0));
    }

    public boolean hasMinigun(){
        return dataWatcher.getWatchableObjectByte(24) == 1;
    }

    public int getAmmoColor(){
        ItemStack ammo = dataWatcher.getWatchableObjectItemStack(25);
        return ammo != null ? ammo.getItem().getColorFromItemStack(ammo, 1) : 0xFF313131;
    }

    public void setAmmoColor(ItemStack color){
        dataWatcher.updateObject(25, color);
    }

    /**
     * Returns true if the newer Entity AI code should be run
     */
    @Override
    public boolean isAIEnabled(){
        return true;
    }

    /**
     * Decrements the entity's air supply when underwater
     */
    @Override
    protected int decreaseAirSupply(int par1){
        return -20;//make drones insta drown.
    }

    /**
     * Moves the entity based on the specified heading.  Args: strafe, forward
     */
    @Override
    public void moveEntityWithHeading(float par1, float par2){
        if(worldObj.isRemote) {
            EntityLivingBase targetEntity = getAttackTarget();
            if(targetEntity != null) {
                if(targetLine == null) targetLine = new RenderProgressingLine(0, -height / 2, 0, 0, 0, 0);
                if(oldTargetLine == null) oldTargetLine = new RenderProgressingLine(0, -height / 2, 0, 0, 0, 0);

                targetLine.endX = targetEntity.posX - posX;
                targetLine.endY = targetEntity.posY + targetEntity.height / 2 - posY;
                targetLine.endZ = targetEntity.posZ - posZ;
                oldTargetLine.endX = targetEntity.prevPosX - prevPosX;
                oldTargetLine.endY = targetEntity.prevPosY + targetEntity.height / 2 - prevPosY;
                oldTargetLine.endZ = targetEntity.prevPosZ - prevPosZ;

                oldTargetLine.setProgress(targetLine.getProgress());
                targetLine.incProgressByDistance(0.3D);
                ignoreFrustumCheck = true; //don't stop rendering the drone when it goes out of the camera frustrum, as we need to render the target lines as well.
            } else {
                ignoreFrustumCheck = false; //don't stop rendering the drone when it goes out of the camera frustrum, as we need to render the target lines as well.
            }
        }
        if(ridingEntity == null && isAccelerating()) {
            double d3 = motionY;
            super.moveEntityWithHeading(par1, par2);
            motionY = d3 * 0.60D;
        } else {
            super.moveEntityWithHeading(par1, par2);
        }
        onGround = true;//set onGround to true so AI pathfinding will keep updating.
    }

    /**
     * Method that's being called to render anything that has to for the Drone. The matrix is already translated to the drone's position.
     * @param partialTicks
     */
    @Override
    @SideOnly(Side.CLIENT)
    public void renderExtras(double transX, double transY, double transZ, float partialTicks){
        if(targetLine != null && oldTargetLine != null) {
            GL11.glPushMatrix();
            GL11.glScaled(1, -1, 1);
            GL11.glDisable(GL11.GL_TEXTURE_2D);
            //GL11.glDisable(GL11.GL_LIGHTING);
            GL11.glColor4d(1, 0, 0, 1);
            targetLine.renderInterpolated(oldTargetLine, partialTicks);
            GL11.glColor4d(1, 1, 1, 1);
            // GL11.glEnable(GL11.GL_LIGHTING);
            GL11.glEnable(GL11.GL_TEXTURE_2D);
            GL11.glPopMatrix();
        }

        double x = lastTickPosX + (posX - lastTickPosX) * partialTicks;
        double y = lastTickPosY + (posY - lastTickPosY) * partialTicks;
        double z = lastTickPosZ + (posZ - lastTickPosZ) * partialTicks;
        getMinigun().render(x, y, z, 0.6);
    }

    public double getRange(){
        return 75;
    }

    @Override
    public boolean interact(EntityPlayer player){
        ItemStack equippedItem = player.getCurrentEquippedItem();
        if(!worldObj.isRemote && equippedItem != null) {
            if(equippedItem.getItem() == Itemss.GPSTool) {
                ChunkPosition gpsLoc = ItemGPSTool.getGPSLocation(equippedItem);
                if(gpsLoc != null) {
                    getNavigator().tryMoveToXYZ(gpsLoc.chunkPosX, gpsLoc.chunkPosY, gpsLoc.chunkPosZ, 0.1D);
                }
            } else {
                int dyeIndex = TileEntityPlasticMixer.getDyeIndex(equippedItem);
                if(dyeIndex >= 0) {
                    setDroneColor(ItemDye.field_150922_c[dyeIndex]);
                    equippedItem.stackSize--;
                    if(equippedItem.stackSize <= 0) {
                        player.setCurrentItemOrArmor(0, null);
                    }
                }
            }
        }
        return false;
    }

    /**
     * Called when a drone is hit by a Pneumatic Wrench.
     */
    @Override
    public boolean rotateBlock(World world, EntityPlayer player, int x, int y, int z, ForgeDirection side){
        if(!naturallySpawned) {
            if(player.capabilities.isCreativeMode) naturallySpawned = true;//don't drop the drone in creative.
            attackEntityFrom(DamageSource.outOfWorld, 2000.0F);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public void onDeath(DamageSource par1DamageSource){
        for(int i = 0; i < inventory.getSizeInventory(); i++) {
            if(inventory.getStackInSlot(i) != null) {
                entityDropItem(inventory.getStackInSlot(i), 0);
                inventory.setInventorySlotContents(i, null);
            }
        }
        if(naturallySpawned) {

        } else {
            ItemStack drone = getDroppedStack();
            if(hasCustomNameTag()) drone.setStackDisplayName(getCustomNameTag());

            entityDropItem(drone, 0);

            if(!worldObj.isRemote) {
                EntityPlayer owner = getOwner();
                if(owner != null) {
                    int x = (int)Math.floor(posX);
                    int y = (int)Math.floor(posY);
                    int z = (int)Math.floor(posZ);
                    if(hasCustomNameTag()) {
                        owner.addChatComponentMessage(new ChatComponentTranslation("death.drone.named", getCustomNameTag(), x, y, z));
                    } else {
                        owner.addChatComponentMessage(new ChatComponentTranslation("death.drone", x, y, z));
                    }
                }
            }
        }
        if(!worldObj.isRemote) ((FakePlayerItemInWorldManager)getFakePlayer().theItemInWorldManager).cancelDigging();
        super.onDeath(par1DamageSource);
    }

    protected ItemStack getDroppedStack(){
        NBTTagCompound tag = new NBTTagCompound();
        writeEntityToNBT(tag);
        tag.setTag("UpgradeInventory", tag.getTag("Inventory"));
        tag.removeTag("Inventory");
        ItemStack drone = new ItemStack(Itemss.drone);
        drone.setTagCompound(tag);
        return drone;
    }

    @Override
    public void setAttackTarget(EntityLivingBase entity){
        super.setAttackTarget(entity);
        if(worldObj.isRemote && targetLine != null && oldTargetLine != null) {
            targetLine.setProgress(0);
            oldTargetLine.setProgress(0);
        }
    }

    @Override
    public float getPressure(ItemStack iStack){
        return dataWatcher.getWatchableObjectFloat(12);
    }

    @Override
    public void addAir(ItemStack iStack, int amount){
        if(!worldObj.isRemote) {
            currentAir += amount;
            dataWatcher.updateObject(12, currentAir / volume);
        }
    }

    @Override
    public float maxPressure(ItemStack iStack){
        return PneumaticValues.DRONE_MAX_PRESSURE;
    }

    @Override
    public void printManometerMessage(EntityPlayer player, List<String> curInfo){
        if(hasCustomNameTag()) curInfo.add(EnumChatFormatting.AQUA + getCustomNameTag());
        curInfo.add("Owner: " + getFakePlayer().getCommandSenderName());
        curInfo.add("Current pressure: " + PneumaticCraftUtils.roundNumberTo(getPressure(null), 1) + " bar.");
        /*for(int i = 0; i < 9; i++) {
            if(upgradeInventory[i] != null) {
                player.addChatMessage("inv " + i + ": " + upgradeInventory[i].stackSize + "x " + upgradeInventory[i].getDisplayName());
            }
        }*/
    }

    @Override
    public void writeEntityToNBT(NBTTagCompound tag){
        super.writeEntityToNBT(tag);
        TileEntityProgrammer.setWidgetsToNBT(progWidgets, tag);
        tag.setBoolean("naturallySpawned", naturallySpawned);
        tag.setFloat("currentAir", currentAir);
        tag.setFloat("propSpeed", propSpeed);
        tag.setBoolean("disabledByHacking", disabledByHacking);
        tag.setBoolean("hackedByOwner", gotoOwnerAI != null);
        tag.setInteger("color", getDroneColor());
        tag.setBoolean("standby", standby);
        tag.setFloat("volume", volume);

        NBTTagCompound variableTag = new NBTTagCompound();
        aiManager.writeToNBT(variableTag);
        tag.setTag("variables", variableTag);

        NBTTagCompound inv = new NBTTagCompound();
        // Write the ItemStacks in the inventory to NBT
        NBTTagList tagList = new NBTTagList();
        for(int currentIndex = 0; currentIndex < inventory.getSizeInventory(); ++currentIndex) {
            if(inventory.getStackInSlot(currentIndex) != null) {
                NBTTagCompound tagCompound = new NBTTagCompound();
                tagCompound.setByte("Slot", (byte)currentIndex);
                inventory.getStackInSlot(currentIndex).writeToNBT(tagCompound);
                tagList.appendTag(tagCompound);
            }
        }
        inv.setTag("Inv", tagList);

        NBTTagList upgradeList = new NBTTagList();
        for(int i = 0; i < 9; i++) {
            if(upgradeInventory[i] != null) {
                NBTTagCompound slotEntry = new NBTTagCompound();
                slotEntry.setByte("Slot", (byte)i);
                upgradeInventory[i].writeToNBT(slotEntry);
                upgradeList.appendTag(slotEntry);
            }
        }
        // save content in Inventory->Items
        inv.setTag("Items", upgradeList);
        tag.setTag("Inventory", inv);

        tank.writeToNBT(tag);

        if(handlingOffer != null) {
            NBTTagCompound subTag = new NBTTagCompound();
            subTag.setBoolean("isCustom", handlingOffer instanceof AmadronOfferCustom);
            handlingOffer.writeToNBT(subTag);
            tag.setTag("amadronOffer", subTag);
            tag.setInteger("offerTimes", offerTimes);
            if(usedTablet != null) usedTablet.writeToNBT(subTag);
            tag.setString("buyingPlayer", buyingPlayer);
        }
    }

    @Override
    public void readEntityFromNBT(NBTTagCompound tag){
        super.readEntityFromNBT(tag);
        progWidgets = TileEntityProgrammer.getWidgetsFromNBT(tag);
        naturallySpawned = tag.getBoolean("naturallySpawned");
        currentAir = tag.getFloat("currentAir");
        volume = tag.getFloat("volume");
        dataWatcher.updateObject(12, currentAir / volume);
        propSpeed = tag.getFloat("propSpeed");
        disabledByHacking = tag.getBoolean("disabledByHacking");
        setGoingToOwner(tag.getBoolean("hackedByOwner"));
        setDroneColor(tag.getInteger("color"));
        aiManager.readFromNBT(tag.getCompoundTag("variables"));
        standby = tag.getBoolean("standby");

        // Read in the ItemStacks in the inventory from NBT
        NBTTagCompound inv = tag.getCompoundTag("Inventory");
        if(inv != null) {
            NBTTagList tagList = inv.getTagList("Inv", 10);

            upgradeInventory = new ItemStack[9];
            NBTTagList upgradeList = inv.getTagList("Items", 10);
            for(int i = 0; i < upgradeList.tagCount(); i++) {
                NBTTagCompound slotEntry = upgradeList.getCompoundTagAt(i);
                int j = slotEntry.getByte("Slot");

                if(j >= 0 && j < 9) {
                    upgradeInventory[j] = ItemStack.loadItemStackFromNBT(slotEntry);
                }
            }

            inventory = new InventoryDrone("Drone Inventory", true, 1 + getUpgrades(ItemMachineUpgrade.UPGRADE_DISPENSER_DAMAGE));
            for(int i = 0; i < tagList.tagCount(); ++i) {
                NBTTagCompound tagCompound = tagList.getCompoundTagAt(i);
                byte slot = tagCompound.getByte("Slot");
                if(slot >= 0 && slot < inventory.getSizeInventory()) {
                    inventory.setInventorySlotContents(slot, ItemStack.loadItemStackFromNBT(tagCompound));
                }
            }
        }

        tank.setCapacity(PneumaticValues.DRONE_TANK_SIZE * (1 + getUpgrades(ItemMachineUpgrade.UPGRADE_DISPENSER_DAMAGE)));
        tank.readFromNBT(tag);

        if(tag.hasKey("amadronOffer")) {
            NBTTagCompound subTag = tag.getCompoundTag("amadronOffer");
            if(subTag.getBoolean("isCustom")) {
                handlingOffer = AmadronOffer.loadFromNBT(subTag);
            } else {
                handlingOffer = AmadronOfferCustom.loadFromNBT(subTag);
            }
            if(subTag.hasKey("id")) {
                usedTablet = ItemStack.loadItemStackFromNBT(subTag);
            } else {
                usedTablet = null;
            }
            buyingPlayer = subTag.getString("buyingPlayer");
        } else {
            handlingOffer = null;
            usedTablet = null;
            buyingPlayer = null;
        }
        offerTimes = tag.getInteger("offerTimes");
    }

    /**
     * This and readFromNBT are _not_ being transfered from/to the Drone item.
     */
    @Override
    public void writeToNBT(NBTTagCompound tag){
        super.writeToNBT(tag);
        if(fakePlayer != null) {
            tag.setString("owner", fakePlayer.getCommandSenderName());
            if(fakePlayer.getGameProfile().getId() != null) tag.setString("ownerUUID", fakePlayer.getGameProfile().getId().toString());
        }
    }

    @Override
    public void readFromNBT(NBTTagCompound tag){
        super.readFromNBT(tag);
        if(tag.hasKey("owner")) {
            playerName = tag.getString("owner");
            playerUUID = tag.hasKey("ownerUUID") ? tag.getString("ownerUUID") : null;
        }
    }

    @Override
    public int getUpgrades(int upgradeDamage){
        int upgrades = 0;
        for(ItemStack stack : upgradeInventory) {
            if(stack != null && stack.getItem() == Itemss.machineUpgrade && stack.getItemDamage() == upgradeDamage) {
                upgrades += stack.stackSize;
            }
        }
        return upgrades;
    }

    @Override
    public DroneFakePlayer getFakePlayer(){
        if(fakePlayer == null && !worldObj.isRemote) {
            initializeFakePlayer();
        }
        return fakePlayer;
    }

    public Minigun getMinigun(){
        if(minigun == null) {
            minigun = new MinigunDrone().setPlayer(getFakePlayer()).setWorld(worldObj).setPressurizable(this, PneumaticValues.DRONE_USAGE_ATTACK);
        }
        return minigun;
    }

    @Override
    public boolean attackEntityAsMob(Entity entity){
        getFakePlayer().attackTargetEntityWithCurrentItem(entity);
        addAir(null, -PneumaticValues.DRONE_USAGE_ATTACK);
        return true;
    }

    @Override
    public boolean attackEntityFrom(DamageSource damageSource, float damage){
        if(damageSource == DamageSource.inWall) {
            isSuffocating = true;
            if(suffocationCounter-- > 0 || !Config.enableDroneSuffocationDamage) {
                return false;
            }
        }
        return super.attackEntityFrom(damageSource, damage);
    }

    @Override
    public IInventory getInventory(){
        return inventory;
    }

    public double getSpeed(){
        return speed;
    }

    public int getEmittingRedstone(ForgeDirection side){
        return emittingRedstoneValues[side.ordinal()];
    }

    @Override
    public void setEmittingRedstone(ForgeDirection side, int value){
        if(emittingRedstoneValues[side.ordinal()] != value) {
            emittingRedstoneValues[side.ordinal()] = value;
            worldObj.notifyBlocksOfNeighborChange((int)Math.floor(posX + width / 2), (int)Math.floor(posY), (int)Math.floor(posZ + width / 2), Blockss.droneRedstoneEmitter);
        }
    }

    @Override
    public boolean isBlockValidPathfindBlock(int x, int y, int z){
        if(worldObj.isAirBlock(x, y, z)) return true;
        Block block = worldObj.getBlock(x, y, z);
        if(PneumaticCraftUtils.isBlockLiquid(block)) {
            return hasLiquidImmunity;
        }
        if(block.getBlocksMovement(worldObj, x, y, z) && block != Blocks.ladder) return true;
        if(PneumaticCraftAPIHandler.getInstance().pathfindableBlocks.containsKey(block)) {
            IPathfindHandler pathfindHandler = PneumaticCraftAPIHandler.getInstance().pathfindableBlocks.get(block);
            return pathfindHandler == null || pathfindHandler.canPathfindThrough(worldObj, x, y, z);
        } else {
            return false;
        }
    }

    @Override
    public void sendWireframeToClient(int x, int y, int z){
        NetworkHandler.sendToAllAround(new PacketShowWireframe(this, x, y, z), worldObj);
    }

    private class InventoryDrone extends InventoryBasic{
        ItemStack oldStack;

        public InventoryDrone(String inventoryName, boolean isNameLocalized, int slots){
            super(inventoryName, isNameLocalized, slots);
        }

        @Override
        public void setInventorySlotContents(int slot, ItemStack stack){
            super.setInventorySlotContents(slot, stack);
            if(slot == 0 && !isChangingCurrentStack) {
                isChangingCurrentStack = true;
                getFakePlayer().inventory.setInventorySlotContents(slot, stack);
                isChangingCurrentStack = false;
                if(oldStack != null) {
                    getFakePlayer().getAttributeMap().removeAttributeModifiers(oldStack.getAttributeModifiers());
                }

                if(stack != null) {
                    getFakePlayer().getAttributeMap().applyAttributeModifiers(stack.getAttributeModifiers());
                }
                oldStack = stack;
            }
        }
    }

    private class InventoryFakePlayer extends InventoryPlayer{
        public InventoryFakePlayer(EntityPlayer par1EntityPlayer){
            super(par1EntityPlayer);
        }

        @Override
        public void setInventorySlotContents(int slot, ItemStack stack){
            super.setInventorySlotContents(slot, stack);
            if(slot == 0 && !isChangingCurrentStack) {
                isChangingCurrentStack = true;
                getInventory().setInventorySlotContents(slot, stack);
                isChangingCurrentStack = false;
            }
        }
    }

    public static class DroneFakePlayer extends EntityPlayerMP{
        private final IDroneBase drone;
        private boolean sneaking;

        public DroneFakePlayer(WorldServer world, GameProfile name, ItemInWorldManager itemManager, IDroneBase drone){
            super(FMLCommonHandler.instance().getMinecraftServerInstance(), world, name, itemManager);
            this.drone = drone;
        }

        @Override
        public void addExperience(int amount){
            Vec3 pos = drone.getPosition();
            EntityXPOrb orb = new EntityXPOrb(drone.getWorld(), pos.xCoord, pos.yCoord, pos.zCoord, amount);
            drone.getWorld().spawnEntityInWorld(orb);
        }

        @Override
        public void setCurrentItemOrArmor(int p_70062_1_, ItemStack p_70062_2_){

            if(p_70062_1_ == 0) {
                inventory.setInventorySlotContents(inventory.currentItem, p_70062_2_);
            } else {
                inventory.armorInventory[p_70062_1_ - 1] = p_70062_2_;
            }
        }

        @Override
        public boolean canCommandSenderUseCommand(int i, String s){
            return false;
        }

        @Override
        public ChunkCoordinates getPlayerCoordinates(){
            return new ChunkCoordinates(0, 0, 0);
        }

        @Override
        public void addChatComponentMessage(IChatComponent chatmessagecomponent){}

        @Override
        public void addStat(StatBase par1StatBase, int par2){}

        @Override
        public void openGui(Object mod, int modGuiId, World world, int x, int y, int z){}

        @Override
        public boolean isEntityInvulnerable(){
            return true;
        }

        @Override
        public boolean canAttackPlayer(EntityPlayer player){
            return false;
        }

        @Override
        public void onDeath(DamageSource source){
            return;
        }

        @Override
        public void onUpdate(){
            return;
        }

        @Override
        public void travelToDimension(int dim){
            return;
        }

        @Override
        public void func_147100_a(C15PacketClientSettings pkt){
            return;
        }

        @Override
        public boolean isSneaking(){
            return sneaking;
        }

        @Override
        public void setSneaking(boolean sneaking){
            this.sneaking = sneaking;
        }
    }

    /**
     * IHackableEntity
     */

    @Override
    public String getId(){
        return null;
    }

    @Override
    public boolean canHack(Entity entity, EntityPlayer player){
        return isAccelerating();
    }

    @Override
    public void addInfo(Entity entity, List<String> curInfo, EntityPlayer player){
        if(playerName.equals(player.getCommandSenderName())) {
            if(isGoingToOwner()) {
                curInfo.add("pneumaticHelmet.hacking.result.resumeTasks");
            } else {
                curInfo.add("pneumaticHelmet.hacking.result.callBack");
            }
        } else {
            curInfo.add("pneumaticHelmet.hacking.result.disable");
        }
    }

    @Override
    public void addPostHackInfo(Entity entity, List<String> curInfo, EntityPlayer player){
        if(playerName.equals(player.getCommandSenderName())) {
            if(isGoingToOwner()) {
                curInfo.add("pneumaticHelmet.hacking.finished.calledBack");
            } else {
                curInfo.add("pneumaticHelmet.hacking.finished.resumedTasks");
            }
        } else {
            curInfo.add("pneumaticHelmet.hacking.finished.disabled");
        }
    }

    @Override
    public int getHackTime(Entity entity, EntityPlayer player){
        return playerName.equals(player.getCommandSenderName()) ? 20 : 100;
    }

    @Override
    public void onHackFinished(Entity entity, EntityPlayer player){
        if(!worldObj.isRemote && player.getGameProfile().equals(getFakePlayer().getGameProfile())) {
            setGoingToOwner(gotoOwnerAI == null);//toggle the state
        } else {
            disabledByHacking = true;
        }
    }

    @Override
    public boolean afterHackTick(Entity entity){
        return false;
    }

    private void setGoingToOwner(boolean state){
        if(state && gotoOwnerAI == null) {
            gotoOwnerAI = new DroneGoToOwner(this);
            tasks.addTask(2, gotoOwnerAI);
            dataWatcher.updateObject(21, (byte)1);
            setActiveProgram(new ProgWidgetGoToLocation());
        } else if(!state && gotoOwnerAI != null) {
            tasks.removeTask(gotoOwnerAI);
            gotoOwnerAI = null;
            dataWatcher.updateObject(21, (byte)0);
        }
    }

    private boolean isGoingToOwner(){
        return dataWatcher.getWatchableObjectByte(21) == (byte)1;
    }

    @Override
    public IFluidTank getTank(){
        return tank;
    }

    /**
     * Returns the owning player. Returns null when the player is not online.
     * @return
     */
    public EntityPlayer getOwner(){
        return MinecraftServer.getServer().getConfigurationManager().func_152612_a(playerName);
    }

    public void setStandby(boolean standby){
        this.standby = standby;
    }

    @Override
    public World getWorld(){
        return worldObj;
    }

    @Override
    public Vec3 getPosition(){
        return Vec3.createVectorHelper(posX, posY, posZ);
    }

    @Override
    public void dropItem(ItemStack stack){
        entityDropItem(stack, 0);
    }

    @Override
    public List<IProgWidget> getProgWidgets(){
        return progWidgets;
    }

    @Override
    public EntityAITasks getTargetAI(){
        return targetTasks;
    }

    @Override
    public boolean isProgramApplicable(IProgWidget widget){
        return true;
    }

    @Override
    public IExtendedEntityProperties getProperty(String key){
        return getExtendedProperties(key);
    }

    @Override
    public void setProperty(String key, IExtendedEntityProperties property){
        registerExtendedProperties(key, property);
    }

    @Override
    public void setName(String string){
        setCustomNameTag(string);
    }

    @Override
    public void setCarryingEntity(Entity entity){
        if(entity == null) {
            if(getCarryingEntity() != null) getCarryingEntity().mountEntity(null);
        } else {
            entity.mountEntity(this);
        }
    }

    @Override
    public Entity getCarryingEntity(){
        return riddenByEntity;
    }

    @Override
    public boolean isAIOverriden(){
        return chargeAI.isExecuting || gotoOwnerAI != null;
    }

    @Override
    public void onItemPickupEvent(EntityItem curPickingUpEntity, int stackSize){
        onItemPickup(curPickingUpEntity, stackSize);
    }

    @Override
    public IPathNavigator getPathNavigator(){
        return (IPathNavigator)getNavigator();
    }

    public void tryFireMinigun(EntityLivingBase target){
        ItemStack ammo = getAmmo();
        if(getMinigun().setAmmo(ammo).tryFireMinigun(target)) {
            for(int i = 0; i < getInventory().getSizeInventory(); i++) {
                if(getInventory().getStackInSlot(i) == ammo) {
                    getInventory().setInventorySlotContents(i, null);
                }
            }
        }
    }

    public ItemStack getAmmo(){
        for(int i = 0; i < getInventory().getSizeInventory(); i++) {
            ItemStack stack = getInventory().getStackInSlot(i);
            if(stack != null && stack.getItem() == Itemss.gunAmmo) {
                return stack;
            }
        }
        return null;
    }

    public void setHandlingOffer(AmadronOffer offer, int times, ItemStack usedTablet, String buyingPlayer){
        handlingOffer = offer;
        offerTimes = times;
        this.usedTablet = usedTablet != null ? usedTablet.copy() : null;
        this.buyingPlayer = buyingPlayer;
    }

    public AmadronOffer getHandlingOffer(){
        return handlingOffer;
    }

    public int getOfferTimes(){
        return offerTimes;
    }

    public ItemStack getUsedTablet(){
        return usedTablet;
    }

    public String getBuyingPlayer(){
        return buyingPlayer;
    }

    @Override
    public void overload(){
        attackEntityFrom(DamageSource.outOfWorld, 2000.0F);
    }

    @Override
    public DroneAIManager getAIManager(){
        return aiManager;
    }

    @Override
    public void updateLabel(){
        dataWatcher.updateObject(26, getAIManager() != null ? getAIManager().getLabel() : "Main");
    }

    public String getLabel(){
        return dataWatcher.getWatchableObjectString(26);
    }

    public SortedSet<DebugEntry> getDebugEntries(){
        return debugEntries;
    }

    @Override
    public void addDebugEntry(String message){
        addDebugEntry(message, null);
    }

    @Override
    public void addDebugEntry(String message, ChunkPosition pos){

        DebugEntry entry = new DebugEntry(message, getActiveWidgetIndex(), pos);
        addDebugEntry(entry);

        PacketSendDroneDebugEntry packet = new PacketSendDroneDebugEntry(entry, this);
        for(EntityPlayerMP player : syncedPlayers) {
            NetworkHandler.sendTo(packet, player);
        }
    }

    public void addDebugEntry(DebugEntry entry){
        if(!debugEntries.isEmpty()) {
            DebugEntry previous = debugEntries.last();
            if(previous.getProgWidgetId() != entry.getProgWidgetId()) {//When we've jumped to another piece
                Iterator<DebugEntry> iterator = debugEntries.iterator();
                while(iterator.hasNext()) {
                    if(iterator.next().getProgWidgetId() == entry.getProgWidgetId()) {
                        iterator.remove(); //Remove the data from last cycle.
                    }
                }
            }
        }
        debugEntries.add(entry);
    }

    public void trackAsDebugged(EntityPlayerMP player){
        NetworkHandler.sendTo(new PacketSyncDroneEntityProgWidgets(this), player);

        for(DebugEntry entry : debugEntries) {
            NetworkHandler.sendTo(new PacketSendDroneDebugEntry(entry, this), player);
        }

        syncedPlayers.add(player);
    }

    public void updateSyncedPlayers(){
        Iterator<EntityPlayerMP> iterator = syncedPlayers.iterator();
        while(iterator.hasNext()) {
            EntityPlayerMP player = iterator.next();
            if(player.isDead || player.getCurrentArmor(3) == null || NBTUtil.getInteger(player.getCurrentArmor(3), NBTKeys.PNEUMATIC_HELMET_DEBUGGING_DRONE) != getEntityId()) {
                iterator.remove();
            }
        }
    }

    private class MinigunDrone extends Minigun{

        public MinigunDrone(){
            super(true);
        }

        @Override
        public boolean isMinigunActivated(){
            return EntityDrone.this.isMinigunActivated();
        }

        @Override
        public void setMinigunActivated(boolean activated){
            EntityDrone.this.setMinigunActivated(activated);
        }

        @Override
        public void setAmmoColorStack(ItemStack ammo){
            setAmmoColor(ammo);
        }

        @Override
        public int getAmmoColor(){
            return EntityDrone.this.getAmmoColor();
        }

        @Override
        public void playSound(String soundName, float volume, float pitch){
            worldObj.playSoundAtEntity(EntityDrone.this, soundName, volume, pitch);
        }

    }
}