/*
Copyright 2014 Google Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/


package dan200.qcraft.shared;

import com.google.common.base.CaseFormat;
import cpw.mods.fml.common.registry.GameRegistry;
import dan200.QCraft;
import net.minecraft.block.Block;
import net.minecraft.block.BlockPane;
import net.minecraft.block.material.Material;
import net.minecraft.entity.Entity;
import net.minecraft.entity.item.EntityItem;
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.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.CompressedStreamTools;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.network.NetworkManager;
import net.minecraft.network.Packet;
import net.minecraft.network.play.server.S35PacketUpdateTileEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.Facing;
import net.minecraft.world.World;
import net.minecraftforge.common.util.Constants;

import java.io.IOException;
import java.util.*;

public class TileEntityQuantumComputer extends TileEntity
{
    public static final EntanglementRegistry<TileEntityQuantumComputer> ComputerRegistry = new EntanglementRegistry<TileEntityQuantumComputer>();
    public static final EntanglementRegistry<TileEntityQuantumComputer> ClientComputerRegistry = new EntanglementRegistry<TileEntityQuantumComputer>();
    private static boolean tooManyPossiblePortals = false;

    public static EntanglementRegistry<TileEntityQuantumComputer> getEntanglementRegistry( World world )
    {
        if( !world.isRemote )
        {
            return ComputerRegistry;
        }
        else
        {
            return ClientComputerRegistry;
        }
    }

    public static class AreaShape
    {
        public int m_xMin;
        public int m_xMax;
        public int m_yMin;
        public int m_yMax;
        public int m_zMin;
        public int m_zMax;

        public boolean equals( AreaShape o )
        {
            return
                o.m_xMin == m_xMin &&
                o.m_xMax == m_xMax &&
                o.m_yMin == m_yMin &&
                o.m_yMax == m_yMax &&
                o.m_zMin == m_zMin &&
                o.m_zMax == m_zMax;
        }
    }

    public static class AreaData
    {
        public AreaShape m_shape;
        public Block[] m_blocks;
        public int[] m_metaData;

        public NBTTagCompound encode()
        {
            NBTTagCompound nbttagcompound = new NBTTagCompound();
            nbttagcompound.setInteger( "xmin", m_shape.m_xMin );
            nbttagcompound.setInteger( "xmax", m_shape.m_xMax );
            nbttagcompound.setInteger( "ymin", m_shape.m_yMin );
            nbttagcompound.setInteger( "ymax", m_shape.m_yMax );
            nbttagcompound.setInteger( "zmin", m_shape.m_zMin );
            nbttagcompound.setInteger( "zmax", m_shape.m_zMax );

            NBTTagList blockNames = new NBTTagList();
            for( int i=0; i<m_blocks.length; ++i )
            {
                String name = null;
                Block block = m_blocks[i];
                if( block != null )
                {
                    name = Block.blockRegistry.getNameForObject( block );
                }
                if( name != null && name.length() > 0 )
                {
                    blockNames.appendTag( new NBTTagString( name ) );
                }
                else
                {
                    blockNames.appendTag( new NBTTagString( "null" ) );
                }
            }
            nbttagcompound.setTag( "blockNames", blockNames );

            nbttagcompound.setIntArray( "metaData", m_metaData );
            return nbttagcompound;
        }

        public static AreaData decode( NBTTagCompound nbttagcompound )
        {
            AreaData storedData = new AreaData();
            storedData.m_shape = new AreaShape();
            storedData.m_shape.m_xMin = nbttagcompound.getInteger( "xmin" );
            storedData.m_shape.m_xMax = nbttagcompound.getInteger( "xmax" );
            storedData.m_shape.m_yMin = nbttagcompound.getInteger( "ymin" );
            storedData.m_shape.m_yMax = nbttagcompound.getInteger( "ymax" );
            storedData.m_shape.m_zMin = nbttagcompound.getInteger( "zmin" );
            storedData.m_shape.m_zMax = nbttagcompound.getInteger( "zmax" );

            int size =
                ( storedData.m_shape.m_xMax - storedData.m_shape.m_xMin + 1 ) *
                ( storedData.m_shape.m_yMax - storedData.m_shape.m_yMin + 1 ) *
                ( storedData.m_shape.m_zMax - storedData.m_shape.m_zMin + 1 );
            storedData.m_blocks = new Block[ size ];
            if( nbttagcompound.hasKey( "blockData" ) )
            {
                int[] blockIDs = nbttagcompound.getIntArray( "blockData" );
                for( int i=0; i<size; ++i )
                {
                    storedData.m_blocks[i] = Block.getBlockById( blockIDs[i] );
                }
            }
            else
            {
                NBTTagList blockNames = nbttagcompound.getTagList( "blockNames", Constants.NBT.TAG_STRING );
                for( int i=0; i<size; ++i )
                {
                    String name = blockNames.getStringTagAt( i );
                    if( name.length() > 0 && !name.equals( "null" ) )
                    {
                        storedData.m_blocks[i] = Block.getBlockFromName( name );
                    }
                }
            }
            storedData.m_metaData = nbttagcompound.getIntArray( "metaData" );
            return storedData;
        }
    }

    // Shared state
    private boolean m_powered;
    private int m_entanglementFrequency;
    private int m_timeSinceEnergize;

    // Area Teleportation state
    private AreaData m_storedData;

    // Server Teleportation state
    private String m_portalID;
    private boolean m_portalNameConflict;
    private String m_remoteServerAddress;
    private String m_remoteServerName;
    private String m_remotePortalID;

    public TileEntityQuantumComputer()
    {
        m_powered = false;
        m_entanglementFrequency = -1;
        m_timeSinceEnergize = 0;

        m_storedData = null;

        m_portalID = null;
        m_portalNameConflict = false;
        m_remoteServerAddress = null;
        m_remoteServerName = null;
        m_remotePortalID = null;
    }

    private EntanglementRegistry<TileEntityQuantumComputer> getEntanglementRegistry()
    {
        return getEntanglementRegistry( worldObj );
    }

    private PortalRegistry getPortalRegistry()
    {
        return PortalRegistry.getPortalRegistry( worldObj );
    }

    @Override
    public void validate()
    {
        super.validate();
        register();
    }

    @Override
    public void invalidate()
    {
        unregister();
        super.invalidate();
    }

    public void onDestroy()
    {
        PortalLocation location = getPortal();
        if( location != null && isPortalDeployed( location ) )
        {
            undeployPortal( location );
        }
        unregisterPortal();
    }

    // Entanglement

    private void register()
    {
        if( m_entanglementFrequency >= 0 )
        {
            getEntanglementRegistry().register( m_entanglementFrequency, this, this.getWorldObj() );
        }
    }

    private void unregister()
    {
        if( m_entanglementFrequency >= 0 )
        {
            getEntanglementRegistry().unregister( m_entanglementFrequency, this, this.getWorldObj() );
        }
    }

    public void setEntanglementFrequency( int frequency )
    {
        if( m_entanglementFrequency != frequency )
        {
            unregister();
            m_entanglementFrequency = frequency;
            register();
        }
    }

    public int getEntanglementFrequency()
    {
        return m_entanglementFrequency;
    }

    private TileEntityQuantumComputer findEntangledTwin()
    {
        if( m_entanglementFrequency >= 0 )
        {
            List<TileEntityQuantumComputer> twins = ComputerRegistry.getEntangledObjects( m_entanglementFrequency );
            if( twins != null )
            {
                Iterator<TileEntityQuantumComputer> it = twins.iterator();
                while( it.hasNext() )
                {
                    TileEntityQuantumComputer computer = it.next();
                    if( computer != this )
                    {
                        return computer;
                    }
                }
            }
        }
        return null;
    }

    // Area Teleportation

    public void setStoredData( AreaData data )
    {
        m_storedData = data;
    }

    public AreaData getStoredData()
    {
        return m_storedData;
    }

    private boolean isPillarBase( int x, int y, int z, int side )
    {
        if( y < 0 || y >= 256 )
        {
            return false;
        }

        TileEntity entity = worldObj.getTileEntity( x, y, z );
        if( entity != null && entity instanceof TileEntityQBlock )
        {
            TileEntityQBlock quantum = (TileEntityQBlock) entity;
            int[] types = quantum.getTypes();
            for( int i = 0; i < 6; ++i )
            {
                if( i == side )
                {
                    if( types[ i ] != 31 ) // GOLD
                    {
                        return false;
                    }
                }
                else
                {
                    if( types[ i ] != 21 ) // OBSIDIAN
                    {
                        return false;
                    }
                }
            }
            return true;
        }
        return false;
    }

    private boolean isPillar( int x, int y, int z )
    {
        if( y < 0 || y >= 256 )
        {
            return false;
        }

        Block block = worldObj.getBlock( x, y, z );
        if( block == Blocks.obsidian )
        {
            return true;
        }
        return false;
    }

    private boolean isGlass( int x, int y, int z )
    {
        if( y < 0 || y >= 256 )
        {
            return false;
        }

        Block block = worldObj.getBlock( x, y, z );
        if( block.getMaterial() == Material.glass && !(block instanceof BlockPane) )
        {
            return true;
        }
        return false;
    }

    private AreaShape calculateShape()
    {
        AreaShape shape = new AreaShape();
        shape.m_xMin = -99;
        shape.m_xMax = -99;
        shape.m_yMin = 0;
        shape.m_yMax = 0;
        shape.m_zMin = -99;
        shape.m_zMax = -99;
        for( int i = 0; i < QCraft.maxQTPSize; ++i )
        {
            if( shape.m_xMin == -99 && isPillarBase( xCoord - i - 1, yCoord, zCoord, 5 ) )
            {
                shape.m_xMin = -i;
            }
            if( shape.m_xMax == -99 && isPillarBase( xCoord + i + 1, yCoord, zCoord, 4 ) )
            {
                shape.m_xMax = i;
            }
            if( shape.m_zMin == -99 && isPillarBase( xCoord, yCoord, zCoord - i - 1, 3 ) )
            {
                shape.m_zMin = -i;
            }
            if( shape.m_zMax == -99 && isPillarBase( xCoord, yCoord, zCoord + i + 1, 2 ) )
            {
                shape.m_zMax = i;
            }
        }

        if( shape.m_xMin != -99 &&
                shape.m_xMax != -99 &&
                shape.m_zMin != -99 &&
                shape.m_zMax != -99 )
        {
            // Find Y Min
            for( int i = 1; i < QCraft.maxQTPSize; ++i )
            {
                if( isPillar( xCoord + shape.m_xMin - 1, yCoord - i, zCoord ) &&
                        isPillar( xCoord + shape.m_xMax + 1, yCoord - i, zCoord ) &&
                        isPillar( xCoord, yCoord - i, zCoord + shape.m_zMin - 1 ) &&
                        isPillar( xCoord, yCoord - i, zCoord + shape.m_zMax + 1 ) )
                {
                    shape.m_yMin = -i;
                }
                else
                {
                    break;
                }
            }

            // Find Y Max
            for( int i = 1; i < QCraft.maxQTPSize; ++i )
            {
                if( isPillar( xCoord + shape.m_xMin - 1, yCoord + i, zCoord ) &&
                        isPillar( xCoord + shape.m_xMax + 1, yCoord + i, zCoord ) &&
                        isPillar( xCoord, yCoord + i, zCoord + shape.m_zMin - 1 ) &&
                        isPillar( xCoord, yCoord + i, zCoord + shape.m_zMax + 1 ) )
                {
                    shape.m_yMax = i;
                }
                else
                {
                    break;
                }
            }

            // Check glass caps
            int top = yCoord + shape.m_yMax + 1;
            if( isGlass( xCoord + shape.m_xMin - 1, top, zCoord ) &&
                    isGlass( xCoord + shape.m_xMax + 1, top, zCoord ) &&
                    isGlass( xCoord, top, zCoord + shape.m_zMin - 1 ) &&
                    isGlass( xCoord, top, zCoord + shape.m_zMax + 1 ) )
            {
                return shape;
            }
        }
        return null;
    }

    private AreaData storeArea()
    {
        AreaShape shape = calculateShape();
        if( shape == null )
        {
            return null;
        }

        AreaData storedData = new AreaData();
        int minX = shape.m_xMin;
        int maxX = shape.m_xMax;
        int minY = shape.m_yMin;
        int maxY = shape.m_yMax;
        int minZ = shape.m_zMin;
        int maxZ = shape.m_zMax;

        int size = ( maxX - minX + 1 ) * ( maxY - minY + 1 ) * ( maxZ - minZ + 1 );
        int index = 0;

        storedData.m_shape = shape;
        storedData.m_blocks = new Block[ size ];
        storedData.m_metaData = new int[ size ];
        for( int y = minY; y <= maxY; ++y )
        {
            for( int x = minX; x <= maxX; ++x )
            {
                for( int z = minZ; z <= maxZ; ++z )
                {
                    int worldX = xCoord + x;
                    int worldY = yCoord + y;
                    int worldZ = zCoord + z;
                    if( !( worldX == xCoord && worldY == yCoord && worldZ == zCoord ) )
                    {
                        TileEntity tileentity = worldObj.getTileEntity( worldX, worldY, worldZ );
                        if( tileentity != null )
                        {
                            return null;
                        }

                        Block block = worldObj.getBlock( worldX, worldY, worldZ );
                        int meta = worldObj.getBlockMetadata( worldX, worldY, worldZ );
                        storedData.m_blocks[ index ] = block;
                        storedData.m_metaData[ index ] = meta;
                    }
                    index++;
                }
            }
        }

        return storedData;
    }

    private void notifyBlockOfNeighborChange( int x, int y, int z )
    {
        worldObj.notifyBlockOfNeighborChange( x, y, z, worldObj.getBlock( x, y, z ) );
    }

    private void notifyEdgeBlocks( AreaShape shape )
    {
        // Notify the newly transported blocks on the edges of the area that their neighbours have changed
        int minX = shape.m_xMin;
        int maxX = shape.m_xMax;
        int minY = shape.m_yMin;
        int maxY = shape.m_yMax;
        int minZ = shape.m_zMin;
        int maxZ = shape.m_zMax;
        for( int x = minX; x <= maxX; ++x )
        {
            for( int y = minY; y <= maxY; ++y )
            {
                notifyBlockOfNeighborChange( xCoord + x, yCoord + y, zCoord + minZ );
                notifyBlockOfNeighborChange( xCoord + x, yCoord + y, zCoord + maxZ );
                notifyBlockOfNeighborChange( xCoord + x, yCoord + y, zCoord + minZ );
                notifyBlockOfNeighborChange( xCoord + x, yCoord + y, zCoord + maxZ + 1 );
            }
        }
        for( int x = minX; x <= maxX; ++x )
        {
            for( int z = minZ; z <= maxZ; ++z )
            {
                notifyBlockOfNeighborChange( xCoord + x, yCoord + minY, zCoord + z );
                notifyBlockOfNeighborChange( xCoord + x, yCoord + maxY, zCoord + z );
                notifyBlockOfNeighborChange( xCoord + x, yCoord + minY - 1, zCoord + z );
                notifyBlockOfNeighborChange( xCoord + x, yCoord + maxY + 1, zCoord + z );
            }
        }
        for( int y = minY; y <= maxY; ++y )
        {
            for( int z = minZ; z <= maxZ; ++z )
            {
                notifyBlockOfNeighborChange( xCoord + minX, yCoord + y, zCoord + z );
                notifyBlockOfNeighborChange( xCoord + maxX, yCoord + y, zCoord + z );
                notifyBlockOfNeighborChange( xCoord + minX - 1, yCoord + y, zCoord + z );
                notifyBlockOfNeighborChange( xCoord + maxX + 1, yCoord + y, zCoord + z );
            }
        }
    }

    private Set<EntityItem> getEntityItemsInArea( AreaShape shape )
    {
        AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox(
                (double) ( xCoord + shape.m_xMin ),
                (double) ( yCoord + shape.m_yMin ),
                (double) ( zCoord + shape.m_zMin ),
                (double) ( xCoord + shape.m_xMax + 1 ),
                (double) ( yCoord + shape.m_yMax + 1 ),
                (double) ( zCoord + shape.m_zMax + 1 )
        );

        List list = worldObj.getEntitiesWithinAABBExcludingEntity( null, aabb );
        Set<EntityItem> set = new HashSet<EntityItem>();
        for( int i = 0; i < list.size(); ++i )
        {
            Entity entity = (Entity) list.get( i );
            if( entity instanceof EntityItem && !entity.isDead )
            {
                set.add( (EntityItem) entity );
            }
        }
        return set;
    }

    private void killNewItems( Set<EntityItem> before, Set<EntityItem> after )
    {
        Iterator<EntityItem> it = after.iterator();
        while( it.hasNext() )
        {
            EntityItem item = it.next();
            if( !item.isDead && !before.contains( item ) )
            {
                item.setDead();
            }
        }
    }

    private void clearArea( AreaShape shape )
    {
        // Cache the loose entities
        Set<EntityItem> before = getEntityItemsInArea( shape );

        // Set the area around us to air, notifying the adjacent blocks
        int minX = shape.m_xMin;
        int maxX = shape.m_xMax;
        int minY = shape.m_yMin;
        int maxY = shape.m_yMax;
        int minZ = shape.m_zMin;
        int maxZ = shape.m_zMax;
        for( int y = minY; y <= maxY; ++y )
        {
            for( int x = minX; x <= maxX; ++x )
            {
                for( int z = minZ; z <= maxZ; ++z )
                {
                    int worldX = xCoord + x;
                    int worldY = yCoord + y;
                    int worldZ = zCoord + z;
                    if( !( worldX == xCoord && worldY == yCoord && worldZ == zCoord ) )
                    {
                        worldObj.setBlockToAir( worldX, worldY, worldZ );
                    }
                }
            }
        }

        // Kill the new entities
        Set<EntityItem> after = getEntityItemsInArea( shape );
        killNewItems( before, after );

        // Notify edge blocks
        notifyEdgeBlocks( shape );
    }

    private void unpackArea( AreaData storedData )
    {
        // Cache the loose entities
        Set<EntityItem> before = getEntityItemsInArea( storedData.m_shape );

        // Set the area around us to the stored data
        int index = 0;
        int minX = storedData.m_shape.m_xMin;
        int maxX = storedData.m_shape.m_xMax;
        int minY = storedData.m_shape.m_yMin;
        int maxY = storedData.m_shape.m_yMax;
        int minZ = storedData.m_shape.m_zMin;
        int maxZ = storedData.m_shape.m_zMax;
        for( int y = minY; y <= maxY; ++y )
        {
            for( int x = minX; x <= maxX; ++x )
            {
                for( int z = minZ; z <= maxZ; ++z )
                {
                    int worldX = xCoord + x;
                    int worldY = yCoord + y;
                    int worldZ = zCoord + z;
                    if( !( worldX == xCoord && worldY == yCoord && worldZ == zCoord ) )
                    {
                        Block block = storedData.m_blocks[ index ];
                        if( block != null )
                        {
                            int meta = storedData.m_metaData[ index ];
                            worldObj.setBlock( worldX, worldY, worldZ, block, meta, 2 );
                        }
                        else
                        {
                            worldObj.setBlockToAir( worldX, worldY, worldZ );
                        }
                    }
                    index++;
                }
            }
        }

        // Kill the new entities
        Set<EntityItem> after = getEntityItemsInArea( storedData.m_shape );
        killNewItems( before, after );

        // Notify edge blocks
        notifyEdgeBlocks( storedData.m_shape );
    }

    private boolean checkCooling()
    {
        for( int i = 0; i < 6; ++i )
        {
            int x = xCoord + Facing.offsetsXForSide[ i ];
            int y = yCoord + Facing.offsetsYForSide[ i ];
            int z = zCoord + Facing.offsetsZForSide[ i ];
            Block block = worldObj.getBlock( x, y, z );
            if( block != null && (block.getMaterial() == Material.ice || block.getMaterial() == Material.packedIce) )
            {
                return true;
            }
        }
        return false;
    }

    public static enum TeleportError
    {
        Ok,
        NoTwin,
        FrameIncomplete,
        DestinationFrameIncomplete,
        FrameMismatch,
        FrameObstructed,
        InsufficientCooling,
        AreaNotTransportable,
        DestinationNotTransportable,
        FrameDeployed,
        NameConflict,
        MultiplePossiblePortalsFound;

        public static String decode( TeleportError error )
        {
            if( error != Ok )
            {
                return "gui.qcraft:computer.error_" + CaseFormat.UPPER_CAMEL.to( CaseFormat.LOWER_UNDERSCORE, error.toString() );
            }
            return null;
        }
    }

    private TeleportError canTeleport()
    {
        // Check entangled:
        TileEntityQuantumComputer twin = null;
        if( m_entanglementFrequency >= 0 )
        {
            // Find entangled twin:
            twin = findEntangledTwin();

            // Check the twin exists:
            if( twin == null )
            {
                return TeleportError.NoTwin;
            }
        }

        // Check the shape is big enough:
        AreaShape localShape = calculateShape();
        if( localShape == null )
        {
            return TeleportError.FrameIncomplete;
        }

        if( twin != null )
        {
            // Check the twin shape matches
            AreaShape twinShape = twin.calculateShape();
            if( twinShape == null )
            {
                return TeleportError.DestinationFrameIncomplete;
            }
            if( !localShape.equals( twinShape ) )
            {
                return TeleportError.FrameMismatch;
            }
        }
        else
        {
            // Check the stored shape matches
            if( m_storedData != null )
            {
                if( !localShape.equals( m_storedData.m_shape ) )
                {
                    return TeleportError.FrameMismatch;
                }
            }
        }

        // Check cooling
        if( !checkCooling() )
        {
            return TeleportError.InsufficientCooling;
        }

        // Store the two areas:
        AreaData localData = storeArea();
        if( localData == null )
        {
            return TeleportError.AreaNotTransportable;
        }

        if( twin != null )
        {
            AreaData twinData = twin.storeArea();
            if( twinData == null )
            {
                return TeleportError.DestinationNotTransportable;
            }
        }

        return TeleportError.Ok;
    }

    private TeleportError tryTeleport()
    {
        TeleportError error = canTeleport();
        if( error == TeleportError.Ok )
        {
            if( m_entanglementFrequency >= 0 )
            {
                // Store the two areas:
                TileEntityQuantumComputer twin = findEntangledTwin();
                if( twin != null )
                {
                    AreaData localData = storeArea();
                    AreaData twinData = twin.storeArea();
                    if( localData != null && twinData != null )
                    {
                        // Unpack the two areas:
                        unpackArea( twinData );
                        twin.unpackArea( localData );
                    }
                }
            }
            else
            {
                // Store the local area:
                AreaData localData = storeArea();

                // Unpack the stored area:
                if( m_storedData != null )
                {
                    unpackArea( m_storedData );
                }
                else
                {
                    clearArea( localData.m_shape );
                }

                m_storedData = localData;
            }

            // Effects
            worldObj.playSoundEffect( xCoord + 0.5, yCoord + 0.5, zCoord + 0.5, "mob.endermen.portal", 1.0F, 1.0F );
        }
        return error;
    }

    // Server Teleportation

    private void registerPortal()
    {
        PortalLocation location = findPortal();
        if( location != null )
        {
            if( m_portalID == null )
            {
                m_portalID = getPortalRegistry().getUnusedID();
                worldObj.markBlockForUpdate( xCoord, yCoord, zCoord );
            }
            if( !getPortalRegistry().register( m_portalID, location ) )
            {
                m_portalNameConflict = true;
            }
            else
            {
                m_portalNameConflict = false;
            }
            EntanglementSavedData.get(this.getWorldObj()).markDirty(); //Notify that this needs to be saved on world save
        }
        tooManyPossiblePortals = false;
    }

    private void unregisterPortal()
    {
        if( m_portalID != null )
        {
            if( !m_portalNameConflict )
            {
                getPortalRegistry().unregister( m_portalID );
                EntanglementSavedData.get(this.getWorldObj()).markDirty(); //Notify that this needs to be saved on world save
            }
            m_portalNameConflict = false;
        }
    }

    public void setPortalID( String id )
    {
        unregisterPortal();
        m_portalID = id;
        registerPortal();
    }

    public String getPortalID()
    {
        return m_portalID;
    }

    public void setRemoteServerAddress( String address )
    {
        m_remoteServerAddress = address;
        m_remoteServerName = getPortalRegistry().getServerName( address );
    }

    public String getRemoteServerAddress()
    {
        return m_remoteServerAddress;
    }

    public String getRemoteServerName()
    {
        return m_remoteServerName;
    }

    public void cycleRemoteServerAddress( String previousAddress )
    {
        m_remoteServerAddress = getPortalRegistry().getServerAddressAfter( previousAddress );
        m_remoteServerName = getPortalRegistry().getServerName( m_remoteServerAddress );
    }

    public void setRemotePortalID( String id )
    {
        m_remotePortalID = id;
    }

    public String getRemotePortalID()
    {
        return m_remotePortalID;
    }

    public boolean isTeleporter()
    {
        if( m_entanglementFrequency >= 0 )
        {
            return false;
        }
        if( !QCraft.canAnybodyCreatePortals() )
        {
            return false;
        }
        PortalLocation location = getPortal();
        if( location != null )
        {
            return true;
        }
        return false;
    }

    public boolean isTeleporterEnergized()
    {
        return canDeactivatePortal() == TeleportError.Ok;
    }

    private boolean isPortalCorner( int x, int y, int z, int dir )
    {
        if( y < 0 || y >= 256 )
        {
            return false;
        }

        TileEntity entity = worldObj.getTileEntity( x, y, z );
        if( entity != null && entity instanceof TileEntityQBlock )
        {
            TileEntityQBlock quantum = (TileEntityQBlock) entity;
            int[] types = quantum.getTypes();
            for( int i = 0; i < 6; ++i )
            {
                if( i == dir || i == Facing.oppositeSide[ dir ] )
                {
                    if( types[ i ] != 31 ) // GOLD
                    {
                        return false;
                    }
                }
                else
                {
                    if( types[ i ] != 21 ) // OBSIDIAN
                    {
                        return false;
                    }
                }
            }
            return true;
        }
        return false;
    }

    public static class PortalLocation
    {
        public int m_dimensionID;
        public int m_x1;
        public int m_x2;
        public int m_y1;
        public int m_y2;
        public int m_z1;
        public int m_z2;
        
        private PortalLocation(int x1, int y1, int z1, int x2, int y2, int z2, int id) {
            m_dimensionID = id;
            m_x1 = x1;
            m_x2 = x2;
            m_y1 = y1;
            m_y2 = y2;
            m_z1 = z1;
            m_z2 = z2;
        }
        
        public NBTTagCompound encode()
        {
            NBTTagCompound nbttagcompound = new NBTTagCompound();
            nbttagcompound.setInteger( "dimensionID", m_dimensionID );
            nbttagcompound.setInteger( "x1", m_x1 );
            nbttagcompound.setInteger( "y1", m_y1 );
            nbttagcompound.setInteger( "z1", m_z1 );
            nbttagcompound.setInteger( "x2", m_x2 );
            nbttagcompound.setInteger( "y2", m_y2 );
            nbttagcompound.setInteger( "z2", m_z2 );
            return nbttagcompound;
        }

        public static PortalLocation decode( NBTTagCompound nbttagcompound )
        {
            int dimID;
            if( nbttagcompound.hasKey( "dimensionID" ) )
            {
                dimID = nbttagcompound.getInteger( "dimensionID" );
            }
            else
            {
                dimID = 0;
            }
            int x1 = nbttagcompound.getInteger( "x1" );
            int y1 = nbttagcompound.getInteger( "y1" );
            int z1 = nbttagcompound.getInteger( "z1" );
            int x2 = nbttagcompound.getInteger( "x2" );
            int y2 = nbttagcompound.getInteger( "y2" );
            int z2 = nbttagcompound.getInteger( "z2" );
            return new PortalLocation(x1, y1, z1, x2, y2, z2, dimID);
        }
    }

    private boolean portalExistsAt( PortalLocation location )
    {
        int lookDir; //direction the portal is looking
        int c1;
        int c2;
        if (location.m_x1 == location.m_x2) {
            lookDir = 4;
            c1 = location.m_z1;
            c2 = location.m_z2;
        } else {
            lookDir = 2;
            c1 = location.m_x1;
            c2 = location.m_x2;
        }
        
        // Check walls from bottom to top
        for( int y = Math.min(location.m_y1, location.m_y2) + 1; y < Math.max(location.m_y1, location.m_y2); ++y )
        {
            if( !isGlass( location.m_x1, y, location.m_z1) )
            {
                return false;
            }
            if( !isGlass( location.m_x2, y, location.m_z2) )
            {
                return false;
            }
        }
        
        // Check ceiling and floor
        for( int xz = Math.min(c1, c2) + 1; xz < Math.max(c1, c2); ++xz )
        {
            if( !isGlass( (location.m_x1 == location.m_x2 ? location.m_x1 : xz) , location.m_y1, (location.m_z1 == location.m_z2 ? location.m_z1 : xz) ) )
            {
                return false;
            }
            if( !isGlass( (location.m_x1 == location.m_x2 ? location.m_x1 : xz), location.m_y2, (location.m_z1 == location.m_z2 ? location.m_z1 : xz) ) )
            {
                return false;
            }
        }
        // Check corners
        if( !isPortalCorner( location.m_x1, location.m_y1, location.m_z1, lookDir ) )
        {
            return false;
        }
        if( !isPortalCorner( location.m_x1, location.m_y2, location.m_z1, lookDir ) )
        {
            return false;
        }
        if( !isPortalCorner( location.m_x2, location.m_y1, location.m_z2, lookDir ) )
        {
            return false;
        }
        if( !isPortalCorner( location.m_x2, location.m_y2, location.m_z2, lookDir ) )
        {
            return false;
        }        
        return true;
    }

    private PortalLocation getPortal()
    {
        if( m_portalID != null )
        {
            if( !m_portalNameConflict )
            {
                PortalLocation portal = getPortalRegistry().getPortal( m_portalID );
                if( portal != null )
                {
                    return portal;
                }
            }
            else
            {
                PortalLocation portal = findPortal();
                if( portal != null )
                {
                    return portal;
                }
            }
        }
        return null;
    }
    
    private ArrayList<PortalLocation> findPortalsAt(int x, int y, int z) {
        ArrayList<PortalLocation> returnValue = new ArrayList();
        ArrayList<int[]> possibleCornerPairs = new ArrayList<int[]>();
        if (isGlass(x,y,z)) { //must find a pair of corner blocks that are on the same line and have the same facing.            
            int x1 = 0;
            int y1 = 0;
            int z1 = 0;        
            for( int dir = 0; dir < 6; ++dir ) {
                int tempX = x;
                int tempY = y;
                int tempZ = z;
                for (int i = 0; i < QCraft.maxPortalSize + 1; i++) { // maximum portal size
                    tempX += Facing.offsetsXForSide[ dir ];
                    tempY += Facing.offsetsYForSide[ dir ];
                    tempZ += Facing.offsetsZForSide[ dir ];
                    if (!isGlass(tempX, tempY, tempZ)) {
                        break;
                    }
                }
                if (dir % 2 == 0) { //every first corner
                     x1 = tempX;
                     y1 = tempY;
                     z1 = tempZ;
                } else { // every second corner
                    if (dir < 2) { //if direction of search was up/down
                        for (int i = 0; i<2; i++) {
                            if (isPortalCorner(tempX, tempY, tempZ, (i+1)*2) &&
                                    isPortalCorner(x1, y1, z1, (i+1)*2) &&
                                    Math.abs(tempY-y1) < QCraft.maxPortalSize + 2) { //+2: +1 for the extra corner block and +1 for the "<" boolean operator
                                int[] temp = {tempX, tempY, tempZ, x1, y1, z1, (i+1)*2};
                                possibleCornerPairs.add(temp);
                                break;
                            }
                        }
                    } else { //if search direction was sideways
                        int perpenDir = ((dir - 1) % 4) + 2;
                        if (isPortalCorner(tempX, tempY, tempZ, perpenDir) &&
                                isPortalCorner(x1, y1, z1, perpenDir) &&
                                Math.abs(tempX-x1) < QCraft.maxPortalSize + 2 &&
                                Math.abs(tempZ-z1) < QCraft.maxPortalSize + 2) {
                            int[] temp = {tempX, tempY, tempZ, x1, y1, z1, perpenDir};
                            possibleCornerPairs.add(temp);
                        }
                    }
                }
            }            
        } else { //if it's a corner
            for( int dir = 0; dir < 6; ++dir ) {
                if (isPortalCorner(x, y, z, dir)) {
                    continue; //skipping the two directions in which this portalCorner can not be part of a portal.
                }
                int tempX = x;
                int tempY = y;
                int tempZ = z;
                for (int i = 0; i < QCraft.maxPortalSize + 1; i++) { // maximum portal size
                    tempX += Facing.offsetsXForSide[ dir ];
                    tempY += Facing.offsetsYForSide[ dir ];
                    tempZ += Facing.offsetsZForSide[ dir ];
                    if (!isGlass(tempX, tempY, tempZ)) {
                        break;
                    }
                }
                if (dir < 2) { //if direction of search was up/down
                    for (int i = 0; i<2; i++) { //northsouth OR eastwest
                        if (isPortalCorner(x, y, z, (i+1)*2) && isPortalCorner(tempX, tempY, tempZ, (i+1)*2)) {
                            int[] temp = {x, y, z, tempX, tempY, tempZ, (i+1)*2};
                            possibleCornerPairs.add(temp);
                            break;
                        }
                    }
                } else { //if search direction was sideways
                    int perpenDir = ((dir - 1) % 4) + 2;
                    if (isPortalCorner(tempX, tempY, tempZ, perpenDir)) { // we already know that THIS portalcorner is in this direction, since we are skipping the other directions
                        int[] temp = {x, y, z, tempX, tempY, tempZ, perpenDir};
                        possibleCornerPairs.add(temp);
                    }
                }
            }
        }
        for (int[] i : possibleCornerPairs) {
            ArrayList<PortalLocation> temp = findRestOfPortal(i);
            if (temp != null) {
                for (PortalLocation location : temp) {
                    returnValue.add(location);
                }                
            }
        }
        return returnValue; //contains 0 up to 6 portal locations
    }
    
    private ArrayList<PortalLocation> findRestOfPortal(int[] cornerPair) {
        ArrayList<PortalLocation> returnValue = new ArrayList();
        int x1 = cornerPair[0]; //x = east/west = dir 4 5
        int y1 = cornerPair[1]; //y = up/down = dir 0 1
        int z1 = cornerPair[2]; //z = north/south = dir 2 3 
        int x2 = cornerPair[3];
        int y2 = cornerPair[4];
        int z2 = cornerPair[5];
        int lookDir = cornerPair[6]; //direction the portal should be looking
        int searchDir = ((lookDir - (lookDir % 2)) % 4) + 2; //converts {2, 3, 4, 5} to {4, 4, 2, 2}
        if (Math.abs(y1 - y2) < 4 && (Math.abs(x1 - x2) < 3) && (Math.abs(z1 - z2) < 3)) { //if the portal would be too small if this pair of corners would make a portal
            return null;
        } else  if (y1 == y2){ //both corners and the glass between them would form the upper OR lower portal border
            for (int dir = 0; dir < 2; dir++) {
                int tempY = y2;
                for (int i = 0; i < QCraft.maxPortalSize + 1; i++) { //check for maximal portal size
                    tempY += Facing.offsetsYForSide[ dir ];
                    if (!isGlass(x1, tempY, z1) || !isGlass(x2, tempY,z2)) { //once connected glass stops
                        break;
                    }
                }
                if (isGlass(x1, tempY, z1) || isGlass(x2, tempY, z2) || Math.abs(y1 - tempY) < 4) { //if not both are non-glass OR the portal wouldn't be high enough
                    continue;
                }
                if (isPortalCorner(x1, tempY, z1, lookDir) && isPortalCorner(x2, tempY, z2, lookDir)) {
                    int c1;
                    int c2;
                    if (x1 == x2) {
                        c1 = z1;
                        c2 = z2;
                    } else {
                        c1 = x1;
                        c2 = x2;
                    }
                    //check for completeness of last horizontal border.
                    for(int i = Math.min(c1, c2) + 1; i < Math.max(c1, c2); i++ ) {
                        if (!isGlass((x1 == x2) ? x1 : i, tempY, (z1 == z2) ? z1 : i )) {
                            break;
                        }
                        if (i == Math.max(c1, c2) - 1) {
                            returnValue.add(new PortalLocation(x1, y1, z1, x2, tempY, z2, worldObj.provider.dimensionId));
                        }
                    }
                }
            }
        } else { //if the z and x coordinates of both corners are equal (corners are above eachother)
            for (int dir = searchDir; dir < searchDir+2 ; dir++) {
                int tempX = x2;
                int tempZ = z2;
                for (int i = 0; i < QCraft.maxPortalSize + 1; i++) { //check for maximal portal size
                    tempX += Facing.offsetsXForSide[ dir ];
                    tempZ += Facing.offsetsZForSide[ dir ];
                    if (!isGlass(tempX, y1, tempZ) || !isGlass(tempX, y2,tempZ)) { //once connected glass stops
                        break;
                    }
                }
                if (isGlass(tempX, y1, tempZ) || isGlass(tempX, y2, tempZ) || (Math.abs(x1 - tempX) < 3 && Math.abs(z1 - tempZ) < 3) ) { //if not both are non-glass OR the portal wouldn't be high enough
                    continue;
                }
                if (isPortalCorner(tempX, y1, tempZ, lookDir) && isPortalCorner(tempX, y2, tempZ, lookDir)) {
                    //check for completeness of last vertical border.
                    for(int i = Math.min(y1, y2) + 1; i < Math.max(y1, y2); i++ ) {
                        if (!isGlass(tempX, i, tempZ )) {
                            break;
                        }
                        if (i == Math.max(y1, y2) - 1) {
                            returnValue.add(new PortalLocation(x1, y1, z1, tempX, y2, tempZ, worldObj.provider.dimensionId));
                        }
                    }                    
                }
            }            
        }
        return returnValue; //contains 0 up to 2 portal locations
    }

    private PortalLocation findPortal() 
    {
        ArrayList<PortalLocation> portalLocations = new ArrayList();
        tooManyPossiblePortals = false;
        for( int dir = 0; dir < 6; ++dir )
        {
            // See if this adjoining block is part of a portal:
            int x = xCoord + Facing.offsetsXForSide[ dir ];
            int y = yCoord + Facing.offsetsYForSide[ dir ];
            int z = zCoord + Facing.offsetsZForSide[ dir ];
            if( !isGlass( x, y, z ) && !isPortalCorner( x, y, z, 2 ) && !isPortalCorner( x, y, z, 4 ) )
            {
                continue;
            }            
            
            ArrayList<PortalLocation> tempLocations = findPortalsAt(x, y, z);
            if ( (tempLocations.size() == 2 && ! (isPortalCorner( x, y, z, 2 ) || isPortalCorner( x, y, z, 4 ) ) ) || tempLocations.size() > 2)  {
                    tooManyPossiblePortals = true;
                    return null;
            }
            portalLocations.addAll(tempLocations);
            if (portalLocations.size() > 2) {
                tooManyPossiblePortals = true;
                return null;
            }
        }
        
        if (portalLocations.size() < 1) {
            return null;
        } else if (portalLocations.size() == 2) {
            PortalLocation portal1 = portalLocations.get(0);
            PortalLocation portal2 = portalLocations.get(1);            
            
            if( Math.min(portal1.m_x1,  portal1.m_x2) == Math.min(portal2.m_x1,  portal2.m_x2) &&
                    Math.min(portal1.m_y1,  portal1.m_y2) == Math.min(portal2.m_y1,  portal2.m_y2) &&
                    Math.min(portal1.m_z1,  portal1.m_z2) == Math.min(portal2.m_z1,  portal2.m_z2))
            {
                return portalLocations.get(0);
            } else {
                tooManyPossiblePortals = true;
                return null;
            }
        } else if (portalLocations.size() > 2) {
            tooManyPossiblePortals = true;
            return null;
        } else {
            return portalLocations.get(0);
        }
    }

    private boolean isPortalClear( PortalLocation portal )
    {
        for( int y = Math.min(portal.m_y1, portal.m_y2) + 1; y < Math.max(portal.m_y1, portal.m_y2); ++y )
        {
            for( int x = Math.min(portal.m_x1, portal.m_x2) + 1; x < Math.max(portal.m_x1, portal.m_x2); ++x )
            {
                if( !worldObj.isAirBlock( x, y, portal.m_z1 ) )
                {
                    return false;
                }
            }
            for( int z = Math.min(portal.m_z1, portal.m_z2) + 1; z < Math.max(portal.m_z1, portal.m_z2); ++z )
            {
                if( !worldObj.isAirBlock( portal.m_x1, y, z ) )
                {
                    return false;
                }
            }
        }
        return true;
    }

    private void deployPortal( PortalLocation portal )
    {
        for( int y = Math.min(portal.m_y1, portal.m_y2) + 1; y < Math.max(portal.m_y1, portal.m_y2); ++y )
        {
            for( int x = Math.min(portal.m_x1, portal.m_x2) + 1; x < Math.max(portal.m_x1, portal.m_x2); ++x )
            {
                worldObj.setBlock( x, y, portal.m_z1, QCraft.Blocks.quantumPortal, 0, 2 );
            }
            for( int z = Math.min(portal.m_z1, portal.m_z2) + 1; z < Math.max(portal.m_z1, portal.m_z2); ++z )
            {
                worldObj.setBlock( portal.m_x1, y, z, QCraft.Blocks.quantumPortal, 0, 2 );
            }
        }
    }

    private void undeployPortal( PortalLocation portal )
    {
        for( int y = Math.min(portal.m_y1, portal.m_y2) + 1; y < Math.max(portal.m_y1, portal.m_y2); ++y )
        {
            for( int x = Math.min(portal.m_x1, portal.m_x2) + 1; x < Math.max(portal.m_x1, portal.m_x2); ++x )
            {
                worldObj.setBlockToAir( x, y, portal.m_z1 );
            }
            for( int z = Math.min(portal.m_z1, portal.m_z2) + 1; z < Math.max(portal.m_z1, portal.m_z2); ++z )
            {
                worldObj.setBlockToAir( portal.m_x1, y, z );
            }
        }
    }

    private boolean isPortalDeployed( PortalLocation portal )
    {
        for( int y = Math.min(portal.m_y1, portal.m_y2) + 1; y < Math.max(portal.m_y1, portal.m_y2); ++y )
        {
            for( int x = Math.min(portal.m_x1, portal.m_x2) + 1; x < Math.max(portal.m_x1, portal.m_x2); ++x )
            {
                if( worldObj.getBlock( x, y, portal.m_z1 ) != QCraft.Blocks.quantumPortal )
                {
                    return false;
                }
            }
            for( int z = Math.min(portal.m_z1, portal.m_z2) + 1; z < Math.max(portal.m_z1, portal.m_z2); ++z )
            {
                if( worldObj.getBlock( portal.m_x1, y, z ) != QCraft.Blocks.quantumPortal )
                {
                    return false;
                }
            }
        }
        return true;
    }

    private TeleportError canActivatePortal()
    {
        if( m_entanglementFrequency >= 0 )
        {
            return TeleportError.FrameIncomplete;
        }
        if( !QCraft.canAnybodyCreatePortals() )
        {
            return TeleportError.FrameIncomplete;
        }

        tooManyPossiblePortals = false;
        PortalLocation location = getPortal();
        if(tooManyPossiblePortals) {
            tooManyPossiblePortals = false;
            return TeleportError.MultiplePossiblePortalsFound;
        }
        
        if( location == null )
        {
            return TeleportError.FrameIncomplete;
        }
        if( isPortalDeployed( location ) )
        {
            return TeleportError.FrameDeployed;
        }
        if( m_portalNameConflict )
        {
            return TeleportError.NameConflict;
        }
        if( !isPortalClear( location ) )
        {
            return TeleportError.FrameObstructed;
        }
        if( !checkCooling() )
        {
            return TeleportError.InsufficientCooling;
        }
        return TeleportError.Ok;
    }

    private TeleportError tryActivatePortal()
    {
        TeleportError error = canActivatePortal();
        if( error == TeleportError.Ok )
        {
            // Deploy
            PortalLocation location = getPortal();
            if( location != null )
            {
                deployPortal( location );
            }

            // Effects
            worldObj.playSoundEffect( xCoord + 0.5, yCoord + 0.5, zCoord + 0.5, "mob.endermen.portal", 1.0F, 1.0F );
        }
        return error;
    }

    public TeleportError canDeactivatePortal()
    {
        if( m_entanglementFrequency >= 0 )
        {
            return TeleportError.FrameIncomplete;
        }
        PortalLocation location = getPortal();
        if( location == null )
        {
            return TeleportError.FrameIncomplete;
        }
        if( !isPortalDeployed( location ) )
        {
            return TeleportError.FrameIncomplete;
        }
        return TeleportError.Ok;
    }

    private TeleportError tryDeactivatePortal()
    {
        TeleportError error = canDeactivatePortal();
        if( error == TeleportError.Ok )
        {
            // Deploy
            PortalLocation location = getPortal();
            if( location != null )
            {
                undeployPortal( location );
            }

            // Effects
            worldObj.playSoundEffect( xCoord + 0.5, yCoord + 0.5, zCoord + 0.5, "mob.endermen.portal", 1.0F, 1.0F );
        }
        return error;
    }


    // Common

    public void setRedstonePowered( boolean powered )
    {
        if( m_powered != powered )
        {
            m_powered = powered;
            if( !worldObj.isRemote )
            {
                if( m_powered && m_timeSinceEnergize >= 4 )
                {
                    tryEnergize();
                }
            }
        }
    }

    public TeleportError canEnergize()
    {
        TeleportError error = canTeleport();
        if( error == TeleportError.Ok )
        {
            return error;
        }

        TeleportError serverError = canActivatePortal();
        if( serverError == TeleportError.Ok )
        {
            return serverError;
        }

        TeleportError deactivateError = canDeactivatePortal();
        if( deactivateError == TeleportError.Ok )
        {
            return deactivateError;
        }

        if( error.ordinal() >= serverError.ordinal() )
        {
            return error;
        }
        else
        {
            return serverError;
        }
    }

    public TeleportError tryEnergize()
    {
        TeleportError error = tryTeleport();
        if( error == TeleportError.Ok )
        {
            m_timeSinceEnergize = 0;
            return error;
        }

        TeleportError serverError = tryActivatePortal();
        if( serverError == TeleportError.Ok )
        {
            m_timeSinceEnergize = 0;
            return serverError;
        }

        TeleportError deactivateError = tryDeactivatePortal();
        if( deactivateError == TeleportError.Ok )
        {
            return deactivateError;
        }

        if( error.ordinal() >= serverError.ordinal() )
        {
            return error;
        }
        else
        {
            return serverError;
        }
    }

    @Override
    public void updateEntity()
    {
        m_timeSinceEnergize++;

        if( !worldObj.isRemote )
        {
            // Try to register conflicted portal
            if( m_portalNameConflict )
            {
                registerPortal();
            }

            // Validate existing portal
            PortalLocation location = getPortal();
            if( location != null )
            {
                if( !portalExistsAt( location ) )
                {
                    if( isPortalDeployed( location ) )
                    {
                        undeployPortal( location );
                    }
                    unregisterPortal();
                    location = null;
                }
                else if( !checkCooling() )
                {
                    if( isPortalDeployed( location ) )
                    {
                        undeployPortal( location );
                    }
                }
            }
            else
            {
                unregisterPortal();
                location = null;
            }

            // Find new portal
            if( location == null )
            {
                registerPortal();
                location = getPortal();
            }

            // Try teleporting entities through portal
            if( location != null && isPortalDeployed( location ) )
            {
                // Search for players
                int x1 = Math.min(location.m_x1, location.m_x2);
                int x2 = Math.max(location.m_x1, location.m_x2);
                int y1 = Math.min(location.m_y1, location.m_y2);
                int y2 = Math.max(location.m_y1, location.m_y2);
                int z1 = Math.min(location.m_z1, location.m_z2);
                int z2 = Math.max(location.m_z1, location.m_z2);
                AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox(
                    ((double) x1 ) + (x1 == x2 ? 0.25 : 1),
                    ((double) y1 + 1),
                    ((double) z1 ) + (z1 == z2 ? 0.25 : 1),
                    ((double) x2 ) + (x1 == x2 ? 0.75 : 0),
                    ((double) y2 ),
                    ((double) z2 ) + (z1 == z2 ? 0.75 : 0)
                );

                List entities = worldObj.getEntitiesWithinAABB( EntityPlayer.class, aabb );
                if( entities != null && entities.size() > 0 )
                {
                    Iterator it = entities.iterator();
                    while( it.hasNext() )
                    {
                        Object next = it.next();
                        if( next != null && next instanceof EntityPlayer )
                        {
                            EntityPlayer player = (EntityPlayer)next;
                            if( player.timeUntilPortal <= 0 && player.ticksExisted >= 200 &&
                                player.ridingEntity == null && player.riddenByEntity == null )
                            {
                                // Teleport them:
                                teleportPlayer( player );
                            }
                        }
                    }
                }
            }
        }
    }

    private void teleportPlayer( EntityPlayer player )
    {
        if( m_remoteServerAddress != null )
        {
            queryTeleportPlayerRemote( player );
        }
        else
        {
            teleportPlayerLocal( player, m_remotePortalID );
        }
    }

    private void queryTeleportPlayerRemote( EntityPlayer player )
    {
        QCraft.requestQueryGoToServer( player, this );
        player.timeUntilPortal = 50;
    }

    public void teleportPlayerRemote( EntityPlayer player, boolean takeItems )
    {
        teleportPlayerRemote( player, m_remoteServerAddress, m_remotePortalID, takeItems );
    }

    public static void teleportPlayerRemote( EntityPlayer player, String remoteServerAddress, String remotePortalID, boolean takeItems )
    {
        // Log the trip
        QCraft.log( "Sending player " + player.getDisplayName() + " to server \"" + remoteServerAddress + "\"" );

        NBTTagCompound luggage = new NBTTagCompound();
        if( takeItems )
        {
            // Remove and encode the items from the players inventory we want them to take with them
            NBTTagList items = new NBTTagList();
            InventoryPlayer playerInventory = player.inventory;
            for( int i = 0; i < playerInventory.getSizeInventory(); ++i )
            {
                ItemStack stack = playerInventory.getStackInSlot( i );
                if( stack != null && stack.stackSize > 0 )
                {
                    // Ignore entangled items
                    if( stack.getItem() == Item.getItemFromBlock( QCraft.Blocks.quantumComputer ) && ItemQuantumComputer.getEntanglementFrequency( stack ) >= 0 )
                    {
                        continue;
                    }
                    if( stack.getItem() == Item.getItemFromBlock( QCraft.Blocks.qBlock ) && ItemQBlock.getEntanglementFrequency( stack ) >= 0 )
                    {
                        continue;
                    }

                    // Store items
                    NBTTagCompound itemTag = new NBTTagCompound();
                    if (stack.getItem() == QCraft.Items.missingItem) {
                        itemTag = stack.stackTagCompound;
                    } else {
                        GameRegistry.UniqueIdentifier uniqueId = GameRegistry.findUniqueIdentifierFor(stack.getItem());
                        String itemName = uniqueId.modId + ":" + uniqueId.name;
                        itemTag.setString("Name", itemName);
                        stack.writeToNBT( itemTag );
                    }
                    items.appendTag( itemTag );

                    // Remove items
                    playerInventory.setInventorySlotContents( i, null );
                }
            }

            if( items.tagCount() > 0 )
            {
                QCraft.log( "Removed " + items.tagCount() + " items from " + player.getDisplayName() + "'s inventory." );
                playerInventory.markDirty();
                luggage.setTag( "items", items );
            }
        }

        // Set the destination portal ID
        if( remotePortalID != null )
        {
            luggage.setString( "destinationPortal", remotePortalID );
        }

        try
        {
            // Cryptographically sign the luggage
            luggage.setString( "uuid", UUID.randomUUID().toString() );
            byte[] luggageData = CompressedStreamTools.compress( luggage );
            byte[] luggageSignature = EncryptionRegistry.Instance.signData( luggageData );
            NBTTagCompound signedLuggage = new NBTTagCompound();
            signedLuggage.setByteArray( "key", EncryptionRegistry.Instance.encodePublicKey( EncryptionRegistry.Instance.getLocalKeyPair().getPublic() ) );
            signedLuggage.setByteArray( "luggage", luggageData );
            signedLuggage.setByteArray( "signature", luggageSignature );

            // Send the player to the remote server with the luggage
            byte[] signedLuggageData = CompressedStreamTools.compress( signedLuggage );
            QCraft.requestGoToServer( player, remoteServerAddress, signedLuggageData );
        }
        catch( IOException e )
        {
            throw new RuntimeException( "Error encoding inventory" );
        }
        finally
        {
            // Prevent the player from being warped twice
            player.timeUntilPortal = 200;
        }
    }

    public void teleportPlayerLocal( EntityPlayer player )
    {
        teleportPlayerLocal( player, m_remotePortalID );
    }

    public static void teleportPlayerLocal( EntityPlayer player, String portalID )
    {
        PortalLocation location = (portalID != null) ?
            PortalRegistry.PortalRegistry.getPortal( portalID ) :
            null;

        if( location != null )
        {
            double xPos = ((double)location.m_x1 + location.m_x2 + 1) / 2;
            double yPos = (double) Math.min(location.m_y1, location.m_y2) + 1;
            double zPos = ((double)location.m_z1 + location.m_z2 + 1) / 2;
            if( location.m_dimensionID == player.dimension )
            {
                player.timeUntilPortal = 40;
                player.setPositionAndUpdate( xPos, yPos, zPos );
            }
            else if( player instanceof EntityPlayerMP )
            {
                player.timeUntilPortal = 40;
                MinecraftServer.getServer().getConfigurationManager().transferPlayerToDimension(
                    (EntityPlayerMP)player,
                    location.m_dimensionID,
                    new QuantumTeleporter(
                        MinecraftServer.getServer().worldServerForDimension( location.m_dimensionID ),
                        xPos, yPos, zPos
                    )
                );
            }
        }
    }

    @Override
    public void readFromNBT( NBTTagCompound nbttagcompound )
    {
        // Read properties
        super.readFromNBT( nbttagcompound );
        m_powered = nbttagcompound.getBoolean( "p" );
        m_timeSinceEnergize = nbttagcompound.getInteger( "tse" );
        m_entanglementFrequency = nbttagcompound.getInteger( "f" );
        if( nbttagcompound.hasKey( "d" ) )
        {
            m_storedData = AreaData.decode( nbttagcompound.getCompoundTag( "d" ) );
        }
        if( nbttagcompound.hasKey( "portalID" ) )
        {
            m_portalID = nbttagcompound.getString( "portalID" );
        }
        m_portalNameConflict = nbttagcompound.getBoolean( "portalNameConflict" );
        if( nbttagcompound.hasKey( "remoteIPAddress" ) )
        {
            m_remoteServerAddress = nbttagcompound.getString( "remoteIPAddress" );
        }
        if( nbttagcompound.hasKey( "remoteIPName" ) )
        {
            m_remoteServerName = nbttagcompound.getString( "remoteIPName" );
        }
        else
        {
            m_remoteServerName = m_remoteServerAddress;
        }
        if( nbttagcompound.hasKey( "remotePortalID" ) )
        {
            m_remotePortalID = nbttagcompound.getString( "remotePortalID" );
        }
    }

    @Override
    public void writeToNBT( NBTTagCompound nbttagcompound )
    {
        // Write properties
        super.writeToNBT( nbttagcompound );
        nbttagcompound.setBoolean( "p", m_powered );
        nbttagcompound.setInteger( "tse", m_timeSinceEnergize );
        nbttagcompound.setInteger( "f", m_entanglementFrequency );
        if( m_storedData != null )
        {
            nbttagcompound.setTag( "d", m_storedData.encode() );
        }
        if( m_portalID != null )
        {
            nbttagcompound.setString( "portalID", m_portalID );
        }
        nbttagcompound.setBoolean( "portalNameConflict", m_portalNameConflict );
        if( m_remoteServerAddress != null )
        {
            nbttagcompound.setString( "remoteIPAddress", m_remoteServerAddress );
        }
        if( m_remoteServerName != null )
        {
            nbttagcompound.setString( "remoteIPName", m_remoteServerName );
        }
        if( m_remotePortalID != null )
        {
            nbttagcompound.setString( "remotePortalID", m_remotePortalID );
        }
    }

    @Override
    public Packet getDescriptionPacket()
    {
        // Communicate networked state
        NBTTagCompound nbttagcompound = new NBTTagCompound();
        nbttagcompound.setInteger( "f", m_entanglementFrequency );
        if( m_portalID != null )
        {
            nbttagcompound.setString( "portalID", m_portalID );
        }
        nbttagcompound.setBoolean( "portalNameConflict", m_portalNameConflict );
        if( m_remoteServerAddress != null )
        {
            nbttagcompound.setString( "remoteIPAddress", m_remoteServerAddress );
        }
        if( m_remoteServerName != null )
        {
            nbttagcompound.setString( "remoteIPName", m_remoteServerName );
        }
        if( m_remotePortalID != null )
        {
            nbttagcompound.setString( "remotePortalID", m_remotePortalID );
        }
        return new S35PacketUpdateTileEntity( this.xCoord, this.yCoord, this.zCoord, 0, nbttagcompound );
    }

    @Override
    public void onDataPacket( NetworkManager net, S35PacketUpdateTileEntity packet )
    {
        switch( packet.func_148853_f() ) // actionType
        {
            case 0:
            {
                // Read networked state
                NBTTagCompound nbttagcompound = packet.func_148857_g(); // data
                setEntanglementFrequency( nbttagcompound.getInteger( "f" ) );
                if( nbttagcompound.hasKey( "portalID" ) )
                {
                    m_portalID = nbttagcompound.getString( "portalID" );
                }
                else
                {
                    m_portalID = null;
                }
                m_portalNameConflict = nbttagcompound.getBoolean( "portalNameConflict" );
                if( nbttagcompound.hasKey( "remoteIPAddress" ) )
                {
                    m_remoteServerAddress = nbttagcompound.getString( "remoteIPAddress" );
                }
                else
                {
                    m_remoteServerAddress = null;
                }
                if( nbttagcompound.hasKey( "remoteIPName" ) )
                {
                    m_remoteServerName = nbttagcompound.getString( "remoteIPName" );
                }
                else
                {
                    m_remoteServerName = null;
                }
                if( nbttagcompound.hasKey( "remotePortalID" ) )
                {
                    m_remotePortalID = nbttagcompound.getString( "remotePortalID" );
                }
                else
                {
                    m_remotePortalID = null;
                }
                break;
            }
            default:
            {
                break;
            }
        }
    }
}