package pneumaticCraft.common.tileentity;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.UUID;

import net.minecraft.block.Block;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.item.EntityBoat;
import net.minecraft.entity.item.EntityExpBottle;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.item.EntityMinecartChest;
import net.minecraft.entity.item.EntityMinecartEmpty;
import net.minecraft.entity.item.EntityMinecartFurnace;
import net.minecraft.entity.item.EntityMinecartHopper;
import net.minecraft.entity.item.EntityMinecartTNT;
import net.minecraft.entity.item.EntityTNTPrimed;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.entity.projectile.EntityArrow;
import net.minecraft.entity.projectile.EntityEgg;
import net.minecraft.entity.projectile.EntityFireball;
import net.minecraft.entity.projectile.EntityPotion;
import net.minecraft.entity.projectile.EntitySnowball;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.inventory.IInventory;
import net.minecraft.inventory.ISidedInventory;
import net.minecraft.item.Item;
import net.minecraft.item.ItemMonsterPlacer;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.world.ChunkPosition;
import net.minecraftforge.common.util.ForgeDirection;

import org.apache.commons.lang3.tuple.Pair;

import pneumaticCraft.api.tileentity.IAirHandler;
import pneumaticCraft.common.block.Blockss;
import pneumaticCraft.common.item.ItemMachineUpgrade;
import pneumaticCraft.common.item.Itemss;
import pneumaticCraft.common.network.DescSynced;
import pneumaticCraft.common.network.GuiSynced;
import pneumaticCraft.common.network.LazySynced;
import pneumaticCraft.common.network.NetworkHandler;
import pneumaticCraft.common.network.PacketPlaySound;
import pneumaticCraft.common.network.PacketSetEntityMotion;
import pneumaticCraft.common.network.PacketSpawnParticle;
import pneumaticCraft.common.thirdparty.computercraft.LuaConstant;
import pneumaticCraft.common.thirdparty.computercraft.LuaMethod;
import pneumaticCraft.common.util.IOHelper;
import pneumaticCraft.common.util.PneumaticCraftUtils;
import pneumaticCraft.lib.PneumaticValues;
import pneumaticCraft.lib.Sounds;
import pneumaticCraft.lib.TileEntityConstants;
import cpw.mods.fml.common.network.NetworkRegistry.TargetPoint;

public class TileEntityAirCannon extends TileEntityPneumaticBase implements ISidedInventory, IInventory,
        IMinWorkingPressure, IRedstoneControl{

    private ItemStack[] inventory;
    private final Random rand = new Random();
    @DescSynced
    @LazySynced
    public float rotationAngle;
    @DescSynced
    @LazySynced
    public float heightAngle;
    @DescSynced
    public float targetRotationAngle;
    @DescSynced
    public float targetHeightAngle;
    @GuiSynced
    public boolean doneTurning = false;
    private boolean redstonePowered = false;
    @GuiSynced
    public int gpsX;
    @GuiSynced
    public int gpsY;
    @GuiSynced
    public int gpsZ;
    @GuiSynced
    public boolean coordWithinReach;
    @GuiSynced
    public int redstoneMode;
    private int oldRangeUpgrades;
    private boolean externalControl;//used in the CC API, to disallow the Cannon to update its angles when things like range upgrades / GPS Tool have changed.
    private boolean entityUpgradeInserted, dispenserUpgradeInserted;
    private final List<EntityItem> trackedItems = new ArrayList<EntityItem>();//Items that are being checked to be hoppering into inventories.
    private Set<UUID> trackedItemIds;
    private ChunkPosition lastInsertingInventory; //Last coordinate where the item went into the inventory (as a result of the Block Tracker upgrade).
    private ForgeDirection lastInsertingInventorySide;
    @GuiSynced
    public boolean insertingInventoryHasSpace = true;

    private final int INVENTORY_SIZE = 6;
    public final int CANNON_SLOT = 0;
    public final int GPS_SLOT = 1;
    public final int UPGRADE_SLOT_1 = 2;
    public final int UPGRADE_SLOT_2 = 3;
    public final int UPGRADE_SLOT_3 = 4;
    public final int UPGRADE_SLOT_4 = 5;

    public TileEntityAirCannon(){
        super(PneumaticValues.DANGER_PRESSURE_AIR_CANNON, PneumaticValues.MAX_PRESSURE_AIR_CANNON, PneumaticValues.VOLUME_AIR_CANNON);
        inventory = new ItemStack[INVENTORY_SIZE];
        setUpgradeSlots(new int[]{UPGRADE_SLOT_1, UPGRADE_SLOT_2, UPGRADE_SLOT_3, UPGRADE_SLOT_4});
    }

    @Override
    public void updateEntity(){
        // GPS Tool read
        if(inventory[1] != null && inventory[1].getItem() == Itemss.GPSTool && !externalControl) {
            if(inventory[1].stackTagCompound != null) {

                NBTTagCompound gpsTag = inventory[1].stackTagCompound;
                int destinationX = gpsTag.getInteger("x");
                int destinationY = gpsTag.getInteger("y");
                int destinationZ = gpsTag.getInteger("z");

                if(destinationX != gpsX || destinationY != gpsY || destinationZ != gpsZ) {

                    gpsX = destinationX;
                    gpsY = destinationY;
                    gpsZ = destinationZ;
                    updateDestination();
                }
            }
        }

        int curRangeUpgrades = Math.min(8, getUpgrades(ItemMachineUpgrade.UPGRADE_RANGE, getUpgradeSlots()));
        if(curRangeUpgrades != oldRangeUpgrades) {
            oldRangeUpgrades = curRangeUpgrades;
            if(!externalControl) updateDestination();
        }

        if(worldObj.getTotalWorldTime() % 40 == 0) {
            boolean isDispenserUpgradeInserted = getUpgrades(ItemMachineUpgrade.UPGRADE_DISPENSER_DAMAGE) > 0;
            boolean isEntityTrackerUpgradeInserted = getUpgrades(ItemMachineUpgrade.UPGRADE_ENTITY_TRACKER) > 0;
            if(dispenserUpgradeInserted != isDispenserUpgradeInserted || entityUpgradeInserted != isEntityTrackerUpgradeInserted) {
                dispenserUpgradeInserted = isDispenserUpgradeInserted;
                entityUpgradeInserted = isEntityTrackerUpgradeInserted;
                updateDestination();
            }
        }

        // update angles
        doneTurning = true;
        float speedMultiplier = getSpeedMultiplierFromUpgrades(getUpgradeSlots());
        if(rotationAngle < targetRotationAngle) {
            if(rotationAngle < targetRotationAngle - TileEntityConstants.CANNON_SLOW_ANGLE) {
                rotationAngle += TileEntityConstants.CANNON_TURN_HIGH_SPEED * speedMultiplier;
            } else {
                rotationAngle += TileEntityConstants.CANNON_TURN_LOW_SPEED * speedMultiplier;
            }
            if(rotationAngle > targetRotationAngle) rotationAngle = targetRotationAngle;
            doneTurning = false;
        }
        if(rotationAngle > targetRotationAngle) {
            if(rotationAngle > targetRotationAngle + TileEntityConstants.CANNON_SLOW_ANGLE) {
                rotationAngle -= TileEntityConstants.CANNON_TURN_HIGH_SPEED * speedMultiplier;
            } else {
                rotationAngle -= TileEntityConstants.CANNON_TURN_LOW_SPEED * speedMultiplier;
            }
            if(rotationAngle < targetRotationAngle) rotationAngle = targetRotationAngle;
            doneTurning = false;
        }
        if(heightAngle < targetHeightAngle) {
            if(heightAngle < targetHeightAngle - TileEntityConstants.CANNON_SLOW_ANGLE) {
                heightAngle += TileEntityConstants.CANNON_TURN_HIGH_SPEED * speedMultiplier;
            } else {
                heightAngle += TileEntityConstants.CANNON_TURN_LOW_SPEED * speedMultiplier;
            }
            if(heightAngle > targetHeightAngle) heightAngle = targetHeightAngle;
            doneTurning = false;
        }
        if(heightAngle > targetHeightAngle) {
            if(heightAngle > targetHeightAngle + TileEntityConstants.CANNON_SLOW_ANGLE) {
                heightAngle -= TileEntityConstants.CANNON_TURN_HIGH_SPEED * speedMultiplier;
            } else {
                heightAngle -= TileEntityConstants.CANNON_TURN_LOW_SPEED * speedMultiplier;
            }
            if(heightAngle < targetHeightAngle) heightAngle = targetHeightAngle;
            doneTurning = false;
        }

        updateTrackedItems();

        super.updateEntity();

    }

    private void updateTrackedItems(){
        if(trackedItemIds != null) {
            trackedItems.clear();
            for(Entity entity : (List<Entity>)worldObj.loadedEntityList) {
                if(trackedItemIds.contains(entity.getUniqueID()) && entity instanceof EntityItem) {
                    trackedItems.add((EntityItem)entity);
                }
            }
            trackedItemIds = null;
        }
        Iterator<EntityItem> iterator = trackedItems.iterator();
        while(iterator.hasNext()) {
            EntityItem item = iterator.next();
            if(item.worldObj != worldObj || item.isDead) {
                iterator.remove();
            } else {
                Map<ChunkPosition, ForgeDirection> positions = new HashMap<ChunkPosition, ForgeDirection>();
                double range = 0.2;
                for(ForgeDirection d : ForgeDirection.VALID_DIRECTIONS) {
                    double posX = item.posX + d.offsetX * range;
                    double posY = item.posY + d.offsetY * range;
                    double posZ = item.posZ + d.offsetZ * range;
                    positions.put(new ChunkPosition((int)Math.floor(posX), (int)Math.floor(posY), (int)Math.floor(posZ)), d.getOpposite());
                }
                for(Entry<ChunkPosition, ForgeDirection> entry : positions.entrySet()) {
                    ChunkPosition pos = entry.getKey();
                    TileEntity te = worldObj.getTileEntity(pos.chunkPosX, pos.chunkPosY, pos.chunkPosZ);
                    IInventory inv = IOHelper.getInventoryForTE(te);
                    ItemStack remainder = IOHelper.insert(inv, item.getEntityItem(), entry.getValue().ordinal(), false);
                    if(remainder != null) {
                        item.setEntityItemStack(remainder);
                    } else {
                        item.setDead();
                        iterator.remove();
                        lastInsertingInventory = new ChunkPosition(te.xCoord, te.yCoord, te.zCoord);
                        lastInsertingInventorySide = entry.getValue();
                        break;
                    }
                }
            }
        }
    }

    // ANGLE METHODS -------------------------------------------------

    private void updateDestination(){
        doneTurning = false;
        // take dispenser upgrade in account
        double payloadFrictionY = 0.98D;// this value will differ when a
                                        // dispenser upgrade is inserted.
        double payloadFrictionX = 0.98D;
        double payloadGravity = 0.04D;
        if(getUpgrades(ItemMachineUpgrade.UPGRADE_ENTITY_TRACKER) > 0) {
            payloadFrictionY = 0.98D;
            payloadFrictionX = 0.91D;
            payloadGravity = 0.08D;
        } else if(getUpgrades(ItemMachineUpgrade.UPGRADE_DISPENSER_DAMAGE, getUpgradeSlots()) > 0 && inventory[0] != null) {// if
            // there
            // is
            // a
            // dispenser
            // upgrade
            // inserted.
            Item item = inventory[0].getItem();
            if(item == Items.potionitem || item == Items.experience_bottle || item == Items.egg || item == Items.snowball) {// EntityThrowable
                payloadFrictionY = 0.99D;
                payloadGravity = 0.03D;
            } else if(item == Items.arrow) {
                payloadFrictionY = 0.99D;
                payloadGravity = 0.05D;
            } else if(item == Items.minecart || item == Items.chest_minecart || item == Items.hopper_minecart || item == Items.tnt_minecart || item == Items.furnace_minecart) {
                payloadFrictionY = 0.95D;
            }
            // else if(itemID == Item.fireballCharge.itemID){
            // payloadGravity = 0.0D;
            // }

            // family items (throwable) which only differ in gravity.
            if(item == Items.potionitem) payloadGravity = 0.05D;
            else if(item == Items.experience_bottle) payloadGravity = 0.07D;

            payloadFrictionX = payloadFrictionY;

            // items which have different frictions for each axis.
            if(item == Items.boat) {
                payloadFrictionX = 0.99D;
                payloadFrictionY = 0.95D;
            }
            if(item == Items.spawn_egg) {
                payloadFrictionY = 0.98D;
                payloadFrictionX = 0.91D;
                payloadGravity = 0.08D;
            }
        }

        // calculate the heading.
        double deltaX = gpsX - xCoord;
        double deltaZ = gpsZ - zCoord;
        float calculatedRotationAngle;
        if(deltaX >= 0 && deltaZ < 0) {
            calculatedRotationAngle = (float)(Math.atan(Math.abs(deltaX / deltaZ)) / Math.PI * 180D);
        } else if(deltaX >= 0 && deltaZ >= 0) {
            calculatedRotationAngle = (float)(Math.atan(Math.abs(deltaZ / deltaX)) / Math.PI * 180D) + 90;
        } else if(deltaX < 0 && deltaZ >= 0) {
            calculatedRotationAngle = (float)(Math.atan(Math.abs(deltaX / deltaZ)) / Math.PI * 180D) + 180;
        } else {
            calculatedRotationAngle = (float)(Math.atan(Math.abs(deltaZ / deltaX)) / Math.PI * 180D) + 270;
        }

        // calculate the height angle.
        double distance = Math.sqrt(deltaX * deltaX + deltaZ * deltaZ);
        double deltaY = gpsY - yCoord;
        float calculatedHeightAngle = calculateBestHeightAngle(distance, deltaY, getForce(), payloadGravity, payloadFrictionX, payloadFrictionY);

        setTargetAngles(calculatedRotationAngle, calculatedHeightAngle);
    }

    private float calculateBestHeightAngle(double distance, double deltaY, float force, double payloadGravity, double payloadFrictionX, double payloadFrictionY){
        double bestAngle = 0;
        double bestDistance = Float.MAX_VALUE;
        if(payloadGravity == 0D) {
            return 90F - (float)(Math.atan(deltaY / distance) * 180F / Math.PI);
        }
        for(double i = Math.PI * 0.25D; i < Math.PI * 0.50D; i += 0.001D) {
            double motionX = Math.cos(i) * force;// calculate the x component of
                                                 // the vector
            double motionY = Math.sin(i) * force;// calculate the y component of
                                                 // the vector
            double posX = 0;
            double posY = 0;
            while(posY > deltaY || motionY > 0) { // simulate movement, until we
                                                  // reach the y-level required
                posX += motionX;
                posY += motionY;
                motionY -= payloadGravity;// gravity
                motionX *= payloadFrictionX;// friction
                motionY *= payloadFrictionY;// friction
            }
            double distanceToTarget = Math.abs(distance - posX);// take the
                                                                // distance
            if(distanceToTarget < bestDistance) {// and return the best angle.
                bestDistance = distanceToTarget;
                bestAngle = i;
            }
        }
        coordWithinReach = bestDistance < 1.5D;
        return 90F - (float)(bestAngle * 180D / Math.PI);
    }

    public synchronized void setTargetAngles(float rotationAngle, float heightAngle){
        targetRotationAngle = rotationAngle;
        targetHeightAngle = heightAngle;
        if(!worldObj.isRemote) scheduleDescriptionPacket();
    }

    // this function calculates with the parsed in X and Z angles and the force
    // the needed, and outputs the X, Y and Z velocities.
    public double[] getVelocityVector(float angleX, float angleZ, float force){
        double[] velocities = new double[3];
        velocities[0] = Math.sin((double)angleZ / 180 * Math.PI);
        velocities[1] = Math.cos((double)angleX / 180 * Math.PI);
        velocities[2] = Math.cos((double)angleZ / 180 * Math.PI) * -1;

        velocities[0] *= Math.sin((double)angleX / 180 * Math.PI);
        velocities[2] *= Math.sin((double)angleX / 180 * Math.PI);
        // calculate the total velocity vector, in relation.
        double vectorTotal = velocities[0] * velocities[0] + velocities[1] * velocities[1] + velocities[2] * velocities[2];
        vectorTotal = force / vectorTotal; // calculate the relation between the
                                           // forces to be shot, and the
                                           // calculated vector (the scale).
        for(int i = 0; i < 3; i++) {
            velocities[i] *= vectorTotal; // scale up the velocities
            // System.out.println("velocities " + i + " = " + velocities[i]);
        }
        return velocities;
    }

    public boolean hasCoordinate(){
        return gpsX != 0 || gpsY != 0 || gpsZ != 0;
    }

    // PNEUMATIC METHODS -----------------------------------------

    @Override
    protected void disperseAir(){
        super.disperseAir();
        List<Pair<ForgeDirection, IAirHandler>> teList = getConnectedPneumatics();
        if(teList.size() == 0) airLeak(ForgeDirection.getOrientation(getBlockMetadata()));
    }

    @Override
    public boolean isConnectedTo(ForgeDirection side){
        return ForgeDirection.getOrientation(worldObj.getBlockMetadata(xCoord, yCoord, zCoord)) == side;
    }

    public float getForce(){
        return 2F + oldRangeUpgrades;
    }

    // INVENTORY METHODS- && NBT
    // ------------------------------------------------------------

    /**
     * Returns the number of slots in the inventory.
     */
    @Override
    public int getSizeInventory(){

        return inventory.length;
    }

    /**
     * Returns the stack in slot i
     */
    @Override
    public ItemStack getStackInSlot(int slot){

        return inventory[slot];
    }

    @Override
    public ItemStack decrStackSize(int slot, int amount){

        ItemStack itemStack = getStackInSlot(slot);
        if(itemStack != null) {
            if(itemStack.stackSize <= amount) {
                setInventorySlotContents(slot, null);
            } else {
                itemStack = itemStack.splitStack(amount);
                if(itemStack.stackSize == 0) {
                    setInventorySlotContents(slot, null);
                }
            }
        }

        return itemStack;
    }

    @Override
    public ItemStack getStackInSlotOnClosing(int slot){

        ItemStack itemStack = getStackInSlot(slot);
        if(itemStack != null) {
            setInventorySlotContents(slot, null);
        }
        return itemStack;
    }

    @Override
    public void setInventorySlotContents(int slot, ItemStack itemStack){
        inventory[slot] = itemStack;
        if(itemStack != null && itemStack.stackSize > getInventoryStackLimit()) {
            itemStack.stackSize = getInventoryStackLimit();
        }
    }

    @Override
    public String getInventoryName(){
        return Blockss.airCannon.getUnlocalizedName();
    }

    @Override
    public int getInventoryStackLimit(){
        return 64;
    }

    @Override
    public void readFromNBT(NBTTagCompound tag){
        super.readFromNBT(tag);
        redstonePowered = tag.getBoolean("redstonePowered");
        targetRotationAngle = tag.getFloat("targetRotationAngle");
        targetHeightAngle = tag.getFloat("targetHeightAngle");
        rotationAngle = tag.getFloat("rotationAngle");
        heightAngle = tag.getFloat("heightAngle");
        gpsX = tag.getInteger("gpsX");
        gpsY = tag.getInteger("gpsY");
        gpsZ = tag.getInteger("gpsZ");
        if(tag.hasKey("fireOnRightAngle")) {
            redstoneMode = tag.getBoolean("fireOnRightAngle") ? 0 : 1; //TODO remove legacy
        } else {
            redstoneMode = tag.getByte("redstoneMode");
        }

        coordWithinReach = tag.getBoolean("targetWithinReach");
        // Read in the ItemStacks in the inventory from NBT
        NBTTagList tagList = tag.getTagList("Items", 10);
        inventory = new ItemStack[getSizeInventory()];
        for(int i = 0; i < tagList.tagCount(); ++i) {
            NBTTagCompound tagCompound = tagList.getCompoundTagAt(i);
            byte slot = tagCompound.getByte("Slot");
            if(slot >= 0 && slot < inventory.length) {
                inventory[slot] = ItemStack.loadItemStackFromNBT(tagCompound);
            }
        }

        trackedItemIds = new HashSet<UUID>();
        tagList = tag.getTagList("trackedItems", 10);
        for(int i = 0; i < tagList.tagCount(); i++) {
            NBTTagCompound t = tagList.getCompoundTagAt(i);
            trackedItemIds.add(new UUID(t.getLong("UUIDMost"), t.getLong("UUIDLeast")));
        }

        if(tag.hasKey("inventoryX")) {
            lastInsertingInventory = new ChunkPosition(tag.getInteger("inventoryX"), tag.getInteger("inventoryY"), tag.getInteger("inventoryZ"));
            lastInsertingInventorySide = ForgeDirection.getOrientation(tag.getByte("inventorySide"));
        } else {
            lastInsertingInventory = null;
            lastInsertingInventorySide = null;
        }
    }

    @Override
    public void writeToNBT(NBTTagCompound tag){
        super.writeToNBT(tag);
        tag.setBoolean("redstonePowered", redstonePowered);
        tag.setFloat("targetRotationAngle", targetRotationAngle);
        tag.setFloat("targetHeightAngle", targetHeightAngle);
        tag.setFloat("rotationAngle", rotationAngle);
        tag.setFloat("heightAngle", heightAngle);
        tag.setInteger("gpsX", gpsX);
        tag.setInteger("gpsY", gpsY);
        tag.setInteger("gpsZ", gpsZ);
        tag.setByte("redstoneMode", (byte)redstoneMode);
        tag.setBoolean("targetWithinReach", coordWithinReach);
        // Write the ItemStacks in the inventory to NBT
        NBTTagList tagList = new NBTTagList();
        for(int currentIndex = 0; currentIndex < inventory.length; ++currentIndex) {
            if(inventory[currentIndex] != null) {
                NBTTagCompound tagCompound = new NBTTagCompound();
                tagCompound.setByte("Slot", (byte)currentIndex);
                inventory[currentIndex].writeToNBT(tagCompound);
                tagList.appendTag(tagCompound);
            }
        }
        tag.setTag("Items", tagList);

        tagList = new NBTTagList();
        for(EntityItem entity : trackedItems) {
            UUID uuid = entity.getUniqueID();
            NBTTagCompound t = new NBTTagCompound();
            t.setLong("UUIDMost", uuid.getMostSignificantBits());
            t.setLong("UUIDLeast", uuid.getLeastSignificantBits());
            tagList.appendTag(t);
        }
        tag.setTag("trackedItems", tagList);

        if(lastInsertingInventory != null) {
            tag.setInteger("inventoryX", lastInsertingInventory.chunkPosX);
            tag.setInteger("inventoryY", lastInsertingInventory.chunkPosY);
            tag.setInteger("inventoryZ", lastInsertingInventory.chunkPosZ);
            tag.setByte("inventorySide", (byte)lastInsertingInventorySide.ordinal());
        }
    }

    @Override
    public boolean isItemValidForSlot(int i, ItemStack itemstack){
        if(i == GPS_SLOT && itemstack != null && itemstack.getItem() != Itemss.GPSTool) return false;
        if(i > GPS_SLOT && i <= UPGRADE_SLOT_4 && itemstack != null && itemstack.getItem() != Itemss.machineUpgrade) return false;
        return true;
    }

    @Override
    public int[] getAccessibleSlotsFromSide(int var1){
        return new int[]{1, 2, 3, 4, 5, 0};
    }

    @Override
    public boolean canInsertItem(int slot, ItemStack itemstack, int side){
        return true;
    }

    @Override
    public boolean canExtractItem(int slot, ItemStack itemstack, int side){
        return true;
    }

    @Override
    public boolean isUseableByPlayer(EntityPlayer var1){
        return isGuiUseableByPlayer(var1);
    }

    // REDSTONE BEHAVIOUR
    // ------------------------------------------------------------

    @Override
    public void handleGUIButtonPress(int buttonID, EntityPlayer player){
        if(buttonID == 0) {
            if(++redstoneMode > 2) redstoneMode = 0;
            if(redstoneMode == 2 && getUpgrades(ItemMachineUpgrade.UPGRADE_BLOCK_TRACKER) == 0) redstoneMode = 0;
        }
    }

    public void onNeighbourBlockChange(int x, int y, int z, Block block){
        if(!block.isAir(worldObj, x, y, z) && worldObj.isBlockIndirectlyGettingPowered(x, y, z) && !redstonePowered && (redstoneMode != 0 || doneTurning) && (redstoneMode != 2 || inventoryCanCarry())) {
            fire();
            redstonePowered = true;
        } else if(!worldObj.isBlockIndirectlyGettingPowered(x, y, z) && redstonePowered) {
            redstonePowered = false;
        }
    }

    private boolean inventoryCanCarry(){
        insertingInventoryHasSpace = true;
        if(lastInsertingInventory == null) return true;
        if(inventory[0] == null) return true;
        TileEntity te = worldObj.getTileEntity(lastInsertingInventory.chunkPosX, lastInsertingInventory.chunkPosY, lastInsertingInventory.chunkPosZ);
        IInventory inv = IOHelper.getInventoryForTE(te);
        if(inv != null) {
            ItemStack remainder = IOHelper.insert(inv, inventory[0].copy(), lastInsertingInventorySide.ordinal(), true);
            insertingInventoryHasSpace = remainder == null;
            return insertingInventoryHasSpace;
        } else {
            lastInsertingInventory = null;
            lastInsertingInventorySide = null;
            return true;
        }
    }

    private synchronized boolean fire(){
        Entity itemShot = getCloseEntityIfUpgraded();
        if(getPressure(ForgeDirection.UNKNOWN) >= PneumaticValues.MIN_PRESSURE_AIR_CANNON && (itemShot != null || inventory[0] != null)) {
            double[] velocity = getVelocityVector(heightAngle, rotationAngle, getForce());
            addAir((int)(-500 * getForce()), ForgeDirection.UNKNOWN);
            boolean shootingInventory = false;
            if(itemShot == null) {
                shootingInventory = true;
                itemShot = getPayloadEntity();

                if(itemShot instanceof EntityItem) {
                    inventory[0] = null;
                    if(getUpgrades(ItemMachineUpgrade.UPGRADE_BLOCK_TRACKER) > 0) {
                        trackedItems.add((EntityItem)itemShot);
                    }
                } else {
                    inventory[0].stackSize--;
                    if(inventory[0].stackSize <= 0) inventory[0] = null;
                }
            } else if(itemShot instanceof EntityPlayer) {
                EntityPlayerMP entityplayermp = (EntityPlayerMP)itemShot;
                if(entityplayermp.playerNetServerHandler.func_147362_b().isChannelOpen()) {
                    entityplayermp.setPositionAndUpdate(xCoord + 0.5D, yCoord + 1.8D, zCoord + 0.5D);
                }
            }

            if(itemShot.isRiding()) {
                itemShot.mountEntity((Entity)null);
            }

            itemShot.setPosition(xCoord + 0.5D, yCoord + 1.8D, zCoord + 0.5D);
            NetworkHandler.sendToAllAround(new PacketSetEntityMotion(itemShot, velocity[0], velocity[1], velocity[2]), new TargetPoint(worldObj.provider.dimensionId, xCoord, yCoord, zCoord, 64));

            if(itemShot instanceof EntityFireball) {
                velocity[0] *= 0.05D;
                velocity[1] *= 0.05D;
                velocity[2] *= 0.05D;
            }

            itemShot.motionX = velocity[0];
            itemShot.motionY = velocity[1];
            itemShot.motionZ = velocity[2];

            itemShot.onGround = false;
            itemShot.isCollided = false;
            itemShot.isCollidedHorizontally = false;
            itemShot.isCollidedVertically = false;
            if(itemShot instanceof EntityLivingBase) ((EntityLivingBase)itemShot).setJumping(true);

            if(shootingInventory && !worldObj.isRemote) worldObj.spawnEntityInWorld(itemShot);

            for(int i = 0; i < 10; i++) {
                double velX = velocity[0] * 0.4D + (rand.nextGaussian() - 0.5D) * 0.05D;
                double velY = velocity[1] * 0.4D + (rand.nextGaussian() - 0.5D) * 0.05D;
                double velZ = velocity[2] * 0.4D + (rand.nextGaussian() - 0.5D) * 0.05D;
                NetworkHandler.sendToAllAround(new PacketSpawnParticle("largesmoke", xCoord + 0.5D, yCoord + 0.7D, zCoord + 0.5D, velX, velY, velZ), worldObj);
            }
            NetworkHandler.sendToAllAround(new PacketPlaySound(Sounds.CANNON_SOUND, xCoord, yCoord, zCoord, 1.0F, rand.nextFloat() / 4F + 0.75F, true), worldObj);
            return true;
        } else {
            return false;
        }
    }

    // warning: no null-check for inventory slot 0
    private Entity getPayloadEntity(){

        if(getUpgrades(ItemMachineUpgrade.UPGRADE_DISPENSER_DAMAGE, getUpgradeSlots()) > 0) {
            Item item = inventory[0].getItem();
            if(item == Item.getItemFromBlock(Blocks.tnt)) {
                EntityTNTPrimed tnt = new EntityTNTPrimed(worldObj);
                tnt.fuse = 80;
                return tnt;
            } else if(item == Items.experience_bottle) return new EntityExpBottle(worldObj);
            else if(item == Items.potionitem) {
                EntityPotion potion = new EntityPotion(worldObj);
                potion.setPotionDamage(inventory[0].getItemDamage());
                return potion;
            } else if(item == Items.arrow) return new EntityArrow(worldObj);
            else if(item == Items.egg) return new EntityEgg(worldObj);
            // else if(itemID == Item.fireballCharge) return new
            // EntitySmallFireball(worldObj);
            else if(item == Items.snowball) return new EntitySnowball(worldObj);
            else if(item == Items.spawn_egg) return ItemMonsterPlacer.spawnCreature(worldObj, inventory[0].getItemDamage(), 0, 0, 0);
            else if(item == Items.minecart) return new EntityMinecartEmpty(worldObj);
            else if(item == Items.chest_minecart) return new EntityMinecartChest(worldObj);
            else if(item == Items.furnace_minecart) return new EntityMinecartFurnace(worldObj);
            else if(item == Items.hopper_minecart) return new EntityMinecartHopper(worldObj);
            else if(item == Items.tnt_minecart) return new EntityMinecartTNT(worldObj);
            else if(item == Items.boat) return new EntityBoat(worldObj);

        }
        EntityItem item = new EntityItem(worldObj);
        item.setEntityItemStack(inventory[0].copy());
        item.age = 4800; // 1200 ticks left to live, = 60s.
        item.lifespan += Math.min(getUpgrades(ItemMachineUpgrade.UPGRADE_ITEM_LIFE, getUpgradeSlots()) * 600, 4800); // add
        // 30s
        // for
        // each
        // life
        // upgrade,
        // to
        // the
        // max
        // of
        // 5
        // min.
        return item;

    }

    private Entity getCloseEntityIfUpgraded(){
        int entityUpgrades = getUpgrades(ItemMachineUpgrade.UPGRADE_ENTITY_TRACKER);
        if(entityUpgrades > 0) {
            entityUpgrades = Math.min(entityUpgrades, 5);
            List<Entity> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, AxisAlignedBB.getBoundingBox(xCoord - entityUpgrades, yCoord - entityUpgrades, zCoord - entityUpgrades, xCoord + 1 + entityUpgrades, yCoord + 1 + entityUpgrades, zCoord + 1 + entityUpgrades));
            Entity closestEntity = null;
            for(Entity entity : entities) {
                if(closestEntity == null || PneumaticCraftUtils.distBetween(closestEntity.posX, closestEntity.posY, closestEntity.posZ, xCoord + 0.5, yCoord + 0.5, zCoord + 0.5) > PneumaticCraftUtils.distBetween(entity.posX, entity.posY, entity.posZ, xCoord + 0.5, yCoord + 0.5, zCoord + 0.5)) {
                    closestEntity = entity;
                }
            }
            return closestEntity;
        }
        return null;
    }

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

    @Override
    public void openInventory(){}

    @Override
    public void closeInventory(){}

    /*
     *  COMPUTERCRAFT API
     */

    @Override
    public String getType(){
        return "airCannon";
    }

    @Override
    protected void addLuaMethods(){
        super.addLuaMethods();
        luaMethods.add(new LuaConstant("getMinWorkingPressure", PneumaticValues.MIN_PRESSURE_AIR_CANNON));

        luaMethods.add(new LuaMethod("setTargetLocation"){
            @Override
            public Object[] call(Object[] args) throws Exception{
                if(args.length == 3) {
                    gpsX = ((Double)args[0]).intValue();
                    gpsY = ((Double)args[1]).intValue();
                    gpsZ = ((Double)args[2]).intValue();
                    updateDestination();
                    return new Object[]{coordWithinReach};
                } else {
                    throw new IllegalArgumentException("setTargetLocation requires 3 parameters (x,y,z)");
                }
            }
        });

        luaMethods.add(new LuaMethod("fire"){
            @Override
            public Object[] call(Object[] args) throws Exception{
                if(args.length == 0) {
                    return new Object[]{fire()};//returns true if the fire succeeded.
                } else {
                    throw new IllegalArgumentException("fire doesn't take any arguments!");
                }
            }
        });
        luaMethods.add(new LuaMethod("isDoneTurning"){
            @Override
            public Object[] call(Object[] args) throws Exception{
                if(args.length == 0) {
                    return new Object[]{doneTurning};
                } else {
                    throw new IllegalArgumentException("isDoneTurning doesn't take any arguments!");
                }
            }
        });

        luaMethods.add(new LuaMethod("setRotationAngle"){
            @Override
            public Object[] call(Object[] args) throws Exception{
                if(args.length == 1) {
                    setTargetAngles(((Double)args[0]).floatValue(), targetHeightAngle);
                    return null;
                } else {
                    throw new IllegalArgumentException("setRotationAngle does take one argument!");
                }
            }
        });

        luaMethods.add(new LuaMethod("setHeightAngle"){
            @Override
            public Object[] call(Object[] args) throws Exception{
                if(args.length == 1) {
                    setTargetAngles(targetRotationAngle, 90 - ((Double)args[0]).floatValue());
                    return null;
                } else {
                    throw new IllegalArgumentException("setHeightAngle does take one argument!");
                }
            }
        });

        luaMethods.add(new LuaMethod("setExternalControl"){
            @Override
            public Object[] call(Object[] args) throws Exception{
                if(args.length == 1) {
                    externalControl = (Boolean)args[0];
                    return null;
                } else {
                    throw new IllegalArgumentException("setExternalControl does take one argument!");
                }
            }
        });
    }

    @Override
    public float getMinWorkingPressure(){
        return PneumaticValues.MIN_PRESSURE_AIR_CANNON;
    }

    @Override
    public int getRedstoneMode(){
        return redstoneMode;
    }
}