public class Log {

	public static Logger getLogger() {
		return ShopkeepersPlugin.getInstance().getLogger();
	}

	public static void info(String message) {
		if (message == null || message.isEmpty()) return;
		getLogger().info(message);
	}

	public static void debug(String message) {
		if (Settings.debug) {
			info(message);
		}
	}

	public static void warning(String message) {
		getLogger().warning(message);
	}

	public static void severe(String message) {
		getLogger().severe(message);
	}
} public class Settings {

	public static String fileEncoding = "UTF-8";
	public static boolean debug = false;

	public static boolean disableOtherVillagers = false;
	public static boolean hireOtherVillagers = false;
	public static boolean blockVillagerSpawns = false;
	public static boolean enableSpawnVerifier = false;
	public static boolean bypassSpawnBlocking = true;
	public static boolean bypassShopInteractionBlocking = false;
	public static boolean enablePurchaseLogging = false;
	public static boolean saveInstantly = true;
	public static boolean skipCustomHeadSaving = true;

	public static boolean enableWorldGuardRestrictions = false;
	public static boolean requireWorldGuardAllowShopFlag = false;
	public static boolean enableTownyRestrictions = false;

	public static boolean createPlayerShopWithCommand = false;
	public static boolean simulateRightClickOnCommand = true;
	public static boolean requireChestRecentlyPlaced = true;
	public static boolean protectChests = true; // TODO does it make sense to not protected shop chests?
	public static boolean deleteShopkeeperOnBreakChest = false;
	public static int maxShopsPerPlayer = 0;
	public static String maxShopsPermOptions = "10,15,25";
	public static int maxChestDistance = 15;
	public static int playerShopkeeperInactiveDays = 0;
	public static boolean preventTradingWithOwnShop = true;
	public static boolean preventTradingWhileOwnerIsOnline = false;
	public static boolean useStrictItemComparison = false;
	public static boolean enableChestOptionOnPlayerShop = false;

	public static int taxRate = 0;
	public static boolean taxRoundUp = false;

	public static Material shopCreationItem = Material.MONSTER_EGG;
	public static int shopCreationItemData = 0;
	public static String shopCreationItemName = "";
	public static List<String> shopCreationItemLore = new ArrayList<String>(0);
	public static String shopCreationItemSpawnEggEntityType = "VILLAGER"; // only works above bukkit 1.11.1, ignored if
																			// empty
	public static boolean preventShopCreationItemRegularUsage = false;
	public static boolean deletingPlayerShopReturnsCreationItem = false;

	public static List<String> enabledLivingShops = Arrays.asList(
			EntityType.VILLAGER.name(),
			EntityType.COW.name(),
			EntityType.MUSHROOM_COW.name(),
			EntityType.SHEEP.name(),
			EntityType.PIG.name(),
			EntityType.CHICKEN.name(),
			EntityType.OCELOT.name(),
			EntityType.RABBIT.name(),
			EntityType.WOLF.name(),
			EntityType.SNOWMAN.name(),
			EntityType.IRON_GOLEM.name(),
			"POLAR_BEAR", // MC 1.10
			EntityType.SKELETON.name(),
			"STRAY", // MC 1.11
			"WITHER_SKELETON", // MC 1.11
			EntityType.SPIDER.name(),
			EntityType.CAVE_SPIDER.name(),
			EntityType.CREEPER.name(),
			EntityType.WITCH.name(),
			EntityType.ENDERMAN.name(),
			EntityType.ZOMBIE.name(),
			"ZOMBIE_VILLAGER", // MC 1.11
			EntityType.PIG_ZOMBIE.name(),
			"HUSK", // MC 1.11
			EntityType.GIANT.name(),
			EntityType.GHAST.name(),
			EntityType.SLIME.name(),
			EntityType.MAGMA_CUBE.name(),
			EntityType.SQUID.name(),
			"EVOKER", // MC 1.11
			"VEX", // MC 1.11
			"VINDICATOR", // MC 1.11
			"ILLUSIONER", // MC 1.12
			"PARROT" // MC 1.12
	);

	public static boolean silenceLivingShopEntities = true;

	public static boolean enableSignShops = true;
	public static boolean enableCitizenShops = false;

	public static String signShopFirstLine = "[SHOP]";
	public static boolean showNameplates = true;
	public static boolean alwaysShowNameplates = false;
	public static String nameplatePrefix = "&a";
	public static String nameRegex = "[A-Za-z0-9 ]{3,32}";
	public static boolean namingOfPlayerShopsViaItem = false;
	public static boolean allowRenamingOfPlayerNpcShops = false;

	public static String editorTitle = "Shopkeeper Editor";
	public static Material nameItem = Material.NAME_TAG;
	public static int nameItemData = 0;
	public static String nameItemName = "";
	public static List<String> nameItemLore = new ArrayList<String>(0);
	public static Material chestItem = Material.CHEST;
	public static int chestItemData = 0;
	public static Material deleteItem = Material.BONE;
	public static int deleteItemData = 0;

	public static Material hireItem = Material.EMERALD;
	public static int hireItemData = 0;
	public static String hireItemName = "";
	public static List<String> hireItemLore = new ArrayList<String>(0);
	public static int hireOtherVillagersCosts = 1;
	public static String forHireTitle = "For Hire";

	public static Material currencyItem = Material.EMERALD;
	public static short currencyItemData = 0;
	public static String currencyItemName = "";
	public static List<String> currencyItemLore = new ArrayList<String>(0);
	public static Material zeroCurrencyItem = Material.BARRIER;
	public static short zeroCurrencyItemData = 0;
	public static String zeroCurrencyItemName = "";
	public static List<String> zeroCurrencyItemLore = new ArrayList<String>(0);

	public static Material highCurrencyItem = Material.EMERALD_BLOCK;
	public static short highCurrencyItemData = 0;
	public static String highCurrencyItemName = "";
	public static List<String> highCurrencyItemLore = new ArrayList<String>(0);
	public static int highCurrencyValue = 9;
	public static int highCurrencyMinCost = 20;
	public static Material highZeroCurrencyItem = Material.BARRIER;
	public static short highZeroCurrencyItemData = 0;
	public static String highZeroCurrencyItemName = "";
	public static List<String> highZeroCurrencyItemLore = new ArrayList<String>(0);

	public static String language = "en";

	public static String msgCreationItemSelected = "&aRight-click to select the shop type.\n"
			+ "&aSneak + right-click to select the object type.\n"
			+ "&aRight-click a chest to select it.\n"
			+ "&aThen right-click a block to place the shopkeeper.";

	public static String msgButtonName = "&aSet Shop Name";
	public static List<String> msgButtonNameLore = Arrays.asList("Lets you rename", "your shopkeeper");
	public static String msgButtonChest = "&aView Chest Inventory";
	public static List<String> msgButtonChestLore = Arrays.asList("Lets you view the inventory", " your shopkeeper is using");
	public static String msgButtonType = "&aChoose Appearance";
	public static List<String> msgButtonTypeLore = Arrays.asList("Changes the look", "of your shopkeeper");
	public static String msgButtonDelete = "&4Delete";
	public static List<String> msgButtonDeleteLore = Arrays.asList("Closes and removes", "this shopkeeper");
	public static String msgButtonHire = "&aHire";
	public static List<String> msgButtonHireLore = Arrays.asList("Buy this shop");

	public static String msgTradingTitlePrefix = "&2";
	public static String msgTradingTitleDefault = "Shopkeeper";

	public static String msgSelectedNormalShop = "&aNormal shopkeeper selected (sells items to players).";
	public static String msgSelectedBookShop = "&aBook shopkeeper selected (sell books).";
	public static String msgSelectedBuyShop = "&aBuying shopkeeper selected (buys items from players).";
	public static String msgSelectedTradeShop = "&aTrading shopkeeper selected (trade items with players).";

	public static String msgSelectedLivingShop = "&aYou selected: &f{type}";
	public static String msgSelectedSignShop = "&aYou selected: &fsign shop";
	public static String msgSelectedCitizenShop = "&aYou selected: &fcitizen npc shop";

	public static String msgSelectedChest = "&aChest selected! Right click a block to place your shopkeeper.";
	public static String msgMustSelectChest = "&aYou must right-click a chest before placing your shopkeeper.";
	public static String msgChestTooFar = "&aThe shopkeeper's chest is too far away!";
	public static String msgChestNotPlaced = "&aYou must select a chest you have recently placed.";
	public static String msgTooManyShops = "&aYou have too many shops.";
	public static String msgShopCreateFail = "&aYou cannot create a shopkeeper there.";
	public static String msgTypeNewName = "&aPlease type the shop's name into the chat.\n"
			+ "  &aType a dash (-) to remove the name.";
	public static String msgNameSet = "&aThe shop's name has been set!";
	public static String msgNameInvalid = "&aThat name is not valid!";
	public static String msgUnknownShopkeeper = "&7No shopkeeper found with that name or id.";
	public static String msgUnknownPlayer = "&7No player found with that name.";
	public static String msgUnknowShopType = "&7Unknown shop type '{type}'.";
	public static String msgShopTypeDisabled = "&7The shop type '{type}' is disabled.";
	public static String msgUnknowShopObjectType = "&7Unknown shop object type '{type}'.";
	public static String msgShopObjectTypeDisabled = "&7The shop object type '{type}' is disabled.";
	public static String msgMustTargetChest = "&7You have to target a chest.";
	public static String msgUnusedChest = "&7No shopkeeper is using this chest.";
	public static String msgNotOwner = "&7You are not the owner of this shopkeeper.";
	// placeholders: {owner} -> new owners name
	public static String msgOwnerSet = "&aNew owner was set to &e{owner}";

	public static String msgTradePermSet = "&aThe shop's trading permission has been set!";
	public static String msgTradePermRemoved = "&aThe shop's trading permission has been removed!";
	public static String msgTradePermView = "&aThe shop's current trading permission is '&e{perm}&a'.";

	public static String msgMustHoldHireItem = "&7You have to hold the required hire item in your hand.";
	public static String msgSetForHire = "&aThe Shopkeeper was set for hire.";
	public static String msgHired = "&aYou have hired this shopkeeper!";
	public static String msgCantHire = "&aYou cannot afford to hire this shopkeeper.";
	// placeholders: {costs}, {hire-item}
	public static String msgVillagerForHire = "&aThe villager offered his services as a shopkeeper in exchange for &6{costs}x {hire-item}&a.";

	public static String msgMissingTradePerm = "&7You do not have the permission to trade with this shop.";
	public static String msgMissingCustomTradePerm = "&7You do not have the permission to trade with this shop.";
	public static String msgCantTradeWhileOwnerOnline = "&7You cannot trade while the owner of this shop ('{owner}') is online.";

	public static String msgPlayerShopCreated = "&aShopkeeper created!\n"
			+ "&aAdd items you want to sell to your chest, then\n"
			+ "&aright-click the shop while sneaking to modify costs.";
	public static String msgBookShopCreated = "&aShopkeeper created!\n"
			+ "&aAdd written books and blank books to your chest, then\n"
			+ "&aright-click the shop while sneaking to modify costs.";
	public static String msgBuyShopCreated = "&aShopkeeper created!\n"
			+ "&aAdd one of each item you want to buy to your chest, then\n"
			+ "&aright-click the shop while sneaking to modify costs.";
	public static String msgTradeShopCreated = "&aShopkeeper created!\n"
			+ "&aAdd items you want to sell to your chest, then\n"
			+ "&aright-click the shop while sneaking to modify costs.";
	public static String msgAdminShopCreated = "&aShopkeeper created!\n"
			+ "&aRight-click the shop while sneaking to modify trades.";

	public static String msgListAdminShopsHeader = "&9There are &e{shopsCount} &9admin shops: &e(Page {page})";
	public static String msgListPlayerShopsHeader = "&9Player '&e{player}&9' has &e{shopsCount} &9shops: &e(Page {page})";
	public static String msgListShopsEntry = "  &e{shopIndex}) &8{shopName}&r&7at &8({location})&7, type: &8{shopType}&7, object type: &8{objectType}";

	public static String msgRemovedAdminShops = "&e{shopsCount} &aadmin shops were removed.";
	public static String msgRemovedPlayerShops = "&e{shopsCount} &ashops of player '&e{player}&a' were removed.";
	public static String msgRemovedAllPlayerShops = "&aAll &e{shopsCount} &aplayer shops were removed.";

	public static String msgConfirmRemoveAdminShops = "&cYou are about to irrevocable remove all admin shops!\n"
			+ "&7Please confirm this action by typing &6/shopkeepers confirm";
	public static String msgConfirmRemoveOwnShops = "&cYou are about to irrevocable remove all your shops!\n"
			+ "&7Please confirm this action by typing &6/shopkeepers confirm";
	public static String msgConfirmRemovePlayerShops = "&cYou are about to irrevocable remove all shops of player &6{player}&c!\n"
			+ "&7Please confirm this action by typing &6/shopkeepers confirm";
	public static String msgConfirmRemoveAllPlayerShops = "&cYou are about to irrevocable remove all player shops of all players!\n"
			+ "&7Please confirm this action by typing &6/shopkeepers confirm";

	public static String msgConfirmationExpired = "&cConfirmation expired.";
	public static String msgNothingToConfirm = "&cThere is nothing to confirm currently.";

	public static String msgNoPermission = "&cYou don't have the permission to do that.";

	public static String msgHelpHeader = "&9***** &8[&6Shopkeepers Help&8] &9*****";
	public static String msgCommandHelp = "&a/shopkeepers help &8- &7Shows this help page.";
	public static String msgCommandReload = "&a/shopkeepers reload &8- &7Reloads this plugin.";
	public static String msgCommandDebug = "&a/shopkeepers debug &8- &7Toggles debug mode on and off.";
	public static String msgCommandList = "&a/shopkeepers list [player|admin] [page] &8- &7Lists all shops for the specified player, or all admin shops.";
	public static String msgCommandRemove = "&a/shopkeepers remove [player|all|admin] &8- &7Removes all shops for the specified player, all players, or all admin shops.";
	public static String msgCommandRemote = "&a/shopkeepers remote <shopName> &8- &7Remotely opens a shop.";
	public static String msgCommandTransfer = "&a/shopkeepers transfer <newOwner> &8- &7Transfers the ownership of a shop.";
	public static String msgCommandSettradeperm = "&a/shopkeepers setTradePerm <shopId> <tradePerm|-|?> &8- &7Sets, removes (-) or displays (?) the trading permission.";
	public static String msgCommandSetforhire = "&a/shopkeepers setForHire &8- &7Sets one of your shops for sale.";
	public static String msgCommandShopkeeper = "&a/shopkeepers [shop type] [object type] &8- &7Creates a shop.";

	private static String toConfigKey(String fieldName) {
		return fieldName.replaceAll("([A-Z][a-z]+)", "-$1").toLowerCase();
	}

	// returns true, if the config misses values which need to be saved
	public static boolean loadConfiguration(Configuration config) {
		boolean misses = false;
		try {
			Field[] fields = Settings.class.getDeclaredFields();
			for (Field field : fields) {
				Class<?> typeClass = field.getType();
				String configKey = toConfigKey(field.getName());

				// initialize the setting with the default value, if it is missing in the config
				if (!config.isSet(configKey)) {
					if (typeClass == Material.class) {
						config.set(configKey, ((Material) field.get(null)).name());
					} else if (typeClass == String.class) {
						config.set(configKey, Utils.decolorize((String) field.get(null)));
					} else if (typeClass == List.class && (Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0] == String.class) {
						config.set(configKey, Utils.decolorize((List<String>) field.get(null)));
					} else {
						config.set(configKey, field.get(null));
					}
					misses = true;
				}

				if (typeClass == String.class) {
					field.set(null, Utils.colorize(config.getString(configKey, (String) field.get(null))));
				} else if (typeClass == int.class) {
					field.set(null, config.getInt(configKey, field.getInt(null)));
				} else if (typeClass == short.class) {
					field.set(null, (short) config.getInt(configKey, field.getShort(null)));
				} else if (typeClass == boolean.class) {
					field.set(null, config.getBoolean(configKey, field.getBoolean(null)));
				} else if (typeClass == Material.class) {
					if (config.contains(configKey)) {
						if (config.isInt(configKey)) {
							@SuppressWarnings("deprecation")
							Material mat = Material.getMaterial(config.getInt(configKey));
							if (mat != null) {
								field.set(null, mat);
							}
						} else if (config.isString(configKey)) {
							Material mat = Material.matchMaterial(config.getString(configKey));
							if (mat != null) {
								field.set(null, mat);
							}
						}
					}
				} else if (typeClass == List.class) {
					Class<?> genericType = (Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
					if (genericType == String.class) {
						field.set(null, Utils.colorize(config.getStringList(configKey)));
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

		// validation:

		if (maxChestDistance > 50) maxChestDistance = 50;
		if (highCurrencyValue <= 0) highCurrencyItem = Material.AIR;
		// certain items cannot be of type AIR:
		if (shopCreationItem == Material.AIR) shopCreationItem = Material.MONSTER_EGG;
		if (hireItem == Material.AIR) hireItem = Material.EMERALD;
		if (currencyItem == Material.AIR) currencyItem = Material.EMERALD;

		return misses;
	}

	public static void loadLanguageConfiguration(Configuration config) {
		try {
			Field[] fields = Settings.class.getDeclaredFields();
			for (Field field : fields) {
				if (field.getType() == String.class && field.getName().startsWith("msg")) {
					String configKey = toConfigKey(field.getName());
					field.set(null, Utils.colorize(config.getString(configKey, (String) field.get(null))));
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	// item utilities:

	// creation item:
	public static ItemStack createShopCreationItem() {
		ItemStack creationItem = Utils.createItemStack(shopCreationItem, 1, (short) shopCreationItemData, shopCreationItemName, shopCreationItemLore);

		// apply spawn egg entity type:
		if (shopCreationItem == Material.MONSTER_EGG && !Utils.isEmpty(shopCreationItemSpawnEggEntityType) && NMSManager.getProvider().supportsSpawnEggEntityType()) {
			EntityType spawnEggEntityType = null;
			try {
				spawnEggEntityType = EntityType.valueOf(shopCreationItemSpawnEggEntityType);
			} catch (IllegalArgumentException e) {
				// unknown entity type, set 'empty' entity type
			}
			NMSManager.getProvider().setSpawnEggEntityType(creationItem, spawnEggEntityType);
		}

		return creationItem;
	}

	public static boolean isShopCreationItem(ItemStack item) {
		if (!Utils.isSimilar(item, Settings.shopCreationItem, (short) Settings.shopCreationItemData, Settings.shopCreationItemName, Settings.shopCreationItemLore)) {
			return false;
		}

		// check spawn egg entity type:
		if (shopCreationItem == Material.MONSTER_EGG && !Utils.isEmpty(shopCreationItemSpawnEggEntityType) && NMSManager.getProvider().supportsSpawnEggEntityType()) {
			EntityType spawnEggEntityType = NMSManager.getProvider().getSpawnEggEntityType(item); // can be null
			EntityType requiredEntityType = null;
			try {
				requiredEntityType = EntityType.valueOf(shopCreationItemSpawnEggEntityType);
			} catch (IllegalArgumentException e) {
				// unknown entity type, require 'empty' entity type
			}
			if (!Objects.equal(spawnEggEntityType, requiredEntityType)) return false;
		}

		return true;
	}

	// naming item:
	public static ItemStack createNameButtonItem() {
		return Utils.createItemStack(nameItem, 1, (short) nameItemData, msgButtonName, msgButtonNameLore);
	}

	public static boolean isNamingItem(ItemStack item) {
		return Utils.isSimilar(item, nameItem, (short) nameItemData, Settings.nameItemName, Settings.nameItemLore);
	}

	// chest button:
	public static ItemStack createChestButtonItem() {
		return Utils.createItemStack(chestItem, 1, (short) chestItemData, msgButtonChest, msgButtonChestLore);
	}

	// delete button:
	public static ItemStack createDeleteButtonItem() {
		return Utils.createItemStack(deleteItem, 1, (short) deleteItemData, msgButtonDelete, msgButtonDeleteLore);
	}

	// hire item:
	public static ItemStack createHireButtonItem() {
		return Utils.createItemStack(hireItem, 1, (short) hireItemData, msgButtonHire, msgButtonHireLore);
	}

	public static boolean isHireItem(ItemStack item) {
		return Utils.isSimilar(item, hireItem, (short) hireItemData, hireItemName, hireItemLore);
	}

	// currency item:
	public static ItemStack createCurrencyItem(int amount) {
		return Utils.createItemStack(Settings.currencyItem, amount, Settings.currencyItemData,
				Settings.currencyItemName, Settings.currencyItemLore);
	}

	public static boolean isCurrencyItem(ItemStack item) {
		return Utils.isSimilar(item, Settings.currencyItem, Settings.currencyItemData,
				Settings.currencyItemName, Settings.currencyItemLore);
	}

	// high currency item:
	public static ItemStack createHighCurrencyItem(int amount) {
		if (Settings.highCurrencyItem == Material.AIR) return null;
		return Utils.createItemStack(Settings.highCurrencyItem, amount, Settings.highCurrencyItemData,
				Settings.highCurrencyItemName, Settings.highCurrencyItemLore);
	}

	public static boolean isHighCurrencyItem(ItemStack item) {
		if (Settings.highCurrencyItem == Material.AIR) {
			return Utils.isEmpty(item);
		}
		return Utils.isSimilar(item, Settings.highCurrencyItem, Settings.highCurrencyItemData,
				Settings.highCurrencyItemName, Settings.highCurrencyItemLore);
	}

	// zero currency item:
	public static ItemStack createZeroCurrencyItem() {
		if (Settings.zeroCurrencyItem == Material.AIR) return null;
		return Utils.createItemStack(Settings.zeroCurrencyItem, 1, Settings.zeroCurrencyItemData,
				Settings.zeroCurrencyItemName, Settings.zeroCurrencyItemLore);
	}

	public static boolean isZeroCurrencyItem(ItemStack item) {
		if (Settings.zeroCurrencyItem == Material.AIR) {
			return Utils.isEmpty(item);
		}
		return Utils.isSimilar(item, Settings.zeroCurrencyItem, Settings.zeroCurrencyItemData,
				Settings.zeroCurrencyItemName, Settings.zeroCurrencyItemLore);
	}

	// high zero currency item:
	public static ItemStack createHighZeroCurrencyItem() {
		if (Settings.highZeroCurrencyItem == Material.AIR) return null;
		return Utils.createItemStack(Settings.highZeroCurrencyItem, 1, Settings.highZeroCurrencyItemData,
				Settings.highZeroCurrencyItemName, Settings.highZeroCurrencyItemLore);
	}

	public static boolean isHighZeroCurrencyItem(ItemStack item) {
		if (Settings.highZeroCurrencyItem == Material.AIR) {
			return Utils.isEmpty(item);
		}
		return Utils.isSimilar(item, Settings.highZeroCurrencyItem, Settings.highZeroCurrencyItemData,
				Settings.highZeroCurrencyItemName, Settings.highZeroCurrencyItemLore);
	}
} public abstract class Shopkeeper {

	private int sessionId;
	private UUID uniqueId;
	protected ShopObject shopObject;
	protected String worldName;
	protected int x;
	protected int y;
	protected int z;
	protected ChunkData chunkData;
	protected String name = "";

	private boolean valid = false;

	protected final Map<String, UIHandler> uiHandlers = new HashMap<String, UIHandler>();
	private boolean uiActive = true; // can be used to deactivate UIs for this shopkeeper

	/**
	 * Creates a not fully initialized shopkeeper object. Do not attempt to use this object until initialization has
	 * been finished!
	 * Only use this from inside a constructor of an extending class.
	 * Depending on how the shopkeeper was created it is required to call either
	 * {@link #initOnLoad(ConfigurationSection)} or {@link #initOnCreation(ShopCreationData)}.
	 * Afterwards it is also required to call {@link #onInitDone()}.
	 */
	protected Shopkeeper() {
	}

	/**
	 * Call this at the beginning of the constructor of an extending class, if the shopkeeper was loaded from config.
	 * This will do the required initialization and then spawn the shopkeeper.
	 * 
	 * @param config
	 *            the config section containing the shopkeeper's data
	 * @throws ShopkeeperCreateException
	 *             if the shopkeeper cannot be properly initialized or loaded
	 */
	protected void initOnLoad(ConfigurationSection config) throws ShopkeeperCreateException {
		this.load(config);
	}

	/**
	 * Call this at the beginning of the constructor of an extending class, if the shopkeeper was freshly created by a
	 * player.
	 * This will do the required initialization and then spawn the shopkeeper.
	 * 
	 * @param creationData
	 *            the shop creation data
	 * @throws ShopkeeperCreateException
	 *             if the shopkeeper cannot be properly initialized
	 */
	protected void initOnCreation(ShopCreationData creationData) throws ShopkeeperCreateException {
		Validate.notNull(creationData.spawnLocation);
		Validate.notNull(creationData.objectType);

		this.uniqueId = UUID.randomUUID();

		Location location = creationData.spawnLocation;
		this.worldName = location.getWorld().getName();
		this.x = location.getBlockX();
		this.y = location.getBlockY();
		this.z = location.getBlockZ();
		this.updateChunkData();

		this.shopObject = creationData.objectType.createObject(this, creationData);
	}

	/**
	 * Call this at the beginning of the constructor of an extending class,
	 * after either {@link #initOnLoad(ConfigurationSection)} or {@link #initOnCreation(ShopCreationData)} have been
	 * called.
	 */
	protected void onInitDone() {
		// nothing by default
	}

	/**
	 * Loads a shopkeeper's saved data from a config section of a config file.
	 * 
	 * @param config
	 *            the config section
	 * @throws ShopkeeperCreateException
	 *             if the shopkeeper cannot be properly loaded
	 */
	protected void load(ConfigurationSection config) throws ShopkeeperCreateException {
		String uniqueIdString = config.getString("uniqueId", "");
		try {
			this.uniqueId = UUID.fromString(uniqueIdString);
		} catch (IllegalArgumentException e) {
			if (!uniqueIdString.isEmpty()) {
				Log.warning("Invalid shop uuid '" + uniqueIdString + "'. Creating a new one.");
			}
			this.uniqueId = UUID.randomUUID();
		}

		this.name = Utils.colorize(config.getString("name", ""));
		this.worldName = config.getString("world");
		this.x = config.getInt("x");
		this.y = config.getInt("y");
		this.z = config.getInt("z");
		this.updateChunkData();

		ShopObjectType objectType = ShopkeepersPlugin.getInstance().getShopObjectTypeRegistry().get(config.getString("object"));
		if (objectType == null) {
			// use default shop object type:
			objectType = ShopkeepersPlugin.getInstance().getDefaultShopObjectType();
			Log.warning("Invalid object type '" + config.getString("object") + "' for shopkeeper '" + uniqueId
					+ "'. Did you edit the save file? Switching to default type '" + objectType.getIdentifier() + "'.");
		}
		this.shopObject = objectType.createObject(this, new ShopCreationData()); // dummy ShopCreationData
		this.shopObject.load(config);
	}

	/**
	 * Saves the shopkeeper's data to the specified configuration section.
	 * 
	 * @param config
	 *            the config section
	 */
	protected void save(ConfigurationSection config) {
		config.set("uniqueId", uniqueId.toString());
		config.set("name", Utils.decolorize(name));
		config.set("world", worldName);
		config.set("x", x);
		config.set("y", y);
		config.set("z", z);
		config.set("type", this.getType().getIdentifier());
		shopObject.save(config);
	}

	/**
	 * Gets the shop's unique id.
	 * 
	 * <p>
	 * This id is meant to be unique and never change.
	 * </p>
	 * 
	 * @return the shop's unique id
	 */
	public UUID getUniqueId() {
		return uniqueId;
	}

	/**
	 * Gets the shop's session id.
	 * 
	 * <p>
	 * This id is unique across all currently loaded shops, but may change across server restarts or when the shops are
	 * getting reloaded.<br>
	 * To reliable identify a shop use {@link #getUniqueId()} instead.
	 * </p>
	 * 
	 * @return the shop's session id
	 */
	public int getSessionId() {
		return sessionId;
	}

	/**
	 * Gets the type of this shopkeeper (ex: admin, normal player, book player, buying player, trading player, etc.).
	 * 
	 * @return the shopkeeper type
	 */
	public abstract ShopType<?> getType();

	public ShopObject getShopObject() {
		return shopObject;
	}

	protected void onChunkLoad() {
		shopObject.onChunkLoad();
	}

	protected void onChunkUnload() {
		shopObject.onChunkUnload();
	}

	/**
	 * Spawns the shopkeeper into the world at its spawn location and overwrites it's AI.
	 */
	public boolean spawn() {
		return shopObject.spawn();
	}

	/**
	 * Whether or not this shopkeeper needs to be spawned and despawned with chunk load and unloads.
	 * 
	 * @return
	 */
	public boolean needsSpawning() {
		return shopObject.getObjectType().needsSpawning();
	}

	/**
	 * Checks if the shopkeeper is active (is present in the world).
	 * 
	 * @return whether the shopkeeper is active
	 */
	public boolean isActive() {
		return shopObject.isActive();
	}

	/**
	 * See {@link ShopObject#check()}.
	 * 
	 * @return whether to update this shopkeeper in the activeShopkeepers collection
	 */
	public boolean check() {
		return shopObject.check();
	}

	/**
	 * Removes this shopkeeper from the world.
	 */
	public void despawn() {
		shopObject.despawn();
	}

	/**
	 * Persistently removes this shopkeeper.
	 */
	public void delete() {
		ShopkeepersPlugin.getInstance().deleteShopkeeper(this);
	}

	protected void onDeletion() {
		shopObject.delete();
		valid = false;
	}

	/**
	 * The shopkeepers gets invalid, when he was deleted.
	 * 
	 * @return <code>true</code> if not deleted
	 */
	public boolean isValid() {
		return valid;
	}

	protected void onRegistration(int sessionId) {
		assert !valid;
		this.sessionId = sessionId;
		valid = true;
	}

	/**
	 * Gets the ChunkData identifying the chunk this shopkeeper spawns in.
	 * 
	 * @return the chunk information
	 */
	public ChunkData getChunkData() {
		return chunkData;
	}

	public String getPositionString() {
		return Utils.getLocationString(worldName, x, y, z);
	}

	public Location getActualLocation() {
		return shopObject.getActualLocation();
	}

	/**
	 * Gets the name of the world this shopkeeper lives in.
	 * 
	 * @return the world name
	 */
	public String getWorldName() {
		return worldName;
	}

	public int getX() {
		return x;
	}

	public int getY() {
		return y;
	}

	public int getZ() {
		return z;
	}

	/**
	 * This only works if the world is loaded.
	 * 
	 * @return null, if the world this shopkeeper is in isn't loaded
	 */
	public Location getLocation() {
		World world = Bukkit.getWorld(worldName);
		if (world == null) return null;
		return new Location(world, x, y, z);
	}

	/**
	 * Sets the stored location of this Shopkeeper.
	 * This will not actually move the shopkeeper entity until the next time teleport() is called.
	 * 
	 * @param location
	 *            The new stored location of this shopkeeper.
	 */
	public void setLocation(Location location) {
		ChunkData oldChunk = this.getChunkData();
		x = location.getBlockX();
		y = location.getBlockY();
		z = location.getBlockZ();
		worldName = location.getWorld().getName();
		this.updateChunkData();

		// update shopkeeper in chunk map:
		ShopkeepersPlugin.getInstance().onShopkeeperMove(this, oldChunk);
	}

	private void updateChunkData() {
		this.chunkData = new ChunkData(worldName, (x >> 4), (z >> 4));
	}

	/**
	 * Gets the shopkeeper's object ID. This is can change when the shopkeeper object (ex. shopkeeper entity) respawns.
	 * 
	 * @return the object id, or null if the shopkeeper object is currently not active
	 */
	public String getObjectId() {
		return shopObject.getId();
	}

	/**
	 * Gets the shopkeeper's trade recipes. This will be a list of ItemStack[3],
	 * where the first two elemets of the ItemStack[] array are the cost, and the third
	 * element is the trade result (the item sold by the shopkeeper).
	 * 
	 * @return the trade recipes of this shopkeeper
	 */
	public abstract List<ItemStack[]> getRecipes();

	// SHOPKEEPER UIs:

	public boolean isUIActive() {
		return uiActive;
	}

	public void deactivateUI() {
		uiActive = false;
	}

	public void activateUI() {
		uiActive = true;
	}

	/**
	 * Deactivates all currently open UIs (purchasing, editing, hiring, etc.) and closes them 1 tick later.
	 */
	public void closeAllOpenWindows() {
		ShopkeepersPlugin.getInstance().getUIManager().closeAllDelayed(this);
	}

	/**
	 * Gets the handler this specific shopkeeper is using for the specified interface type.
	 * 
	 * @param uiIdentifier
	 *            Specifies the user interface type.
	 * @return The handler, or null if this shopkeeper is not supporting the specified interface type
	 */
	public UIHandler getUIHandler(String uiIdentifier) {
		return uiHandlers.get(uiIdentifier);
	}

	/**
	 * 
	 * @param uiManager
	 *            Specifies the type of user interface.
	 * @return
	 */
	public UIHandler getUIHandler(UIType uiManager) {
		return uiManager != null ? uiHandlers.get(uiManager.getIdentifier()) : null;
	}

	/**
	 * Registers an ui handler for a specific type of user interface for this specific shopkeeper.
	 * 
	 * @param uiHandler
	 *            The handler
	 */
	public void registerUIHandler(UIHandler uiHandler) {
		Validate.notNull(uiHandler);
		uiHandlers.put(uiHandler.getUIType().getIdentifier(), uiHandler);
	}

	/**
	 * Attempts to open the interface specified by the given identifier for the specified player.
	 * Fails if this shopkeeper doesn't support the specified interface type, if the player
	 * cannot open this interface type for this shopkeeper (for example because of missing permissions),
	 * or if something else goes wrong.
	 * 
	 * @param uiIdentifier
	 *            Specifies the user interface type.
	 * @param player
	 *            the player requesting the specified interface
	 * @return true the player's request was successful and the interface was opened, false otherwise
	 */
	public boolean openWindow(String uiIdentifier, Player player) {
		return ShopkeepersPlugin.getInstance().getUIManager().requestUI(uiIdentifier, this, player);
	}

	// shortcuts for the default window types:

	/**
	 * Attempts to open the editor interface of this shopkeeper for the specified player.
	 * 
	 * @param player
	 *            the player requesting the editor interface
	 * @return whether or not the player's request was successful and the player is now editing
	 */
	public boolean openEditorWindow(Player player) {
		return this.openWindow(DefaultUIs.EDITOR_WINDOW.getIdentifier(), player);
	}

	/**
	 * Attempts to open the trading interface of this shopkeeper for the specified player.
	 * 
	 * @param player
	 *            the player requesting the trading interface
	 * @return whether or not the player's request was successful and the player is now trading
	 */
	public boolean openTradingWindow(Player player) {
		return this.openWindow(DefaultUIs.TRADING_WINDOW.getIdentifier(), player);
	}

	/**
	 * Attempts to open the hiring interface of this shopkeeper for the specified player.
	 * Fails if this shopkeeper type doesn't support hiring (ex. admin shops).
	 * 
	 * @param player
	 *            the player requesting the hiring interface
	 * @return whether or not the player's request was successful and the player is now hiring
	 */
	public boolean openHireWindow(Player player) {
		return this.openWindow(DefaultUIs.HIRING_WINDOW.getIdentifier(), player);
	}

	/**
	 * Attempts to open the chest inventory of this shopkeeper for the specified player.
	 * Fails if this shopkeeper type doesn't have a chest (ex. admin shops).
	 * 
	 * @param player
	 *            the player requesting the chest inventory window
	 * @return whether or not the player's request was successful and the inventory window was opened
	 */
	public boolean openChestWindow(Player player) {
		return false;
	}

	// NAMING:

	public String getName() {
		return name;
	}

	public void setName(String name) {
		if (name == null) name = "";
		name = Utils.colorize(name);
		this.name = shopObject.trimToNameLength(name);
		shopObject.setName(this.name);
	}

	public void startNaming(Player player) {
		ShopkeepersPlugin.getInstance().onNaming(player, this);
	}

	public boolean isValidName(String name) {
		return name != null && name.matches("^" + Settings.nameRegex + "$");
	}

	public boolean requestNameChange(Player player, String newName) {
		if (player == null) return false;
		if (!this.isValid()) return false;

		// update name:
		if (newName.isEmpty() || newName.equals("-")) {
			// remove name:
			newName = "";
		} else {
			// validate name:
			if (!this.isValidName(newName)) {
				Utils.sendMessage(player, Settings.msgNameInvalid);
				return false;
			}
		}

		// apply new name:
		this.setName(newName);

		// inform player:
		Utils.sendMessage(player, Settings.msgNameSet);

		// close all open windows:
		this.closeAllOpenWindows(); // TODO really needed?

		// run shopkeeper-edited event:
		Bukkit.getPluginManager().callEvent(new ShopkeeperEditedEvent(player, this));

		// save:
		ShopkeepersPlugin.getInstance().save();

		return true;
	}

	// HANDLE INTERACTION:

	/**
	 * Called when a player interacts with this shopkeeper.
	 * 
	 * @param player
	 *            the interacting player
	 */
	protected void onPlayerInteraction(Player player) {
		assert player != null;
		if (player.isSneaking()) {
			// open editor window:
			this.openEditorWindow(player);
		} else {
			// open trading window:
			this.openTradingWindow(player);
		}
	}
} public interface ShopkeepersAPI {

	public static final String HELP_PERMISSION = "shopkeeper.help";
	public static final String TRADE_PERMISSION = "shopkeeper.trade";
	public static final String RELOAD_PERMISSION = "shopkeeper.reload";
	public static final String DEBUG_PERMISSION = "shopkeeper.debug";
	public static final String LIST_OWN_PERMISSION = "shopkeeper.list.own";
	public static final String LIST_OTHERS_PERMISSION = "shopkeeper.list.others";
	public static final String LIST_ADMIN_PERMISSION = "shopkeeper.list.admin";
	public static final String REMOVE_OWN_PERMISSION = "shopkeeper.remove.own";
	public static final String REMOVE_OTHERS_PERMISSION = "shopkeeper.remove.others";
	public static final String REMOVE_ALL_PERMISSION = "shopkeeper.remove.all";
	public static final String REMOVE_ADMIN_PERMISSION = "shopkeeper.remove.admin";
	public static final String REMOTE_PERMISSION = "shopkeeper.remote";
	public static final String TRANSFER_PERMISSION = "shopkeeper.transfer";
	public static final String SETTRADEPERM_PERMISSION = "shopkeeper.settradeperm";
	public static final String SETFORHIRE_PERMISSION = "shopkeeper.setforhire";
	public static final String HIRE_PERMISSION = "shopkeeper.hire";
	public static final String BYPASS_PERMISSION = "shopkeeper.bypass";
	public static final String ADMIN_PERMISSION = "shopkeeper.admin";
	public static final String PLAYER_NORMAL_PERMISSION = "shopkeeper.player.normal";
	public static final String PLAYER_BUY_PERMISSION = "shopkeeper.player.buy";
	public static final String PLAYER_TRADE_PERMISSION = "shopkeeper.player.trade";
	public static final String PLAYER_BOOK_PERMISSION = "shopkeeper.player.book";

	/**
	 * Checks if the given player has the permission to create any shopkeeper.
	 * 
	 * @param player
	 * @return <code>false</code> if he cannot create shops at all, <code>true</code> otherwise
	 */
	public boolean hasCreatePermission(Player player);

	/**
	 * Creates a new admin shopkeeper and spawns it into the world.
	 * 
	 * @param shopCreationData
	 *            a container holding the necessary arguments (spawn location, object type, etc.) for creating this
	 *            shopkeeper
	 * @return the shopkeeper created, or <code>null</code> if creation wasn't successful for some reason
	 */
	public Shopkeeper createNewAdminShopkeeper(ShopCreationData shopCreationData);

	/**
	 * Creates a new player-based shopkeeper and spawns it into the world.
	 * 
	 * @param shopCreationData
	 *            a container holding the necessary arguments (spawn location, object type, owner, etc.) for creating
	 *            this shopkeeper
	 * @return the shopkeeper created, or <code>null</code> if creation wasn't successful for some reason
	 */
	public Shopkeeper createNewPlayerShopkeeper(ShopCreationData shopCreationData);

	/**
	 * Gets the shopkeeper by its unique id.
	 * 
	 * <p>
	 * Note: This is not the entity uuid, but the id from {@link Shopkeeper#getUniqueId()}.
	 * </p>
	 * 
	 * @param shopkeeperId
	 *            the shopkeeper's unique id
	 * @return the shopkeeper for the given id, or <code>null</code>
	 */
	public Shopkeeper getShopkeeper(UUID shopkeeperId);

	/**
	 * Gets the shopkeeper by its current session id.
	 * 
	 * <p>
	 * This id is only guaranteed to be valid until the next server restart or reload of the shopkeepers. See
	 * {@link Shopkeeper#getSessionId()} for details.
	 * </p>
	 * 
	 * @param shopkeeperSessionId
	 *            the shopkeeper's session id
	 * @return the shopkeeper for the given session id, or <code>null</code>
	 */
	public Shopkeeper getShopkeeper(int shopkeeperSessionId);

	/**
	 * Tries to find a shopkeeper with the given name.
	 * 
	 * <p>
	 * This search ignores colors in the shop names.<br>
	 * Note: Shop names are not unique!
	 * </p>
	 * 
	 * @param shopName
	 *            the shop name
	 * @return the shopkeeper, or <code>null</code>
	 */
	public Shopkeeper getShopkeeperByName(String shopName);

	/**
	 * Gets the shopkeeper for a given entity.
	 * 
	 * @param entity
	 *            the entity
	 * @return the shopkeeper, or <code>null</code> if the given entity is not a shopkeeper
	 */
	public Shopkeeper getShopkeeperByEntity(Entity entity);

	/**
	 * Gets the shopkeeper for a given block (ex: sign shops).
	 * 
	 * @param block
	 *            the block
	 * @return the shopkeeper, or <code>null</code> if the given block is not a shopkeeper
	 */
	public Shopkeeper getShopkeeperByBlock(Block block);

	/**
	 * Gets all shopkeepers for a given chunk. Returns <code>null</code> if there are no shopkeepers in that chunk.
	 * 
	 * @param worldName
	 *            the world name
	 * @param x
	 *            chunk x-coordinate
	 * @param z
	 *            chunk z-coordinate
	 * @return an unmodifiable list of shopkeepers, or <code>null</code> if there are none
	 */
	public List<Shopkeeper> getShopkeepersInChunk(String worldName, int x, int z);

	/**
	 * Gets all shopkeepers for a given chunk. Returns <code>null</code> if there are no shopkeepers in that chunk.
	 * Similar to {@link #getShopkeepersInChunk(String, int, int)}.
	 * 
	 * @param chunkData
	 *            specifies the chunk
	 * @return an unmodifiable list of the shopkeepers in the specified chunk, or <code>null</code> if there are none
	 */
	public List<Shopkeeper> getShopkeepersInChunk(ChunkData chunkData);

	/**
	 * Checks if a given entity is a Shopkeeper.
	 * 
	 * @param entity
	 *            the entity to check
	 * @return whether the entity is a Shopkeeper
	 */
	public boolean isShopkeeper(Entity entity);

	/**
	 * Gets all shopkeepers.
	 * 
	 * @return an unmodifiable view on all shopkeepers
	 */
	public Collection<Shopkeeper> getAllShopkeepers();

	/**
	 * Gets all loaded shopkeepers grouped by the chunks they are in.
	 * 
	 * @return all loaded shopkeepers
	 * @deprecated Use {@link #getAllShopkeepers()} instead.
	 */
	@Deprecated
	public Collection<List<Shopkeeper>> getAllShopkeepersByChunks();

	/**
	 * Gets all active shopkeepers. Some shopkeeper types might be always active (like sign shops),
	 * others are only active as long as their chunk they are in is loaded.
	 * 
	 * @return an unmodifiable view on all active shopkeepers
	 */
	public Collection<Shopkeeper> getActiveShopkeepers();

	/**
	 * Requests a save of all the loaded shopkeepers data.
	 * The actual saving might happen delayed depending on the 'save-instantly' setting from the config.
	 */
	public void save();

	/**
	 * Instantly saves the shopkeepers data of all loaded shopkeepers to file.
	 * File IO is going to happen asynchronous.
	 */
	public void saveReal();
} public class ShopkeepersPlugin extends JavaPlugin implements ShopkeepersAPI {

	private static ShopkeepersPlugin plugin;

	public static ShopkeepersPlugin getInstance() {
		return plugin;
	}

	// shop types manager:
	private final SelectableTypeRegistry<ShopType<?>> shopTypesManager = new SelectableTypeRegistry<ShopType<?>>() {

		@Override
		protected String getTypeName() {
			return "shop type";
		}

		@Override
		public boolean canBeSelected(Player player, ShopType<?> type) {
			// TODO This currently skips the admin shop type. Maybe included the admin shop types here for players
			// which are admins, because there /could/ be different types of admin shops in the future (?)
			return super.canBeSelected(player, type) && type.isPlayerShopType();
		}
	};

	// shop object types manager:
	private final SelectableTypeRegistry<ShopObjectType> shopObjectTypesManager = new SelectableTypeRegistry<ShopObjectType>() {

		@Override
		protected String getTypeName() {
			return "shop object type";
		}
	};

	// default shop and shop object types:
	private DefaultShopTypes defaultShopTypes;
	private DefaultShopObjectTypes defaultShopObjectTypes;

	// ui manager:
	private final UIManager uiManager = new UIManager();

	// all shopkeepers:
	private final Map<UUID, Shopkeeper> shopkeepersById = new LinkedHashMap<UUID, Shopkeeper>();
	private final Collection<Shopkeeper> allShopkeepersView = Collections.unmodifiableCollection(shopkeepersById.values());
	private int nextShopSessionId = 1;
	private final Map<Integer, Shopkeeper> shopkeepersBySessionId = new LinkedHashMap<Integer, Shopkeeper>();
	private final Map<ChunkData, List<Shopkeeper>> shopkeepersByChunk = new HashMap<ChunkData, List<Shopkeeper>>();
	private final Map<String, Shopkeeper> activeShopkeepers = new HashMap<String, Shopkeeper>(); // TODO remove this (?)
	private final Collection<Shopkeeper> activeShopkeepersView = Collections.unmodifiableCollection(activeShopkeepers.values());

	private final Map<String, ConfirmEntry> confirming = new HashMap<String, ConfirmEntry>();
	private final Map<String, Shopkeeper> naming = Collections.synchronizedMap(new HashMap<String, Shopkeeper>());
	private final Map<String, List<String>> recentlyPlacedChests = new HashMap<String, List<String>>();
	private final Map<String, Block> selectedChest = new HashMap<String, Block>();

	// protected chests:
	private final ProtectedChests protectedChests = new ProtectedChests();

	// saving:
	// flag to (temporary) turn off saving
	private boolean skipSaving = false;
	private boolean dirty = false;
	private int chunkLoadSaveTask = -1;
	// keeps track about certain stats and information during a save, gets reused
	private final SaveInfo saveInfo = new SaveInfo();
	// the task which performs file io during a save
	private int saveIOTask = -1;
	// determines if there was another saveReal()-request while another saveIOTask was still in progress
	private boolean saveRealAgain = false;

	// listeners:
	private CreatureForceSpawnListener creatureForceSpawnListener = null;
	private SignShopListener signShopListener = null;

	@Override
	public void onEnable() {
		plugin = this;
		skipSaving = false;

		// try to load suitable NMS code:
		NMSManager.load(this);
		if (NMSManager.getProvider() == null) {
			Log.severe("Incompatible server version: Shopkeepers cannot be enabled.");
			this.setEnabled(false);
			return;
		}

		// load config:
		File file = new File(this.getDataFolder(), "config.yml");
		if (!file.exists()) {
			this.saveDefaultConfig();
		}
		this.reloadConfig();
		Configuration config = this.getConfig();
		if (Settings.loadConfiguration(config)) {
			// if values were missing -> add those to the file and save it
			this.saveConfig();
		}

		// load lang config:
		String lang = Settings.language;
		File langFile = new File(this.getDataFolder(), "language-" + lang + ".yml");
		if (!langFile.exists() && this.getResource("language-" + lang + ".yml") != null) {
			this.saveResource("language-" + lang + ".yml", false);
		}
		if (langFile.exists()) {
			try {
				YamlConfiguration langConfig = new YamlConfiguration();
				langConfig.load(langFile);
				Settings.loadLanguageConfiguration(langConfig);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		// process additional permissions
		String[] perms = Settings.maxShopsPermOptions.replace(" ", "").split(",");
		for (String perm : perms) {
			if (Bukkit.getPluginManager().getPermission("shopkeeper.maxshops." + perm) == null) {
				Bukkit.getPluginManager().addPermission(new Permission("shopkeeper.maxshops." + perm, PermissionDefault.FALSE));
			}
		}

		// initialize default shop and shop object types (after config has been loaded):
		defaultShopTypes = new DefaultShopTypes();
		defaultShopObjectTypes = new DefaultShopObjectTypes();

		// register default stuff:
		shopTypesManager.registerAll(defaultShopTypes.getAllShopTypes());
		shopObjectTypesManager.registerAll(defaultShopObjectTypes.getAllObjectTypes());
		uiManager.registerAll(DefaultUIs.getAllUITypes());

		// inform ui manager (registers ui event handlers):
		uiManager.onEnable(this);

		// inform ProtectedChests:
		protectedChests.onEnable(this);

		// register events
		PluginManager pm = Bukkit.getPluginManager();
		pm.registerEvents(new PluginListener(), this);
		pm.registerEvents(new WorldListener(this), this);
		pm.registerEvents(new PlayerJoinQuitListener(this), this);
		pm.registerEvents(new ShopNamingListener(this), this);
		pm.registerEvents(new ChestListener(this), this);
		pm.registerEvents(new CreateListener(this), this);
		pm.registerEvents(new VillagerInteractionListener(this), this);
		pm.registerEvents(new LivingEntityShopListener(this), this);

		if (Settings.enableSignShops) {
			this.signShopListener = new SignShopListener(this);
			pm.registerEvents(signShopListener, this);
		}

		// enable citizens handler:
		CitizensHandler.enable();

		if (Settings.blockVillagerSpawns) {
			pm.registerEvents(new BlockVillagerSpawnListener(), this);
		}

		if (Settings.protectChests) {
			pm.registerEvents(new ChestProtectListener(this), this);
		}
		if (Settings.deleteShopkeeperOnBreakChest) {
			pm.registerEvents(new RemoveShopOnChestBreakListener(this), this);
		}

		// register force-creature-spawn event handler:
		if (Settings.bypassSpawnBlocking) {
			creatureForceSpawnListener = new CreatureForceSpawnListener();
			Bukkit.getPluginManager().registerEvents(creatureForceSpawnListener, this);
		}

		// register command handler:
		CommandManager commandManager = new CommandManager(this);
		this.getCommand("shopkeeper").setExecutor(commandManager);

		// load shopkeeper saved data:
		if (!this.load()) {
			// detected issue during loading, disable plugin without saving, to prevent loss of shopkeeper data:
			Log.severe("Detected an issue during loading of the shopkeeper data! Disabling plugin!");
			skipSaving = true;
			Bukkit.getPluginManager().disablePlugin(this);
			return;
		}

		// activate (spawn) shopkeepers in loaded chunks:
		for (World world : Bukkit.getWorlds()) {
			this.loadShopkeepersInWorld(world);
		}

		Bukkit.getScheduler().runTaskLater(this, new Runnable() {

			@Override
			public void run() {
				// remove invalid citizens shopkeepers:
				CitizensHandler.removeInvalidCitizensShopkeepers();
				// remove inactive player shopkeepers:
				removeInactivePlayerShops();
			}
		}, 5L);

		// start teleporter task:
		Bukkit.getScheduler().runTaskTimer(this, new Runnable() {
			public void run() {
				List<Shopkeeper> readd = new ArrayList<Shopkeeper>();
				Iterator<Map.Entry<String, Shopkeeper>> iter = activeShopkeepers.entrySet().iterator();
				while (iter.hasNext()) {
					Shopkeeper shopkeeper = iter.next().getValue();
					boolean update = shopkeeper.check();
					if (update) {
						// if the shopkeeper had to be respawned it's shop id changed:
						// this removes the entry which was stored with the old shop id and later adds back the
						// shopkeeper with it's new id
						readd.add(shopkeeper);
						iter.remove();
					}
				}
				if (!readd.isEmpty()) {
					for (Shopkeeper shopkeeper : readd) {
						if (shopkeeper.isActive()) {
							_activateShopkeeper(shopkeeper);
						}
					}

					// shopkeepers might have been respawned, request save:
					save();
				}
			}
		}, 200, 200); // 10 seconds

		// start verifier task:
		if (Settings.enableSpawnVerifier) {
			Bukkit.getScheduler().runTaskTimer(this, new Runnable() {
				public void run() {
					int count = 0;
					for (Entry<ChunkData, List<Shopkeeper>> chunkEntry : shopkeepersByChunk.entrySet()) {
						ChunkData chunk = chunkEntry.getKey();
						if (chunk.isChunkLoaded()) {
							List<Shopkeeper> shopkeepers = chunkEntry.getValue();
							for (Shopkeeper shopkeeper : shopkeepers) {
								if (shopkeeper.needsSpawning() && !shopkeeper.isActive()) {
									// deactivate by old object id:
									_deactivateShopkeeper(shopkeeper);

									boolean spawned = shopkeeper.spawn();
									if (spawned) {
										// activate with new object id:
										_activateShopkeeper(shopkeeper);
										count++;
									} else {
										Log.debug("Failed to spawn shopkeeper at " + shopkeeper.getPositionString());
									}
								}
							}
						}
					}
					if (count > 0) {
						Log.debug("Spawn verifier: " + count + " shopkeepers respawned");
						save();
					}
				}
			}, 600, 1200); // 30,60 seconds
		}

		// start save task:
		if (!Settings.saveInstantly) {
			Bukkit.getScheduler().runTaskTimer(this, new Runnable() {
				public void run() {
					if (dirty) {
						saveReal();
					}
				}
			}, 6000, 6000); // 5 minutes
		}

		// let's update the shopkeepers for all already online players:
		for (Player player : Bukkit.getOnlinePlayers()) {
			if (Utils.isNPC(player)) continue;
			this.updateShopkeepersForPlayer(player.getUniqueId(), player.getName());
		}
	}

	@Override
	public void onDisable() {
		// close all open windows:
		uiManager.closeAll();
		// inform ui manager about disable:
		uiManager.onDisable(this);

		// despawn shopkeepers:
		for (Shopkeeper shopkeeper : activeShopkeepers.values()) {
			shopkeeper.despawn();
		}

		// disable citizens handler:
		CitizensHandler.disable();

		// save:
		if (dirty) {
			this.saveReal(false); // not async here
		}

		// inform ProtectedChests:
		protectedChests.onDisable(this);

		// cleanup:
		activeShopkeepers.clear();
		shopkeepersByChunk.clear();
		shopkeepersById.clear();
		shopkeepersBySessionId.clear();
		nextShopSessionId = 1;

		shopTypesManager.clearAllSelections();
		shopObjectTypesManager.clearAllSelections();

		confirming.clear();
		naming.clear();
		selectedChest.clear();

		// clear all types of registers:
		shopTypesManager.clearAll();
		shopObjectTypesManager.clearAll();
		uiManager.clearAll();

		HandlerList.unregisterAll(this);
		Bukkit.getScheduler().cancelTasks(this);

		plugin = null;
	}

	/**
	 * Reloads the plugin.
	 */
	public void reload() {
		this.onDisable();
		this.onEnable();
	}

	void onPlayerQuit(Player player) {
		String playerName = player.getName();
		shopTypesManager.clearSelection(player);
		shopObjectTypesManager.clearSelection(player);
		uiManager.onInventoryClose(player);

		selectedChest.remove(playerName);
		recentlyPlacedChests.remove(playerName);
		naming.remove(playerName);
		this.endConfirmation(player);
	}

	// bypassing creature blocking plugins ('region protection' plugins):
	public void forceCreatureSpawn(Location location, EntityType entityType) {
		if (creatureForceSpawnListener != null && Settings.bypassSpawnBlocking) {
			creatureForceSpawnListener.forceCreatureSpawn(location, entityType);
		}
	}

	public void cancelNextBlockPhysics(Location location) {
		if (signShopListener != null) {
			signShopListener.cancelNextBlockPhysics(location);
		}
	}

	// UI

	public UIManager getUIManager() {
		return uiManager;
	}

	// PROTECTED CHESTS:

	public ProtectedChests getProtectedChests() {
		return protectedChests;
	}

	// SHOP TYPES

	public SelectableTypeRegistry<ShopType<?>> getShopTypeRegistry() {
		return shopTypesManager;
	}

	public DefaultShopTypes getDefaultShopTypes() {
		return defaultShopTypes;
	}

	// SHOP OBJECT TYPES

	public SelectableTypeRegistry<ShopObjectType> getShopObjectTypeRegistry() {
		return shopObjectTypesManager;
	}

	public DefaultShopObjectTypes getDefaultShopObjectTypes() {
		return defaultShopObjectTypes;
	}

	/**
	 * Gets the default shop object type.
	 * 
	 * <p>
	 * Usually this will be the villager entity shop object type.<br>
	 * However, there are no guarantees that this might not get changed or be configurable in the future.
	 * </p>
	 * 
	 * @return the default shop object type
	 */
	public ShopObjectType getDefaultShopObjectType() {
		// default: villager entity shop object type:
		return this.getDefaultShopObjectTypes().getLivingEntityObjectTypes().getObjectType(EntityType.VILLAGER);
	}

	// RECENTLY PLACED CHESTS

	void onChestPlacement(Player player, Block chest) {
		assert player != null && chest != null && Utils.isChest(chest.getType());
		String playerName = player.getName();
		List<String> recentlyPlaced = recentlyPlacedChests.get(playerName);
		if (recentlyPlaced == null) {
			recentlyPlaced = new LinkedList<String>();
			recentlyPlacedChests.put(playerName, recentlyPlaced);
		}
		recentlyPlaced.add(chest.getWorld().getName() + "," + chest.getX() + "," + chest.getY() + "," + chest.getZ());
		if (recentlyPlaced.size() > 5) {
			recentlyPlaced.remove(0);
		}
	}

	public boolean wasRecentlyPlaced(Player player, Block chest) {
		assert player != null && chest != null && Utils.isChest(chest.getType());
		String playerName = player.getName();
		List<String> recentlyPlaced = recentlyPlacedChests.get(playerName);
		return recentlyPlaced != null && recentlyPlaced.contains(chest.getWorld().getName() + "," + chest.getX() + "," + chest.getY() + "," + chest.getZ());
	}

	// SELECTED CHEST

	void selectChest(Player player, Block chest) {
		assert player != null;
		String playerName = player.getName();
		if (chest == null) selectedChest.remove(playerName);
		else {
			assert Utils.isChest(chest.getType());
			selectedChest.put(playerName, chest);
		}
	}

	public Block getSelectedChest(Player player) {
		assert player != null;
		return selectedChest.get(player.getName());
	}

	// COMMAND CONFIRMING

	void waitForConfirm(final Player player, Runnable action, int delay) {
		assert player != null && delay > 0;
		int taskId = new BukkitRunnable() {

			@Override
			public void run() {
				endConfirmation(player);
				Utils.sendMessage(player, Settings.msgConfirmationExpired);
			}
		}.runTaskLater(this, delay).getTaskId();
		ConfirmEntry oldEntry = confirming.put(player.getName(), new ConfirmEntry(action, taskId));
		if (oldEntry != null) {
			// end old confirmation task:
			Bukkit.getScheduler().cancelTask(oldEntry.getTaskId());
		}
	}

	Runnable endConfirmation(Player player) {
		ConfirmEntry entry = confirming.remove(player.getName());
		if (entry != null) {
			// end confirmation task:
			Bukkit.getScheduler().cancelTask(entry.getTaskId());

			// return action:
			return entry.getAction();
		}
		return null;
	}

	void onConfirm(Player player) {
		assert player != null;
		Runnable action = this.endConfirmation(player);
		if (action != null) {
			// execute confirmed task:
			action.run();
		} else {
			Utils.sendMessage(player, Settings.msgNothingToConfirm);
		}
	}

	// SHOPKEEPER NAMING

	void onNaming(Player player, Shopkeeper shopkeeper) {
		assert player != null && shopkeeper != null;
		naming.put(player.getName(), shopkeeper);
	}

	Shopkeeper getCurrentlyNamedShopkeeper(Player player) {
		assert player != null;
		return naming.get(player.getName());
	}

	boolean isNaming(Player player) {
		assert player != null;
		return this.getCurrentlyNamedShopkeeper(player) != null;
	}

	Shopkeeper endNaming(Player player) {
		assert player != null;
		return naming.remove(player.getName());
	}

	// SHOPKEEPER MEMORY STORAGE

	private void addShopkeeperToChunk(Shopkeeper shopkeeper, ChunkData chunkData) {
		List<Shopkeeper> list = shopkeepersByChunk.get(chunkData);
		if (list == null) {
			list = new ArrayList<Shopkeeper>();
			shopkeepersByChunk.put(chunkData, list);
		}
		list.add(shopkeeper);
	}

	private void removeShopkeeperFromChunk(Shopkeeper shopkeeper, ChunkData chunkData) {
		List<Shopkeeper> byChunk = shopkeepersByChunk.get(chunkData);
		if (byChunk == null) return;
		if (byChunk.remove(shopkeeper) && byChunk.isEmpty()) {
			shopkeepersByChunk.remove(chunkData);
		}
	}

	// this needs to be called right after a new shopkeeper was created..
	void registerShopkeeper(Shopkeeper shopkeeper) {
		assert shopkeeper != null;
		// assert !this.isRegistered(shopkeeper);

		// add default trading handler, if none is provided:
		if (shopkeeper.getUIHandler(DefaultUIs.TRADING_WINDOW) == null) {
			shopkeeper.registerUIHandler(new TradingHandler(DefaultUIs.TRADING_WINDOW, shopkeeper));
		}

		// store by unique id:
		shopkeepersById.put(shopkeeper.getUniqueId(), shopkeeper);

		// assign session id:
		int shopSessionId = nextShopSessionId;
		nextShopSessionId++;
		shopkeepersBySessionId.put(shopSessionId, shopkeeper);

		// inform shopkeeper:
		shopkeeper.onRegistration(shopSessionId);

		// add shopkeeper to chunk:
		ChunkData chunkData = shopkeeper.getChunkData();
		this.addShopkeeperToChunk(shopkeeper, chunkData);

		// activate shopkeeper:
		if (!shopkeeper.needsSpawning()) {
			this._activateShopkeeper(shopkeeper);
		} else if (!shopkeeper.isActive() && chunkData.isChunkLoaded()) {
			boolean spawned = shopkeeper.spawn();
			if (spawned) {
				this._activateShopkeeper(shopkeeper);
			} else {
				Log.debug("Failed to spawn shopkeeper at " + shopkeeper.getPositionString());
			}
		}
	}

	@Override
	public Shopkeeper getShopkeeper(UUID shopkeeperUUID) {
		return shopkeepersById.get(shopkeeperUUID);
	}

	@Override
	public Shopkeeper getShopkeeper(int shopkeeperSessionId) {
		return shopkeepersBySessionId.get(shopkeeperSessionId);
	}

	@Override
	public Shopkeeper getShopkeeperByName(String shopName) {
		if (shopName == null) return null;
		shopName = ChatColor.stripColor(shopName);
		for (Shopkeeper shopkeeper : this.getAllShopkeepers()) {
			String shopkeeperName = shopkeeper.getName();
			if (shopkeeperName != null && ChatColor.stripColor(shopkeeperName).equalsIgnoreCase(shopName)) {
				return shopkeeper;
			}
		}
		return null;
	}

	@Override
	public Shopkeeper getShopkeeperByEntity(Entity entity) {
		if (entity == null) return null;
		Shopkeeper shopkeeper = this.getActiveShopkeeper(LivingEntityShop.getId(entity));
		if (shopkeeper != null) return shopkeeper;
		// check if this is a citizens npc shopkeeper:
		Integer npcId = CitizensHandler.getNPCId(entity);
		if (npcId == null) return null;
		return this.getActiveShopkeeper(CitizensShop.getId(npcId));
	}

	@Override
	public Shopkeeper getShopkeeperByBlock(Block block) {
		if (block == null) return null;
		return this.getActiveShopkeeper(SignShop.getId(block));
	}

	public Shopkeeper getActiveShopkeeper(String objectId) {
		return activeShopkeepers.get(objectId);
	}

	@Override
	public boolean isShopkeeper(Entity entity) {
		return this.getShopkeeperByEntity(entity) != null;
	}

	@Override
	public Collection<Shopkeeper> getAllShopkeepers() {
		return allShopkeepersView;
	}

	@Override
	public Collection<List<Shopkeeper>> getAllShopkeepersByChunks() {
		return Collections.unmodifiableCollection(shopkeepersByChunk.values());
	}

	@Override
	public Collection<Shopkeeper> getActiveShopkeepers() {
		return activeShopkeepersView;
	}

	@Override
	public List<Shopkeeper> getShopkeepersInChunk(String worldName, int x, int z) {
		return this.getShopkeepersInChunk(new ChunkData(worldName, x, z));
	}

	@Override
	public List<Shopkeeper> getShopkeepersInChunk(ChunkData chunkData) {
		List<Shopkeeper> byChunk = shopkeepersByChunk.get(chunkData);
		if (byChunk == null) return null;
		return Collections.unmodifiableList(byChunk);
	}

	// LOADING/UNLOADING/REMOVAL

	// performs some validation before actually activating a shopkeeper:
	// returns false if some validation failed
	private boolean _activateShopkeeper(Shopkeeper shopkeeper) {
		assert shopkeeper != null;
		String objectId = shopkeeper.getObjectId();
		if (objectId == null) {
			// currently only null is considered invalid,
			// prints 'null' to log then:
			Log.warning("Detected shopkeeper with invalid object id: " + objectId);
			return false;
		} else if (activeShopkeepers.containsKey(objectId)) {
			Log.warning("Detected shopkeepers with duplicate object id: " + objectId);
			return false;
		} else {
			// activate shopkeeper:
			activeShopkeepers.put(objectId, shopkeeper);
			return true;
		}
	}

	private boolean _deactivateShopkeeper(Shopkeeper shopkeeper) {
		assert shopkeeper != null;
		String objectId = shopkeeper.getObjectId();
		if (activeShopkeepers.get(objectId) == shopkeeper) {
			activeShopkeepers.remove(objectId);
			return true;
		}
		return false;
	}

	private void activateShopkeeper(Shopkeeper shopkeeper) {
		assert shopkeeper != null;
		if (shopkeeper.needsSpawning() && !shopkeeper.isActive()) {
			// deactivate shopkeeper by old shop object id, in case there is one:
			if (this._deactivateShopkeeper(shopkeeper)) {
				if (Settings.debug && shopkeeper.getShopObject() instanceof LivingEntityShop) {
					LivingEntityShop livingShop = (LivingEntityShop) shopkeeper.getShopObject();
					LivingEntity oldEntity = livingShop.getEntity();
					Log.debug("Old, active shopkeeper was found (unloading probably has been skipped earlier): "
							+ (oldEntity == null ? "null" : (oldEntity.getUniqueId() + " | " + (oldEntity.isDead() ? "dead | " : "alive | ")
									+ (oldEntity.isValid() ? "valid" : "invalid"))));
				}
			}

			// spawn and activate:
			boolean spawned = shopkeeper.spawn();
			if (spawned) {
				// activate with new object id:
				this._activateShopkeeper(shopkeeper);
			} else {
				Log.warning("Failed to spawn shopkeeper at " + shopkeeper.getPositionString());
			}
		}
	}

	private void deactivateShopkeeper(Shopkeeper shopkeeper, boolean closeWindows) {
		assert shopkeeper != null;
		if (closeWindows) {
			// delayed closing of all open windows:
			shopkeeper.closeAllOpenWindows();
		}
		this._deactivateShopkeeper(shopkeeper);
		shopkeeper.despawn();
	}

	public void deleteShopkeeper(Shopkeeper shopkeeper) {
		assert shopkeeper != null;
		// deactivate shopkeeper:
		this.deactivateShopkeeper(shopkeeper, true);

		// inform shopkeeper:
		shopkeeper.onDeletion();

		// remove shopkeeper by id and session id:
		shopkeepersById.remove(shopkeeper.getUniqueId());
		shopkeepersBySessionId.remove(shopkeeper.getSessionId());

		// remove shopkeeper from chunk:
		ChunkData chunkData = shopkeeper.getChunkData();
		this.removeShopkeeperFromChunk(shopkeeper, chunkData);
	}

	public void onShopkeeperMove(Shopkeeper shopkeeper, ChunkData oldChunk) {
		assert oldChunk != null;
		ChunkData newChunk = shopkeeper.getChunkData();
		if (!oldChunk.equals(newChunk)) {
			// remove from old chunk:
			this.removeShopkeeperFromChunk(shopkeeper, oldChunk);

			// add to new chunk:
			this.addShopkeeperToChunk(shopkeeper, newChunk);
		}
	}

	/**
	 * Loads (activates) all shopkeepers in the given chunk.
	 * 
	 * @param chunk
	 *            the chunk
	 * @return the number of shops in the affected chunk
	 */
	int loadShopkeepersInChunk(Chunk chunk) {
		assert chunk != null;
		int affectedShops = 0;
		List<Shopkeeper> shopkeepers = shopkeepersByChunk.get(new ChunkData(chunk));
		if (shopkeepers != null) {
			affectedShops = shopkeepers.size();
			Log.debug("Loading " + affectedShops + " shopkeepers in chunk " + chunk.getX() + "," + chunk.getZ());
			for (Shopkeeper shopkeeper : shopkeepers) {
				// inform shopkeeper about chunk load:
				shopkeeper.onChunkLoad();

				// activate:
				this.activateShopkeeper(shopkeeper);
			}

			// save:
			dirty = true;
			if (Settings.saveInstantly) {
				if (chunkLoadSaveTask == -1) {
					chunkLoadSaveTask = Bukkit.getScheduler().runTaskLater(plugin, new Runnable() {
						public void run() {
							if (dirty) {
								saveReal();
							}
							chunkLoadSaveTask = -1;
						}
					}, 600).getTaskId();
				}
			}
		}
		return affectedShops;
	}

	/**
	 * Unloads (deactivates) all shopkeepers in the given chunk.
	 * 
	 * @param chunk
	 *            the chunk
	 * @return the number of shops in the affected chunk
	 */
	int unloadShopkeepersInChunk(Chunk chunk) {
		assert chunk != null;
		int affectedShops = 0;
		List<Shopkeeper> shopkeepers = this.getShopkeepersInChunk(new ChunkData(chunk));
		if (shopkeepers != null) {
			affectedShops = shopkeepers.size();
			Log.debug("Unloading " + affectedShops + " shopkeepers in chunk " + chunk.getX() + "," + chunk.getZ());
			for (Shopkeeper shopkeeper : shopkeepers) {
				// inform shopkeeper about chunk unload:
				shopkeeper.onChunkUnload();

				// skip shopkeepers which are kept active all the time (ex. sign, citizens shops):
				if (!shopkeeper.needsSpawning()) continue;

				// deactivate:
				this.deactivateShopkeeper(shopkeeper, false);
			}
		}
		return affectedShops;
	}

	/**
	 * Loads all shopkeepers in the given world.
	 * 
	 * @param world
	 *            the world
	 */
	void loadShopkeepersInWorld(World world) {
		assert world != null;
		int affectedShops = 0;
		for (Chunk chunk : world.getLoadedChunks()) {
			affectedShops += this.loadShopkeepersInChunk(chunk);
		}
		Log.debug("Loaded " + affectedShops + " shopkeepers in world " + world.getName());
	}

	/**
	 * Unloads all shopkeepers in the given world.
	 * 
	 * @param world
	 *            the world
	 */
	void unloadShopkeepersInWorld(World world) {
		assert world != null;
		int affectedShops = 0;
		for (Chunk chunk : world.getLoadedChunks()) {
			affectedShops += this.unloadShopkeepersInChunk(chunk);
		}
		Log.debug("Unloaded " + affectedShops + " shopkeepers in world " + world.getName());
	}

	// SHOPKEEPER CREATION:

	@Override
	public boolean hasCreatePermission(Player player) {
		if (player == null) return false;
		return shopTypesManager.getSelection(player) != null && shopObjectTypesManager.getSelection(player) != null;
	}

	@Override
	public Shopkeeper createNewAdminShopkeeper(ShopCreationData creationData) {
		try {
			if (creationData == null || creationData.spawnLocation == null || creationData.objectType == null) {
				throw new ShopkeeperCreateException("null");
			}
			if (creationData.shopType == null) {
				creationData.shopType = DefaultShopTypes.ADMIN();
			} else if (creationData.shopType.isPlayerShopType()) {
				// we are expecting an admin shop type here..
				throw new ShopkeeperCreateException("Expecting admin shop type, got player shop type!");
			}

			// create the shopkeeper (and spawn it):
			Shopkeeper shopkeeper = creationData.shopType.createShopkeeper(creationData);
			if (shopkeeper == null) {
				throw new ShopkeeperCreateException("ShopType returned null shopkeeper!");
			}
			assert shopkeeper != null;

			// save:
			this.save();

			// send creation message to creator:
			Utils.sendMessage(creationData.creator, creationData.shopType.getCreatedMessage());

			// run shopkeeper-created-event:
			Bukkit.getPluginManager().callEvent(new ShopkeeperCreatedEvent(creationData.creator, shopkeeper));

			return shopkeeper;
		} catch (ShopkeeperCreateException e) {
			Log.warning("Couldn't create admin shopkeeper: " + e.getMessage());
			return null;
		}
	}

	@Override
	public Shopkeeper createNewPlayerShopkeeper(ShopCreationData creationData) {
		try {
			if (creationData == null || creationData.shopType == null || creationData.objectType == null
					|| creationData.creator == null || creationData.chest == null || creationData.spawnLocation == null) {
				throw new ShopkeeperCreateException("null");
			}

			// check if this chest is already used by some other shopkeeper:
			if (this.getProtectedChests().isChestProtected(creationData.chest, null)) {
				Utils.sendMessage(creationData.creator, Settings.msgShopCreateFail);
				return null;
			}

			// check worldguard:
			if (Settings.enableWorldGuardRestrictions) {
				if (!WorldGuardHandler.isShopAllowed(creationData.creator, creationData.spawnLocation)) {
					Utils.sendMessage(creationData.creator, Settings.msgShopCreateFail);
					return null;
				}
			}

			// check towny:
			if (Settings.enableTownyRestrictions) {
				if (!TownyHandler.isCommercialArea(creationData.spawnLocation)) {
					Utils.sendMessage(creationData.creator, Settings.msgShopCreateFail);
					return null;
				}
			}

			int maxShops = this.getMaxShops(creationData.creator);

			// call event:
			CreatePlayerShopkeeperEvent event = new CreatePlayerShopkeeperEvent(creationData, maxShops);
			Bukkit.getPluginManager().callEvent(event);
			if (event.isCancelled()) {
				Log.debug("CreatePlayerShopkeeperEvent was cancelled!");
				return null;
			} else {
				creationData.spawnLocation = event.getSpawnLocation();
				creationData.shopType = event.getType();
				maxShops = event.getMaxShopsForPlayer();
			}

			// count owned shops:
			if (maxShops > 0) {
				int count = this.countShopsOfPlayer(creationData.creator);
				if (count >= maxShops) {
					Utils.sendMessage(creationData.creator, Settings.msgTooManyShops);
					return null;
				}
			}

			// create and spawn the shopkeeper:
			Shopkeeper shopkeeper = creationData.shopType.createShopkeeper(creationData);
			if (shopkeeper == null) {
				throw new ShopkeeperCreateException("ShopType returned null shopkeeper!");
			}
			assert shopkeeper != null;

			// save:
			this.save();

			// send creation message to creator:
			Utils.sendMessage(creationData.creator, creationData.shopType.getCreatedMessage());

			// run shopkeeper-created-event
			Bukkit.getPluginManager().callEvent(new ShopkeeperCreatedEvent(creationData.creator, shopkeeper));

			return shopkeeper;
		} catch (ShopkeeperCreateException e) {
			Log.warning("Couldn't create player shopkeeper: " + e.getMessage());
			return null;
		}
	}

	public int countShopsOfPlayer(Player player) {
		int count = 0;
		for (Shopkeeper shopkeeper : shopkeepersById.values()) {
			if (shopkeeper instanceof PlayerShopkeeper && ((PlayerShopkeeper) shopkeeper).isOwner(player)) {
				count++;
			}
		}
		return count;
	}

	public int getMaxShops(Player player) {
		int maxShops = Settings.maxShopsPerPlayer;
		String[] maxShopsPermOptions = Settings.maxShopsPermOptions.replace(" ", "").split(",");
		for (String perm : maxShopsPermOptions) {
			if (Utils.hasPermission(player, "shopkeeper.maxshops." + perm)) {
				maxShops = Integer.parseInt(perm);
			}
		}
		return maxShops;
	}

	// INACTIVE SHOPS

	private void removeInactivePlayerShops() {
		if (Settings.playerShopkeeperInactiveDays <= 0) return;

		final Set<UUID> playerUUIDs = new HashSet<UUID>();
		for (Shopkeeper shopkeeper : shopkeepersById.values()) {
			if (shopkeeper instanceof PlayerShopkeeper) {
				PlayerShopkeeper playerShop = (PlayerShopkeeper) shopkeeper;
				playerUUIDs.add(playerShop.getOwnerUUID());
			}
		}
		if (playerUUIDs.isEmpty()) {
			// no player shops found:
			return;
		}

		// fetch OfflinePlayers async:
		final int playerShopkeeperInactiveDays = Settings.playerShopkeeperInactiveDays;
		Bukkit.getScheduler().runTaskAsynchronously(this, new Runnable() {

			@Override
			public void run() {
				final List<OfflinePlayer> inactivePlayers = new ArrayList<OfflinePlayer>(playerUUIDs.size());
				long now = System.currentTimeMillis();
				for (UUID uuid : playerUUIDs) {
					OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid);
					if (!offlinePlayer.hasPlayedBefore()) continue;

					long lastPlayed = offlinePlayer.getLastPlayed();
					if ((lastPlayed > 0) && ((now - lastPlayed) / 86400000 > playerShopkeeperInactiveDays)) {
						inactivePlayers.add(offlinePlayer);
					}
				}

				if (inactivePlayers.isEmpty()) {
					// no inactive players found:
					return;
				}

				// continue in main thread:
				Bukkit.getScheduler().runTask(ShopkeepersPlugin.this, new Runnable() {

					@Override
					public void run() {
						List<PlayerShopkeeper> forRemoval = new ArrayList<PlayerShopkeeper>();
						for (OfflinePlayer inactivePlayer : inactivePlayers) {
							// remove all shops of this inactive player:
							UUID playerUUID = inactivePlayer.getUniqueId();

							for (Shopkeeper shopkeeper : shopkeepersById.values()) {
								if (shopkeeper instanceof PlayerShopkeeper) {
									PlayerShopkeeper playerShop = (PlayerShopkeeper) shopkeeper;
									UUID ownerUUID = playerShop.getOwnerUUID();
									if (ownerUUID.equals(playerUUID)) {
										forRemoval.add(playerShop);
									}
								}
							}
						}

						// remove those shopkeepers:
						if (!forRemoval.isEmpty()) {
							for (PlayerShopkeeper shopkeeper : forRemoval) {
								shopkeeper.delete();
								Log.info("Shopkeeper owned by " + shopkeeper.getOwnerAsString() + " at "
										+ shopkeeper.getPositionString() + " has been removed for owner inactivity.");
							}

							// save:
							save();
						}
					}
				});
			}
		});
	}

	// HANDLING PLAYER NAME CHANGES:

	// updates owner names for the shopkeepers of the specified player:
	void updateShopkeepersForPlayer(UUID playerUUID, String playerName) {
		boolean dirty = false;
		for (Shopkeeper shopkeeper : shopkeepersById.values()) {
			if (shopkeeper instanceof PlayerShopkeeper) {
				PlayerShopkeeper playerShop = (PlayerShopkeeper) shopkeeper;
				UUID ownerUUID = playerShop.getOwnerUUID();
				String ownerName = playerShop.getOwnerName();

				if (ownerUUID.equals(playerUUID)) {
					if (!ownerName.equals(playerName)) {
						// update the stored name, because the player must have changed it:
						playerShop.setOwner(playerUUID, playerName);
						dirty = true;
					} else {
						// The shop was already updated to uuid based identification and the player's name hasn't
						// changed.
						// If we assume that this is consistent among all shops of this player
						// we can stop checking the other shops here:
						return;
					}
				}
			}
		}

		if (dirty) {
			this.save();
		}
	}

	// SHOPS LOADING AND SAVING

	private static class SaveInfo {
		long startTime;
		long packingDuration;
		long ioStartTime;
		long ioDuration;
		long fullDuration;

		public void printDebugInfo() {
			Log.debug("Saved shopkeeper data (" + fullDuration + "ms (Data packing: " + packingDuration + "ms, Async IO: " + ioDuration + "ms))");
		}
	}

	private File getSaveFile() {
		return new File(this.getDataFolder(), "save.yml");
	}

	// returns false if there was some issue during loading
	private boolean load() {
		File file = this.getSaveFile();
		if (!file.exists()) {
			// file does not exist yet -> no shopkeeper data available
			return true;
		}

		YamlConfiguration shopkeepersConfig = new YamlConfiguration();
		Scanner scanner = null;
		FileInputStream stream = null;
		try {
			if (Settings.fileEncoding != null && !Settings.fileEncoding.isEmpty()) {
				stream = new FileInputStream(file);
				scanner = new Scanner(stream, Settings.fileEncoding);
				scanner.useDelimiter("\\A");
				if (!scanner.hasNext()) {
					return true; // file is completely empty -> no shopkeeper data is available
				}
				String data = scanner.next();
				shopkeepersConfig.loadFromString(data);
			} else {
				shopkeepersConfig.load(file);
			}
		} catch (Exception e) {
			// issue detected:
			e.printStackTrace();
			return false; // disable without save
		} finally {
			if (scanner != null) {
				scanner.close();
			}
			if (stream != null) {
				try {
					stream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

		Set<String> ids = shopkeepersConfig.getKeys(false);
		for (String id : ids) {
			ConfigurationSection shopkeeperSection = shopkeepersConfig.getConfigurationSection(id);
			ShopType<?> shopType = shopTypesManager.get(shopkeeperSection.getString("type"));
			// unknown shop type
			if (shopType == null) {
				// got an owner entry? -> default to normal player shop type
				if (shopkeeperSection.contains("owner")) {
					Log.warning("No valid shop type specified for shopkeeper '" + id + "': defaulting to "
							+ DefaultShopTypes.PLAYER_NORMAL().getIdentifier());
					shopType = DefaultShopTypes.PLAYER_NORMAL();
				} else {
					// no valid shop type given..
					Log.warning("Failed to load shopkeeper '" + id + "': unknown type");
					return false; // disable without save
				}
			}

			// MC 1.11: convert old skeleton and zombie variants to new mob types
			// TODO remove again in future updates
			boolean hasStrayType = false;
			boolean hasWitherSkeletonType = false;
			boolean hasZombieVillagerType = false;
			try {
				hasStrayType = (EntityType.valueOf("STRAY") != null);
			} catch (Exception e) {
			}
			try {
				hasWitherSkeletonType = (EntityType.valueOf("WITHER_SKELETON") != null);
			} catch (Exception e) {
			}
			try {
				hasZombieVillagerType = (EntityType.valueOf("ZOMBIE_VILLAGER") != null);
			} catch (Exception e) {
			}

			if (hasStrayType || hasWitherSkeletonType || hasZombieVillagerType) {
				String objectType = shopkeeperSection.getString("object");
				if ("skeleton".equalsIgnoreCase(objectType)) {
					String skeletonType = shopkeeperSection.getString("skeletonType");
					if (hasStrayType && "STRAY".equalsIgnoreCase(skeletonType)) {
						Log.warning("Converting skeleton shopkeeper '" + id + "' with stray variant to new stray shopkeeper.");
						shopkeeperSection.set("object", "stray");
					} else if (hasWitherSkeletonType && "WITHER".equalsIgnoreCase(skeletonType)) {
						Log.warning("Converting skeleton shopkeeper '" + id + "' with wither variant to new wither-skeleton shopkeeper.");
						shopkeeperSection.set("object", "wither_skeleton");
					}
				}
				if ("zombie".equalsIgnoreCase(objectType)) {
					if (hasZombieVillagerType && shopkeeperSection.getBoolean("villagerZombie")) {
						Log.warning("Converting zombie shopkeeper '" + id + "' with zombie-villager variant to new zombie-villager shopkeeper.");
						shopkeeperSection.set("object", "zombie_villager");
					}
				}
			}

			// load shopkeeper:
			try {
				Shopkeeper shopkeeper = shopType.loadShopkeeper(shopkeeperSection);
				if (shopkeeper == null) {
					throw new ShopkeeperCreateException("ShopType returned null shopkeeper!");
				}
			} catch (ShopkeeperCreateException e) {
				Log.warning("Failed to load shopkeeper '" + id + "': " + e.getMessage());
				return false;
			}
		}
		return true;
	}

	@Override
	public void save() {
		if (Settings.saveInstantly) {
			this.saveReal();
		} else {
			dirty = true;
		}
	}

	@Override
	public void saveReal() {
		this.saveReal(true);
	}

	// should only get called sync on disable:
	private void saveReal(boolean async) {
		if (skipSaving) {
			Log.debug("Skipped saving due to flag.");
			return;
		}

		// is another async save task already running?
		if (async && saveIOTask != -1) {
			// set flag which triggers a new save once that current task is done:
			saveRealAgain = true;
			return;
		}

		// store shopkeeper data into memory configuration:
		saveInfo.startTime = System.currentTimeMillis();
		final YamlConfiguration config = new YamlConfiguration();
		int counter = 1;
		for (Shopkeeper shopkeeper : shopkeepersById.values()) {
			String sectionKey = String.valueOf(counter++);
			ConfigurationSection section = config.createSection(sectionKey);
			try {
				shopkeeper.save(section);
			} catch (Exception e) {
				// error while saving shopkeeper data:
				// skip this shopkeeper and print warning + stacktrace:
				config.set(sectionKey, null);
				Log.warning("Couldn't save shopkeeper '" + shopkeeper.getUniqueId() + "' at " + shopkeeper.getPositionString() + "!");
				e.printStackTrace();
			}
		}
		// time to store shopkeeper data in memory configuration:
		saveInfo.packingDuration = System.currentTimeMillis() - saveInfo.startTime;

		dirty = false;

		if (!async) {
			// sync file io:
			this.saveDataToFile(config, null);
			// print debug info:
			saveInfo.printDebugInfo();
		} else {
			// async file io:
			saveIOTask = Bukkit.getScheduler().runTaskAsynchronously(this, new Runnable() {

				@Override
				public void run() {
					saveDataToFile(config, new Runnable() {

						@Override
						public void run() {
							// continue sync:
							Bukkit.getScheduler().runTask(ShopkeepersPlugin.this, new Runnable() {

								@Override
								public void run() {
									saveIOTask = -1;

									// print debug info:
									saveInfo.printDebugInfo();

									// did we get another request to saveReal() in the meantime?
									if (saveRealAgain) {
										// trigger another full save with latest data:
										saveRealAgain = false;
										saveReal();
									}
								}
							});
						}
					});
				}
			}).getTaskId();
		}
	}

	// max total delay: 500ms
	private static final int SAVING_MAX_ATTEMPTS = 20;
	private static final long SAVING_ATTEMPTS_DELAY_MILLIS = 25;

	// can be run async and sync:
	private void saveDataToFile(FileConfiguration config, Runnable callback) {
		assert config != null;

		saveInfo.ioStartTime = System.currentTimeMillis();

		File saveFile = this.getSaveFile();
		File tempSaveFile = new File(saveFile.getParentFile(), saveFile.getName() + ".temp");

		// first trying to save to a temporary save file
		// if all goes well, the save file gets replaced with the temporary file

		int savingAttempt = 0;
		boolean printStacktrace = true;
		String error;
		Exception exception;

		while (++savingAttempt <= SAVING_MAX_ATTEMPTS) {
			boolean problem = false;
			error = null;
			exception = null;

			// remove old temporary file, if there is one:
			if (!problem) {
				if (tempSaveFile.exists()) {
					if (!tempSaveFile.canWrite()) {
						error = "Cannot write to temporary save file! (" + tempSaveFile.getName() + ")";
						problem = true;
					} else {
						// remove old temporary save file:
						if (!tempSaveFile.delete()) {
							error = "Couldn't delete existing temporary save file! (" + tempSaveFile.getName() + ")";
							problem = true;
						}
					}
				}
			}

			// make sure that the parent directories exist:
			if (!problem) {
				File parentDir = tempSaveFile.getParentFile();
				if (parentDir != null && !parentDir.exists()) {
					if (!parentDir.mkdirs()) {
						error = "Couldn't create parent directories for temporary save file! (" + parentDir.getAbsolutePath() + ")";
						problem = true;
					}
				}
			}

			// create new temporary save file:
			if (!problem) {
				try {
					tempSaveFile.createNewFile();
				} catch (IOException e) {
					error = "Couldn't create temporary save file! (" + tempSaveFile.getName() + ")";
					exception = e;
					problem = true;
				}
			}

			// write shopkeeper data to temporary save file:
			if (!problem) {
				PrintWriter writer = null;
				try {
					if (Settings.fileEncoding != null && !Settings.fileEncoding.isEmpty()) {
						writer = new PrintWriter(tempSaveFile, Settings.fileEncoding);
						writer.write(config.saveToString());
					} else {
						config.save(tempSaveFile);
					}
				} catch (Exception e) {
					error = "Couldn't save data to temporary save file!(" + tempSaveFile.getName() + ")";
					exception = e;
					problem = true;
				} finally {
					if (writer != null) {
						writer.close();
					}
				}
			}

			// delete old save file:
			if (!problem) {
				if (saveFile.exists()) {
					if (!saveFile.canWrite()) {
						error = "Cannot write to save file! (" + saveFile.getName() + ")";
						problem = true;
					} else {
						// remove old save file:
						if (!saveFile.delete()) {
							error = "Couldn't delete existing old save file! (" + saveFile.getName() + ")";
							problem = true;
						}
					}
				}
			}

			// rename temporary save file:
			if (!problem) {
				if (!tempSaveFile.renameTo(saveFile)) {
					error = "Couldn't rename temporary save file! (" + tempSaveFile.getName() + " to " + saveFile.getName() + ")";
					problem = true;
				}
			}

			// handle problem situation:
			if (problem) {
				// don't spam with errors and stacktraces, only print them once for the first try:
				if (exception != null && printStacktrace) {
					printStacktrace = false;
					exception.printStackTrace();
				}
				Log.severe("Saving attempt " + savingAttempt + " failed: " + (error != null ? error : "Unknown error"));

				if (savingAttempt < SAVING_MAX_ATTEMPTS) {
					// try again after a small delay:
					try {
						Thread.sleep(SAVING_ATTEMPTS_DELAY_MILLIS);
					} catch (InterruptedException e) {
					}
				} else {
					// saving failed even after a bunch of retries:
					Log.severe("Saving failed! Save data might be lost! :(");
					break;
				}
			} else {
				// saving was successful:
				break;
			}
		}

		long now = System.currentTimeMillis();
		saveInfo.ioDuration = now - saveInfo.ioStartTime; // time for pure io
		saveInfo.fullDuration = now - saveInfo.startTime; // time from saveReal() call to finished save

		if (callback != null) {
			callback.run();
		}
	}

	private static class ConfirmEntry {

		private final Runnable action;
		private final int taskId;

		public ConfirmEntry(Runnable action, int taskId) {
			this.taskId = taskId;
			this.action = action;
		}

		public int getTaskId() {
			return taskId;
		}

		public Runnable getAction() {
			return action;
		}
	}
} public class Utils {

	/**
	 * Creates a clone of the given {@link ItemStack} with amount <code>1</code>.
	 * 
	 * @param item
	 *            the item to get a normalized version of
	 * @return the normalized item
	 */
	public static ItemStack getNormalizedItem(ItemStack item) {
		if (item == null) return null;
		ItemStack normalizedClone = item.clone();
		normalizedClone.setAmount(1);
		return normalizedClone;
	}

	// private static final ItemStack EMPTY_ITEM = new ItemStack(Material.AIR, 0);

	public static boolean isEmpty(ItemStack item) {
		return item == null || item.getType() == Material.AIR || item.getAmount() <= 0;
	}

	public static ItemStack getNullIfEmpty(ItemStack item) {
		return isEmpty(item) ? null : item;
	}

	/*public static ItemStack getEmptyIfNull(ItemStack item) {
		return item == null ? getEmptyItem() : item;
	}
	
	public static ItemStack normalizedIfEmpty(ItemStack item) {
		return isEmpty(item) ? EMPTY_ITEM : item;
	}

	public static ItemStack getEmptyItem() {
		return EMPTY_ITEM.clone();
	}*/

	public static boolean isChest(Material material) {
		return material == Material.CHEST || material == Material.TRAPPED_CHEST;
	}

	public static boolean isSign(Material material) {
		return material == Material.WALL_SIGN || material == Material.SIGN_POST || material == Material.SIGN;
	}

	// TODO temporary, due to a bukkit bug custom head item can currently not be saved
	public static boolean isCustomHeadItem(ItemStack item) {
		if (item == null) return false;
		if (item.getType() != Material.SKULL_ITEM) {
			return false;
		}
		if (item.getDurability() != SkullType.PLAYER.ordinal()) {
			return false;
		}

		ItemMeta meta = item.getItemMeta();
		if (meta instanceof SkullMeta) {
			SkullMeta skullMeta = (SkullMeta) meta;
			if (skullMeta.hasOwner() && skullMeta.getOwner() == null) {
				// custom head items usually don't have a valid owner
				return true;
			}
		}
		return false;
	}

	/**
	 * Checks if the given {@link BlockFace} is valid to be used for a wall sign.
	 * 
	 * @param blockFace
	 * @return
	 */
	public static boolean isWallSignFace(BlockFace blockFace) {
		return blockFace == BlockFace.NORTH || blockFace == BlockFace.SOUTH || blockFace == BlockFace.EAST || blockFace == BlockFace.WEST;
	}

	/**
	 * Determines the axis-aligned {@link BlockFace} for the given direction.
	 * If modY is zero only {@link BlockFace}s facing horizontal will be returned.
	 * This method takes into account that the values for EAST/WEST and NORTH/SOUTH
	 * were switched in some past version of bukkit. So it should also properly work
	 * with older bukkit versions.
	 * 
	 * @param modX
	 * @param modY
	 * @param modZ
	 * @return
	 */
	public static BlockFace getAxisBlockFace(double modX, double modY, double modZ) {
		double xAbs = Math.abs(modX);
		double yAbs = Math.abs(modY);
		double zAbs = Math.abs(modZ);

		if (xAbs >= zAbs) {
			if (xAbs >= yAbs) {
				if (modX >= 0.0D) {
					// EAST/WEST and NORTH/SOUTH values were switched in some past bukkit version:
					// with this additional checks it should work across different versions
					if (BlockFace.EAST.getModX() == 1) {
						return BlockFace.EAST;
					} else {
						return BlockFace.WEST;
					}
				} else {
					if (BlockFace.EAST.getModX() == 1) {
						return BlockFace.WEST;
					} else {
						return BlockFace.EAST;
					}
				}
			} else {
				if (modY >= 0.0D) {
					return BlockFace.UP;
				} else {
					return BlockFace.DOWN;
				}
			}
		} else {
			if (zAbs >= yAbs) {
				if (modZ >= 0.0D) {
					if (BlockFace.SOUTH.getModZ() == 1) {
						return BlockFace.SOUTH;
					} else {
						return BlockFace.NORTH;
					}
				} else {
					if (BlockFace.SOUTH.getModZ() == 1) {
						return BlockFace.NORTH;
					} else {
						return BlockFace.SOUTH;
					}
				}
			} else {
				if (modY >= 0.0D) {
					return BlockFace.UP;
				} else {
					return BlockFace.DOWN;
				}
			}
		}
	}

	/**
	 * Tries to find the nearest wall sign {@link BlockFace} facing towards the given direction.
	 * 
	 * @param direction
	 * @return a valid wall sign face
	 */
	public static BlockFace toWallSignFace(Vector direction) {
		assert direction != null;
		return getAxisBlockFace(direction.getX(), 0.0D, direction.getZ());
	}

	/**
	 * Gets the block face a player is looking at.
	 * 
	 * @param player
	 *            the player
	 * @param targetBlock
	 *            the block the player is looking at
	 * @return the block face, or null if none was found
	 */
	public static BlockFace getTargetBlockFace(Player player, Block targetBlock) {
		Location intersection = getBlockIntersection(player, targetBlock);
		if (intersection == null) return null;
		Location blockCenter = targetBlock.getLocation().add(0.5D, 0.5D, 0.5D);
		Vector centerToIntersection = intersection.subtract(blockCenter).toVector();
		double x = centerToIntersection.getX();
		double y = centerToIntersection.getY();
		double z = centerToIntersection.getZ();
		return getAxisBlockFace(x, y, z);
	}

	/**
	 * Determines the exact intersection point of a players view and a targeted block.
	 * 
	 * @param player
	 *            the player
	 * @param targetBlock
	 *            the block the player is looking at
	 * @return the intersection point of the players view and the target block,
	 *         or null if no intersection was found
	 */
	public static Location getBlockIntersection(Player player, Block targetBlock) {
		if (player == null || targetBlock == null) return null;

		// block bounds:
		double minX = targetBlock.getX();
		double minY = targetBlock.getY();
		double minZ = targetBlock.getZ();

		double maxX = minX + 1.0D;
		double maxY = minY + 1.0D;
		double maxZ = minZ + 1.0D;

		// ray origin:
		Location origin = player.getEyeLocation();
		double originX = origin.getX();
		double originY = origin.getY();
		double originZ = origin.getZ();

		// ray direction
		Vector dir = origin.getDirection();
		double dirX = dir.getX();
		double dirY = dir.getY();
		double dirZ = dir.getZ();

		// tiny improvement to save a few divisions below:
		double divX = 1.0D / dirX;
		double divY = 1.0D / dirY;
		double divZ = 1.0D / dirZ;

		// intersection interval:
		double t0 = 0.0D;
		double t1 = Double.MAX_VALUE;

		double tmin;
		double tmax;

		double tymin;
		double tymax;

		double tzmin;
		double tzmax;

		if (dirX >= 0.0D) {
			tmin = (minX - originX) * divX;
			tmax = (maxX - originX) * divX;
		} else {
			tmin = (maxX - originX) * divX;
			tmax = (minX - originX) * divX;
		}

		if (dirY >= 0.0D) {
			tymin = (minY - originY) * divY;
			tymax = (maxY - originY) * divY;
		} else {
			tymin = (maxY - originY) * divY;
			tymax = (minY - originY) * divY;
		}

		if ((tmin > tymax) || (tymin > tmax)) {
			return null;
		}

		if (tymin > tmin) tmin = tymin;
		if (tymax < tmax) tmax = tymax;

		if (dirZ >= 0.0D) {
			tzmin = (minZ - originZ) * divZ;
			tzmax = (maxZ - originZ) * divZ;
		} else {
			tzmin = (maxZ - originZ) * divZ;
			tzmax = (minZ - originZ) * divZ;
		}

		if ((tmin > tzmax) || (tzmin > tmax)) {
			return null;
		}

		if (tzmin > tmin) tmin = tzmin;
		if (tzmax < tmax) tmax = tzmax;

		if ((tmin >= t1) || (tmax <= t0)) {
			return null;
		}

		// intersection:
		Location intersection = origin.add(dir.multiply(tmin));
		return intersection;
	}

	// messages:

	public static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.##", new DecimalFormatSymbols(Locale.US));
	static {
		DECIMAL_FORMAT.setGroupingUsed(false);
	}

	public static String getLocationString(Location location) {
		return getLocationString(location.getWorld().getName(), location.getX(), location.getY(), location.getZ());
	}

	public static String getLocationString(Block block) {
		return getLocationString(block.getWorld().getName(), block.getX(), block.getY(), block.getZ());
	}

	public static String getLocationString(String worldName, double x, double y, double z) {
		return worldName + "," + DECIMAL_FORMAT.format(x) + "," + DECIMAL_FORMAT.format(y) + "," + DECIMAL_FORMAT.format(z);
	}

	public static String translateColorCodesToAlternative(char altColorChar, String textToTranslate) {
		char[] b = textToTranslate.toCharArray();
		for (int i = 0; i < b.length - 1; i++) {
			if (b[i] == ChatColor.COLOR_CHAR && "0123456789AaBbCcDdEeFfKkLlMmNnOoRr".indexOf(b[i + 1]) > -1) {
				b[i] = altColorChar;
				b[i + 1] = Character.toLowerCase(b[i + 1]);
			}
		}
		return new String(b);
	}

	public static String decolorize(String colored) {
		if (colored == null) return null;
		return Utils.translateColorCodesToAlternative('&', colored);
	}

	public static List<String> decolorize(List<String> colored) {
		if (colored == null) return null;
		List<String> decolored = new ArrayList<String>(colored.size());
		for (String string : colored) {
			decolored.add(Utils.translateColorCodesToAlternative('&', string));
		}
		return decolored;
	}

	public static String colorize(String message) {
		if (message == null || message.isEmpty()) return message;
		return ChatColor.translateAlternateColorCodes('&', message);
	}

	public static List<String> colorize(List<String> messages) {
		if (messages == null) return messages;
		List<String> colored = new ArrayList<String>(messages.size());
		for (String message : messages) {
			colored.add(Utils.colorize(message));
		}
		return colored;
	}

	public static void sendMessage(CommandSender sender, String message, String... args) {
		// skip if sender is null or message is "empty":
		if (sender == null || message == null || message.isEmpty()) return;
		if (args != null && args.length >= 2) {
			// replace arguments (key-value replacement):
			String key;
			String value;
			for (int i = 1; i < args.length; i += 2) {
				key = args[i - 1];
				value = args[i];
				if (key == null || value == null) continue; // skip invalid arguments
				message = message.replace(key, value);
			}
		}

		String[] msgs = message.split("\n");
		for (String msg : msgs) {
			sender.sendMessage(msg);
		}
	}

	public static boolean isEmpty(String string) {
		return string == null || string.isEmpty();
	}

	public static String normalize(String identifier) {
		if (identifier == null) return null;
		return identifier.trim().replace('_', '-').replace(' ', '-').toLowerCase(Locale.ROOT);
	}

	public static List<String> normalize(List<String> identifiers) {
		if (identifiers == null) return null;
		List<String> normalized = new ArrayList<String>(identifiers.size());
		for (String identifier : identifiers) {
			normalized.add(normalize(identifier));
		}
		return normalized;
	}

	/**
	 * Performs a permissions check and logs debug information about it.
	 * 
	 * @param permissible
	 * @param permission
	 * @return
	 */
	public static boolean hasPermission(Permissible permissible, String permission) {
		assert permissible != null;
		boolean hasPerm = permissible.hasPermission(permission);
		if (!hasPerm && (permissible instanceof Player)) {
			Log.debug("Player '" + ((Player) permissible).getName() + "' does not have permission '" + permission + "'.");
		}
		return hasPerm;
	}

	// entity utilities:

	public static boolean isNPC(Entity entity) {
		return entity.hasMetadata("NPC");
	}

	public static List<Entity> getNearbyEntities(Location location, double radius, EntityType... types) {
		List<Entity> entities = new ArrayList<Entity>();
		if (location == null) return entities;
		if (radius <= 0.0D) return entities;

		double radius2 = radius * radius;
		int chunkRadius = ((int) (radius / 16)) + 1;
		Chunk center = location.getChunk();
		int startX = center.getX() - chunkRadius;
		int endX = center.getX() + chunkRadius;
		int startZ = center.getZ() - chunkRadius;
		int endZ = center.getZ() + chunkRadius;
		World world = location.getWorld();
		for (int chunkX = startX; chunkX <= endX; chunkX++) {
			for (int chunkZ = startZ; chunkZ <= endZ; chunkZ++) {
				if (!world.isChunkLoaded(chunkX, chunkZ)) continue;
				Chunk chunk = world.getChunkAt(chunkX, chunkZ);
				for (Entity entity : chunk.getEntities()) {
					Location entityLoc = entity.getLocation();
					// TODO: this is a workaround: for some yet unknown reason entities sometimes report to be in a
					// different world..
					if (!entityLoc.getWorld().equals(world)) {
						Log.debug("Found an entity which reports to be in a different world than the chunk we got it from:");
						Log.debug("Location=" + location + ", Chunk=" + chunk + ", ChunkWorld=" + chunk.getWorld()
								+ ", entityType=" + entity.getType() + ", entityLocation=" + entityLoc);
						continue; // skip this entity
					}

					if (entityLoc.distanceSquared(location) <= radius2) {
						if (types == null) {
							entities.add(entity);
						} else {
							EntityType type = entity.getType();
							for (EntityType t : types) {
								if (type.equals(t)) {
									entities.add(entity);
									break;
								}
							}
						}
					}
				}
			}
		}
		return entities;
	}

	// itemstack utilities:

	public static ItemStack createItemStack(Material type, int amount, short data, String displayName, List<String> lore) {
		// TODO return null in case of type AIR?
		ItemStack item = new ItemStack(type, amount, data);
		return setItemStackNameAndLore(item, displayName, lore);
	}

	public static ItemStack setItemStackNameAndLore(ItemStack item, String displayName, List<String> lore) {
		if (item == null) return null;
		ItemMeta meta = item.getItemMeta();
		if (meta != null) {
			meta.setDisplayName(displayName);
			meta.setLore(lore);
			item.setItemMeta(meta);
		}
		return item;
	}

	public static String getSimpleItemInfo(ItemStack item) {
		if (item == null) return "none";
		StringBuilder sb = new StringBuilder();
		sb.append(item.getType()).append('~').append(item.getDurability());
		return sb.toString();
	}

	public static String getSimpleRecipeInfo(ItemStack[] recipe) {
		if (recipe == null) return "none";
		StringBuilder sb = new StringBuilder();
		sb.append("[0=").append(getSimpleItemInfo(recipe[0]))
				.append(",1=").append(getSimpleItemInfo(recipe[1]))
				.append(",2=").append(getSimpleItemInfo(recipe[2])).append("]");
		return sb.toString();
	}

	/**
	 * Same as {@link ItemStack#isSimilar(ItemStack)}, but taking into account that both given ItemStacks might be
	 * <code>null</code>.
	 * 
	 * @param item1
	 *            an itemstack
	 * @param item2
	 *            another itemstack
	 * @return <code>true</code> if the given item stacks are both <code>null</code> or similar
	 */
	public static boolean isSimilar(ItemStack item1, ItemStack item2) {
		if (item1 == null) return (item2 == null);
		return item1.isSimilar(item2);
	}

	/**
	 * Checks if the given item matches the specified attributes.
	 * 
	 * @param item
	 *            the item
	 * @param type
	 *            The item type.
	 * @param data
	 *            The data value/durability. If -1 is is ignored.
	 * @param displayName
	 *            The displayName. If null or empty it is ignored.
	 * @param lore
	 *            The item lore. If null or empty it is ignored.
	 * @return <code>true</code> if the item has similar attributes
	 */
	public static boolean isSimilar(ItemStack item, Material type, short data, String displayName, List<String> lore) {
		if (item == null) return false;
		if (item.getType() != type) return false;
		if (data != -1 && item.getDurability() != data) return false;

		ItemMeta itemMeta = null;
		// compare display name:
		if (displayName != null && !displayName.isEmpty()) {
			if (!item.hasItemMeta()) return false;
			itemMeta = item.getItemMeta();
			if (itemMeta == null) return false;

			if (!itemMeta.hasDisplayName() || !displayName.equals(itemMeta.getDisplayName())) {
				return false;
			}
		}

		// compare lore:
		if (lore != null && !lore.isEmpty()) {
			if (itemMeta == null) {
				if (!item.hasItemMeta()) return false;
				itemMeta = item.getItemMeta();
				if (itemMeta == null) return false;
			}

			if (!itemMeta.hasLore() || !lore.equals(itemMeta.getLore())) {
				return false;
			}
		}

		return true;
	}

	// save and load itemstacks from config, including attributes:

	/**
	 * Saves the given {@link ItemStack} to the given configuration section.
	 * Also saves the item's attributes in the same section at '{node}_attributes'.
	 * 
	 * @param section
	 *            a configuration section
	 * @param node
	 *            where to save the item stack inside the section
	 * @param item
	 *            the item stack to save, can be null
	 */
	public static void saveItem(ConfigurationSection section, String node, ItemStack item) {
		assert section != null && node != null;
		section.set(node, item);
		// saving attributes manually, as they weren't saved by bukkit in the past:
		String attributes = NMSManager.getProvider().saveItemAttributesToString(item);
		if (attributes != null && !attributes.isEmpty()) {
			String attributesNode = node + "_attributes";
			section.set(attributesNode, attributes);
		}
	}

	/**
	 * Loads an {@link ItemStack} from the given configuration section.
	 * Also attempts to load attributes saved at '{node}_attributes'.
	 * 
	 * @param section
	 *            a configuration section
	 * @param node
	 *            where to load the item stack from inside the section
	 * @return the loaded item stack, possibly null
	 */
	public static ItemStack loadItem(ConfigurationSection section, String node) {
		assert section != null && node != null;
		ItemStack item = section.getItemStack(node);
		// loading separately stored attributes:
		String attributesNode = node + "_attributes";
		if (item != null && section.contains(attributesNode)) {
			String attributes = section.getString(attributesNode);
			if (attributes != null && !attributes.isEmpty()) {
				item = NMSManager.getProvider().loadItemAttributesFromString(item, attributes);
			}
		}
		return item;
	}

	// inventory utilities:

	public static List<ItemCount> getItemCountsFromInventory(Inventory inventory, Filter<ItemStack> filter) {
		List<ItemCount> itemCounts = new ArrayList<ItemCount>();
		if (inventory != null) {
			ItemStack[] contents = inventory.getContents();
			for (ItemStack item : contents) {
				if (isEmpty(item)) continue;
				if (filter != null && !filter.accept(item)) continue;

				// check if we already have a counter for this type of item:
				ItemCount itemCount = ItemCount.findSimilar(itemCounts, item);
				if (itemCount != null) {
					// increase item count:
					itemCount.addAmount(item.getAmount());
				} else {
					// add new item entry:
					itemCounts.add(new ItemCount(item, item.getAmount()));
				}
			}
		}
		return itemCounts;
	}

	/**
	 * Checks if the given inventory contains at least a certain amount of items which match the specified attributes.
	 * 
	 * @param inv
	 * @param type
	 *            The item type.
	 * @param data
	 *            The data value/durability. If -1 is is ignored.
	 * @param displayName
	 *            The displayName. If null it is ignored.
	 * @param lore
	 *            The item lore. If null or empty it is ignored.
	 * @param ignoreNameAndLore
	 * @param amount
	 * @return
	 */
	public static boolean hasInventoryItemsAtLeast(Inventory inv, Material type, short data, String displayName, List<String> lore, int amount) {
		for (ItemStack is : inv.getContents()) {
			if (!Utils.isSimilar(is, type, data, displayName, lore)) continue;
			int currentAmount = is.getAmount() - amount;
			if (currentAmount >= 0) {
				return true;
			} else {
				amount = -currentAmount;
			}
		}
		return false;
	}

	/**
	 * Adds the given {@link ItemStack} to the given contents.
	 * 
	 * <p>
	 * This will first try to fill similar partial {@link ItemStack}s in the contents up to the item's max stack size.
	 * Afterwards it will insert the remaining amount into empty slots, splitting at the item's max stack size.<br>
	 * This does not modify the original item stacks. If it has to modify the amount of an item stack, it first replaces
	 * it with a copy. So in case those item stacks are mirroring changes to their minecraft counterpart, those don't
	 * get affected directly.
	 * </p>
	 * 
	 * @param contents
	 *            The contents to add the given {@link ItemStack} to.
	 * @param item
	 *            The {@link ItemStack} to add to the given contents.
	 * @return The amount of items which couldn't be added (0 on full success).
	 */
	public static int addItems(ItemStack[] contents, ItemStack item) {
		Validate.notNull(contents);
		Validate.notNull(item);
		int amount = item.getAmount();
		Validate.isTrue(amount >= 0);
		if (amount == 0) return 0;

		// search for partially fitting item stacks:
		int maxStackSize = item.getMaxStackSize();
		int size = contents.length;
		for (int slot = 0; slot < size; slot++) {
			ItemStack slotItem = contents[slot];

			// slot empty? - skip, because we are currently filling existing item stacks up
			if (isEmpty(slotItem)) continue;

			// slot already full?
			int slotAmount = slotItem.getAmount();
			if (slotAmount >= maxStackSize) continue;

			if (slotItem.isSimilar(item)) {
				// copy itemstack, so we don't modify the original itemstack:
				slotItem = slotItem.clone();
				contents[slot] = slotItem;

				int newAmount = slotAmount + amount;
				if (newAmount <= maxStackSize) {
					// remaining amount did fully fit into this stack:
					slotItem.setAmount(newAmount);
					return 0;
				} else {
					// did not fully fit:
					slotItem.setAmount(maxStackSize);
					amount -= (maxStackSize - slotAmount);
					assert amount != 0;
				}
			}
		}

		// we have items remaining:
		assert amount > 0;

		// search for free slots:
		for (int slot = 0; slot < size; slot++) {
			ItemStack slotItem = contents[slot];
			if (isEmpty(slotItem)) {
				// found free slot:
				if (amount > maxStackSize) {
					// add full stack:
					ItemStack stack = item.clone();
					stack.setAmount(maxStackSize);
					contents[slot] = stack;
					amount -= maxStackSize;
				} else {
					// completely fits:
					ItemStack stack = item.clone(); // create a copy, just in case
					stack.setAmount(amount); // stack of remaining amount
					contents[slot] = stack;
					return 0;
				}
			}
		}

		// not all items did fit into the inventory:
		return amount;
	}

	/**
	 * Removes the given {@link ItemStack} from the given contents.
	 * 
	 * <p>
	 * If the amount of the given {@link ItemStack} is {@link Integer#MAX_VALUE}, then all similar items are being
	 * removed from the contents.<br>
	 * This does not modify the original item stacks. If it has to modify the amount of an item stack, it first replaces
	 * it with a copy. So in case those item stacks are mirroring changes to their minecraft counterpart, those don't
	 * get affected directly.
	 * </p>
	 * 
	 * @param contents
	 *            The contents to remove the given {@link ItemStack} from.
	 * @param item
	 *            The {@link ItemStack} to remove from the given contents.
	 * @return The amount of items which couldn't be removed (0 on full success).
	 */
	public static int removeItems(ItemStack[] contents, ItemStack item) {
		Validate.notNull(contents);
		Validate.notNull(item);
		int amount = item.getAmount();
		Validate.isTrue(amount >= 0);
		if (amount == 0) return 0;

		boolean removeAll = (amount == Integer.MAX_VALUE);
		int size = contents.length;
		for (int slot = 0; slot < size; slot++) {
			ItemStack slotItem = contents[slot];
			if (slotItem == null) continue;
			if (item.isSimilar(slotItem)) {
				if (removeAll) {
					contents[slot] = null;
				} else {
					int newAmount = slotItem.getAmount() - amount;
					if (newAmount > 0) {
						// copy itemstack, so we don't modify the original itemstack:
						slotItem = slotItem.clone();
						contents[slot] = slotItem;
						slotItem.setAmount(newAmount);
						// all items were removed:
						return 0;
					} else {
						contents[slot] = null;
						amount = -newAmount;
						if (amount == 0) {
							// all items were removed:
							return 0;
						}
					}
				}
			}
		}

		if (removeAll) return 0;
		return amount;
	}

	/**
	 * Removes the specified amount of items which match the specified attributes from the given inventory.
	 * 
	 * @param inv
	 * @param type
	 *            The item type.
	 * @param data
	 *            The data value/durability. If -1 is is ignored.
	 * @param displayName
	 *            The displayName. If null it is ignored.
	 * @param lore
	 *            The item lore. If null or empty it is ignored.
	 * @param ignoreNameAndLore
	 * @param amount
	 */
	public static void removeItemsFromInventory(Inventory inv, Material type, short data, String displayName, List<String> lore, int amount) {
		for (ItemStack is : inv.getContents()) {
			if (!Utils.isSimilar(is, type, data, displayName, lore)) continue;
			int newamount = is.getAmount() - amount;
			if (newamount > 0) {
				is.setAmount(newamount);
				break;
			} else {
				inv.remove(is);
				amount = -newamount;
				if (amount == 0) break;
			}
		}
	}

	@SuppressWarnings("deprecation")
	public static void updateInventoryLater(final Player player) {
		Bukkit.getScheduler().runTaskLater(ShopkeepersPlugin.getInstance(), new Runnable() {

			@Override
			public void run() {
				player.updateInventory();
			}
		}, 3L);
	}

	public static Integer parseInt(String intString) {
		try {
			return Integer.parseInt(intString);
		} catch (NumberFormatException e) {
			return null;
		}
	}
} public final class NMSManager {

	private static NMSCallProvider provider;

	public static NMSCallProvider getProvider() {
		return NMSManager.provider;
	}

	public static void load(Plugin plugin) {
		final String packageName = plugin.getServer().getClass().getPackage().getName();
		String cbversion = packageName.substring(packageName.lastIndexOf('.') + 1);
		try {
			final Class<?> clazz = Class.forName("com.nisovin.shopkeepers.compat." + cbversion + ".NMSHandler");
			if (NMSCallProvider.class.isAssignableFrom(clazz)) {
				NMSManager.provider = (NMSCallProvider) clazz.getConstructor().newInstance();
			} else {
				throw new Exception("Nope");
			}
		} catch (final Exception e) {
			Log.severe("Potentially incompatible server version: " + cbversion);
			Log.severe("Shopkeepers is trying to run in 'compatibility mode'.");
			Log.info("Check for updates at: " + plugin.getDescription().getWebsite());

			try {
				NMSManager.provider = new FailedHandler();
			} catch (Exception e_u) {
				// uncomment for debugging:
				// e_u.printStackTrace();
			}
		}
	}
} public class OpenTradeEvent extends Event implements Cancellable {

	private final Player player;
	private final Shopkeeper shopkeeper;

	private boolean cancelled;

	public OpenTradeEvent(Player player, Shopkeeper shopkeeper) {
		this.player = player;
		this.shopkeeper = shopkeeper;
	}

	/**
	 * Gets the player attempting to trade.
	 * 
	 * @return the player
	 */
	public Player getPlayer() {
		return player;
	}

	/**
	 * Gets the shopkeeper the player is attempting to trade with.
	 * 
	 * @return the shopkeeper
	 */
	public Shopkeeper getShopkeeper() {
		return shopkeeper;
	}

	/**
	 * If cancelled the shop won't be opened.
	 */
	@Override
	public boolean isCancelled() {
		return cancelled;
	}

	/**
	 * If cancelled the shop won't be opened.
	 */
	@Override
	public void setCancelled(boolean cancelled) {
		this.cancelled = cancelled;
	}

	private static final HandlerList handlers = new HandlerList();

	@Override
	public HandlerList getHandlers() {
		return handlers;
	}

	public static HandlerList getHandlerList() {
		return handlers;
	}
} public class ShopkeeperTradeCompletedEvent extends Event {

	private final ShopkeeperTradeEvent completedTrade;

	public ShopkeeperTradeCompletedEvent(ShopkeeperTradeEvent completedTrade) {
		this.completedTrade = completedTrade;
	}

	/**
	 * Gets the trading player.
	 * 
	 * @return the player involved in this trade
	 */
	public Player getPlayer() {
		return completedTrade.getPlayer();
	}

	/**
	 * Gets the trading {@link Shopkeeper}.
	 * 
	 * @return the {@link Shopkeeper} involved in this trade
	 */
	public Shopkeeper getShopkeeper() {
		return completedTrade.getShopkeeper();
	}

	/**
	 * Gets the {@link InventoryClickEvent} which originally triggered this trade.
	 * 
	 * @return the original {@link InventoryClickEvent}
	 */
	public InventoryClickEvent getClickEvent() {
		return completedTrade.getClickEvent();
	}

	/**
	 * Gets the completed {@link ShopkeeperTradeEvent}.
	 * 
	 * <p>
	 * The {@link ShopkeeperTradeEvent} might hold additional information available about the completed trade.<br>
	 * Do not attempt to modify the {@link ShopkeeperTradeEvent} in any way!
	 * </p>
	 * 
	 * @return the completed {@link ShopkeeperTradeEvent}
	 */
	public ShopkeeperTradeEvent getCompletedTrade() {
		return completedTrade;
	}

	private static final HandlerList handlers = new HandlerList();

	@Override
	public HandlerList getHandlers() {
		return handlers;
	}

	public static HandlerList getHandlerList() {
		return handlers;
	}
} public abstract class PlayerShopkeeper extends Shopkeeper {

	protected static abstract class PlayerShopEditorHandler extends EditorHandler {

		protected PlayerShopEditorHandler(UIType uiType, PlayerShopkeeper shopkeeper) {
			super(uiType, shopkeeper);
		}

		@Override
		public PlayerShopkeeper getShopkeeper() {
			return (PlayerShopkeeper) super.getShopkeeper();
		}

		@Override
		protected boolean canOpen(Player player) {
			return super.canOpen(player) && (this.getShopkeeper().isOwner(player) || Utils.hasPermission(player, ShopkeepersAPI.BYPASS_PERMISSION));
		}

		@Override
		protected void onInventoryClick(InventoryClickEvent event, Player player) {
			int slot = event.getRawSlot();
			// prevent shift clicks on player inventory items:
			if (slot >= 27 && event.isShiftClick()) {
				event.setCancelled(true);
			}
			if (slot >= 18 && slot <= 25) {
				// change low cost:
				int column = slot - 18;
				ItemStack soldItem = event.getInventory().getItem(column);
				if (Utils.isEmpty(soldItem)) return;
				this.handleUpdateTradeCostItemOnClick(event, Settings.createCurrencyItem(1), Settings.createZeroCurrencyItem());
			} else if (slot >= 9 && slot <= 16) {
				// change high cost:
				int column = slot - 9;
				ItemStack soldItem = event.getInventory().getItem(column);
				if (Utils.isEmpty(soldItem)) return;
				this.handleUpdateTradeCostItemOnClick(event, Settings.createHighCurrencyItem(1), Settings.createHighZeroCurrencyItem());
			} else {
				super.onInventoryClick(event, player);
			}
		}

		protected void handleUpdateItemAmountOnClick(InventoryClickEvent event, int minAmount) {
			// cancel event:
			event.setCancelled(true);
			// ignore in certain situations:
			ItemStack clickedItem = event.getCurrentItem();
			if (Utils.isEmpty(clickedItem)) return;

			// get new item amount:
			int currentItemAmount = clickedItem.getAmount();
			if (minAmount <= 0) minAmount = 0;
			int newItemAmount = this.getNewAmountAfterEditorClick(event, currentItemAmount, minAmount, clickedItem.getMaxStackSize());
			assert newItemAmount >= minAmount;
			assert newItemAmount <= clickedItem.getMaxStackSize();

			// update item in inventory:
			if (newItemAmount == 0) {
				// empty item slot:
				event.setCurrentItem(null);
			} else {
				clickedItem.setAmount(newItemAmount);
			}
		}

		protected void handleUpdateTradeCostItemOnClick(InventoryClickEvent event, ItemStack currencyItem, ItemStack zeroCurrencyItem) {
			// cancel event:
			event.setCancelled(true);
			// ignore in certain situations:
			if (Utils.isEmpty(currencyItem)) return;

			// get new item amount:
			ItemStack clickedItem = event.getCurrentItem(); // can be null
			int currentItemAmount = 0;
			boolean isCurrencyItem = Utils.isSimilar(clickedItem, currencyItem);
			if (isCurrencyItem) {
				assert clickedItem != null;
				currentItemAmount = clickedItem.getAmount();
			}
			int newItemAmount = this.getNewAmountAfterEditorClick(event, currentItemAmount, 0, currencyItem.getMaxStackSize());
			assert newItemAmount >= 0;
			assert newItemAmount <= currencyItem.getMaxStackSize();

			// update item in inventory:
			if (newItemAmount == 0) {
				// place zero-currency item:
				event.setCurrentItem(zeroCurrencyItem);
			} else {
				if (isCurrencyItem) {
					// only update item amount of already existing currency item:
					clickedItem.setAmount(newItemAmount);
				} else {
					// place currency item with new amount:
					currencyItem.setAmount(newItemAmount);
					event.setCurrentItem(currencyItem);
				}
			}
		}

		protected void setEditColumnCost(Inventory inventory, int column, int cost) {
			if (cost > 0) {
				if (Settings.highCurrencyItem != Material.AIR && cost > Settings.highCurrencyMinCost) {
					int highCost = cost / Settings.highCurrencyValue;
					int lowCost = cost % Settings.highCurrencyValue;
					if (highCost > 0) {
						ItemStack item = Settings.createHighCurrencyItem(highCost);
						if (highCost > item.getMaxStackSize()) {
							lowCost += (highCost - item.getMaxStackSize()) * Settings.highCurrencyValue;
							item.setAmount(item.getMaxStackSize());
						}
						inventory.setItem(column + 9, item);
					} else {
						inventory.setItem(column + 9, Settings.createHighZeroCurrencyItem());
					}
					if (lowCost > 0) {
						ItemStack item = Settings.createCurrencyItem(lowCost);
						inventory.setItem(column + 18, item);
					} else {
						inventory.setItem(column + 18, Settings.createZeroCurrencyItem());
					}
				} else {
					ItemStack item = Settings.createCurrencyItem(cost);
					inventory.setItem(column + 18, item);
					if (Settings.highCurrencyItem != Material.AIR) {
						inventory.setItem(column + 9, Settings.createHighZeroCurrencyItem());
					}
				}
			} else {
				inventory.setItem(column + 18, Settings.createZeroCurrencyItem());
				if (Settings.highCurrencyItem != Material.AIR) {
					inventory.setItem(column + 9, Settings.createHighZeroCurrencyItem());
				}
			}
		}

		protected int getPriceFromColumn(Inventory inventory, int column) {
			ItemStack lowCostItem = inventory.getItem(column + 18);
			ItemStack highCostItem = inventory.getItem(column + 9);
			int cost = 0;
			if (lowCostItem != null && lowCostItem.getType() == Settings.currencyItem && lowCostItem.getAmount() > 0) {
				cost += lowCostItem.getAmount();
			}
			if (Settings.highCurrencyItem != Material.AIR && highCostItem != null && highCostItem.getType() == Settings.highCurrencyItem && highCostItem.getAmount() > 0) {
				cost += highCostItem.getAmount() * Settings.highCurrencyValue;
			}
			return cost;
		}
	}

	protected static abstract class PlayerShopTradingHandler extends TradingHandler {

		protected PlayerShopTradingHandler(UIType uiType, PlayerShopkeeper shopkeeper) {
			super(uiType, shopkeeper);
		}

		@Override
		public PlayerShopkeeper getShopkeeper() {
			return (PlayerShopkeeper) super.getShopkeeper();
		}

		@Override
		protected boolean canOpen(Player player) {
			if (!super.canOpen(player)) return false;
			final PlayerShopkeeper shopkeeper = this.getShopkeeper();

			// stop opening if trading shall be prevented while the owner is offline:
			if (Settings.preventTradingWhileOwnerIsOnline && !Utils.hasPermission(player, ShopkeepersAPI.BYPASS_PERMISSION)) {
				Player ownerPlayer = shopkeeper.getOwner();
				if (ownerPlayer != null) {
					Log.debug("Blocked trade window opening from " + player.getName() + " because the owner is online");
					Utils.sendMessage(player, Settings.msgCantTradeWhileOwnerOnline, "{owner}", ownerPlayer.getName());
					return false;
				}
			}
			return true;
		}

		@Override
		protected void onPurchaseClick(InventoryClickEvent event, Player player, ItemStack[] usedRecipe, ItemStack offered1, ItemStack offered2) {
			super.onPurchaseClick(event, player, usedRecipe, offered1, offered2);
			if (event.isCancelled()) return;
			final PlayerShopkeeper shopkeeper = this.getShopkeeper();

			if (Settings.preventTradingWithOwnShop && shopkeeper.isOwner(player) && !player.isOp()) {
				event.setCancelled(true);
				Log.debug("Cancelled trade from " + player.getName() + " because he can't trade with his own shop");
				return;
			}

			if (Settings.preventTradingWhileOwnerIsOnline && !Utils.hasPermission(player, ShopkeepersAPI.BYPASS_PERMISSION)) {
				Player ownerPlayer = shopkeeper.getOwner();
				if (ownerPlayer != null && !shopkeeper.isOwner(player)) {
					Utils.sendMessage(player, Settings.msgCantTradeWhileOwnerOnline, "{owner}", ownerPlayer.getName());
					event.setCancelled(true);
					Log.debug("Cancelled trade from " + event.getWhoClicked().getName() + " because the owner is online");
					return;
				}
			}
		}
	}

	protected static class PlayerShopHiringHandler extends HiringHandler {

		protected PlayerShopHiringHandler(UIType uiType, PlayerShopkeeper shopkeeper) {
			super(uiType, shopkeeper);
		}

		@Override
		public PlayerShopkeeper getShopkeeper() {
			return (PlayerShopkeeper) super.getShopkeeper();
		}

		@Override
		protected boolean canOpen(Player player) {
			return this.getShopkeeper().isForHire() && super.canOpen(player);
		}

		@Override
		protected boolean openWindow(Player player) {
			final PlayerShopkeeper shopkeeper = this.getShopkeeper();
			Inventory inventory = Bukkit.createInventory(player, 9, Settings.forHireTitle);

			ItemStack hireItem = Settings.createHireButtonItem();
			inventory.setItem(2, hireItem);
			inventory.setItem(6, hireItem);

			ItemStack hireCost = shopkeeper.getHireCost();
			if (hireCost == null) return false;
			inventory.setItem(4, hireCost);

			player.openInventory(inventory);
			return true;
		}

		@Override
		protected void onInventoryClick(InventoryClickEvent event, final Player player) {
			super.onInventoryClick(event, player);
			final PlayerShopkeeper shopkeeper = this.getShopkeeper();
			int slot = event.getRawSlot();
			if (slot == 2 || slot == 6) {
				ItemStack[] inventory = player.getInventory().getContents();
				ItemStack hireCost = shopkeeper.getHireCost().clone();
				for (int i = 0; i < inventory.length; i++) {
					ItemStack item = inventory[i];
					if (item != null && item.isSimilar(hireCost)) {
						if (item.getAmount() > hireCost.getAmount()) {
							item.setAmount(item.getAmount() - hireCost.getAmount());
							hireCost.setAmount(0);
							break;
						} else if (item.getAmount() == hireCost.getAmount()) {
							inventory[i] = null;
							hireCost.setAmount(0);
							break;
						} else {
							hireCost.setAmount(hireCost.getAmount() - item.getAmount());
							inventory[i] = null;
						}
					}
				}

				if (hireCost.getAmount() == 0) {
					int maxShops = ShopkeepersPlugin.getInstance().getMaxShops(player);

					// call event:
					PlayerShopkeeperHiredEvent hireEvent = new PlayerShopkeeperHiredEvent(player, shopkeeper, maxShops);
					Bukkit.getPluginManager().callEvent(hireEvent);
					if (hireEvent.isCancelled()) {
						// close window for this player:
						this.closeDelayed(player);
						return;
					}

					// check max shops limit:
					maxShops = hireEvent.getMaxShopsForPlayer();
					if (maxShops > 0) {
						int count = ShopkeepersPlugin.getInstance().countShopsOfPlayer(player);
						if (count >= maxShops) {
							Utils.sendMessage(player, Settings.msgTooManyShops);
							this.closeDelayed(player);
							return;
						}
					}

					// hire it:
					player.getInventory().setContents(inventory); // apply inventory changes
					shopkeeper.setForHire(false, null);
					shopkeeper.setOwner(player);
					ShopkeepersPlugin.getInstance().save();
					Utils.sendMessage(player, Settings.msgHired);

					// close all open windows for this shopkeeper:
					shopkeeper.closeAllOpenWindows();
					return;
				} else {
					// not enough money:
					Utils.sendMessage(player, Settings.msgCantHire);
					// close window for this player:
					this.closeDelayed(player);
				}
			}
		}
	}

	protected UUID ownerUUID; // not null after successful initialization
	protected String ownerName;
	protected int chestX;
	protected int chestY;
	protected int chestZ;
	protected boolean forHire = false;
	protected ItemStack hireCost = null;

	/**
	 * For use in extending classes.
	 */
	protected PlayerShopkeeper() {
	}

	@Override
	protected void initOnCreation(ShopCreationData creationData) throws ShopkeeperCreateException {
		super.initOnCreation(creationData);
		Player owner = creationData.creator;
		Block chest = creationData.chest;
		Validate.notNull(owner);
		Validate.notNull(chest);

		this.ownerUUID = owner.getUniqueId();
		this.ownerName = owner.getName();
		this.setChest(chest.getX(), chest.getY(), chest.getZ());
	}

	@Override
	protected void onInitDone() {
		super.onInitDone();
		this.registerUIHandler(new PlayerShopHiringHandler(DefaultUIs.HIRING_WINDOW, this));
	}

	@Override
	protected void onRegistration(int sessionId) {
		super.onRegistration(sessionId);

		// register protected chest:
		ShopkeepersPlugin.getInstance().getProtectedChests().addChest(worldName, chestX, chestY, chestZ, this);
	}

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

		// unregister previously protected chest:
		ShopkeepersPlugin.getInstance().getProtectedChests().removeChest(worldName, chestX, chestY, chestZ, this);
	}

	@Override
	public boolean openChestWindow(Player player) {
		Log.debug("checking open chest window ..");
		// make sure the chest still exists
		Block chest = this.getChest();
		if (Utils.isChest(chest.getType())) {
			// open the chest directly as the player (no need for a custom UI)
			Log.debug("opening chest inventory window");
			Inventory inv = ((Chest) chest.getState()).getInventory();
			player.openInventory(inv);
			return true;
		}
		return false;
	}

	@Override
	protected void onPlayerInteraction(final Player player) {
		// naming via item:
		ItemStack itemInHand = player.getItemInHand();
		if (Settings.namingOfPlayerShopsViaItem && Settings.isNamingItem(itemInHand)) {
			// check if player can edit this shopkeeper:
			PlayerShopEditorHandler editorHandler = (PlayerShopEditorHandler) this.getUIHandler(DefaultUIs.EDITOR_WINDOW.getIdentifier());
			if (editorHandler.canOpen(player)) {
				// rename with the player's item in hand:
				String newName;
				ItemMeta itemMeta;
				if (!itemInHand.hasItemMeta() || (itemMeta = itemInHand.getItemMeta()) == null || !itemMeta.hasDisplayName()) {
					newName = "";
				} else {
					newName = itemMeta.getDisplayName();
				}

				// handled name changing:
				if (this.requestNameChange(player, newName)) {
					// manually remove rename item from player's hand after this event is processed:
					Bukkit.getScheduler().runTask(ShopkeepersPlugin.getInstance(), new Runnable() {
						public void run() {
							ItemStack itemInHand = player.getItemInHand();
							if (itemInHand.getAmount() <= 1) {
								player.setItemInHand(null);
							} else {
								itemInHand.setAmount(itemInHand.getAmount() - 1);
								player.setItemInHand(itemInHand);
							}
						}
					});
				}

				return;
			}
		}

		// TODO what if something is replacing the default PlayerShopHiringHandler with some other kind of handler?
		PlayerShopHiringHandler hiringHandler = (PlayerShopHiringHandler) this.getUIHandler(DefaultUIs.HIRING_WINDOW.getIdentifier());
		if (!player.isSneaking() && hiringHandler.canOpen(player)) {
			// show hiring interface:
			this.openHireWindow(player);
		} else {
			super.onPlayerInteraction(player);
		}
	}

	@Override
	protected void load(ConfigurationSection config) throws ShopkeeperCreateException {
		super.load(config);
		try {
			ownerUUID = UUID.fromString(config.getString("owner uuid"));
		} catch (Exception e) {
			// uuid invalid or non-existent:
			throw new ShopkeeperCreateException("Missing owner uuid!");
		}
		ownerName = config.getString("owner", "unknown");

		if (!config.isInt("chestx") || !config.isInt("chesty") || !config.isInt("chestz")) {
			throw new ShopkeeperCreateException("Missing chest coordinate(s)");
		}

		// update chest:
		this.setChest(config.getInt("chestx"), config.getInt("chesty"), config.getInt("chestz"));

		forHire = config.getBoolean("forhire");
		hireCost = config.getItemStack("hirecost");
		if (forHire && hireCost == null) {
			Log.warning("Couldn't load hire cost! Disabling 'for hire' for shopkeeper at " + this.getPositionString());
			forHire = false;
		}
	}

	@Override
	protected void save(ConfigurationSection config) {
		super.save(config);
		config.set("owner uuid", ownerUUID.toString());
		config.set("owner", ownerName);
		config.set("chestx", chestX);
		config.set("chesty", chestY);
		config.set("chestz", chestZ);
		config.set("forhire", forHire);
		config.set("hirecost", hireCost);
	}

	/**
	 * Sets the owner of this shop (sets name and uuid).
	 * 
	 * @param player
	 *            the owner of this shop
	 */
	public void setOwner(Player player) {
		this.setOwner(player.getUniqueId(), player.getName());
	}

	public void setOwner(UUID ownerUUID, String ownerName) {
		this.ownerUUID = ownerUUID;
		this.ownerName = ownerName;
		// TODO do this in a more abstract way
		if (!Settings.allowRenamingOfPlayerNpcShops && this.getShopObject().getObjectType() == DefaultShopObjectTypes.CITIZEN()) {
			// update the npc's name:
			((CitizensShop) this.getShopObject()).setName(ownerName);
		} else if (this.getShopObject().getObjectType() == DefaultShopObjectTypes.SIGN()) {
			// update sign:
			((SignShop) this.getShopObject()).updateSign();
		}
	}

	/**
	 * Gets the uuid of the player who owns this shop.
	 * 
	 * @return the owners player uuid
	 */
	public UUID getOwnerUUID() {
		return ownerUUID;
	}

	/**
	 * Gets the last known name of the player who owns this shop.
	 * 
	 * @return the owners last known player name
	 */
	public String getOwnerName() {
		return ownerName;
	}

	public String getOwnerAsString() {
		return ownerName + "(" + ownerUUID.toString() + ")";
	}

	/**
	 * Checks if the given owner is owning this shop.
	 * 
	 * @param player
	 *            the player to check
	 * @return <code>true</code> if the given player owns this shop
	 */
	public boolean isOwner(Player player) {
		return player.getUniqueId().equals(ownerUUID);
	}

	/**
	 * Gets the owner of this shop IF he is online.
	 * 
	 * @return the owner of this shop, or <code>null</code> if the owner is offline
	 */
	public Player getOwner() {
		return Bukkit.getPlayer(ownerUUID);
	}

	public boolean isForHire() {
		return forHire;
	}

	public void setForHire(boolean forHire, ItemStack hireCost) {
		if (forHire) {
			Validate.notNull(hireCost);
		} else {
			Validate.isTrue(hireCost == null);
		}

		this.forHire = forHire;
		this.hireCost = hireCost;
		if (forHire) {
			this.setName(Settings.forHireTitle);
		} else {
			this.setName("");
		}
	}

	public ItemStack getHireCost() {
		return hireCost;
	}

	private void setChest(int chestX, int chestY, int chestZ) {
		if (this.isValid()) {
			// unregister previously protected chest:
			ShopkeepersPlugin.getInstance().getProtectedChests().removeChest(worldName, chestX, chestY, chestZ, this);
		}

		// update chest:
		this.chestX = chestX;
		this.chestY = chestY;
		this.chestZ = chestZ;

		if (this.isValid()) {
			// register new protected chest:
			ShopkeepersPlugin.getInstance().getProtectedChests().addChest(worldName, chestX, chestY, chestZ, this);
		}
	}

	/**
	 * Checks whether this shop uses the indicated chest.
	 * 
	 * @param chest
	 *            the chest to check
	 * @return
	 */
	public boolean usesChest(Block chest) {
		if (!chest.getWorld().getName().equals(worldName)) return false;
		int x = chest.getX();
		int y = chest.getY();
		int z = chest.getZ();
		if (x == chestX && y == chestY && z == chestZ) return true;
		if (x == chestX + 1 && y == chestY && z == chestZ) return true;
		if (x == chestX - 1 && y == chestY && z == chestZ) return true;
		if (x == chestX && y == chestY && z == chestZ + 1) return true;
		if (x == chestX && y == chestY && z == chestZ - 1) return true;
		return false;
	}

	public Block getChest() {
		return Bukkit.getWorld(worldName).getBlockAt(chestX, chestY, chestZ);
	}

	protected void setRecipeCost(ItemStack[] recipe, int cost) {
		int lowCostSlot = 0;
		int lowCost = cost;

		if (Settings.highCurrencyItem != Material.AIR && cost > Settings.highCurrencyMinCost) {
			int highCost = cost / Settings.highCurrencyValue;
			lowCost = cost % Settings.highCurrencyValue;
			if (highCost > 0) {
				lowCostSlot = 1; // we put the high cost in the first slot instead
				ItemStack item = Settings.createHighCurrencyItem(highCost);
				recipe[0] = item;
				int maxStackSize = item.getMaxStackSize();
				if (highCost > maxStackSize) {
					item.setAmount(maxStackSize);
					lowCost += (highCost - maxStackSize) * Settings.highCurrencyValue;
				}
			}
		}

		if (lowCost > 0) {
			ItemStack item = Settings.createCurrencyItem(lowCost);
			recipe[lowCostSlot] = item;
			int maxStackSize = item.getMaxStackSize();
			if (lowCost > maxStackSize) {
				Log.warning("Shopkeeper at " + worldName + "," + x + "," + y + "," + z + " owned by " + ownerName + " has an invalid cost!");
			}
		}
	}

	protected int getCurrencyInChest() {
		int total = 0;
		Block chest = this.getChest();
		if (Utils.isChest(chest.getType())) {
			Inventory inv = ((Chest) chest.getState()).getInventory();
			ItemStack[] contents = inv.getContents();
			for (ItemStack item : contents) {
				if (Settings.isCurrencyItem(item)) {
					total += item.getAmount();
				} else if (Settings.isHighCurrencyItem(item)) {
					total += item.getAmount() * Settings.highCurrencyValue;
				}
			}
		}
		return total;
	}

	protected List<ItemCount> getItemsFromChest(Filter<ItemStack> filter) {
		Inventory chestInventory = null;
		Block chest = this.getChest();
		if (Utils.isChest(chest.getType())) {
			chestInventory = ((Chest) chest.getState()).getInventory();
		}
		return Utils.getItemCountsFromInventory(chestInventory, filter);
	}
} public abstract class UIHandler {

	private final UIType uiType;
	private final Shopkeeper shopkeeper;

	protected UIHandler(UIType uiType, Shopkeeper shopkeeper) {
		this.uiType = uiType;
		this.shopkeeper = shopkeeper;
	}

	public UIType getUIType() {
		return uiType;
	}

	/**
	 * Informs the uiManager that the window is/gets closed for this player.
	 * This is only needed if the inventory gets manually closed by a plugin.
	 * 
	 * @param player
	 *            the player whose user interface inventory gets closed
	 */
	protected void informOnClose(Player player) {
		assert player != null;
		ShopkeepersPlugin.getInstance().getUIManager().onInventoryClose(player);
	}

	/**
	 * Gets the shopkeeper for which this object is handling the specific interface type for.
	 * 
	 * @return the shopkeeper
	 */
	public Shopkeeper getShopkeeper() {
		return shopkeeper;
	}

	/**
	 * Temporary deactivates ui's for the affected shopkeeper and closes the window (inventory)
	 * for the given player after a tiny delay.
	 * 
	 * @param player
	 */
	protected void closeDelayed(final Player player) {
		// temporary deactivate ui and close open window delayed for this player:
		shopkeeper.deactivateUI();
		Bukkit.getScheduler().runTaskLater(ShopkeepersPlugin.getInstance(), new Runnable() {

			@Override
			public void run() {
				informOnClose(player);
				player.closeInventory();

				// reactivate ui:
				shopkeeper.activateUI();
			}
		}, 1L);
	}

	/**
	 * Checks whether or not the given player can open the handled interface for this shopkeeper.
	 * This for example gets called when a player requests the interface for this shopkeeper.
	 * It should perform the necessary permission checks.
	 * 
	 * @param player
	 *            a player
	 * @return true, if the given player is allowed to open the interface window type this class is handling
	 */
	protected abstract boolean canOpen(Player player);

	/**
	 * This method should open the interface window for the given player.
	 * Generally {@link #canOpen(Player) canOpen} should be checked before this method gets called,
	 * however this method should not rely on that.
	 * 
	 * @param player
	 *            a player
	 * @return true, if the interface window was successfully opened
	 */
	protected abstract boolean openWindow(Player player);

	/**
	 * Checks whether or not the given inventory is a custom inventory created by this handler (for example
	 * by comparing the inventory titles). The result of this method gets checked before any inventory events
	 * are passed through to this handler.
	 * 
	 * @param inventory
	 *            an inventory
	 * @return true, if the given inventory is representing a custom interface window created and handled by this
	 *         handler
	 */
	public abstract boolean isWindow(Inventory inventory);

	// handling of interface window interaction

	/**
	 * Called when a player closes an inventory for which (@link #isInterface(Inventory) isInterface) returned true.
	 * It is not guaranteed that this method gets called for all user interface windows which were opened by this
	 * handler (for example plugin triggered closing of a players inventory might not trigger a call to this method). So
	 * don't rely on it for cleanup.
	 * 
	 * @param event
	 *            the event which triggered this method call
	 * @param player
	 *            the player who closed the inventory
	 */
	protected abstract void onInventoryClose(InventoryCloseEvent event, Player player);

	/**
	 * Called when a player triggers an InventoryClickEvent for an inventory for which {@link #isWindow(Inventory)}
	 * returned true.
	 * 
	 * @param event
	 *            the event which triggered this method call
	 * @param player
	 *            the clicking player
	 */
	protected abstract void onInventoryClick(InventoryClickEvent event, Player player);
} public class UIType extends AbstractType {

	public UIType(String windowIdentifier, String permission) {
		super(windowIdentifier, permission);
	}
}
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.MerchantInventory;
import com.nisovin.shopkeepers.Log;
import com.nisovin.shopkeepers.Settings;
import com.nisovin.shopkeepers.Shopkeeper;
import com.nisovin.shopkeepers.ShopkeepersAPI;
import com.nisovin.shopkeepers.ShopkeepersPlugin;
import com.nisovin.shopkeepers.Utils;
import com.nisovin.shopkeepers.compat.NMSManager;
import com.nisovin.shopkeepers.events.OpenTradeEvent;
import com.nisovin.shopkeepers.events.ShopkeeperTradeCompletedEvent;
import com.nisovin.shopkeepers.events.ShopkeeperTradeEvent;
import com.nisovin.shopkeepers.shoptypes.PlayerShopkeeper;
import com.nisovin.shopkeepers.ui.UIHandler;
import com.nisovin.shopkeepers.ui.UIType;
package com.nisovin.shopkeepers.ui.defaults;




public class TradingHandler extends UIHandler {

	public TradingHandler(UIType uiType, Shopkeeper shopkeeper) {
		super(uiType, shopkeeper);
	}

	@Override
	protected boolean canOpen(Player player) {
		assert player != null;
		if (!Utils.hasPermission(player, ShopkeepersAPI.TRADE_PERMISSION)) {
			Log.debug("Blocked trade window opening from " + player.getName() + ": missing trade permission");
			Utils.sendMessage(player, Settings.msgMissingTradePerm);
			return false;
		}
		return true;
	}

	@Override
	protected boolean openWindow(Player player) {
		final Shopkeeper shopkeeper = this.getShopkeeper();
		OpenTradeEvent event = new OpenTradeEvent(player, shopkeeper);
		Bukkit.getPluginManager().callEvent(event);
		if (event.isCancelled()) {
			Log.debug("Trade window not opened: cancelled by another plugin");
			return false;
		}

		// create and open trading window:
		String title = this.getInventoryTitle();
		return NMSManager.getProvider().openTradeWindow(title, shopkeeper.getRecipes(), player);