public class BetterCommandHandler extends CommandHandler{

	@Override
	public void registerCommands() {
		addCommands(new BetterTeleportServer("BetterTeleportServer"));
		addCommands(new CreatePortal("CreatePortal"));
		addCommands(new DeletePortal("DeletePortal"));
		addCommands(new ExcludeServer("ExcludeServer"));
		addCommands(new JoinPortal("JoinPortal"));
		addCommands(new RemovePortal("RemovePortal"));
		addCommands(new Teleport("Teleport"));
		addCommands(new TeleportServer("TeleportServer"));
	}

} public class DatabaseManager {

	private BetterShardsPlugin plugin = BetterShardsPlugin.getInstance();
	private Config config;
	private ManagedDatasource db;
	
	private Map<UUID, Map<InventoryIdentifier, byte[]>> invCache = new HashMap<UUID, Map<InventoryIdentifier, byte[]>>();
	private Map<UUID, Long> invCacheFreshness = new HashMap<UUID, Long>();
	private long invCacheTimeout = 30000; // how long does a cached inventory stick around?
	private ExecutorService executor = Executors.newSingleThreadExecutor();

	private List<String> respawnExclusionCache = Collections.emptyList();
	private List<String> respawnExclusionCacheImmutable = Collections.unmodifiableList(respawnExclusionCache);
	private long respawnExclusionCacheExpires = 0;
	private final long SPAWN_EXCLUSION_TIMEOUT = 5 * 60 * 1000;  // 5 minutes in ms

	private Map<String, PriorityInfo> respawnPriorityCache = Collections.emptyMap();
	private Map<String, PriorityInfo> respawnPriorityCacheImmutable = Collections.unmodifiableMap(respawnPriorityCache);
	private long respawnPriorityCacheExpires = 0;
	private final long SPAWN_PRIORITY_TIMEOUT = 5 * 60 * 1000;  // 5 minutes in ms

	private static final String insertPlayerData = "INSERT INTO createPlayerData(uuid, server, entity, config_sect) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE entity = VALUES(entity), config_sect = VALUES(config_sect);";
	private static final String getPlayerData = "select * from createPlayerData where uuid = ? and server = ?;";
	private static final String removePlayerData = "delete from createPlayerData where uuid = ? and server = ?;";
	
	private static final String getLock = "INSERT INTO playerDataLock(uuid, inv_id) VALUES (?, ?);";
	private static final String checkLock = "SELECT last_upd FROM playerDataLock WHERE uuid = ? AND inv_id = ?;";
	private static final String releaseLock = "DELETE FROM playerDataLock WHERE uuid = ? AND inv_id = ?;";
	
	private static final String addPortalLoc = "insert into createPortalLocData(x1, y1, z1, x2, y2, z2, world, id) values (?,?,?,?,?,?,?,?);";
	private static final String getPortalLocByWorld = "select * from createPortalLocData where world = ?;";
	private static final String getPortalLoc = "select * from createPortalLocData where id = ?;";
	private static final String removePortalLoc = "delete from createPortalDataTable where id = ?;";
	
	private static final String addPortalData = "insert into createPortalDataTable(id, server_name, portal_type, partner_id) values(?,?,?,?);";
	private static final String getPortalData = "select * from createPortalDataTable where id = ?;";
	private static final String removePortalData = "delete from createPortalLocData where id = ?;";
	private static final String updatePortalData = "update createPortalDataTable set partner_id = ? where id = ?;";
	
	private static final String addExclude = "insert ignore into excludedServers(name) values(?);";
	private static final String removeExclude = "delete from excludedServers where name = ?;";
	private static final String getAllExclude = "select * from excludedServers;";
	
	private static final String addPriority = "insert into priorityServers(name, cap) values(?,?);";
	private static final String removePriority = "delete from priorityServers where name = ?;";
	private static final String getAllPriority = "select name, cap from priorityServers;";

	private static final String addBedLocation = "insert into player_beds (uuid, server, world_name, x, y, z) values (?,?,?,?,?,?)";
	private static final String getAllBedLocation = "select * from player_beds;";
	private static final String removeBedLocation = "delete from player_beds where uuid = ?;";
	
	private String cleanupLocks;

	private BukkitTask lockCleanup;
	
	private Logger logger;

	/**
	 * Does a few key things.
	 * 
	 * Builds a new {@link ManagedDatasource} from the <code>mysql</code> section in the Config. 
	 * Then, sets up migrations into the new format. Note that first time you run while on the old migration path,
	 * the server will shutdown. Restart, and it'll be good (this is the auto-migration to new migration system).
	 * 
	 * Next, optionally sets up cleanup if configured to do so.
	 */
	public DatabaseManager(){
		config = plugin.GetConfig();
		logger = plugin.getLogger();
		setupDatabase();
		try {
			db.getConnection().close();
		} catch (Exception se) {
			logger.log(Level.WARNING, "Could not connect to Database, shutting down!");
			Bukkit.shutdown();
		}

		prepareLocalStatements();
		prepareMigrations();
		
		if (!db.updateDatabase()) {
			logger.log(Level.WARNING, "Failed to migrate to latest Database, shutting down!");
			Bukkit.shutdown();
		}
		
		setupCleanup();
	}
	
	@CivConfigs({
		@CivConfig(name = "mysql.host", def = "localhost", type = CivConfigType.String),
		@CivConfig(name = "mysql.port", def = "3306", type = CivConfigType.Int),
		@CivConfig(name = "mysql.username", type = CivConfigType.String),
		@CivConfig(name = "mysql.password", type = CivConfigType.String),
		@CivConfig(name = "mysql.dbname", def = "BetterShardsDB", type = CivConfigType.String),
		@CivConfig(name = "mysql.poolsize", def = "10", type = CivConfigType.Int),
		@CivConfig(name = "mysql.connection_timeout", def = "10000", type = CivConfigType.Long),
		@CivConfig(name = "mysql.idle_timeout", def = "600000", type = CivConfigType.Long),
		@CivConfig(name = "mysql.max_lifetime", def = "7200000", type = CivConfigType.Long)
	})
	private void setupDatabase(){
		String username = config.get("mysql.username").getString();
		String host = config.get("mysql.host").getString();
		int port = config.get("mysql.port").getInt();
		String password = config.get("mysql.password").getString();
		String dbname = config.get("mysql.dbname").getString();
		int poolsize = config.get("mysql.poolsize").getInt();
		long connectionTimeout = config.get("mysql.connection_timeout").getLong();
		long idleTimeout = config.get("mysql.idle_timeout").getLong();
		long maxLifetime = config.get("mysql.max_lifetime").getLong();
		this.db = new ManagedDatasource(plugin, username, password, host, port, dbname, poolsize, connectionTimeout, idleTimeout, maxLifetime);
	}
	
	private void prepareMigrations() {
		// First migration is conversion from old system to new
		db.registerMigration(-1, false,
				new Callable<Boolean>() {
					@Override
					public Boolean call() {
						return false; // Force a failure. Migrations doesn't check the current migration per step, only at beginning.
						// So, we force a shutdown failure on first run. Then on second run, the Migration table will hold the correct values.
					}
				},
				"INSERT INTO managed_plugin_data (plugin_name, current_migration_number, last_migration)"
						+ " SELECT '" + plugin.getName() + "', max(db_version), timestamp(update_time) FROM bettershards_version LIMIT 1;");

		this.db.registerMigration(0, false,
				"create table if not exists createPlayerData("
						+ "uuid varchar(36) not null,"
						+ "entity blob,"
						+ "server int not null,"
						+ "primary key (uuid, server));",
				"create table if not exists createPortalDataTable("
						+ "id varchar(255) not null,"
						+ "server_name varchar(255) not null,"
						+ "portal_type int not null,"
						+ "partner_id varchar(255),"
						+ "primary key(id));",
				"create table if not exists createPortalLocData("
						+ "x1 int not null,"
						+ "y1 int not null,"
						+ "z1 int not null,"
						+ "x2 int not null,"
						+ "y2 int not null,"
						+ "z2 int not null,"
						+ "world varchar(255) not null,"
						+ "id varchar(255) not null,"
						+ "primary key loc_id (x1, y1, z1, x2, y2, z2, world, id));",
				"create table if not exists excludedServers("
						+ "name varchar(20) not null,"
						+ "primary key name_id(name));",
				"create table if not exists priorityServers("
						+ "name varchar(20) not null,"
						+ "cap int not null,"
						+ "primary key name_id(name));",
				"create table if not exists player_beds("
						+ "uuid varchar(36) not null,"
						+ "server varchar(36) not null,"
						+ "world_name varchar(36) not null,"
						+ "x int not null,"
						+ "y int not null,"
						+ "z int not null,"
						+ "primary key bed_id(uuid));",
				"create table if not exists bettershards_version("
						+ "db_version int not null,"
						+ "update_time varchar(24));");
		this.db.registerMigration(1, false, "alter table createPlayerData add config_sect text;");
		this.db.registerMigration(2, false, "CREATE TABLE IF NOT EXISTS playerDataLock("
					+ "uuid VARCHAR(36) NOT NULL,"
					+ "inv_id INT NOT NULL,"
					+ "last_upd TIMESTAMP NOT NULL DEFAULT NOW(),"
					+ "PRIMARY KEY (uuid, inv_id));");
	}
	
	@CivConfigs({
		@CivConfig(name = "locks.cleanup_minutes", def = "1", type = CivConfigType.Int),
	})
	private void prepareLocalStatements(){
		cleanupLocks = "DELETE FROM playerDataLock WHERE last_upd <= TIMESTAMPADD(MINUTE, -" + config.get("locks.cleanup_minutes").getInt() + ", NOW());";
	}

	@CivConfigs({
		@CivConfig(name = "locks.cleanup", def = "true", type = CivConfigType.Bool),
		@CivConfig(name = "locks.interval", def = "1200", type = CivConfigType.Long),
		@CivConfig(name = "cache.freshness_period", def = "30000", type = CivConfigType.Long)
	})
	private void setupCleanup() {
		this.invCacheTimeout = config.get("cache.freshness_period").getLong();
		if (config.get("locks.cleanup").getBool()) {  // no forever locks
			this.lockCleanup = plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, new Runnable() {

				@Override
				public void run() {
					try (Connection connection = db.getConnection();
							Statement statement = connection.createStatement();){
						statement.execute(cleanupLocks);
					} catch (SQLException se) {
						logger.log(Level.WARNING, "Failed to clean up outstanding locks", se);
					}
				}
				
			}, 20l, config.get("locks.interval").getLong());
		}
	}

	/**
	 * Closes things up.
	 */
	public void cleanup() {
		if (lockCleanup != null) {
			lockCleanup.cancel();
		}
		try {
			this.db.close();
		} catch (SQLException se) {
			
		}
	}


	/**
	 * Threadsafe accessor for the inventory cache. Respects the cache timeout; old caches are discarded, not returned.
	 * Cache for that player / inventory is discarded on retrieval. This is a freshness bounded pre-load cache, only.
	 *
	 * @param uuid The player to lookup
	 * @param id The inventory to retrieve
	 * @returns a byte array for the raw inventory data or null if none found or cache expired.
	 */
	private byte[] queryCache(UUID uuid, InventoryIdentifier id) {
		synchronized(invCache) {
			Map<InventoryIdentifier, byte[]> playerInvCache = invCache.get(uuid);
			if (playerInvCache != null) {

				// Check for freshness.
				Long freshness = invCacheFreshness.get(uuid);
				if (freshness != null && (System.currentTimeMillis() - freshness) > invCacheTimeout) {
					// Not Fresh. Clear the cache and return null.
					playerInvCache.clear();
					invCacheFreshness.remove(uuid);
					return null;
				}
				// Fresh or doesn't exist, either way carry on.
				return playerInvCache.remove(id);
			}
			return null;
		}
	}

	/**
	 * Threadsafe mutator for the inventory cache. Updates cache timeout.
	 * Be warned that since this doesn't check the cache timeout or clear out old caches, it is best to have some other process
	 * that periodically clears up old caches.
	 *
	 * @param uuid The player to cache data for
	 * @param id The inventory to cache into
	 * @param data The inventory data to cache
	 */
	private void updateCache(UUID uuid, InventoryIdentifier id, byte[] data) {
		synchronized(invCache) {
			Map<InventoryIdentifier, byte[]> playerInvCache = invCache.get(uuid);
			if (playerInvCache == null) {
				playerInvCache = new HashMap<InventoryIdentifier, byte[]>();
				invCache.put(uuid, playerInvCache);
			}
			playerInvCache.put(id, data);
			invCacheFreshness.put(uuid, System.currentTimeMillis());
		}
	}

	/**
	 * Threadsafe cleanup for the inventory cache. Respects freshness; if not fresh removes all inventories
	 * Can be used otherwise to remove a single cached inventory or all inventories.
	 * 
	 * @param uuid The player whose cache to modify
	 * @param id The inventory to remove from cache, null to clear all inventories
	 */
	private void clearCache(UUID uuid, InventoryIdentifier id) {
		synchronized(invCache) {
			Map<InventoryIdentifier, byte[]> playerInvCache = invCache.get(uuid);
			if (playerInvCache == null) {
				return;
			}
			if (id == null) { // clear all
				playerInvCache.clear();
				invCacheFreshness.remove(uuid);
			} else {
				// Check freshness
				Long freshness = invCacheFreshness.get(uuid);
				if (freshness != null && (System.currentTimeMillis() - freshness) > invCacheTimeout) {
					// Not fresh so clear all the cache anyway
					playerInvCache.clear();
					invCacheFreshness.remove(uuid);
				} else {
					// Fresh so just remove this one inventory
					playerInvCache.remove(id);
				}
			}
		}
	}
	
	/**
	 * Adds a portal instance to the database. Should be called only when
	 * initially creating a Portal Object.
	 */
	public void addPortal(Portal portal){
		try (Connection connection = db.getConnection();
				PreparedStatement addPortalLoc = connection.prepareStatement(DatabaseManager.addPortalLoc)){
			if (portal instanceof CuboidPortal){
				CuboidPortal p = (CuboidPortal) portal;
				Location first = p.getFirst();
				Location second = p.getSecond();
				addPortalLoc.setInt(1, first.getBlockX());
				addPortalLoc.setInt(2, first.getBlockY());
				addPortalLoc.setInt(3, first.getBlockZ());
				addPortalLoc.setInt(4, second.getBlockX());
				addPortalLoc.setInt(5, second.getBlockY());
				addPortalLoc.setInt(6, second.getBlockZ());
				addPortalLoc.setString(7, first.getWorld().getName());
				addPortalLoc.setString(8, p.getName());
			}
			else if (portal instanceof WorldBorderPortal) {
				WorldBorderPortal p = (WorldBorderPortal) portal;
				LocationWrapper firstW = p.getFirst();
				LocationWrapper secondW = p.getSecond();
				Location first = firstW.getFakeLocation();
				Location second = secondW.getFakeLocation();
				addPortalLoc.setInt(1, first.getBlockX());
				addPortalLoc.setInt(2, first.getBlockY());
				addPortalLoc.setInt(3, first.getBlockZ());
				addPortalLoc.setInt(4, second.getBlockX());
				addPortalLoc.setInt(5, second.getBlockY());
				addPortalLoc.setInt(6, second.getBlockZ());
				addPortalLoc.setString(7, firstW.getActualWorld());
				addPortalLoc.setString(8, p.getName());
			}
			else if (portal instanceof CircularPortal) {
				CircularPortal p = (CircularPortal) portal;
				Location first = p.getFirst();
				Location second = p.getSecond();
				addPortalLoc.setInt(1, first.getBlockX());
				addPortalLoc.setInt(2, first.getBlockY());
				addPortalLoc.setInt(3, first.getBlockZ());
				addPortalLoc.setInt(4, second.getBlockX());
				addPortalLoc.setInt(5, second.getBlockY());
				addPortalLoc.setInt(6, second.getBlockZ());
				addPortalLoc.setString(7, first.getWorld().getName());
				addPortalLoc.setString(8, p.getName());
			}
			addPortalLoc.execute();
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Add Portal DB failure: ", e);
		}
	}
	
	/**
	 * This method is called internally to remove the player from the cache.
	 * Minecraft executes the code to save the player so nothing needs to be
	 * done from this plugin's point of view.
	 * @param uuid The uuid of the player
	 */
	public void playerQuitServer(UUID uuid) {
	    clearCache(uuid, null);
	}
	
	private String serverName = MercuryAPI.serverName();
	public void addPortalData(Portal portal, Portal connection){
		try (Connection connect = db.getConnection();
				PreparedStatement addPortalData = connect.prepareStatement(DatabaseManager.addPortalData);) {
			addPortalData.setString(1, portal.getName());
			addPortalData.setString(2, serverName);
			addPortalData.setInt(3, portal.specialId);
			String name = null;
			if (connection != null)
				name = connection.getName();
			addPortalData.setString(4, name);
			addPortalData.execute();
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Add Portal Data DB failure: ", e);
		}
	}
	
	@CivConfigs({
		@CivConfig(name="locks.show_culprit", def="false", type = CivConfigType.Bool)
	})
	public boolean getPlayerLock(UUID uuid, InventoryIdentifier id) {
		try (Connection connection = db.getConnection();
				PreparedStatement getLock = connection.prepareStatement(DatabaseManager.getLock);) {
			getLock.setString(1, uuid.toString());
			getLock.setInt(2, id.ordinal());
			getLock.executeUpdate();
			return true;
		} catch (SQLException nolockforyou) {
			// TODO ideally only return false for known duplicate key failure error; otherwise raise holy hell.
			if (config.get("locks.show_culprit").getBool()) {
				// Who is responsible for this expected travesty
				logger.log(Level.WARNING, "Someone wanted a save-lock but was late to the party: ", nolockforyou);
			}
			return false;
		}
	}
	
	public boolean releasePlayerLock(UUID uuid, InventoryIdentifier id) {
		try (Connection connection = db.getConnection();
				PreparedStatement releaseLock = connection.prepareStatement(DatabaseManager.releaseLock);) {
			releaseLock.setString(1, uuid.toString());
			releaseLock.setInt(2, id.ordinal());
			return releaseLock.executeUpdate() > 0;
		} catch (SQLException nolockforyou) {
			plugin.getLogger().log(Level.INFO, "Unable to release lock for {0}, please investigate", uuid);
			plugin.getLogger().log(Level.INFO, "Unable to release lock, exception: ", nolockforyou);
			return false;
		}
	}
	
	public boolean isPlayerLocked(UUID uuid, InventoryIdentifier id){
		try (Connection connection = db.getConnection();
				PreparedStatement checkLock = connection.prepareStatement(DatabaseManager.checkLock)) {
			checkLock.setString(1, uuid.toString());
			checkLock.setInt(2, id.ordinal());
			ResultSet rs = checkLock.executeQuery();
			return rs.first();
		} catch (SQLException se) {
			plugin.getLogger().log(Level.INFO, "Could not check on lock for {0}, please investigate", uuid);
			plugin.getLogger().log(Level.INFO, "Unable to check lock, exception: ", se);
			return true;
		}
	}
	
	/**
	 * Saves player data synchronously. Use for operations where that is okay, e.g. server shutdown but not for
	 * ordinary operations (scheduled saves, quit saves). Use {@link #savePlayerDataAsync(UUID, ByteArrayOutputStream, InventoryIdentifier, ConfigurationSection)} instead.
	 * 
	 * @param uuid
	 * @param output
	 * @param id
	 * @param section
	 */
	public void savePlayerData(UUID uuid, ByteArrayOutputStream output, InventoryIdentifier id, 
			ConfigurationSection section) {
		logger.log(Level.FINER, "savePlayer Sync player data {0}", uuid);
		
		/*
		 * Some notes. 
		 * 
		 * For code that has a great many race conditions spread across multiple servers, you need something to play traffic cop.
		 * 
		 * Now usually, just change transaction isolation and you're good to go w/ automatic row-locks.
		 * 
		 * In our case, we explicitly want to _prevent_ read of the data if a save is _pending_. So we externalize our lock using
		 * an ultralightweight fast-failure locking mechanism.
		 * 
		 * It also helps up detect and prevent the "usual" badboys of simultaneous saves from multiple shards; a condition
		 * that should never occur but _if_ we externalize the lock, we can find it.
		 */
		if (!getPlayerLock(uuid, id)) { // someone beat us to it?
			logger.log(Level.WARNING, "Unable to grab rowlock for save of {0}, another process or server is saving at the same time as me.", uuid);
			shortTrace();
			return;
		}
		clearCache(uuid, id); // So if it is loaded again it is recaught.

		doSavePlayerData(uuid, output, id, section);
	}
	
	/**
	 * Used by Sync and Async calls.
	 * @param uuid
	 * @param output
	 * @param id
	 * @param section
	 */
	private void doSavePlayerData(UUID uuid, ByteArrayOutputStream output, InventoryIdentifier id, 
			ConfigurationSection section) {
		plugin.getLogger().log(Level.FINER, "doSave player data {0}", uuid);
		try (Connection connection = db.getConnection();
				PreparedStatement insertPlayerDataPS = connection.prepareStatement(DatabaseManager.insertPlayerData);) {
			insertPlayerDataPS.setString(1, uuid.toString());
			insertPlayerDataPS.setInt(2, id.ordinal());

			byte[] outputBytes = null;
			if (output == null) {
				insertPlayerDataPS.setNull(3, Types.BLOB);
			} else {
				outputBytes = output.toByteArray();
				insertPlayerDataPS.setBytes(3, outputBytes);
			}
			YamlConfiguration yaml = (YamlConfiguration) section;
			if (yaml == null) {
				insertPlayerDataPS.setNull(4, Types.VARCHAR);
			} else {
				String ymlStr = yaml.saveToString();
				insertPlayerDataPS.setString(4, ymlStr);
			}
			insertPlayerDataPS.execute();
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to doSavePlayerData for {0}", uuid);
			logger.log(Level.SEVERE, "Failed to doSavePlayerData exception:", e);
		} catch (Exception ididntthinkofthis) {
			logger.log(Level.SEVERE, "Failed to doSavePlayerData for {0}", uuid);
			logger.log(Level.SEVERE, "Failed to doSavePlayerData exception:", ididntthinkofthis);
		} finally {
			if (!releasePlayerLock(uuid, id)) { // Both calling methods require release always, so we'll just do it here.
				plugin.getLogger().log(Level.WARNING, "Unable to release lock for {0}, lock is already released", uuid);
			} else {
				plugin.getLogger().log(Level.FINER, "DONE doSave player data {0}", uuid);
			}
		}
	}

	/**
	 * Defers the actual saving; establishes a software lock prior to save and fails fast if it can't.
	 * 
	 * This method is the preferred route to save!
	 * 
	 * @param uuid
	 * @param output
	 * @param id
	 * @param section
	 */
	public void savePlayerDataAsync(final UUID uuid, final ByteArrayOutputStream output, 
			final InventoryIdentifier id, final ConfigurationSection section) {
		plugin.getLogger().log(Level.FINER, "savePlayer Async player data {0}", uuid);
		
		if (!getPlayerLock(uuid, id)) { // someone beat us to it?
			plugin.getLogger().log(Level.WARNING, "Unable to grab rowlock for save of {0}, some other server or process is saving at the same time as me.", uuid);
			shortTrace();
			return;
		}
		clearCache(uuid, id); // So if it is loaded again it is recaught.

		// So, we get the lock synchronously, then do our save asynch. When done, we end.
		executor.submit( new Runnable() {
			public void run() {
				doSavePlayerData(uuid, output, id, section);
			}
		});
	}
	
	public void clearPrefetch(UUID uuid, InventoryIdentifier id) {
		clearCache(uuid, id);
	}

	/**
	 * This loadPlayerData ignores any data locks. PLEASE USE WITH CARE. Calls to this method might return old data!
	 * <br><br>
	 * This method _reads_ from the cache if something is there (was pre-fetched).
	 * 
	 * @param uuid
	 * @param id
	 * @return
	 */
	public ByteArrayInputStream loadPlayerData(UUID uuid, InventoryIdentifier id){
		// Here we had it caches before hand so no need to load it again.
		byte[] bais = queryCache(uuid, id);
		if (bais != null) {
			plugin.getLogger().log(Level.FINER, "Getting player data sync from cache for {0}", uuid);
			return new ByteArrayInputStream(bais);
		}
			
		plugin.getLogger().log(Level.FINER, "IGNORING LOCKS: Getting player data sync for {0}", uuid);
		bais = doLoadPlayerData(uuid, id);
		plugin.getLogger().log(Level.FINER, "IGNORING LOCKS: Done getting player data sync for {0}", uuid);
		return new ByteArrayInputStream(bais);
	}
	
	/**
	 * Internal method does the real loading for both sync and async
	 * 
	 * @param uuid
	 * @param id
	 * @return
	 */
	private byte[] doLoadPlayerData(UUID uuid, InventoryIdentifier id){
		try (Connection connection = db.getConnection();
				PreparedStatement getPlayerData = connection.prepareStatement(DatabaseManager.getPlayerData);) {
			getPlayerData.setString(1, uuid.toString());
			getPlayerData.setInt(2, id.ordinal());
			ResultSet set = getPlayerData.executeQuery();
			if (!set.next()) {
				return new byte[0];
			}
			YamlConfiguration sect = new YamlConfiguration();
			String sectString = set.getString("config_sect");
			if (sectString != null) {
				sect.loadFromString(sectString);
			}
			CustomWorldNBTStorage.getWorldNBTStorage().loadConfigurationSectionForPlayer(uuid, sect);
			return set.getBytes("entity");			
		} catch (SQLException e) {
			plugin.getLogger().log(Level.SEVERE, "Error retrieving player data from database for {0}", uuid);
			plugin.getLogger().log(Level.SEVERE, "Error retrieving player data from database exception:", e);
		} catch (InvalidConfigurationException e) {
			plugin.getLogger().log(Level.WARNING, "Configuration is invalid for {0}", uuid);
			plugin.getLogger().log(Level.WARNING, "Configuration is invalid exception:", e);
		}
		return new byte[0];
	}

	/**
	 * @see #loadPlayerDataAsync(UUID, InventoryIdentifier, boolean)
	 * Calls it with prefetch = false
	 */
	public Future<ByteArrayInputStream> loadPlayerDataAsync(final UUID uuid, final InventoryIdentifier id) {
		return loadPlayerDataAsync(uuid, id, false);
	}
	
	/**
	 * Asynchronously gets the player data. Gets a little exotic but the basis is simply. Returns a Future object that holds the eventual result of the command.
	 * <br><br>
	 * Use .get on the Future to wait for it to load and return the result. 
	 * <br><br>
	 * EXTERNAL PLUGINS SHOULD USE THIS EXCLUSIVELY
	 * <br><br>
	 * This method can behave as a pre-fetch, putting data on the pre-fetch queue. It never consumes from the cache.
	 * <br><br>
	 * So, if you want to preload your data but not _use_ it, call loadPlayerDataAsync with the flag set in an async method, then
	 * call loadPlayerData in a sync method. If you're lucky and there aren't any race conditions in play to get that
	 * data (multiple consumers) then the sync call will pull from the cache.
	 * 
	 * @param uuid
	 * @param id
	 * @param prefetch set to true to save to cache.
	 * @return
	 */
	public Future<ByteArrayInputStream> loadPlayerDataAsync(final UUID uuid, final InventoryIdentifier id, final boolean prefetch) {
		return executor.submit( new Callable<ByteArrayInputStream>(){
				@Override
				public ByteArrayInputStream call() throws Exception {
					plugin.getLogger().log(Level.FINER, "Getting player data async for {0}", uuid);
					long sleepSoFar = (long) (Math.random() * 10.0);
					// basic spinlock.
					while (isPlayerLocked(uuid, id)) {
						Thread.sleep( sleepSoFar );
						if (sleepSoFar > 1000l) { // let's not get crazy!
							sleepSoFar = 1000l;
						}
						sleepSoFar += (long) (Math.random() * 10.0); // but let's keep the random, to prevent synchronization of multiple spinlocks.
					} 
					/* This leaves an opening for race conditions, but with a very small interval size (< 10ms) which is
					 * far superior to previous.
					 */ 
					byte[] bais = doLoadPlayerData(uuid, id);
					if (prefetch) {
						updateCache(uuid, id, bais);
					}
					plugin.getLogger().log(Level.FINER, "Done getting player data async for {0}", uuid);
					return new ByteArrayInputStream(bais);
				}
			}
		);
	}

	
	/**
	 * Can only be from worlds that are valid on this server.
	 * This is to prevent possible NullPointExceptions from trying to 
	 * get portals from Worlds that are not present on this server.
	 * 
	 * @param worlds
	 * @return
	 */
	public List<Portal> getAllPortalsByWorld(World[] worlds){
		List<Portal> portals = new ArrayList<Portal>();
		for (World w: worlds){
			String world = w.getName();
			try (Connection connection = db.getConnection();
					PreparedStatement getPortalLocation = connection.prepareStatement(DatabaseManager.getPortalLocByWorld);) {
				getPortalLocation.setString(1, world);
				ResultSet set = getPortalLocation.executeQuery();
				while (set.next()) {
					int x1 = set.getInt("x1");
					int y1 = set.getInt("y1");
					int z1 = set.getInt("z1");
					int x2 = set.getInt("x2");
					int y2 = set.getInt("y2");
					int z2 = set.getInt("z2");
					String id = set.getString("id");

					LocationWrapper first = new LocationWrapper(new Location(w, x1, y1, z1));
					LocationWrapper second = new LocationWrapper(new Location(w, x2, y2, z2));
					Portal p = getPortalData(id, first, second);
					portals.add(p);
				}
			} catch (SQLException e) {
				logger.log(Level.SEVERE, "Failed to getAllPortalsByWorld, exception:", e);
			}
		}
		return portals;
	}

	public Portal getPortal(String name) {
		try (Connection connection = db.getConnection();
				PreparedStatement getPortalData = connection.prepareStatement(DatabaseManager.getPortalLoc);) {
			getPortalData.setString(1, name);
			ResultSet set = getPortalData.executeQuery();
			if (!set.next())
				return null;
			int x1 = set.getInt("x1");
			int y1 = set.getInt("y1");
			int z1 = set.getInt("z1");
			int x2 = set.getInt("x2");
			int y2 = set.getInt("y2");
			int z2 = set.getInt("z2");
			String world = set.getString("world");
			World w = Bukkit.getWorld(world);
			LocationWrapper first = null, second = null;
			
			// If the World object does not equal null then we know that it is on this server.
			if (w != null) {
				first = new LocationWrapper(new Location(w, x1, y1, z1));
				second = new LocationWrapper(new Location(w, x2, y2, z2));
			}
			else {
				first = new LocationWrapper(world, x1, y1, z1);
				second = new LocationWrapper(world, x2, y2, z2);
			}
			
			return getPortalData(name, first, second);
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to getPortal for {0}", name);
			logger.log(Level.SEVERE, "Failed to getPortal, exception:", e);
		}
		return null;
	}
	
	private Portal getPortalData(String name, LocationWrapper first, LocationWrapper second) {
		try (Connection connection = db.getConnection();
				PreparedStatement getPortalData = connection.prepareStatement(DatabaseManager.getPortalData);) {
			getPortalData.setString(1, name);
			ResultSet set = getPortalData.executeQuery();
			if (!set.next())
				return null;
			int specialId = set.getInt("portal_type"); // determine the type of portal.
			String serverName = set.getString("server_name");
			String partner = set.getString("partner_id");
			boolean currentServer = serverName.equals(MercuryAPI.serverName());
			switch (specialId) {
			case 0:
				CuboidPortal p = new CuboidPortal(name, first.getFakeLocation(), second.getFakeLocation(), partner, currentServer);
				p.setServerName(serverName);
				return p;
			case 1:
				WorldBorderPortal wb = new WorldBorderPortal(name, partner, currentServer, first, second);
				wb.setServerName(serverName);
				return wb;
			case 2:
				CircularPortal cp = new CircularPortal(name, partner, currentServer, first.getFakeLocation(), second.getFakeLocation());
				cp.setServerName(serverName);
				return cp;
			default:
				return null;
			}
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to getPortalData for {0}", name);
			logger.log(Level.SEVERE, "Failed to getPortalData, exception:", e);
		}
		return null;
	}

	public void removePlayerData(UUID uuid, InventoryIdentifier id) {
		clearCache(uuid, id);
		try (Connection connection = db.getConnection();
				PreparedStatement removePlayerData = connection.prepareStatement(DatabaseManager.removePlayerData);) {
			removePlayerData.setString(1, uuid.toString());
			removePlayerData.setInt(2, id.ordinal());
			removePlayerData.execute();
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to removePlayerData, uuid: {0} id: {1}", new Object[]{uuid, id});
			logger.log(Level.SEVERE, "Failed to removePlayerData, exception:", e);
		}
	}

	public void removePortalLoc(Portal p) {
		try (Connection connection = db.getConnection();
				PreparedStatement removePortalLoc = connection.prepareStatement(DatabaseManager.removePortalLoc);) {
			removePortalLoc.setString(1, p.getName());
			removePortalLoc.execute();
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to removePortalLoc, exception:", e);
		}
	}

	public void removePortalData(Portal p) {
		try (Connection connection = db.getConnection();
				PreparedStatement removePortalData = connection.prepareStatement(DatabaseManager.removePortalData);) {
			removePortalData.setString(1, p.getName());
			removePortalData.execute();
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to removePortalData, exception:", e);
		}
	}

	public void updatePortalData(Portal p) {
		try (Connection connection = db.getConnection();
				PreparedStatement updatePortalData = connection.prepareStatement(DatabaseManager.updatePortalData);) {
			String partner = null;
			if (p.getPartnerPortal() != null)
				partner = p.getPartnerPortal().getName();
			updatePortalData.setString(1, partner);
			updatePortalData.setString(2, p.getName());
			updatePortalData.execute();
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to updatePortalData, exception:", e);
		}
	}

	public void addExclude(String server) {
		if (respawnExclusionCache.contains(server)) {
			return;
		}
		respawnExclusionCache.add(server);
		try (Connection connection = db.getConnection();
				PreparedStatement addExclude = connection.prepareStatement(DatabaseManager.addExclude);) {
			addExclude.setString(1, server);
			addExclude.execute();
		} catch (SQLException e) {
	 		logger.log(Level.SEVERE, "Failed to addExclude for {0}", server);
	 		logger.log(Level.SEVERE, "Failed to addExclude, exception:", e);
		}
	}

	public List<String> getAllExclude() {
		return getAllExclude(false);
	}

	public List<String> getAllExclude(boolean forceRefresh) {
		long currentTime = System.currentTimeMillis();
		if (forceRefresh || respawnExclusionCacheExpires <= currentTime) {
			respawnExclusionCacheExpires = currentTime + SPAWN_EXCLUSION_TIMEOUT;
			respawnExclusionCache = retrieveAllExcludeFromDb();
			respawnExclusionCacheImmutable = Collections.unmodifiableList(respawnExclusionCache);
		}
		return respawnExclusionCacheImmutable;
	}

	public List <String> retrieveAllExcludeFromDb() {
		List <String> result = new LinkedList<String>();
		try (Connection connection = db.getConnection();
				PreparedStatement getAllExclude = connection.prepareStatement(DatabaseManager.getAllExclude);) {
			ResultSet set = getAllExclude.executeQuery();
			while (set.next()) {
				result.add(set.getString("name"));
			}
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to retrieveAllExcludeFromDb, exception:", e);
		}
		return result;
	}

	public void removeExclude(String server) {
		if (!respawnExclusionCache.remove(server)) {
			return;
		}
		try (Connection connection = db.getConnection();
				PreparedStatement removeExclude = connection.prepareStatement(DatabaseManager.removeExclude);) {
			removeExclude.setString(1, server);
			removeExclude.execute();
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to removeExclude for {0}", server);
			logger.log(Level.SEVERE, "Failed to removeExclude, exception:", e);
		}
	}

	public void addPriorityServer(String server, int populationCap) {
		if (respawnPriorityCache.containsKey(server)) {
			return;
		}
		respawnPriorityCache.put(server, new PriorityInfo(server, populationCap));
		try (Connection connection = db.getConnection();
				PreparedStatement addPriority = connection.prepareStatement(DatabaseManager.addPriority);) {
			addPriority.setString(1, server);
			addPriority.setInt(2, populationCap);
			addPriority.execute();
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to addPriorityServer {0}", server);
			logger.log(Level.SEVERE, "Failed to addPriorityServer, exception:", e);
		}
	}

	public Map<String, PriorityInfo> getPriorityServers() {
		return getPriorityServers(false);
	}

	public Map<String, PriorityInfo> getPriorityServers(boolean forceRefresh) {
		long currentTime = System.currentTimeMillis();
		if (forceRefresh || respawnPriorityCacheExpires <= currentTime) {
			respawnPriorityCacheExpires = currentTime + SPAWN_PRIORITY_TIMEOUT;
			respawnPriorityCache = retrieveAllPriorityFromDb();
			respawnPriorityCacheImmutable = Collections.unmodifiableMap(respawnPriorityCache);
		}
		return respawnPriorityCacheImmutable;
	}

	public Map<String, PriorityInfo> retrieveAllPriorityFromDb() {
		Map<String, PriorityInfo> result = new HashMap<>();
		try (Connection connection = db.getConnection();
				PreparedStatement getAllPriority = connection.prepareStatement(DatabaseManager.getAllPriority);) {
			ResultSet set = getAllPriority.executeQuery();
			while (set.next()) {
				String server = set.getString("name");
				result.put(server, new PriorityInfo(server, set.getInt("cap")));
			}
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to retrieveAllPriorityFromDb, exception:", e);
		}
		return result;
	}

	public void removePriorityServer(String server) {
		if (respawnPriorityCache.remove(server) == null) {
			return;
		}
		try (Connection connection = db.getConnection();
				PreparedStatement removePriority = connection.prepareStatement(DatabaseManager.removePriority);) {
			removePriority.setString(1, server);
			removePriority.execute();
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to removePriorityServer {0}", server);
			logger.log(Level.SEVERE, "Failed to removePriorityServer, exception:", e);
		}
	}

	public void addBedLocation(BedLocation bed) {
		TeleportInfo info = bed.getTeleportInfo();
		try (Connection connection = db.getConnection();
				PreparedStatement addBedLocation = connection.prepareStatement(DatabaseManager.addBedLocation);) {
			addBedLocation.setString(1, bed.getUUID().toString());
			addBedLocation.setString(2, bed.getServer());
			addBedLocation.setString(3, info.getWorld());
			addBedLocation.setInt(4, info.getX());
			addBedLocation.setInt(5, info.getY());
			addBedLocation.setInt(6, info.getZ());
			addBedLocation.execute();
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to addBedLocation, exception:", e);
		}
	}

	public List<BedLocation> getAllBedLocations() {
		List<BedLocation> beds = new ArrayList<BedLocation>();
		try (Connection connection = db.getConnection();
				PreparedStatement getAllBedLocation = connection.prepareStatement(DatabaseManager.getAllBedLocation);) {
			ResultSet set = getAllBedLocation.executeQuery();
			while (set.next()) {
				UUID uuid = UUID.fromString(set.getString("uuid"));
				String server = set.getString("server");
				String world_name = set.getString("world_name");
				int x = set.getInt("x");
				int y = set.getInt("y");
				int z = set.getInt("z");
				TeleportInfo info = new TeleportInfo(world_name, server, x, y, z);
				BedLocation bed = new BedLocation(uuid, info);
				beds.add(bed);
			}
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to getAllBedLocations, exception:", e);
		}
		return beds;
	}

	public void removeBed(UUID uuid) {
		try (Connection connection = db.getConnection();
				PreparedStatement removeBedLocation = connection.prepareStatement(DatabaseManager.removeBedLocation);) {
			removeBedLocation.setString(1, uuid.toString());
			removeBedLocation.execute();
		} catch (SQLException e) {
			logger.log(Level.SEVERE, "Failed to removeBed for {0}", uuid);
			logger.log(Level.SEVERE, "Failed to removeBed, exception:", e);
		}
	}

	private void shortTrace() {
		StackTraceElement[] ste = Thread.currentThread().getStackTrace();
		if (ste.length < 2) return;

		StringBuffer sb = new StringBuffer();
		sb.append(ste[1].toString());
		for (int i = 2; i < Math.min(5, ste.length); i++) {
			sb.append("\n").append(ste[i].toString());
		}

		logger.log(Level.INFO, "Short Traceback: \n {0}", sb);
	}


	public class PriorityInfo {
		private String server;
		private int populationCap;

		public PriorityInfo(String server, int populationCap) {
			this.server = server;
			this.populationCap = populationCap;
		}

		public String getServer() { return this.server; }
		public int getPopulationCap() { return this.populationCap; }
	}
} public class CombatTagManager {

	private NpcPlayerHelper combatTagPlusApi;
	private boolean combatTagPlusEnabled = false;
	private TagManager tagManager;
	
	public CombatTagManager(Server server) {
		if (server.getPluginManager().getPlugin("CombatTagPlus") != null){
			combatTagPlusApi = ((CombatTagPlus) server.getPluginManager().getPlugin("CombatTagPlus")).getNpcPlayerHelper();
			tagManager = ((CombatTagPlus) server.getPluginManager().getPlugin("CombatTagPlus")).getTagManager();
			combatTagPlusEnabled = true;
			server.getPluginManager().registerEvents(new CombatTagListener(), BetterShardsPlugin.getInstance());
		}
	}

	public boolean isCombatTagNPC(Player player) {
		return combatTagPlusEnabled && combatTagPlusApi.isNpc(player);
	}
	
	public NpcIdentity getCombatTagNPCIdentity(Player player){
		return combatTagPlusApi.getIdentity(player);
	}
	
	public boolean isInCombatTag(Player p) {
		return combatTagPlusEnabled && tagManager != null && tagManager.isTagged(p.getUniqueId());
	}
	
	public boolean unCombatTag(Player p){
		return combatTagPlusEnabled && tagManager != null && tagManager.untag(p.getUniqueId());
	}
} public class MercuryManager {

	public static void initializeChannelsAndPing() {
		registerMercuryChannels();
		Bukkit.getScheduler().runTaskTimer(BetterShardsPlugin.getInstance(), new Runnable() {

			@Override
			public void run() {
				InetAddress address = null;
				try {
					address = InetAddress.getByName("127.0.0.1");
				} catch (UnknownHostException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				ServerListPingEvent event = new ServerListPingEvent(address, Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(),
						Bukkit.getMaxPlayers());
				Bukkit.getPluginManager().callEvent(event);
				sendPlayerCount(event.getMaxPlayers());
			}
			
		}, 100, 100);
	}

	public static void sendPortalDelete(String name) {
		MercuryAPI.sendGlobalMessage("delete|" + name, "BetterShards");
	}

	/**
	 * Used to tell mercury that a player is teleporting. 
	 * This is useful to the player arrives at the right portal.
	 * @param uuid
	 * @param p
	 */
	public static void teleportPlayer(UUID uuid, Portal p, Object ... data) {
		StringBuilder sb = new StringBuilder();
		sb.append("teleport|portal|" + uuid.toString() + "|" + p.getName());
		for(Object o:data) {
			sb.append("|");
			sb.append(o.toString());
		}
		MercuryAPI.sendMessage(p.getServerName(), sb.toString(), "BetterShards");
	}

	/**
	 * Sends the info to servers that a player needs to be teleported.
	 * @param info Use the TeleportInfo object.
	 */
	public static void teleportPlayer(String server, UUID uuid, TeleportInfo info) {
		MercuryAPI.sendMessage(server, "teleport|command|" + uuid.toString() + "|" + info , "BetterShards");
	}
	
	public static void teleportPlayer(String server, UUID playerToTeleportUUID, UUID targetPlayerUUID) {
		MercuryAPI.sendMessage(server, "teleport|command|" + playerToTeleportUUID + "|" + targetPlayerUUID , "BetterShards");
	}
	
	public static void teleportPlayer(String server, UUID playerToTeleportUUID, String x, String y, String z) {
		MercuryAPI.sendMessage(server, "teleport|command|" + playerToTeleportUUID + "|" + x + "|" + y +"|" + z , "BetterShards");
	}
	
	public static void teleportPlayer(String server, UUID playerToTeleportUUID, String x, String y, String z, String world) {
		MercuryAPI.sendMessage(server, "teleport|command|" + playerToTeleportUUID + "|" + world + "|" + x + "|" + y +"|" + z, "BetterShards");
	}

	private static void registerMercuryChannels() {
		MercuryAPI.addChannels("BetterShards");
	}

	public static void sendBedLocation(BedLocation bed) {
		String info = bed.getUUID().toString() + "|" + bed.getServer() + "|" + bed.getTeleportInfo();
		MercuryAPI.sendGlobalMessage("bed|add|" + info, "BetterShards");
	}
	
	public static void notifyRandomSpawn(String server, UUID player) {
		MercuryAPI.sendMessage(server, "teleport|randomspawn|" + player.toString(), "BetterShards");
	}

	public static void removeBedLocation(BedLocation bed) {
		MercuryAPI.sendGlobalMessage("bed|remove|" + bed.getUUID().toString(), "BetterShards");
	}

	public static void sendBungeeUpdateMessage() {
		// Remove Server is just the message being sent.
		// Bungee ends up calling the db to get all the update tables.
		MercuryAPI.sendGlobalMessage("removeServer", "BetterShards");
	}
	
	public static void sendPortalJoin(Portal main, Portal con) {
		MercuryAPI.sendGlobalMessage("portal|connect|"+main.getName()+"|"+con.getName(), "BetterShards");
	}
	
	public static void removePortalJoin(Portal main) {
		MercuryAPI.sendGlobalMessage("portal|remove|"+main.getName(), "BetterShards");
	}
	
	public static void sendPlayerCount(int count) {
		MercuryAPI.sendGlobalMessage("count|" + count, "BetterShards");
	}
	
	/**
	 * Called after a player successfully arrived
	 */
	public static void notifyOfArrival(UUID player) {
		MercuryAPI.sendGlobalMessage("arrival|" + player.toString(), "BetterShards");
	}
	
	/**
	 * Called when a player is sent to another server, so the target server prepares and put the player in arrival transit
	 */
	public static void warnOfArrival(UUID player, String targetServer) {
		MercuryAPI.sendMessage(targetServer, "exit|" + player.toString() + "|" + BetterShardsPlugin.getCurrentServerName(), "BetterShards");
	}
} public class BetterShardsListener implements Listener{
	
	private BetterShardsPlugin plugin;
	private DatabaseManager db;
	private Config config;
	private PortalsManager pm;
	private CustomWorldNBTStorage st;
	private RandomSpawnManager rs;
	
	public BetterShardsListener(){
		plugin = BetterShardsPlugin.getInstance();
		db = BetterShardsPlugin.getDatabaseManager();
		pm = BetterShardsPlugin.getPortalManager();
		config = plugin.GetConfig();
		rs = BetterShardsPlugin.getRandomSpawn();
		Bukkit.getScheduler().runTask(plugin, new Runnable() {

			@Override
			public void run() {
				st = CustomWorldNBTStorage.getWorldNBTStorage();
			}
			
		});
	}
	
	@EventHandler(priority = EventPriority.MONITOR)
	public void playerPreLoginCacheInv(AsyncPlayerPreLoginEvent event) {
		UUID uuid = event.getUniqueId();
		if (uuid != null) {
			plugin.getLogger().log(Level.FINER, "Preparing to pre-load player data: {0}", uuid);
		} else { 
			return;
		}
		if (st == null){ // Small race condition if someone logs on as soon as the server starts.
			event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, "Please try to log in again in a moment, server is not ready to accept log-ins.");
			plugin.getLogger().log(Level.INFO, "Player {0} logged on before async process was ready, skipping.", uuid);
			return;
		}
		// caching disabled due to fail
		Future<ByteArrayInputStream> soondata = db.loadPlayerDataAsync(uuid, st.getInvIdentifier(uuid), true); // wedon't use the data, but know that it caches behind the scenes.
		
		try {
			ByteArrayInputStream after = soondata.get(); // I want to _INTENTIONALLY_ delay accepting the user's login until I know for sure I've got the data loaded asynchronously.
			if (after == null) {
				plugin.getLogger().log(Level.INFO, "Pre-load for player data {0} came back empty. New player? Error?", uuid);
			} else {
				plugin.getLogger().log(Level.FINER, "Pre-load for player data {0} complete.", uuid);
			}
			
		} catch (InterruptedException | ExecutionException e) {
			plugin.getLogger().log(Level.SEVERE, "Failed to pre-load player data: {0}", uuid);
			e.printStackTrace();
		}
		
		// We do this so it fetches the cache, then when called for real
		// by our CustomWorldNBTStorage class it doesn't have to wait and server won't lock.
	}

	@CivConfig(name = "lobby", def = "false", type = CivConfigType.Bool)
	@EventHandler(priority = EventPriority.HIGHEST)
	public void playerJoinEvent(PlayerJoinEvent event){
		// Defer to next tick so join is fully complete before we unlock
		final UUID player = event.getPlayer().getUniqueId();
		Bukkit.getScheduler().runTask(plugin, new Runnable() {
			public void run() {
				//tell other server to remove player from transit
				MercuryManager.notifyOfArrival(player);
				BetterShardsPlugin.getTransitManager().notifySuccessfullArrival(player);
			}
		});
		if (config.get("lobby").getBool()) {
		    World w = BetterShardsPlugin.getRandomSpawn().getWorld();
		    event.getPlayer().teleport(w.getSpawnLocation());
		    return;
		}
		if (!event.getPlayer().hasPlayedBefore()) {
			rs.handleFirstJoin(event.getPlayer());
			return;
		}
		Location loc = MercuryListener.getTeleportLocation(event.getPlayer().getUniqueId());
		if (loc == null)
			return;
		loc.setDirection(new Vector(loc.getBlockX() * -1, 0, loc.getBlockZ() * -1));
		pm.addArrivedPlayer(event.getPlayer());
		event.getPlayer().teleport(loc);
	}
	
	@EventHandler(priority = EventPriority.LOWEST)
	public void playerJoinedLobbyServer(AsyncPlayerPreLoginEvent event) {
		if (!config.get("lobby").getBool())
			return;
		UUID uuid = event.getUniqueId();
		if (st == null){ // Small race condition if someone logs on as soon as the server starts.
			plugin.getLogger().log(Level.INFO, "Player logged on before async process was ready, skipping.");
			return;
		}
		st.setInventoryIdentifier(uuid, InventoryIdentifier.IGNORE_INV);
	}
	
	@EventHandler(priority = EventPriority.LOWEST)
	public void playerQuitEvent(PlayerQuitEvent event) {
		Player p = event.getPlayer();
		UUID uuid = p.getUniqueId();
		db.playerQuitServer(uuid);
		TransitManager tm = BetterShardsPlugin.getTransitManager();
		if (tm.isPlayerInArrivalTransit(uuid)) {
			//quitting while in arrival transit. We dont allow the player to save his inventory in this state, but we need to keep his location
			MercuryListener.stageTeleport(uuid, p.getLocation());
		}
		else {
			if (!BetterShardsPlugin.getTransitManager().isPlayerInTransit(uuid)) {
				st.save(p, st.getInvIdentifier(uuid), true);
			}
		}
	}
	
	//without this method players are able to detect who is in their shard as 
	//the default behavior for tab completion is trying to complete the name of a player on the server
	@EventHandler(priority = EventPriority.LOWEST)
	public void playerTabComplete(PlayerChatTabCompleteEvent event) {
		Collection <String> res = event.getTabCompletions();
		res.clear();
		String lower = event.getLastToken() != null ? event.getLastToken() : "";
		for(String player : MercuryAPI.getAllPlayers()) {
			if (player.toLowerCase().startsWith(lower)) {
				res.add(player);
			}
		}
	}
	
	@EventHandler(priority = EventPriority.LOWEST)
	public void consoleStopEvent(ServerCommandEvent event) {
		if (event.getCommand().equals("stop") || event.getCommand().equals("/stop")) {
			forcePlayerSave();
		}
	}
	
	@EventHandler(priority = EventPriority.LOWEST)
	public void opStopEvent(PlayerCommandPreprocessEvent event) {
		if (event.getPlayer() != null && event.getPlayer().isOp() && (event.getMessage().equals("stop") || event.getMessage().equals("/stop") ) ) {
			forcePlayerSave();
		}
	}
	
	/**
	 * Forces all player save. Should be tied to low-priority event listeners (so they get called first) surrounding /stop.
	 **/
	private void forcePlayerSave() {
		plugin.getLogger().log(Level.INFO, "Forcing online Player save to DB.");

		Collection<Player> online = (Collection<Player>) Bukkit.getOnlinePlayers();
		for (Player p : online) {
			st.save(p, st.getInvIdentifier(p.getUniqueId()),false);
		}
	}
	
	@EventHandler(priority = EventPriority.MONITOR)
	public void playerMoveEvent(PlayerMoveEvent event) {
		Location from = event.getFrom();
        Location to = event.getTo();

        if (from.getBlockX() == to.getBlockX()
                && from.getBlockY() == to.getBlockY()
                && from.getBlockZ() == to.getBlockZ()
                && from.getWorld().equals(to.getWorld())) {
            // Player didn't move by at least one block.
            return;
        }
        Player player = event.getPlayer();
        
        Portal p = pm.getPortal(to);
        if (p == null || p.getPartnerPortal() == null) {
        	return;
        }
        if (!MercuryAPI.getAllConnectedServers().contains(p.getPartnerPortal().getServerName())) {
        	player.sendMessage(ChatColor.RED + "This server is currently offline. Please try again later");
        	return;
        }
        // We need this check incase the player just teleported inside the field.
        // We know he does not need to teleport back.
        if (pm.canTransferPlayer(player)) {
        	p.teleport(player);
        }
	}
	
	@CivConfig(name = "allow_portals_build", def = "false", type = CivConfigType.Bool)
	@EventHandler(priority = EventPriority.HIGHEST)
	public void portalCreateEvent(PlayerInteractEvent event){
		if (!config.get("allow_portals_build").getBool())
			return;
		Player p = event.getPlayer();
		Action a = event.getAction();
		if ((a != Action.RIGHT_CLICK_BLOCK ||
				a != Action.LEFT_CLICK_BLOCK) &&
				(p.getInventory().getItemInMainHand().getType() != Material.WATER_LILY) || 
				!(p.hasPermission("bettershards.build") || p.isOp()))
			return;
		Block block = event.getClickedBlock();
		if (block == null)
			return;
		
		Location loc = block.getLocation();
		Grid g = Grid.getPlayerGrid(p);
		String message = ChatColor.YELLOW + "";
		if (a == Action.LEFT_CLICK_BLOCK) {
			g.setLeftClickLocation(loc);
			message += "Your primary block selection has been set.";
		}
		else {
			g.setRightClickLocation(loc);
			message += "Your secondary block selection has been set.";
		}
		p.sendMessage(message);
		// Send fake block update.
		event.setCancelled(true);
	}
	
	@EventHandler(priority = EventPriority.NORMAL)
	public void playerRespawnEvent(PlayerRespawnEvent event) {
		if (config.get("lobby").getBool())
			return;
		final Player p = event.getPlayer();
		UUID uuid = p.getUniqueId();
		final BedLocation bed = BetterShardsPlugin.getBedManager().getBed(uuid);
		//if the player has no bed or an invalid bed location, we just want to randomspawn
		//him. This needs to be delayed by 1 tick, because the respawn event has to complete first
		if (bed == null || (bed.getServer() != null && bed.getServer().equals(MercuryAPI.serverName()) 
			&& Bukkit.getWorld(bed.getTeleportInfo().getWorld()) == null)) {
			Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() {
				@Override
				public void run() {
					rs.handleDeath(p);
				}
				
			});
			return;
		}
		final TeleportInfo teleportInfo = bed.getTeleportInfo();
		if(bed.getServer() != null && bed.getServer().equals(MercuryAPI.serverName())){ //Player's bed is on the current server
			event.setRespawnLocation(new Location(Bukkit.getWorld(teleportInfo.getWorld()), teleportInfo.getX(), teleportInfo.getY(), teleportInfo.getZ()));
			return;
		}
		//bed is on a different server, so we send the player there and send the according mercury message
		Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() {

			@Override
			public void run() {
				//just a badly named method, this only sends a mercury message
				MercuryManager.teleportPlayer(bed.getServer(), bed.getUUID(), teleportInfo);
				try {
					BetterShardsAPI.connectPlayer(p, bed.getServer(), PlayerChangeServerReason.BED);
				} catch (PlayerStillDeadException e) {
					e.printStackTrace();
				}
			}
			
		});
	}
	
	@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
	public void bedBreak(BlockBreakEvent event) {
		if (config.get("lobby").getBool()) {
			return;
		}
		bedBreak(event.getBlock());
	}
	
	@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
	public void bedExplode(EntityExplodeEvent e) {
		if (config.get("lobby").getBool())
			return;
		for(Block b : e.blockList()) {
			bedBreak(b);
		}
	}
	
	@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
	public void pistonDestroyBed(BlockPistonExtendEvent e) {
		if (config.get("lobby").getBool()) 
			return;
		bedBreak(e.getBlock());
		for(Block b : e.getBlocks()) {
			bedBreak(b);
		}
	}
	
	@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
	public void bedBreakExplosion(BlockExplodeEvent event) {
		if (config.get("lobby").getBool())
			return;
		for (Block b : event.blockList()) {
			bedBreak(b);
		}
	}
	
	private void bedBreak(Block b) {
		if (b == null || b.getType() != Material.BED_BLOCK) {
			return;
		}
		Block real = getRealFace(b);
		List<BedLocation> toBeRemoved = new ArrayList<BedLocation>();
		for (BedLocation bed: BetterShardsPlugin.getBedManager().getAllBeds()) {
			if (!bed.getServer().equals(MercuryAPI.serverName()))
					continue;
			TeleportInfo loc = new TeleportInfo(real.getWorld().getName(), bed.getServer(), real.getX(), real.getY(), real.getZ());
			if (bed.getTeleportInfo().equals(loc)) {
				toBeRemoved.add(bed);
			}
		}
		if (toBeRemoved.size() == 0) {
		    BetterShardsPlugin.getInstance().warning("Bed was broken at " + b.getLocation() + " but no bed was found in storage?");
		}
		for (BedLocation bed: toBeRemoved) {
			BetterShardsAPI.removeBedLocation(bed);
		}
	}
	
	private Block getRealFace(Block block) {
		if (((Bed) block.getState().getData()).isHeadOfBed())
			block = block.getRelative(((Bed) block.getState().getData()).getFacing().getOppositeFace());
		return block;
	}
	
	/**
	 * Sets the player bed location by right-clicking on a bed.
	 * This allows players to set their bed at any time of the day.
	 * Highest event priority so it ignores events canceled by citadel
	 * @param event The event args
	 */
	@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
	public void playerSleepInBed(PlayerInteractEvent event) {
		if (config.get("lobby").getBool())
			return;
		
		if (!event.getAction().equals(Action.RIGHT_CLICK_BLOCK) || !event.getClickedBlock().getType().equals(Material.BED_BLOCK)) {
			return;
		}
		
		UUID uuid = event.getPlayer().getUniqueId();
		String server = MercuryAPI.serverName();
		Location loc = getRealFace(event.getClickedBlock()).getLocation();
		TeleportInfo info = new TeleportInfo(loc.getWorld().getName(), server, loc.getBlockX(),
				loc.getBlockY(), loc.getBlockZ());
		plugin.getLogger().log(Level.INFO, "Player {0} bed location set to {1}", 
				new Object[] { uuid, info });
		BedLocation bed = new BedLocation(uuid, info);
		BetterShardsAPI.addBedLocation(uuid, bed);
		event.getPlayer().sendMessage(ChatColor.GREEN + "You set your bed location.");
	}
	
	// Start of methods to try and stop interaction when transferring.
	
	@EventHandler(priority = EventPriority.LOWEST)
	public void playerDropItem(PlayerDropItemEvent event) {
		Player player = event.getPlayer();
		if(BetterShardsPlugin.getTransitManager().isPlayerInTransit(player.getUniqueId())) {
			event.setCancelled(true);
		}
	}
	
	@EventHandler(priority = EventPriority.LOWEST)
	public void playerDamageEvent(EntityDamageEvent event) {
		if (!(event.getEntity() instanceof Player)) {
			return;
		}
		Player p = (Player) event.getEntity();
		if (BetterShardsPlugin.getTransitManager().isPlayerInTransit(p.getUniqueId())) {
			event.setCancelled(true);
		}
	}
	
	@EventHandler(priority = EventPriority.LOWEST)
	public void playerMoveEventWhenInTransit(PlayerMoveEvent event) {
		Location from = event.getFrom();
        Location to = event.getTo();

        if (from.getBlockX() == to.getBlockX()
                && from.getBlockY() == to.getBlockY()
                && from.getBlockZ() == to.getBlockZ()
                && from.getWorld().equals(to.getWorld())) {
            // Player didn't move by at least one block.
            return;
        }
		Player p = event.getPlayer();
		if (BetterShardsPlugin.getTransitManager().isPlayerInTransit(p.getUniqueId())) {
			event.setCancelled(true);
		}
	}
	
	@EventHandler
	public void inventoryClick(InventoryClickEvent event) {
		Player p = (Player) event.getWhoClicked();
		if (BetterShardsPlugin.getTransitManager().isPlayerInTransit(p.getUniqueId())) {
			event.setCancelled(true);
		}
	}
	
	@EventHandler(priority = EventPriority.LOWEST)
	public void playerPickupEvent(PlayerPickupItemEvent event) {
		Player p = event.getPlayer();
		if (BetterShardsPlugin.getTransitManager().isPlayerInTransit(p.getUniqueId())) {
			event.setCancelled(true);
		}
	}
	
	@EventHandler(priority = EventPriority.LOWEST)
	public void playerInteractEvent(PlayerInteractEvent event) {
		Player p = (Player) event.getPlayer();
		if (BetterShardsPlugin.getTransitManager().isPlayerInTransit(p.getUniqueId())) {
			event.setCancelled(true);
		}
	}
	
	@EventHandler(priority = EventPriority.LOWEST)
	public void blockBreakEvent(BlockBreakEvent event) {
		Player p = event.getPlayer();
		if (BetterShardsPlugin.getTransitManager().isPlayerInTransit(p.getUniqueId())) {
			event.setCancelled(true);
		}
	}
	
	@EventHandler(priority = EventPriority.LOWEST)
	public void blockPlaceEvent(BlockPlaceEvent event) {
		Player p = event.getPlayer();
		if (BetterShardsPlugin.getTransitManager().isPlayerInTransit(p.getUniqueId())) {
			event.setCancelled(true);
		}
	}
} public class MercuryListener implements Listener{
	
	private String channelName = "BetterShards";
	private PortalsManager portalManager;
	private BetterShardsPlugin plugin;
	private RandomSpawnManager randomSpawnManager;
	
	private static Map<UUID, Location> uuids = new ConcurrentHashMap<UUID, Location>();
	
	public MercuryListener() {
		portalManager = BetterShardsPlugin.getPortalManager();
		plugin = BetterShardsPlugin.getInstance();
		randomSpawnManager = BetterShardsPlugin.getRandomSpawn();
	}
	
	@EventHandler(priority = EventPriority.NORMAL)
	public void asyncPluginBroadcastMessageEvent(AsyncPluginBroadcastMessageEvent event) {
		String channel = event.getChannel();
		if (!channel.equals(this.channelName))
			return;
		String message = event.getMessage();
		final String[] content = message.split("\\|");
		if (content[0].equals("delete")) {
			Portal p = portalManager.getPortal(content[1]);
			if (p != null)
				portalManager.deletePortalLocally(p);
		}
		else if (content[0].equals("teleport")) {
			Bukkit.getScheduler().runTask(BetterShardsPlugin.getInstance(), new Runnable() {

				@Override
				public void run() {
					String action = content[1];
					UUID uuid = null;
					if (action.equals("portal")) {
						uuid = UUID.fromString(content[2]);
						String p = content[3];
						Portal portal = portalManager.getPortal(p);
						if (!portal.isOnCurrentServer())
							return;
						Location targetLoc = null;
						if (content.length > 4) {
							if (portal instanceof WorldBorderPortal) {
								double angle = Double.valueOf(content[4]);
								targetLoc = ((WorldBorderPortal) portal).calculateSpawnLocation(angle);
							}
							else if(portal instanceof CircularPortal) {
								double xScale = Double.valueOf(content[4]);
								double zScale = Double.valueOf(content[5]);
								targetLoc = ((CircularPortal) portal).calculateLocation(xScale, zScale);
							}
						}
						if (targetLoc == null) {
							targetLoc = portal.findSpawnLocation();
							//just randomize it
							plugin.getLogger().log(Level.INFO,
									"No known portal handler for {0}, sending player {1} to {2} instead",
									new Object[]{p, uuid, targetLoc});
						}
						uuids.put(uuid, targetLoc);
					}
					else if (action.equals("command")) {
						uuid = UUID.fromString(content[2]);
						Location loc = null;
						if(content.length == 4){
							Player targetPlayer = Bukkit.getPlayer(UUID.fromString(content[3]));
							loc = targetPlayer.getLocation();
						} else if(content.length == 6){ //use default overworld
							loc = new Location(Bukkit.getWorlds().get(0), Integer.parseInt(content[3]), Integer.parseInt(content[4]), Integer.parseInt(content[5]));
						} else if(content.length == 7){
							loc = new Location(Bukkit.getWorld(content[3]), Integer.parseInt(content[4]), Integer.parseInt(content[5]), Integer.parseInt(content[6]));
						}
						uuids.put(uuid, loc);
					}
					else if (action.equals("randomspawn")) {
						Location loc = BetterShardsPlugin.getRandomSpawn().getLocation();
						uuid =UUID.fromString(content[2]);		
						uuids.put(uuid, loc);
					}
					else if (action.equals("connect")){
						Player player = Bukkit.getPlayer(UUID.fromString(content[2]));
						if(player != null){
							try {
								BetterShardsAPI.connectPlayer(player, content[3], PlayerChangeServerReason.valueOf(content[4]));
							} catch (PlayerStillDeadException e) {
								// TODO Auto-generated catch block
								e.printStackTrace();
							}
							plugin.getLogger().log(Level.INFO, "Connect received from Mercury, letting randomspawn happen while waiting for connect for {0}",
									content[2]);
						}
					}
					
					Player p = Bukkit.getPlayer(uuid);
					if (p != null){
						Location loc = getTeleportLocation(uuid);
						plugin.getLogger().log(Level.INFO, "Sending player {0} to location: {1}", new Object[] { uuid, loc });
						p.teleport(loc);
					}
				}
				
			});
		}
		else if (content[0].equals("bed")) { 
			if (content[1].equals("add")) {
				UUID uuid = UUID.fromString(content[2]);
				String server = content[3];
				TeleportInfo info = new TeleportInfo(content[4], server, Integer.parseInt(content[5]), Integer.parseInt(content[6]),
						Integer.parseInt(content[7]));
				BedLocation bed = new BedLocation(uuid, info);
				BetterShardsPlugin.getBedManager().addBedLocation(uuid, bed);
			}
			else if (content[1].equals("remove")) {
				UUID uuid = UUID.fromString(content[2]);
				BetterShardsPlugin.getBedManager().removeBed(uuid);
			}
		}
		else if (content[0].equals("portal")) {
			if (content[1].equals("connect")) {
				Portal p1 = portalManager.getPortal(content[2]);
				Portal p2 = portalManager.getPortal(content[3]);
				if (p1 == null || p2 == null)
					return;
				p1.setPartnerPortal(p2);
			}
			else if (content[1].equals("remove")) {
				Portal p1 = portalManager.getPortal(content[2]);
				if (p1 == null)
					return;
				p1.setPartnerPortal(null);
			}
		}
		else if (content[0].equals("info")) {
			if (content[1].equals("randomspawn")) {
				if (content[2].equals("disable")) {
					randomSpawnManager.setFirstJoin(true);
				}
			}
		}
		else if (content [0].equals("arrival")) {
			UUID uuid = UUID.fromString(content [1]);
			BetterShardsPlugin.getTransitManager().notifySuccessfullExit(uuid);
		}
		else if (content [0].equals("exit")) {
			UUID uuid = UUID.fromString(content [1]);
			String server = content [2];
			BetterShardsPlugin.getTransitManager().addPlayerToArrivalTransit(uuid, server);
		}
	}
	
	public static void stageTeleport(UUID uuid, Location loc){
		uuids.put(uuid, loc);
	}
	
	public static Location getTeleportLocation(UUID uuid) {
		Location loc = uuids.get(uuid);
		uuids.remove(uuid);
		return loc;
	}
	
	public static Map<UUID, Location> getAllRemainingTeleports() {
		return uuids;
	}
} public class BedManager {

	private DatabaseManager db;
	private Map<UUID, BedLocation> beds;

	public BedManager() {
		db = BetterShardsPlugin.getDatabaseManager();
		beds = new HashMap<UUID, BedLocation>();
		loadAllBeds();
	}

	/**
	 * Loads all bed locations from the database into the cache
	 */
	private void loadAllBeds() {
		List<BedLocation> db_beds = db.getAllBedLocations();
		for (BedLocation bed : db_beds) {
			beds.put(bed.getUUID(), bed);
		}
	}

	/**
	 * Adds the given bed location to the cache
	 * 
	 * @param uuid
	 *            The UUID of the player
	 * @param bed
	 *            The BedLocation object
	 */
	public void addBedLocation(UUID uuid, BedLocation bed) {
		beds.put(uuid, bed);
	}

	/**
	 * Gets the current bed location of the player from the cache
	 * 
	 * @param uuid
	 *            The uuid of the player.
	 * @return BedLocation of the player or null if none exists
	 */
	public BedLocation getBed(UUID uuid) {
		return beds.get(uuid);
	}

	/**
	 * Removes the given players bed location from the cache, if he has one
	 * 
	 * @param uuid
	 *            The UUID of the player.
	 */
	public void removeBed(UUID uuid) {
		beds.remove(uuid);
	}

	/**
	 * @return All bed locations currently held in the cache
	 */
	public Collection<BedLocation> getAllBeds() {
		return beds.values();
	}

} public class ConnectionManager {
	
	private TransitManager transitManager;
	private CombatTagManager combatManager;
	
	public ConnectionManager() {
		this.transitManager = BetterShardsPlugin.getTransitManager();
		this.combatManager = BetterShardsPlugin.getCombatTagManager();
	}
	
	/**
	 * Sends a bungee request to get the player to be sent to current server.
	 * There must be at least one player on the server for this to work.
	 * @param name Name of a player.
	 */
	public void teleportOtherServerPlayer(String name) {
		ByteArrayDataOutput out = ByteStreams.newDataOutput();
		out.writeUTF("ConnectOther");
		out.writeUTF(name);
		out.writeUTF(MercuryAPI.serverName());
		Bukkit.getOnlinePlayers().iterator().next().sendPluginMessage(BetterShardsPlugin.getInstance(), "BungeeCord", out.toByteArray());
	}
	
	public boolean teleportPlayerToServer(UUID playerUUID, String server, PlayerChangeServerReason reason) throws PlayerStillDeadException {
		//Get the server of the player
		String currentServer = MercuryAPI.getServerforAccount(playerUUID).getServerName();
		if(currentServer == null){
			return false;
		}
		
		if(currentServer.equals(MercuryAPI.serverName())){
			teleportPlayerToServer(Bukkit.getPlayer(playerUUID), server, reason);
			return true;
		}
		
		MercuryAPI.sendMessage(currentServer, "teleport|connect|" + playerUUID + "|" + server + "|" + reason, "BetterShards");
		return true;
	}
	
	/**
	 * Teleports a player to a specific server.
	 * @param p The Player to teleport.
	 * @param server The server to teleport the player to.
	 */
	public boolean teleportPlayerToServer(Player p, String server, PlayerChangeServerReason reason) throws PlayerStillDeadException {
		if (transitManager.isPlayerInTransit(p.getUniqueId())) { // Somehow this got triggered twice for one reason or another
				return false; // We dont wan't to continue twice because it could cause issues with the db.
		}
		PlayerChangeServerEvent event = new PlayerChangeServerEvent(reason, p.getUniqueId(), server);
		Bukkit.getPluginManager().callEvent(event);
		if (event.isCancelled()) {
			return false;
		}
		if (combatManager.isCombatTagNPC(p)) {
			return false;
		}
		combatManager.unCombatTag(p);
		if (p.isInsideVehicle()) {
			BetterShardsPlugin.getInstance().getLogger().log(Level.INFO, "During BetterShards teleport, removing player {0} from vehicle", p.getUniqueId());
			p.getVehicle().eject();
		}
		if (p.isDead()) {
			throw new PlayerStillDeadException();
		}
		transitManager.addPlayerToExitTransit(p.getUniqueId(), server); // So the player isn't tried to be sent twice.
		MercuryManager.warnOfArrival(p.getUniqueId(), server); //so target server prepares
		CustomWorldNBTStorage st = CustomWorldNBTStorage.getWorldNBTStorage();
		st.save(p, st.getInvIdentifier(p.getUniqueId()), true);
		ByteArrayDataOutput out = ByteStreams.newDataOutput();
		out.writeUTF("Connect");
		out.writeUTF(server);
		p.sendPluginMessage(BetterShardsPlugin.getInstance(), "BungeeCord", out.toByteArray());
		return true;
	}
} public class PortalsManager {

	private DatabaseManager db = BetterShardsPlugin.getDatabaseManager();
	private Map<String, Portal> portals;
	private Map<UUID, Long> arrivedPlayers = new ConcurrentHashMap<UUID,Long>();
	private final long portalCoolDown = 10000L;  //10 seconds
	
	public PortalsManager() {
		super();
		portals = new HashMap<String, Portal>();
		registerParticleRunnable();
	}
	
	public void loadPortalsManager() {
		loadPortalsFromServer();
		removeTeleportedPlayers();
		autoSaveTimer();
	}
	
	public void createPortal(Portal portal){
		portals.put(portal.getName(), portal);
		db.addPortal(portal);
		db.addPortalData(portal, null); // At this point it won't have a connection
	}
	
	public void deletePortal(Portal portal) {
		deletePortalLocally(portal);
		db.removePortalData(portal);
		db.removePortalLoc(portal);
		MercuryManager.sendPortalDelete(portal.getName());
	}
	
	public void deletePortalLocally(Portal portal) {
		List <Portal> toRemove = new LinkedList<Portal>();
		for(Entry <String, Portal> entry : portals.entrySet()) {
			if (entry.getValue().getPartnerPortal() == portal) {
				toRemove.add(entry.getValue());
			}
		}
		for(Portal p : toRemove) {
			p.setPartnerPortal(null);
		}
		portals.remove(portal.getName());
	}
	
	/*
	 * It is possible there may be one or more portals in an area but that would
	 * be stupid of admin to do as each player can only go to one server.
	 * So instead if it just finds one valid location what ever it may be
	 * that is the portal you will get.
	 */
	public Portal getPortal(Location loc){
		for(Portal p : portals.values()) {
			if (p.isOnCurrentServer() && p.inPortal(loc)) {
				return p;
			}
		}
		return null; // Like the evil that is nothingness.
	}
	
	public Portal getPortal(String name){
		Portal p = portals.get(name);
		if (p == null)
			p = db.getPortal(name);
		if (p == null)
			return null;
		portals.put(name, p);
		return p;
	}
	
	/** 
	 * Only loads portals from this server
	 */
	public void loadPortalsFromServer(){
		List<World> worlds = Bukkit.getWorlds();
		List<Portal> portals = db.getAllPortalsByWorld(worlds.toArray(new World[worlds.size()]));
		for (Portal p: portals) {
			this.portals.put(p.getName(), p);
		}
	}
	
	private void removeTeleportedPlayers(){
		Bukkit.getScheduler().scheduleSyncRepeatingTask(BetterShardsPlugin.getInstance(), 
				new Runnable() {

					@Override
					public void run() {
						List<UUID> toRemove = new LinkedList<UUID>();
						long currTime = System.currentTimeMillis();
						for (Entry <UUID, Long> entry: arrivedPlayers.entrySet()){
							Player p = Bukkit.getPlayer(entry.getKey());
							if (p == null) {
								continue;
							}
							if (getPortal(p.getLocation()) != null) {
								//the player is still in a portal
								continue;
							}
							if ((currTime - entry.getValue()) > portalCoolDown) {
								toRemove.add(entry.getKey());
							}
						}
						for(UUID rem : toRemove) {
							arrivedPlayers.remove(rem);
						}
					}	
		}, 5, 5);
	}
	
	public boolean canTransferPlayer(Player p) {
		if (BetterShardsPlugin.getCombatTagManager().isInCombatTag(p)){
			return false;
		} else{
			return !arrivedPlayers.containsKey(p.getUniqueId());
		}
	}
	
	public void addArrivedPlayer(Player p) {
		arrivedPlayers.put(p.getUniqueId(), System.currentTimeMillis());
	}
	
	// We want it sync incase a mercury message comes through we don't want it to override the db before 
	// mercury gets a chance to update the portal.
	private void autoSaveTimer() {
		Bukkit.getScheduler().scheduleSyncRepeatingTask(BetterShardsPlugin.getInstance(), new Runnable() {

			@Override
			public void run() {
				Collection<Portal> ports = portals.values();
				for (Portal p: ports) {
					if (p.isDirty()) {
						db.updatePortalData(p);
						p.setDirty(false);
					}
				}
			}
			
		}, 500, 1000);
	}
	
	private void registerParticleRunnable() {
	    Bukkit.getScheduler().scheduleSyncRepeatingTask(BetterShardsPlugin.getInstance(), new Runnable() {
	        
	        @Override
	        public void run() {
	    	for(Portal portal : portals.values()) {
	    	    if (!portal.isOnCurrentServer() || portal.getPartnerPortal() == null) {
	    	    	continue;
	    	    }
	    	    for(Player p : Bukkit.getOnlinePlayers()) {
	    	    	portal.showParticles(p);
	    	    }
	    	}
	        }
	    }, 4L, 4L);
	}
} public class RandomSpawnManager {
	private DatabaseManager dbm;
	private int spawnRange;
	private World w;
	private boolean disableFirstJoin = false;
	private List <Material> ignoreMaterials;

	
	public RandomSpawnManager() {
		Config config = BetterShardsPlugin.getInstance().GetConfig();
		this.spawnRange = config.get("randomspawn.range").getInt();
		String worldName = config.get("randomspawn.spawnworld").getString();
		this.w = BetterShardsPlugin.getInstance().getServer().getWorld(worldName);
		if (this.w == null) {
			BetterShardsPlugin.getInstance().severe("Could not find world " + worldName + " for random spawning. Defaulting to standard world");
			this.w = Bukkit.getWorlds().get(0);
		}
		List <String> ignoreMats = config.get("randomspawn.ignoreMaterials").getStringList();
		this.ignoreMaterials = new ArrayList<Material>();
		for(String ign : ignoreMats) {
		    try {
		    	Material m = Material.valueOf(ign);
		    	BetterShardsPlugin.getInstance().info("Ignoring " + m.toString() + " for random spawning");
		    	ignoreMaterials.add(m);
		    } catch (IllegalArgumentException e) {
		    	BetterShardsPlugin.getInstance().warning("The randomspawn ignore material specified as " + ign + " is not valid. It was ignored");
		    }
		}
		dbm = BetterShardsPlugin.getDatabaseManager();
	}

	/**
	 * Called whenever a player dies without having a bed to determine on which
	 * server he randomspawns, to get the player to that server and to ensure
	 * that the player is randomspawned on that server
	 * 
	 * @param p Player who just died
	 */
	@CivConfigs({
		@CivConfig(name = "randomspawn.range", def = "1000", type = CivConfigType.Int),
		@CivConfig(name = "randomspawn.spawnworld", def = "world", type = CivConfigType.String),
		@CivConfig(name = "randomspawn.ignoreMaterials", type = CivConfigType.String_List)
	})
	public void handleDeath(Player p) {
		List<String> servers = getAllowedServers();
		if (servers.isEmpty()) {
			p.teleport(getLocation());
			return;
		}
		Map<String, DatabaseManager.PriorityInfo> priorityServers = dbm.getPriorityServers();
		int serverIndex = -1;
		if (!priorityServers.isEmpty()) {
			List<String> rndPriorityServers = new ArrayList <String> (priorityServers.keySet());
			Collections.shuffle(rndPriorityServers);
			for (String rndServer : rndPriorityServers) {
				int currentPopulation = MercuryAPI.getAllAccountsByServer(rndServer).size();
				if (rndServer.equals(MercuryAPI.serverName()))
					currentPopulation--;
				if (currentPopulation < priorityServers.get(rndServer).getPopulationCap()) {
					serverIndex = servers.indexOf(rndServer);
					break;
				}
			}
		}
		if (serverIndex < 0) {
			serverIndex = (int) (Math.random() * servers.size());
		}
		if (servers.get(serverIndex).equalsIgnoreCase(MercuryAPI.serverName())) {
			// same server
			p.teleport(getLocation());
		} else {
			try {
				BetterShardsAPI.connectPlayer(p, servers.get(serverIndex),
						PlayerChangeServerReason.RANDOMSPAWN);
			} catch (PlayerStillDeadException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			MercuryManager.notifyRandomSpawn(servers.get(serverIndex), p.getUniqueId());
		}
	}
	
	/**
	 * Sets whether or not to disable or enable randomspawn for players who have just joined.
	 * @param disabled Set this to true to disable randomspawn for players who are new
	 * and set to false to random spawn players who are new.
	 */
	public void setFirstJoin(boolean disabled) {
		this.disableFirstJoin = disabled;
	}
	
	/**
	 * Called when it is the first time a player joins.
	 * @param p Player who just joined.
	 */
	public void handleFirstJoin(Player p) {
		if (disableFirstJoin)
			return;
		final Player player = p;
		Bukkit.getScheduler().scheduleSyncDelayedTask(BetterShardsPlugin.getInstance(), new Runnable() {
			@Override
			public void run() {
				handleDeath(player);
			}
		});
	}

	/**
	 * @return names of all the servers players are allowed to randomspawn in
	 */
	public List<String> getAllowedServers() {
		List<String> servers = new LinkedList<String>();
		servers.add(MercuryAPI.serverName());
		for (String s : MercuryAPI.getAllConnectedServers()) {
			servers.add(s);
		}
		for (String s : dbm.getAllExclude()) {
			servers.remove(s);
		}
		return servers;
	}
	
	/**
	 * Gets a random spawn location on this server in the world which was
	 * specified as spawn world in the config
	 * 
	 * @return random spawn location
	 */
	public Location getLocation() {
	    return getLocation(0);
	}

	/**
	 * Attempts to find a valid spawn location recursively. This method will not spawn in caves initially, 
	 * but once it has failed to find a valid spawning spot 10 times, it might
	 * 
	 * @param depth How often a valid spawning spot was attempted to be found recursively
	 * @return valid spawning spot
	 */
	private Location getLocation(int depth) {
	    
		int currentDepth = depth + 1;
		int x = (int) (spawnRange * Math.random());
		x = x * (Math.random() > 0.5 ? 1 : -1);
		int z = (int) (spawnRange * Math.random());
		z = z * (Math.random() > 0.5 ? 1 : -1);

		if (Math.sqrt((double) ((x * x) + (z * z))) > spawnRange) {
			// the location is outside the circle, even though x and z are in
			// range, so we try again
			return getLocation(currentDepth);
		}
		int y = 253;

		while (y >= 0) {
			if (w.getBlockAt(x, y, z).getType() != Material.AIR) {
				if (w.getBlockAt(x, y, z).getType().isSolid() && !ignoreMaterials.contains(w.getBlockAt(x, y, z).getType())
						&& w.getBlockAt(x, y + 1, z).getType() == Material.AIR
						&& w.getBlockAt(x, y + 2, z).getType() == Material.AIR) {
					return centerLocation(new Location(w, x, y + 1, z)); //+1 because player position is in lower body half
				}
				else {
				    if (currentDepth <= 10) {
					return getLocation(currentDepth);
				    }
				}
			}
			y--;
		}
		return getLocation(currentDepth);
	}
	
	/**
	 * When using flat block coordinates to spawn players, they will always spawn right on the edge of a block or possibly inside an adjacent block
	 * To fix this, this method will increase x and z by 0.5 to center the spawn point on the chosen block
	 * @param loc Location to adjust
	 * @return New adjusted location
	 */
	public static Location centerLocation(Location loc) {
	    return new Location(loc.getWorld(), loc.getX() + 0.5, loc.getY(), loc.getZ() + 0.5);
	}
	
	public World getWorld() {
	    return w;
	}

} public class TransitManager {

	private Map<UUID, String> exitTransit;
	private Map<UUID, String> arrivalTransit;
	private Map<UUID, Long> lastTransit;

	public TransitManager() {
		exitTransit = new ConcurrentHashMap<UUID, String>();
		arrivalTransit = new ConcurrentHashMap<UUID, String>();
		lastTransit = new ConcurrentHashMap<UUID, Long>();
	}

	/**
	 * Checks if a player is in any transit.
	 */
	public boolean isPlayerInTransit(UUID uuid) {
		return isPlayerInExitTransit(uuid) || isPlayerInArrivalTransit(uuid);
	}
	
	public boolean isPlayerInExitTransit(UUID uuid) {
		return exitTransit.containsKey(uuid);
	}
	
	public boolean isPlayerInArrivalTransit(UUID uuid) {
		return arrivalTransit.containsKey(uuid);
	}

	/**
	 * This adds a player to a list that can be checked to see if a player is in
	 * transit.
	 */
	public void addPlayerToExitTransit(final UUID uuid, final String server) {
		exitTransit.put(uuid, server);
		lastTransit.put(uuid, System.currentTimeMillis());
		Bukkit.getScheduler().runTaskLater(BetterShardsPlugin.getInstance(), new Runnable() {

			@Override
			public void run() {
				if (isPlayerInExitTransit(uuid)) {
					BetterShardsPlugin.getInstance().info(
							uuid + " failed to transit to " + server + ", was removed by timeout");
					PlayerFailedToTransitEvent event = new PlayerFailedToTransitEvent(uuid, server);
					Bukkit.getPluginManager().callEvent(event);
					exitTransit.remove(uuid);
				}
			}

		}, 600); // 30 seconds timeout
	}
	
	public void addPlayerToArrivalTransit(final UUID uuid, final String server) {
		arrivalTransit.put(uuid, server);
		Bukkit.getScheduler().runTaskLater(BetterShardsPlugin.getInstance(), new Runnable() {

			@Override
			public void run() {
				if (isPlayerInArrivalTransit(uuid)) {
					BetterShardsPlugin.getInstance().info(
							uuid + " failed to transit from " + server + ", was removed by timeout");
					PlayerFailedToTransitEvent event = new PlayerFailedToTransitEvent(uuid, server);
					Bukkit.getPluginManager().callEvent(event);
					arrivalTransit.remove(uuid);
				}
			}

		}, 600); // 30 seconds timeout
	}

	/**
	 * Called by the MercuryListener when the target server of a transfer
	 * indicates that the player arrived
	 * 
	 * @param player UUID of the transferred player
	 */
	public void notifySuccessfullExit(UUID player) {
		if (isPlayerInExitTransit(player)) {
			PlayerEnsuredToTransitEvent e = new PlayerEnsuredToTransitEvent(player, exitTransit.get(player));
			Bukkit.getPluginManager().callEvent(e);
			exitTransit.remove(player);
		}
	}
	
	/**
	 * Called when a player's data is fully loaded
	 * 
	 * @param player UUID of the transferred player
	 */
	public void notifySuccessfullArrival(UUID player) {
		if (isPlayerInArrivalTransit(player)) {
			PlayerArrivedChangeServerEvent event = new PlayerArrivedChangeServerEvent(Bukkit.getPlayer(player), arrivalTransit.get(player));
			Bukkit.getPluginManager().callEvent(event);
			arrivalTransit.remove(player);
		}
	}
	
	/**
	 * Gets a unix timestamp of when the player last left this shard to connect to another. This is reset
	 * on server restart and it might be null if the player hasnt exited from this shard during the current
	 * runtime yet
	 * 
	 * @param uuid Player to check for
	 * @return Unix timestamp of last shard switch from this shard or null if none was found
	 */
	public Long getLastTransit(UUID uuid) {
		return lastTransit.get(uuid);
	}
	
	/**
	 * Checks whether the given player has transferred to another shard from this one within the given timeframe
	 * @param player Player to check for
	 * @param milliSeconds How long ago the transfer might have been maximum (in milliseconds)
	 * @return True if the player did transfer in that timeframe, false if not
	 */
	public boolean hasTransferredRecently(UUID player, long milliSeconds) {
		Long last = lastTransit.get(player);
		if (last == null) {
			return false;
		}
		if ((System.currentTimeMillis() - last) <= milliSeconds) {
			return true;
		}
		return false;
	}	
} public class CustomWorldNBTStorage extends ServerNBTManager {

	private DatabaseManager db;
	
	private static CustomWorldNBTStorage storage;
	private Map<UUID, InventoryIdentifier> invs = new ConcurrentHashMap<UUID, InventoryIdentifier>(); 
	private Map<UUID, ConfigurationSection> sect = new ConcurrentHashMap<UUID, ConfigurationSection>();
	private Logger logger = BetterShardsPlugin.getInstance().getLogger();

	public CustomWorldNBTStorage(File file, String s, boolean flag) {
		super(file, s, flag, new DataConverterManager(0));
		db = BetterShardsPlugin.getDatabaseManager();
		storage = this;
	}

	@Override
	public void save(EntityHuman entityhuman) {
		try {
			UUID uuid = entityhuman.getUniqueID();
			logger.log(Level.FINER, "EntityHuman]* Save player data for {0}", uuid);
			
			NBTTagCompound nbttagcompound = new NBTTagCompound();

			entityhuman.e(nbttagcompound);
			//logInventory(nbttagcompound);
			ByteArrayOutputStream output = new ByteArrayOutputStream();
			NBTCompressedStreamTools.a(nbttagcompound, output);

			// Now to run our custom mysql code
			if (!BetterShardsPlugin.getTransitManager().isPlayerInTransit(uuid)) {
				db.savePlayerData(uuid, output, getInvIdentifier(uuid), sect.get(uuid));
			}
			else {
				logger.log(Level.WARNING, "Did not save data for {0}, because he was in transit", entityhuman.getName());
			}
		} catch (Exception localException) {
			logger.log(Level.SEVERE, "EntityHuman]* Failed to save player data for {0}", entityhuman.getName());
		}
	}
	
	public void save(NBTTagCompound nbttagcompound, UUID uuid) {
		try {
			logger.log(Level.FINER, "NBTTagCompound,UUID] Save player data for {0}", uuid);

			//logInventory(nbttagcompound);
			ByteArrayOutputStream output = new ByteArrayOutputStream();
			NBTCompressedStreamTools.a(nbttagcompound, output);

			// Now to run our custom mysql code
			db.savePlayerData(uuid, output, getInvIdentifier(uuid), sect.get(uuid));
		} catch (Exception localException) {
			logger.log(Level.SEVERE, "NBTTagCompound,UUID] Failed to save player data for {0}", uuid);
		}
	}

	@Override
	public NBTTagCompound load(EntityHuman entityhuman) {
		NBTTagCompound nbttagcompound = null;
		try {
			UUID uuid = entityhuman.getUniqueID();
			logger.log(Level.FINER, "EntityHuman]* Load for {0}", uuid);

			ByteArrayInputStream input = db.loadPlayerData(uuid, getInvIdentifier(uuid));

			nbttagcompound = NBTCompressedStreamTools.a(input);
		} catch (Exception localException) {
			logger.log(Level.SEVERE, "EntityHuman]* Failed to load player data for {0}", entityhuman.getName());
		}
		
		if (nbttagcompound != null) {
			//logInventory(nbttagcompound);
			if ((entityhuman instanceof EntityPlayer)) {
				CraftPlayer player = (CraftPlayer) entityhuman.getBukkitEntity();

				long modified = player.getLastPlayed();
				if (modified < player.getFirstPlayed()) {
					player.setFirstPlayed(modified);
				}
			}
			entityhuman.f(nbttagcompound);
		} else {
			logger.log(Level.INFO, "EntityHuman]* No player data found for {0}", entityhuman.getName());
		}
		return nbttagcompound;
	}

	@Override
	public NBTTagCompound getPlayerData(String s) {
		try {
			UUID uuid = UUID.fromString(s);
			logger.log(Level.FINER, "String]* get / load for " + uuid);
			
			ByteArrayInputStream input = db.loadPlayerData(uuid, getInvIdentifier(uuid));
			NBTTagCompound nbttagcompound = NBTCompressedStreamTools.a(input);
			//logInventory(nbttagcompound);
			
			return nbttagcompound;
		} catch (Exception localException) {
			logger.log(Level.SEVERE, "String]* Failed to get / load player data for {0}", s);
		}
		return null;
	}

	public NBTTagCompound getPlayerData(UUID uuid) {
		try {
			logger.log(Level.FINER, "UUID] Get / Load player data for {0}", uuid);
			
			ByteArrayInputStream input = db.loadPlayerDataAsync(uuid, getInvIdentifier(uuid)).get();
			NBTTagCompound nbttagcompound = NBTCompressedStreamTools.a(input);
			//logInventory(nbttagcompound);
			
			return nbttagcompound;
		} catch (Exception localException) {
			logger.log(Level.SEVERE, "UUID] Failed to get / load player data for {0}", uuid);
			logger.log(Level.SEVERE, "UUID] Failed to get / load player data exception:", localException);
		}
		return null;
	}
	
	public void load(Player p, InventoryIdentifier iden) {
		UUID uuid = p.getUniqueId();
		logger.log(Level.FINER, "Player,InvIdent] Load player data for {0}", uuid);
		
		CraftPlayer cPlayer = (CraftPlayer) p;
		
		NBTTagCompound nbttagcompound = null;
		ByteArrayInputStream input = db.loadPlayerData(uuid, iden);
		if (input != null) {
			try {
				nbttagcompound = NBTCompressedStreamTools.a(input);
			} catch (IOException e) {
				logger.log(Level.SEVERE, "Player,InvIdent] Failed to load player data for {0}", uuid);
				e.printStackTrace();
			}
		}
		//logInventory(nbttagcompound);
		if (nbttagcompound == null) {
			logger.log(Level.SEVERE, "Player,InvIdent] (Load) No player data for {0}", uuid);
		}
		cPlayer.getHandle().f(nbttagcompound); // why clear the inventory b/c load failed?
	}
	
	public void save(Player p, InventoryIdentifier iden) {
		save(p, iden, false);
	}
	
	public void save(Player p, InventoryIdentifier iden, boolean async) {
		UUID uuid = p.getUniqueId();
		logger.log(Level.FINER, "Player,InvIdent,bool] Save player data for {0}", uuid);
		
		CraftPlayer cPlayer = (CraftPlayer) p;
		
		NBTTagCompound nbttagcompound = new NBTTagCompound();
		cPlayer.getHandle().e(nbttagcompound);
		//logInventory(nbttagcompound);
		ByteArrayOutputStream output = new ByteArrayOutputStream();
		try {
			NBTCompressedStreamTools.a(nbttagcompound, output);
		} catch (IOException e) {
			logger.log(Level.SEVERE, "Player,InvIdent,bool] Failed to save player data for {0}", uuid);
			logger.log(Level.SEVERE, "Player,InvIdent,bool] Failed to save player data, exception:", e);
		}
		
		if (nbttagcompound == null || output == null) {
			logger.log(Level.SEVERE, "Player,InvIdent,bool] (Save) No player data for {0}", uuid);
		}
		
		if (async){
			db.savePlayerDataAsync(uuid, output, iden, sect.get(uuid));
		} else {
			db.savePlayerData(uuid, output, iden, sect.get(uuid));
		}
	}
	
	public void save(UUID uuid, NBTTagCompound nbttagcompound, InventoryIdentifier iden) {
		save(uuid, nbttagcompound, iden, false);
	}
	
	public void save(UUID uuid, NBTTagCompound nbttagcompound, InventoryIdentifier iden, boolean async) {
		logger.log(Level.FINER, "UUID,NBTTagCompound,InvIdent,bool] Save player data for {0}", uuid);
		
		//logInventory(nbttagcompound);
		ByteArrayOutputStream output = new ByteArrayOutputStream();
		try {
			NBTCompressedStreamTools.a(nbttagcompound, output);
		} catch (IOException e) {
			logger.log(Level.SEVERE, "UUID,NBTTagCompound,InvIdent,bool] Failed to save player data for {0}", uuid);
			logger.log(Level.SEVERE, "UUID,NBTTagCompound,InvIdent,bool] Failed to save player data, exception: ", e);
		}

		if (nbttagcompound == null || output == null) {
			logger.log(Level.SEVERE, "UUID,NBTTagCompound,InvIdent,bool] (Save) No player data for {0}", uuid);
		}
		
		if (async) {
			db.savePlayerDataAsync(uuid, output, iden, sect.get(uuid));
		} else {
			db.savePlayerData(uuid, output, iden, sect.get(uuid));
		}
	}

	public static CustomWorldNBTStorage getWorldNBTStorage() {
		return storage;
	}
	
	public InventoryIdentifier getInvIdentifier(UUID uuid) {
		if (!invs.containsKey(uuid))
			invs.put(uuid, InventoryIdentifier.MAIN_INV);
		return invs.get(uuid);
	}
	
	public void loadConfigurationSectionForPlayer(UUID uuid, ConfigurationSection section) {
		sect.put(uuid, section);
	}
	
	/** 
	 * Gets a ConfigurationSection that allows other plugins to store data that will be saved in the 
	 * PlayerNBT Storage. Keep in mind the ConfigurationSection returned will be autosaved on save.
	 * If there is a previous ConfigurationSection this plugin will load it and return it.
	 * @param uuid The uuid of the player.
	 * @param plugin The name of the plugin for tracking purposes.
	 * @return Returns a ConfigurationSection that can be used by a plugin.  
	 */
	public ConfigurationSection getConfigurationSection(UUID uuid, JavaPlugin plugin) {
		ConfigurationSection configSect = sect.get(uuid);
		if (configSect == null) {
			configSect = new YamlConfiguration();
			sect.put(uuid, configSect);
		}
		ConfigurationSection pluginSect = configSect.getConfigurationSection(plugin.getName());
		if (pluginSect == null) {
			return configSect.createSection(plugin.getName());
		}
		return pluginSect;
	}
	
	/**
	 * Sets the InventoryIdentifier of a player while logging in.
	 * It is very important that you call this method during the 
	 * AsyncPreLoginEvent with an event priority below Monitor to ensure
	 * that the inventory you want is loaded.
	 * Otherwise just call load(Player, InventoryIdentifier) to load your inv.
	 * You need to remember to reset InventoryIdentifier back to normal load if
	 * you wish for to be set back to that.  It will not do that on its own.
	 * @param uuid The uuid of the player.
	 * @param iden The Inventory you wan't to load for a player.
	 */
	public void setInventoryIdentifier(UUID uuid, InventoryIdentifier iden) {
		invs.put(uuid, iden);
	}
	
	/**
	 * Only called when enabling the plugin to replace minecraft's internal player data handling with BetterShards'
	 */
	public static void setWorldNBTStorage() {
		for (World w: Bukkit.getWorlds()) {
			WorldServer nmsWorld = ((CraftWorld) w).getHandle();
			Field fieldName;
			try {
				fieldName = net.minecraft.server.v1_10_R1.World.class.getDeclaredField("dataManager");
				fieldName.setAccessible(true);
				
				IDataManager manager = nmsWorld.getDataManager();
				
				// Spigot has a file lock we want to try remove before invoking our own stuff.
				WorldNBTStorage nbtManager = ((WorldNBTStorage) manager);
				
				try {
					Field f = nbtManager.getClass().getSuperclass().getDeclaredField("sessionLock");
					f.setAccessible(true);
					FileLock sessionLock = (FileLock) f.get(nbtManager);
					sessionLock.close();
				} catch (Exception e) {
				}
				
				CustomWorldNBTStorage newStorage = new CustomWorldNBTStorage(manager.getDirectory(), "", true);
				setFinalStatic(fieldName, newStorage, nmsWorld);
				
				MinecraftServer.getServer().getPlayerList().playerFileData = newStorage;
			} catch (NoSuchFieldException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (SecurityException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	private static void setFinalStatic(Field field, Object newValue, Object obj) {
		try {
			field.setAccessible(true);

			// remove final modifier from field
			Field modifiersField;
			modifiersField = Field.class.getDeclaredField("modifiers");
			modifiersField.setAccessible(true);
			modifiersField
					.setInt(field, field.getModifiers() & ~Modifier.PROTECTED);
			
			field.set(obj, newValue);
		} catch (NoSuchFieldException e) {
			e.printStackTrace();
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}
	}
	
	@CivConfig(name = "import_playerdata", def = "false", type = CivConfigType.Bool)
	public static void uploadExistingPlayers() {
		if (!BetterShardsPlugin.getInstance().GetConfig().get("import_playerdata").getBool()) {
			return;
		}
		for (World w: Bukkit.getWorlds()) {
			WorldServer nmsWorld = ((CraftWorld) w).getHandle();
			IDataManager data = nmsWorld.getDataManager();
			String[] names = data.getPlayerFileData().getSeenPlayers();
			for (String name: names) {
				Bukkit.getLogger().log(Level.INFO, "Updating player " + name + " to mysql.");
				try {
					File file = new File(data.getDirectory() + File.separator + "playerdata", name + ".dat");
					if (!file.exists())
						continue;
					InputStream stream = new FileInputStream(file);
					
					ByteArrayOutputStream output = new ByteArrayOutputStream();
					output.write(IOUtils.toByteArray(stream));
					// Now to run our custom mysql code
					UUID uuid = UUID.fromString(name);
					CustomWorldNBTStorage storage = getWorldNBTStorage();
					BetterShardsPlugin.getDatabaseManager().savePlayerDataAsync(uuid, output, storage.getInvIdentifier(uuid), new YamlConfiguration());
					file.delete();
					stream.close();
				} catch (Exception localException) {
					localException.printStackTrace();
				}
			}
		}
	}
}
import org.bukkit.Bukkit;
import vg.civcraft.mc.bettershards.command.BetterCommandHandler;
import vg.civcraft.mc.bettershards.database.DatabaseManager;
import vg.civcraft.mc.bettershards.external.CombatTagManager;
import vg.civcraft.mc.bettershards.external.MercuryManager;
import vg.civcraft.mc.bettershards.listeners.BetterShardsListener;
import vg.civcraft.mc.bettershards.listeners.MercuryListener;
import vg.civcraft.mc.bettershards.manager.BedManager;
import vg.civcraft.mc.bettershards.manager.ConnectionManager;
import vg.civcraft.mc.bettershards.manager.PortalsManager;
import vg.civcraft.mc.bettershards.manager.RandomSpawnManager;
import vg.civcraft.mc.bettershards.manager.TransitManager;
import vg.civcraft.mc.bettershards.misc.CustomWorldNBTStorage;
import vg.civcraft.mc.civmodcore.ACivMod;
import vg.civcraft.mc.mercury.config.MercuryConfigManager;
package vg.civcraft.mc.bettershards;



public class BetterShardsPlugin extends ACivMod{

	private static BetterShardsPlugin instance;
	private PortalsManager pm;
	private DatabaseManager db;
	private String servName;
	private CombatTagManager combatManager;