public abstract class SelectableTypeRegistry<T extends SelectableType> extends TypeRegistry<T> {

	private class LinkData {
		private T next = null;
	}

	private final Map<String, LinkData> links = new HashMap<String, LinkData>();
	private T first = null;
	private T last = null;

	@Override
	public boolean register(T type) {
		if (super.register(type)) {
			if (first == null) {
				first = type;
			}
			LinkData data = new LinkData();
			links.put(type.getIdentifier(), data);
			if (last != null) {
				links.get(last.getIdentifier()).next = type;
			}
			last = type;
			return true;
		}
		return false;
	}

	protected T getFirst() {
		return first;
	}

	protected T getNext(T current) {
		LinkData data = current != null ? links.get(current.getIdentifier()) : null;
		return (data == null || data.next == null) ? first : data.next;
	}

	protected boolean canBeSelected(Player player, T type) {
		assert player != null && type != null;
		return type.isEnabled() && type.hasPermission(player);
	}

	protected T getNext(Player player, T current) {
		assert player != null;
		T next = current;

		int count = this.numberOfRegisteredTypes();
		while (count > 0) {
			next = this.getNext(next); // automatically selects the first type, if next is null or if next is the last
										// type
			if (this.canBeSelected(player, next)) {
				break;
			}
			count--;
		}

		// use the currently selected type (can be null) after it went through all types and didn't find one the player
		// can use:
		if (count == 0) {
			// check if the currently selected type can still be used by this player:
			if (current != null && !this.canBeSelected(player, current)) current = null;
			next = current;
		}
		return next;
	}

	// SELECTION MANAGEMENT

	protected final Map<String, T> selections = new HashMap<String, T>();

	/**
	 * Gets the first select-able type for this player, starting at the default one.
	 * 
	 * @param player
	 *            a player
	 * @return the first select-able type for this player, or null if this player can't select/use any type at all
	 */
	public T getDefaultSelection(Player player) {
		return this.getNext(player, null);
	}

	// public

	/**
	 * Gets the first select-able type for this player, starting at the currently selected one.
	 * 
	 * @param player
	 * @return the first select-able type for this player, or null if this player can't select/use any type at all
	 */
	public T getSelection(Player player) {
		Validate.notNull(player);
		String playerName = player.getName();
		T current = selections.get(playerName);
		// if none is currently selected, let's search for the first type this player can use:
		if (current == null || !this.canBeSelected(player, current)) current = this.getNext(player, current);
		return current; // returns null if the player can use no type at all
	}

	public T selectNext(Player player) {
		Validate.notNull(player);
		String playerName = player.getName();
		T current = selections.get(playerName);
		T next = this.getNext(player, current);
		if (next != null) {
			selections.put(playerName, next);
			next.onSelect(player);
		} else {
			// for now remember the current selection
			// selections.remove(playerName);
		}
		return next;
	}

	public void clearSelection(Player player) {
		assert player != null;
		String playerName = player.getName();
		selections.remove(playerName);
	}

	public void clearAllSelections() {
		selections.clear();
	}
} 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 CreatePlayerShopkeeperEvent extends Event implements Cancellable {

	private final ShopCreationData creationData;

	private int maxShops;
	private boolean cancelled;

	public CreatePlayerShopkeeperEvent(ShopCreationData creationData, int maxShops) {
		this.creationData = creationData;
		this.maxShops = maxShops;
	}

	/**
	 * Gets the raw shop creation data.
	 * 
	 * <p>
	 * Only modify the returned object if you know what you are doing.
	 * </p>
	 * 
	 * @return the raw shop creation data
	 */
	public ShopCreationData getShopCreationData() {
		return creationData;
	}

	/**
	 * Gets the player trying to create the shop.
	 * 
	 * @return the player
	 */
	public Player getPlayer() {
		return creationData.creator;
	}

	/**
	 * Gets the chest block that will be backing this shop.
	 * 
	 * @return the chest block
	 */
	public Block getChest() {
		return creationData.chest;
	}

	/**
	 * Gets the location the villager will spawn at.
	 * 
	 * @return the spawn location
	 */
	public Location getSpawnLocation() {
		return creationData.spawnLocation;
	}

	/**
	 * Gets the type of shopkeeper that will spawn (ex: normal, book, buying, trading, etc.).
	 * 
	 * @return the shopkeeper type
	 */
	public ShopType<?> getType() {
		return creationData.shopType;
	}

	/**
	 * Gets the maximum number of shops this player can have.
	 * 
	 * @return player max shops
	 */
	public int getMaxShopsForPlayer() {
		return maxShops;
	}

	/**
	 * Sets the location the villager will spawn at.
	 * Do not use an invalid location here!
	 * 
	 * @param location
	 *            the spawn location
	 */
	public void setSpawnLocation(Location location) {
		Validate.notNull(location);
		creationData.spawnLocation = location;
	}

	/**
	 * Sets the type of shopkeeper. This cannot be set to an admin shop.
	 * 
	 * @param shopType
	 *            shopkeeper type
	 */
	public void setType(ShopType<?> shopType) {
		Validate.notNull(shopType);
		Validate.isTrue(shopType.isPlayerShopType());
		creationData.shopType = shopType;
	}

	/**
	 * Sets the maximum number of shops the creating player can have.
	 * 
	 * <p>
	 * If the player has more than this number, the shop will not be created.
	 * </p>
	 * 
	 * @param maxShops
	 *            the player's max shops
	 */
	public void setMaxShopsForPlayer(int maxShops) {
		this.maxShops = maxShops;
	}

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

	/**
	 * If cancelled the shop won't be created.
	 */
	@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 CitizensHandler {

	public static final String PLUGIN_NAME = "Citizens";
	private static boolean enabled = false;
	private static TraitInfo shopkeeperTrait = null;
	private static CitizensListener citizensListener = null;

	public static Plugin getPlugin() {
		return Bukkit.getPluginManager().getPlugin(PLUGIN_NAME);
	}

	public static boolean isEnabled() {
		return enabled;
	}

	public static void enable() {
		if (enabled) {
			// disable first, to perform cleanup if needed:
			disable();
		}

		if (Settings.enableCitizenShops) {
			Plugin citizensPlugin = getPlugin();
			if (citizensPlugin != null && citizensPlugin.isEnabled()) {
				Log.info("Citizens found, enabling NPC shopkeepers.");
				// register shopkeeper trait:
				assert shopkeeperTrait == null;
				shopkeeperTrait = TraitInfo.create(CitizensShopkeeperTrait.class).withName(CitizensShopkeeperTrait.TRAIT_NAME);
				try {
					CitizensAPI.getTraitFactory().registerTrait(shopkeeperTrait);
				} catch (Throwable ex) {
					Log.debug("Shopkeeper trait registration error: " + ex.getMessage());
					if (Settings.debug) {
						ex.printStackTrace();
					}
				}

				// register citizens listener:
				assert citizensListener == null;
				citizensListener = new CitizensListener();
				Bukkit.getPluginManager().registerEvents(citizensListener, ShopkeepersPlugin.getInstance());

				// enabled:
				enabled = true;
			} else {
				Log.warning("Citizens Shops enabled, but Citizens plugin not found or disabled.");
			}
		}
	}

	public static void disable() {
		if (!enabled) {
			// already disabled
			return;
		}

		Plugin citizensPlugin = getPlugin();
		if (citizensPlugin != null) {
			// unregister shopkeeper trait:
			if (shopkeeperTrait != null) {
				try {
					CitizensAPI.getTraitFactory().deregisterTrait(shopkeeperTrait);
				} catch (Throwable ex) {
					Log.debug("Shopkeeper trait unregistration error: " + ex.getMessage());
					if (Settings.debug) {
						ex.printStackTrace();
					}
				} finally {
					shopkeeperTrait = null;
				}
			}
		}

		// unregister citizens listener:
		if (citizensListener != null) {
			HandlerList.unregisterAll(citizensListener);
			citizensListener = null;
		}

		// disabled:
		enabled = false;
	}

	// returns null if this entity is no citizens npc (or citizens or citizens shops are disabled)
	public static Integer getNPCId(Entity entity) {
		if (enabled) {
			NPC npc = CitizensAPI.getNPCRegistry().getNPC(entity);
			return npc != null ? npc.getId() : null;
		} else {
			return null;
		}
	}

	// returns the id of the created npc, or null
	public static Integer createNPC(Location location, EntityType entityType, String name) {
		if (!enabled) return null;
		NPC npc = CitizensAPI.getNPCRegistry().createNPC(entityType, name);
		if (npc == null) return null;
		// look towards near players:
		npc.getTrait(LookClose.class).lookClose(true);
		// this will log a debug message from citizens if it currently cannot spawn this npc,
		// but will then later attempt to spawn it when the chunk is loaded:
		npc.spawn(location);
		// npc.teleport(loc, PlayerTeleportEvent.TeleportCause.PLUGIN);
		return npc.getId();
	}

	public static void removeInvalidCitizensShopkeepers() {
		if (!enabled) {
			// cannot determine which shopkeepers have a backing npc if citizens isn't running:
			return;
		}
		List<Shopkeeper> forRemoval = new ArrayList<Shopkeeper>();
		for (Shopkeeper shopkeeper : ShopkeepersPlugin.getInstance().getAllShopkeepers()) {
			if (shopkeeper.getShopObject() instanceof CitizensShop) {
				CitizensShop citizensShop = (CitizensShop) shopkeeper.getShopObject();
				Integer npcId = citizensShop.getNpcId();
				if (npcId == null) {
					// npc wasn't created yet, which is only the case if a shopkeeper got somehow created without
					// citizens being enabled:
					forRemoval.add(shopkeeper);
					Log.warning("Removing citizens shopkeeper at " + shopkeeper.getPositionString()
							+ ": NPC has not been created.");
				} else if (CitizensAPI.getNPCRegistry().getById(npcId.intValue()) == null) {
					// there is no npc with the stored id:
					forRemoval.add(shopkeeper);
					Log.warning("Removing citizens shopkeeper at " + shopkeeper.getPositionString()
							+ ": No NPC existing with id '" + npcId + "'.");
				} else if (ShopkeepersPlugin.getInstance().getActiveShopkeeper(shopkeeper.getObjectId()) != shopkeeper) {
					// there is already another citizens shopkeeper using this npc id:
					forRemoval.add(shopkeeper);
					Log.warning("Removing citizens shopkeeper at " + shopkeeper.getPositionString()
							+ ": There exists another shopkeeper using the same NPC with id '" + npcId + "'.");
				}
			}
		}

		// remove those shopkeepers:
		if (!forRemoval.isEmpty()) {
			for (Shopkeeper shopkeeper : forRemoval) {
				shopkeeper.delete();
			}

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

	// unused
	public static void removeShopkeeperTraits() {
		if (!enabled) return;
		Iterator<NPC> npcs = CitizensAPI.getNPCRegistry().iterator();
		while (npcs.hasNext()) {
			NPC npc = npcs.next();
			if (npc.hasTrait(CitizensShopkeeperTrait.class)) {
				npc.removeTrait(CitizensShopkeeperTrait.class);
			}
		}
	}

	private static class CitizensListener implements Listener {

		CitizensListener() {
		}

		@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
		void onNPCRemoved(NPCRemoveEvent event) {
			NPC npc = event.getNPC();
			if (npc.hasTrait(CitizensShopkeeperTrait.class)) {
				CitizensShopkeeperTrait shopkeeperTrait = npc.getTrait(CitizensShopkeeperTrait.class);
				shopkeeperTrait.onTraitDeletion();
			}
		}

		@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
		void onTraitRemoved(NPCRemoveTraitEvent event) {
			if (event.getTrait() instanceof CitizensShopkeeperTrait) {
				CitizensShopkeeperTrait shopkeeperTrait = (CitizensShopkeeperTrait) event.getTrait();
				shopkeeperTrait.onTraitDeletion();
			}
		}
	}
} public class TownyHandler {

	public static boolean isCommercialArea(Location loc) {
		Plugin plugin = Bukkit.getPluginManager().getPlugin("Towny");
		if (plugin != null) {
			TownBlock townBlock = TownyUniverse.getTownBlock(loc);
			return townBlock.getType() == TownBlockType.COMMERCIAL;
		} else {
			return false;
		}
	}
} public class WorldGuardHandler {

	private static final String PLUGIN_NAME = "WorldGuard";

	public static Plugin getPlugin() {
		return Bukkit.getPluginManager().getPlugin(PLUGIN_NAME);
	}

	public static boolean isShopAllowed(Player player, Location loc) {
		Plugin plugin = getPlugin();
		if (plugin != null) {
			WorldGuardPlugin wgPlugin = (WorldGuardPlugin) plugin;
			boolean allowShopFlag = wgPlugin.getRegionManager(loc.getWorld()).getApplicableRegions(loc).testState(null, DefaultFlag.ENABLE_SHOP);
			if (Settings.requireWorldGuardAllowShopFlag) {
				// allow shops ONLY in regions with the ENABLE_SHOP flag set:
				return allowShopFlag;
			} else {
				// allow shops in regions where the ENABLE_SHOP flag is set OR the player can build:
				return allowShopFlag || wgPlugin.canBuild(player, loc);
			}
		} else {
			return true;
		}
	}
} public class CitizensShop extends ShopObject {

	public static String getId(int npcId) {
		return "NPC-" + npcId;
	}

	private Integer npcId = null;
	// used by citizen shopkeeper traits: if false, this will not remove the npc on deletion:
	private boolean destroyNPC = true;

	protected CitizensShop(Shopkeeper shopkeeper, ShopCreationData creationData) {
		super(shopkeeper, creationData);
		// can be null here, as currently only NPC shopkeepers created by the shopkeeper trait provide the npc id via
		// the creation data:
		this.npcId = creationData.npcId;
	}

	@Override
	protected void load(ConfigurationSection config) {
		super.load(config);
		if (config.contains("npcId")) {
			npcId = config.getInt("npcId");
		}
	}

	@Override
	protected void save(ConfigurationSection config) {
		super.save(config);
		if (npcId != null) {
			config.set("npcId", npcId);
		}
	}

	@Override
	protected void onInit() {
		super.onInit();
		if (this.isActive()) return;
		if (!CitizensHandler.isEnabled()) return;

		// create npc:
		EntityType entityType;
		String name;
		if (shopkeeper.getType().isPlayerShopType()) {
			// player shops will use a player npc:
			entityType = EntityType.PLAYER;
			name = ((PlayerShopkeeper) shopkeeper).getOwnerName();
		} else {
			entityType = EntityType.VILLAGER;
			name = "Shopkeeper";
		}

		// prepare location:
		World world = Bukkit.getWorld(shopkeeper.getWorldName());
		Location location = new Location(world, shopkeeper.getX() + 0.5D, shopkeeper.getY() + 0.5D, shopkeeper.getZ() + 0.5D);

		// create npc:
		npcId = CitizensHandler.createNPC(location, entityType, name);
	}

	@Override
	public ShopObjectType getObjectType() {
		return DefaultShopObjectTypes.CITIZEN();
	}

	@Override
	public boolean spawn() {
		return false; // handled by citizens
	}

	@Override
	public boolean isActive() {
		return npcId != null && CitizensHandler.isEnabled();
	}

	@Override
	public String getId() {
		return npcId == null ? null : getId(npcId);
	}

	// can be null if not set yet
	public Integer getNpcId() {
		return npcId;
	}

	public NPC getNPC() {
		if (!this.isActive()) return null;
		return CitizensAPI.getNPCRegistry().getById(npcId);
	}

	public Entity getEntity() {
		NPC npc = this.getNPC();
		if (npc == null) return null;
		return npc.getEntity();
	}

	@Override
	public Location getActualLocation() {
		Entity entity = this.getEntity();
		return entity != null ? entity.getLocation() : null;
	}

	@Override
	public void setName(String name) {
		NPC npc = this.getNPC();
		if (npc == null) return;

		if (Settings.showNameplates && name != null && !name.isEmpty()) {
			if (Settings.nameplatePrefix != null && !Settings.nameplatePrefix.isEmpty()) {
				name = Settings.nameplatePrefix + name;
			}
			name = this.trimToNameLength(name);
			// set entity name plate:
			npc.setName(name);
			// this.entity.setCustomNameVisible(Settings.alwaysShowNameplates);
		} else {
			// remove name plate:
			npc.setName("");
			// this.entity.setCustomNameVisible(false);
		}
	}

	@Override
	public int getNameLengthLimit() {
		return 32; // TODO citizens seem to have different limits depending on mob type (16 for mobs, 64 for players)
	}

	@Override
	public void setItem(ItemStack item) {
		// TODO: No Citizens API for equipping items?
	}

	@Override
	public boolean check() {
		NPC npc = this.getNPC();
		if (npc != null) {
			String worldName = shopkeeper.getWorldName();
			World world = Bukkit.getWorld(worldName);
			int x = shopkeeper.getX();
			int y = shopkeeper.getY();
			int z = shopkeeper.getZ();

			Location currentLocation = npc.getStoredLocation();
			Location expectedLocation = new Location(world, x + 0.5D, y + 0.5D, z + 0.5D);
			if (currentLocation == null) {
				npc.teleport(expectedLocation, PlayerTeleportEvent.TeleportCause.PLUGIN);
				Log.debug("Shopkeeper NPC (" + worldName + "," + x + "," + y + "," + z + ") had no location, teleported");
			} else if (!currentLocation.getWorld().equals(expectedLocation.getWorld()) || currentLocation.distanceSquared(expectedLocation) > 1.0D) {
				shopkeeper.setLocation(currentLocation);
				Log.debug("Shopkeeper NPC (" + worldName + "," + x + "," + y + "," + z + ") out of place, re-indexing");
			}
		} else {
			// Not going to force Citizens creation, this seems like it could go really wrong.
		}

		return false;
	}

	@Override
	public void despawn() {
		// handled by citizens
	}

	protected void onTraitRemoval() {
		destroyNPC = false;
	}

	@Override
	public void delete() {
		if (destroyNPC) {
			NPC npc = this.getNPC();
			if (npc != null) {
				if (npc.hasTrait(CitizensShopkeeperTrait.class)) {
					// let the trait handle npc related cleanup:
					npc.getTrait(CitizensShopkeeperTrait.class).onShopkeeperRemove();
				} else {
					npc.destroy(); // the npc was created by us, so we remove it again
				}
			}
		}
		npcId = null;
	}

	@Override
	public ItemStack getSubTypeItem() {
		// TODO: A menu of entity types here would be cool
		return null;
	}

	@Override
	public void cycleSubType() {
	}
} public class DefaultShopObjectTypes {

	private final LivingEntityObjectTypes livingEntityObjectTypes = new LivingEntityObjectTypes();
	private final ShopObjectType signShopObjectType = new SignShopObjectType();
	private final ShopObjectType citizensShopObjectType = new CitizensShopObjectType();

	// TODO maybe change object type permissions to 'shopkeeper.object.<type>'?

	public DefaultShopObjectTypes() {
	}

	public List<ShopObjectType> getAllObjectTypes() {
		List<ShopObjectType> shopObjectTypes = new ArrayList<ShopObjectType>();
		shopObjectTypes.addAll(livingEntityObjectTypes.getAllObjectTypes());
		shopObjectTypes.add(signShopObjectType);
		shopObjectTypes.add(citizensShopObjectType);
		return shopObjectTypes;
	}

	public LivingEntityObjectTypes getLivingEntityObjectTypes() {
		return livingEntityObjectTypes;
	}

	public ShopObjectType getSignShopObjectType() {
		return signShopObjectType;
	}

	public ShopObjectType getCitizensShopObjectType() {
		return citizensShopObjectType;
	}

	// STATICS (for convenience):

	public static ShopObjectType SIGN() {
		return ShopkeepersPlugin.getInstance().getDefaultShopObjectTypes().getSignShopObjectType();
	}

	public static ShopObjectType CITIZEN() {
		return ShopkeepersPlugin.getInstance().getDefaultShopObjectTypes().getCitizensShopObjectType();
	}
} public class SignShop extends ShopObject {

	public static String getId(Block block) {
		if (block == null) return null;
		return "block" + block.getWorld().getName() + "," + block.getX() + "," + block.getY() + "," + block.getZ();
	}

	private BlockFace signFacing;

	// update the sign content at least once after plugin start, in case some settings have changed which affect the
	// sign content:
	private boolean updateSign = true;

	protected SignShop(Shopkeeper shopkeeper, ShopCreationData creationData) {
		super(shopkeeper, creationData);
		this.signFacing = creationData.blockFace;
	}

	@Override
	protected void load(ConfigurationSection config) {
		super.load(config);
		if (config.isString("signFacing")) {
			String signFacingName = config.getString("signFacing");
			if (signFacingName != null) {
				try {
					signFacing = BlockFace.valueOf(signFacingName);
				} catch (IllegalArgumentException e) {
				}
			}
		}

		// in case no sign facing is stored: try getting the current sign facing from sign in the world
		// if it is not possible (for ex. because the world isn't loaded yet), we will reattempt this
		// during the periodically checks
		if (signFacing == null) {
			signFacing = this.getSignFacingFromWorld();
		}
	}

	@Override
	protected void save(ConfigurationSection config) {
		super.save(config);
		if (signFacing != null) {
			config.set("signFacing", signFacing.name());
		}
	}

	@Override
	protected void onInit() {
		super.onInit();
		this.spawn();
	}

	@Override
	public ShopObjectType getObjectType() {
		return DefaultShopObjectTypes.SIGN();
	}

	public Sign getSign() {
		Location signLocation = this.getActualLocation();
		if (signLocation == null) return null;
		Block signBlock = signLocation.getBlock();
		if (!Utils.isSign(signBlock.getType())) return null;
		return (Sign) signBlock.getState();
	}

	private BlockFace getSignFacingFromWorld() {
		// try getting the current sign facing from the sign in the world:
		Sign sign = this.getSign();
		if (sign != null) {
			return ((Attachable) sign.getData()).getFacing();
		}
		return null;
	}

	@Override
	protected void onChunkLoad() {
		super.onChunkLoad();
		// get the sign facing, in case we weren't able yet, for example because the world wasn't loaded earlier:
		if (signFacing == null) {
			signFacing = this.getSignFacingFromWorld();
		}

		// update sign content if requested:
		if (updateSign) {
			updateSign = false;
			this.updateSign();
		}
	}

	@Override
	public boolean spawn() {
		Location signLocation = this.getActualLocation();
		if (signLocation == null) return false;

		Block signBlock = signLocation.getBlock();
		if (signBlock.getType() != Material.AIR) {
			return false;
		}

		// place sign: // TODO maybe also allow non-wall signs?
		// cancel block physics for this placed sign if needed:
		ShopkeepersPlugin.getInstance().cancelNextBlockPhysics(signLocation);
		signBlock.setType(Material.WALL_SIGN);
		// cleanup state if no block physics were triggered:
		ShopkeepersPlugin.getInstance().cancelNextBlockPhysics(null);

		// in case sign placement has failed for some reason:
		if (!Utils.isSign(signBlock.getType())) {
			return false;
		}

		// set sign facing:
		if (signFacing != null) {
			Sign signState = (Sign) signBlock.getState();
			((Attachable) signState.getData()).setFacingDirection(signFacing);
			// apply facing:
			signState.update();
		}

		// init sign content:
		updateSign = false;
		this.updateSign();

		return true;
	}

	@Override
	public boolean isActive() {
		Location signLocation = this.getActualLocation();
		if (signLocation == null) return false;
		Block signBlock = signLocation.getBlock();
		return Utils.isSign(signBlock.getType());
	}

	@Override
	public String getId() {
		Location location = shopkeeper.getLocation();
		if (location == null) return null;
		return getId(location.getBlock());
	}

	@Override
	public Location getActualLocation() {
		return shopkeeper.getLocation();
	}

	@Override
	public void setName(String name) {
		// always uses the name of the shopkeeper:
		this.updateSign();
	}

	@Override
	public int getNameLengthLimit() {
		return 15;
	}

	@Override
	public void setItem(ItemStack item) {
	}

	public void updateSign() {
		Sign sign = this.getSign();
		if (sign == null) {
			updateSign = true; // request update, once the sign is available again
			return;
		}

		// line 0: header
		sign.setLine(0, Settings.signShopFirstLine);

		// line 1: shop name
		String name = shopkeeper.getName();
		String line1 = "";
		if (name != null) {
			name = this.trimToNameLength(name);
			line1 = name;
		}
		sign.setLine(1, line1);

		// line 2: owner name
		String line2 = "";
		if (shopkeeper instanceof PlayerShopkeeper) {
			line2 = ((PlayerShopkeeper) shopkeeper).getOwnerName();
		}
		sign.setLine(2, line2);

		// line 3: empty
		sign.setLine(3, "");

		// apply sign changes:
		sign.update();
	}

	@Override
	public boolean check() {
		if (!shopkeeper.getChunkData().isChunkLoaded()) {
			// only verify sign, if the chunk is currently loaded:
			return false;
		}

		Sign sign = this.getSign();
		if (sign == null) {
			String worldName = shopkeeper.getWorldName();
			int x = shopkeeper.getX();
			int y = shopkeeper.getY();
			int z = shopkeeper.getZ();

			// removing the shopkeeper, because re-spawning might fail (ex. attached block missing) or could be abused
			// (sign drop farming):
			Log.debug("Shopkeeper sign at (" + worldName + "," + x + "," + y + "," + z + ") is no longer existing! Attempting respawn now.");
			if (!this.spawn()) {
				Log.warning("Shopkeeper sign at (" + worldName + "," + x + "," + y + "," + z + ") could not be replaced! Removing shopkeeper now!");
				// delayed removal:
				Bukkit.getScheduler().runTask(ShopkeepersPlugin.getInstance(), new Runnable() {

					@Override
					public void run() {
						shopkeeper.delete();
					}
				});
			}
			return true;
		}

		// update sign content if requested:
		if (updateSign) {
			updateSign = false;
			this.updateSign();
		}

		return false;
	}

	@Override
	public void despawn() {
	}

	@Override
	public void delete() {
		World world = Bukkit.getWorld(shopkeeper.getWorldName());
		if (world != null) {
			// this should load the chunk if necessary, making sure that the block gets removed (though, might not work
			// on server stops..):
			Block signBlock = world.getBlockAt(shopkeeper.getX(), shopkeeper.getY(), shopkeeper.getZ());
			if (Utils.isSign(signBlock.getType())) {
				// remove sign:
				signBlock.setType(Material.AIR);
			}
			// TODO trigger an unloadChunkRequest if the chunk had to be loaded? (for now let's assume that the server
			// handles that kind of thing automatically)
		} else {
			// well: world unloaded and we didn't get an event.. not our fault
			// TODO actually, we are not removing the sign on world unloads..
		}
	}

	@Override
	public ItemStack getSubTypeItem() {
		return null;
	}

	@Override
	public void cycleSubType() {
	}
} public class LivingEntityShop extends ShopObject {

	public static String getId(Entity entity) {
		if (entity != null) {
			return "entity" + entity.getEntityId();
		}
		return null;
	}

	protected final LivingEntityObjectType livingObjectType;
	protected LivingEntity entity;
	private String uuid;
	private int respawnAttempts = 0;

	protected LivingEntityShop(Shopkeeper shopkeeper, ShopCreationData creationData, LivingEntityObjectType livingObjectType) {
		super(shopkeeper, creationData);
		this.livingObjectType = livingObjectType;
	}

	@Override
	public ShopObjectType getObjectType() {
		return livingObjectType;
	}

	@Override
	protected void load(ConfigurationSection config) {
		super.load(config);
		if (config.contains("uuid")) {
			this.uuid = config.getString("uuid");
		}
	}

	@Override
	protected void save(ConfigurationSection config) {
		super.save(config);
		// let's save last known uuid nevertheless, for the case that the entity somehow wasn't properly removed before
		// (which seems to still happen sometimes during server shutdowns)
		if (uuid != null && !uuid.isEmpty()) {
			config.set("uuid", uuid);
		}
	}

	public EntityType getEntityType() {
		return livingObjectType.getEntityType();
	}

	public LivingEntity getEntity() {
		return entity;
	}

	protected void assignShopkeeperMetadata(LivingEntity entity) {
		entity.setMetadata("shopkeeper", new FixedMetadataValue(ShopkeepersPlugin.getInstance(), true));
	}

	protected void removeShopkeeperMetadata(LivingEntity entity) {
		entity.removeMetadata("shopkeeper", ShopkeepersPlugin.getInstance());
	}

	// returns true if we find a valid entity:
	protected boolean searchOldEntity(Location location) {
		assert location != null && !this.isActive();
		if (uuid != null && !uuid.isEmpty()) {
			for (Entity e : location.getChunk().getEntities()) {
				if (e.isValid() && !e.isDead() && e.getType() == this.getEntityType() && e.getUniqueId().toString().equalsIgnoreCase(uuid)) {
					Log.debug("  Found old shopkeeper entity, using it now");
					entity = (LivingEntity) e;
					// entity.setHealth(entity.getMaxHealth());
					entity.teleport(location);
					assert this.isActive(); // let's assume that the found entity is still valid since we found it
					return true;
				}
			}
		}
		return false;
	}

	@Override
	public boolean spawn() {
		// check if our current old entity is still valid:
		if (this.isActive()) return true;
		if (entity != null) {
			// clean up metadata before replacing the currently stored entity with a new one:
			this.removeShopkeeperMetadata(entity);
		}

		// prepare location:
		World world = Bukkit.getWorld(shopkeeper.getWorldName());
		Location location = new Location(world, shopkeeper.getX() + 0.5D, shopkeeper.getY() + 0.5D, shopkeeper.getZ() + 0.5D);
		// find old shopkeeper entity, else spawn a new one:
		if (!this.searchOldEntity(location)) {
			// TODO check if the block is passable before spawning there?
			// try to bypass entity-spawn blocking plugins:
			EntityType entityType = this.getEntityType();
			ShopkeepersPlugin.getInstance().forceCreatureSpawn(location, entityType);
			entity = (LivingEntity) world.spawnEntity(location, entityType);
			uuid = entity.getUniqueId().toString();
		}

		if (this.isActive()) {
			// assign metadata for easy identification by other plugins:
			this.assignShopkeeperMetadata(entity);
			this.setName(shopkeeper.getName());

			// configure some entity attributes:
			entity.eject(); // some entities might automatically mount on nearby entities (like baby zombies on chicken)
			entity.setRemoveWhenFarAway(false);
			entity.setCanPickupItems(false);

			// disable breeding:
			if (entity instanceof Ageable) {
				Ageable ageable = ((Ageable) entity);
				ageable.setBreed(false);
				ageable.setAgeLock(true);
			}

			// remove potion effects:
			for (PotionEffect potionEffect : entity.getActivePotionEffects()) {
				entity.removePotionEffect(potionEffect.getType());
			}

			// overwrite AI:
			this.overwriteAI();

			// apply sub type:
			this.applySubType();

			// success:
			return true;
		} else {
			// failure:
			entity = null;
			return false;
		}
	}

	protected void applySubType() {
		if (!this.isActive()) return;
		// nothing to do by default
	}

	@Override
	public ItemStack getSubTypeItem() {
		// no sub types by default
		return null;
	}

	@Override
	public void cycleSubType() {
		// no sub types to cycle through by default
	}

	@Override
	public boolean isActive() {
		// validate the entity:
		return entity != null && !entity.isDead() && entity.isValid();
	}

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

	@Override
	public Location getActualLocation() {
		if (this.isActive()) {
			return entity.getLocation();
		} else {
			return null;
		}
	}

	@Override
	public void setName(String name) {
		if (!this.isActive()) return;
		if (Settings.showNameplates && name != null && !name.isEmpty()) {
			if (Settings.nameplatePrefix != null && !Settings.nameplatePrefix.isEmpty()) {
				name = Settings.nameplatePrefix + name;
			}
			name = this.trimToNameLength(name);
			// set entity name plate:
			entity.setCustomName(name);
			entity.setCustomNameVisible(Settings.alwaysShowNameplates);
		} else {
			// remove name plate:
			entity.setCustomName(null);
			entity.setCustomNameVisible(false);
		}
	}

	@Override
	public int getNameLengthLimit() {
		return 32;
	}

	@Override
	public void setItem(ItemStack item) {
		if (this.isActive()) {
			entity.getEquipment().setItemInHand(item);
			entity.getEquipment().setItemInHandDropChance(0);
		}
	}

	@Override
	public boolean check() {
		String worldName = shopkeeper.getWorldName();
		int x = shopkeeper.getX();
		int y = shopkeeper.getY();
		int z = shopkeeper.getZ();

		if (!this.isActive()) {
			Log.debug("Shopkeeper (" + worldName + "," + x + "," + y + "," + z + ") missing, triggering respawn now");
			boolean silentlyUnloaded = (entity != null && !entity.isValid());
			if (silentlyUnloaded) {
				// the chunk was silently unloaded before:
				Log.debug("  Chunk was silently unloaded before: Loading it now and requesting controlled unload");
			}
			boolean spawned = this.spawn(); // this will load the chunk if necessary
			if (spawned) {
				respawnAttempts = 0;
				if (silentlyUnloaded) {
					World world = Bukkit.getWorld(worldName);
					Location location = new Location(world, x + 0.5D, y + 0.5D, z + 0.5D);
					Chunk chunk = location.getChunk();
					// request a safe chunk unload which will call an ChunkUnloadEvent then: (in order to not keep the
					// chunks loaded by constantly calling of this method)
					world.unloadChunkRequest(chunk.getX(), chunk.getZ(), true); // TODO: this doesn't seem to actually
																				// call the ChunkUnloadEvent if world
																				// saving is disabled..
				}
				return true;
			} else {
				// TODO maybe add a setting to remove shopkeeper if it can't be spawned a certain amount of times?
				Log.debug("  Respawn failed");
				return (++respawnAttempts > 5);
			}
		} else {
			// teleport back:
			World world = Bukkit.getWorld(worldName);
			Location loc = new Location(world, x + 0.5D, y, z + 0.5D, entity.getLocation().getYaw(), entity.getLocation().getPitch());
			if (entity.getLocation().distanceSquared(loc) > 0.4D) {
				entity.teleport(loc);
				this.overwriteAI();
				Log.debug("Shopkeeper (" + worldName + "," + x + "," + y + "," + z + ") out of place, teleported back");
			}

			// remove potion effects:
			for (PotionEffect potionEffect : entity.getActivePotionEffects()) {
				entity.removePotionEffect(potionEffect.getType());
			}
			return false;
		}
	}

	@Override
	public void despawn() {
		if (entity != null) {
			if (!entity.isValid()) {
				String worldName = shopkeeper.getWorldName();
				int x = shopkeeper.getX();
				int y = shopkeeper.getY();
				int z = shopkeeper.getZ();
				Log.debug("Chunk was silently unloaded at (" + worldName + "," + x + "," + y + "," + z + "): Loading it now to remove old entity");
				World world = Bukkit.getWorld(worldName);
				if (world != null) {
					Location location = new Location(world, x + 0.5D, y + 0.5D, z + 0.5D);
					this.searchOldEntity(location); // this will load the chunk
					// request a safe chunk unload which will call an ChunkUnloadEvent then: (for now let's assume that
					// the server can handle this automatically)
					// Chunk chunk = location.getChunk();
					// world.unloadChunkRequest(chunk.getX(), chunk.getZ(), true);
				}
			}
			this.removeShopkeeperMetadata(entity);
			entity.remove();
			entity = null;
			// TODO chunk loading and removal doesn't seem to work during server shutdown.. :( so we are now storing the
			// last known entity uuid
		}
	}

	@Override
	public void delete() {
		this.despawn();
	}

	protected void overwriteAI() {
		NMSManager.getProvider().overwriteLivingEntityAI(entity);
		if (Settings.silenceLivingShopEntities) {
			NMSManager.getProvider().setEntitySilent(entity, true);
		}

		// set the NoAI tag for certain entity types:
		switch (livingObjectType.getEntityType()) {
		case BAT:
		case ENDER_DRAGON:
		case ENDERMAN:
		case WITHER:
		case SILVERFISH:
		case BLAZE:
			NMSManager.getProvider().setNoAI(entity);
			break;
		default:
			break;
		}
	}
} 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 class UIManager extends TypeRegistry<UIType> {

	private final Map<String, UISession> playerSessions = new HashMap<String, UISession>();
	private UIListener uiListener;

	public UIManager() {
	}

	public void onEnable(ShopkeepersPlugin plugin) {
		assert uiListener == null;
		uiListener = new UIListener(this);
		Bukkit.getPluginManager().registerEvents(uiListener, plugin);
	}

	public void onDisable(ShopkeepersPlugin plugin) {
		assert uiListener != null;
		HandlerList.unregisterAll(uiListener);
		uiListener = null;
	}

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

	public boolean requestUI(String uiIdentifier, Shopkeeper shopkeeper, Player player) {
		UIType uiManager = this.get(uiIdentifier);
		if (uiManager == null) {
			Log.debug("Unknown interface type: " + uiIdentifier);
			return false;
		}

		if (player == null || shopkeeper == null) {
			Log.debug("Cannot open " + uiIdentifier + ": invalid argument(s): null");
			return false;
		}

		UIHandler uiHandler = shopkeeper.getUIHandler(uiIdentifier);
		if (uiHandler == null) {
			Log.debug("Cannot open " + uiIdentifier + ": this shopkeeper is not handling/supporting this type of interface window.");
			return false;
		}

		String playerName = player.getName();
		if (!uiHandler.canOpen(player)) {
			Log.debug("Cannot open " + uiIdentifier + " for '" + playerName + "'.");
			return false;
		}

		UISession oldSession = this.getSession(player);
		// filtering out duplicate open requests:
		if (oldSession != null && oldSession.getShopkeeper().equals(shopkeeper) && oldSession.getUIHandler().equals(uiHandler)) {
			Log.debug(uiIdentifier + " is already opened for '" + playerName + "'.");
			return false;
		}

		Log.debug("Opening " + uiIdentifier + "...");
		boolean isOpen = uiHandler.openWindow(player);
		if (isOpen) {
			Log.debug(uiIdentifier + " opened");
			// old window already should automatically have been closed by the new window.. no need currently, to do
			// that here
			playerSessions.put(playerName, new UISession(shopkeeper, uiHandler));
			return true;
		} else {
			Log.debug(uiIdentifier + " NOT opened");
			return false;
		}
	}

	UISession getSession(Player player) {
		if (player != null) {
			return playerSessions.get(player.getName());
		}
		return null;
	}

	public UIType getOpenInterface(Player player) {
		UISession session = this.getSession(player);
		return session != null ? session.getUIType() : null;
	}

	public void onInventoryClose(Player player) {
		if (player == null) return;
		playerSessions.remove(player.getName());
	}

	// TODO make sure that this is delayed where needed
	public void closeAll(Shopkeeper shopkeeper) {
		if (shopkeeper == null) return;
		assert shopkeeper != null;
		Iterator<Entry<String, UISession>> iter = playerSessions.entrySet().iterator();
		while (iter.hasNext()) {
			Entry<String, UISession> entry = iter.next();
			UISession session = entry.getValue();
			if (session.getShopkeeper().equals(shopkeeper)) {
				iter.remove();
				Player player = Bukkit.getPlayerExact(entry.getKey());
				if (player != null) {
					player.closeInventory();
				}
			}
		}
	}

	public void closeAllDelayed(final Shopkeeper shopkeeper) {
		if (shopkeeper == null) return;

		// deactivate currently active UIs:
		shopkeeper.deactivateUI();

		// delayed because this is/was originally called from inside the PlayerCloseInventoryEvent
		Bukkit.getScheduler().runTaskLater(ShopkeepersPlugin.getInstance(), new Runnable() {
			public void run() {
				closeAll(shopkeeper);

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

	public void closeAll() {
		for (String playerName : playerSessions.keySet()) {
			Player player = Bukkit.getPlayerExact(playerName);
			if (player != null) {
				player.closeInventory();
			}
		}
		playerSessions.clear();
	}
} public class DefaultUIs {

	public static List<UIType> getAllUITypes() {
		List<UIType> defaults = new ArrayList<UIType>();
		defaults.add(EDITOR_WINDOW);
		defaults.add(TRADING_WINDOW);
		defaults.add(HIRING_WINDOW);
		return defaults;
	}

	// DEFAULT UIs:

	public static final UIType EDITOR_WINDOW = new UIType("editor-window", null);

	public static final UIType TRADING_WINDOW = new UIType("trading-window", null);

	public static final UIType HIRING_WINDOW = new UIType("hiring-window", ShopkeepersAPI.HIRE_PERMISSION);

} 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);
	}

	protected String getInventoryTitle() {
		String title = this.getShopkeeper().getName();
		if (title == null || title.isEmpty()) {
			title = Settings.msgTradingTitleDefault;
		}
		return Settings.msgTradingTitlePrefix + title;
	}

	@Override
	public boolean isWindow(Inventory inventory) {
		return inventory instanceof MerchantInventory;
	}

	@Override
	protected void onInventoryClose(InventoryCloseEvent event, Player player) {
		// nothing to do by default
	}

	@Override
	protected void onInventoryClick(InventoryClickEvent event, Player player) {
		assert event != null && player != null;
		final Shopkeeper shopkeeper = this.getShopkeeper();
		String playerName = player.getName();
		if (event.isCancelled()) {
			Log.debug("Some plugin has cancelled the click in trading window for "
					+ playerName + " at " + shopkeeper.getPositionString() + ".");
			return;
		}

		int rawSlot = event.getRawSlot();

		// prevent special clicks:
		boolean unwantedSpecialClick = false;
		InventoryAction action = event.getAction();
		if (action == InventoryAction.COLLECT_TO_CURSOR) {
			unwantedSpecialClick = true;
		} else if (rawSlot == 2) {
			// TODO allow certain special clicks on the result slot again?
			if (!event.isLeftClick() || (event.isShiftClick() && !this.isShiftTradeAllowed(event))) {
				unwantedSpecialClick = true;
			}
		}

		if (unwantedSpecialClick) {
			Log.debug("Prevented special click in trading window by " + playerName + " at " + shopkeeper.getPositionString() + ".");
			event.setCancelled(true);
			Utils.updateInventoryLater(player);
			return;
		}

		// result slot clicked?
		if (rawSlot != 2) {
			return;
		}

		MerchantInventory inventory = (MerchantInventory) event.getInventory();
		ItemStack resultItem = inventory.getItem(2);
		if (Utils.isEmpty(resultItem)) {
			Log.debug("Not handling trade: There is no item in the clicked result slot (no trade available).");
			return; // no trade available
		}

		ItemStack item1 = inventory.getItem(0);
		ItemStack item2 = inventory.getItem(1);
		if (Utils.isEmpty(item2)) {
			// use null here instead of air, consistent behavior with previous versions:
			item2 = null;
		}
		// minecraft is also allowing the trade, if the second offered item matches the first required one and the first
		// slot is empty:
		// so let's as well assume the item in slot 2 would be in the currently empty slot 1
		if (Utils.isEmpty(item1)) {
			item1 = item2;
			item2 = null;
		}

		// find the recipe minecraft is using for the trade:
		ItemStack[] usedRecipe = NMSManager.getProvider().getUsedTradingRecipe(inventory);

		if (usedRecipe == null) {
			// this shouldn't happen..
			Log.debug("Invalid trade by " + playerName + " with shopkeeper at " + shopkeeper.getPositionString() + ": "
					+ "Minecraft offered a trade, but we didn't find the used recipe!");
			event.setCancelled(true);
			Utils.updateInventoryLater(player);
			return;
		}

		// detecting and preventing issue due to minecraft bug MC-81687 (traded items not being properly removed):
		// TODO should be fixed in newer versions (1.9+), remove when no longer needed
		if (NMSManager.getProvider().getVersionId().startsWith("1_8_")) {
			assert usedRecipe[0] != null && item1 != null;
			if (Utils.isSimilar(item1, item2)) {
				assert usedRecipe[1] != null && item2 != null;
				if (item1.getAmount() < usedRecipe[0].getAmount() || item2.getAmount() < usedRecipe[1].getAmount()) {
					Log.debug("Preventing trade by " + playerName + " with shopkeeper at " + shopkeeper.getPositionString() + ": "
							+ "Due to a minecraft bug (MC-81687), which players can use to exploit, this trade might not get properly handled.");
					event.setCancelled(true);
					Utils.updateInventoryLater(player);
					return;
				}
			}
		}

		if (Settings.useStrictItemComparison) {
			// verify the recipe items are perfectly matching:
			if (!this.isStrictMatchingRecipe(usedRecipe, item1, item2)) {
				if (Settings.debug) { // additional check so we don't do the item comparisons if not really needed
					Log.debug("Invalid trade by " + playerName + " with shopkeeper at " + shopkeeper.getPositionString() + " using strict item comparison:");
					Log.debug("Used recipe: " + Utils.getSimpleRecipeInfo(usedRecipe));
					Log.debug("Recipe item 1: " + (Utils.isSimilar(usedRecipe[0], item1) ? "similar" : "not similar"));
					Log.debug("Recipe item 2: " + (Utils.isSimilar(usedRecipe[1], item2) ? "similar" : "not similar"));
				}
				event.setCancelled(true);
				Utils.updateInventoryLater(player);
				return;
			}
		}

		ItemStack cursor = event.getCursor();
		if (!Utils.isEmpty(cursor)) {
			// minecraft doesn't handle the trading in case the cursor cannot hold the resulting items
			// so we have to make sure that our trading logic is as well not run:
			if (!cursor.isSimilar(resultItem) || cursor.getAmount() + resultItem.getAmount() > cursor.getMaxStackSize()) {
				Log.debug("Skip trade by " + playerName + " with shopkeeper at " + shopkeeper.getPositionString() + ": the cursor cannot carry the resulting items");
				event.setCancelled(true); // making sure minecraft really doesn't process the trading
				return;
			}
		}

		// call trade event, giving other plugins a chance to cancel the trade before the shopkeeper processes it:
		ShopkeeperTradeEvent tradeEvent = new ShopkeeperTradeEvent(shopkeeper, player, event, usedRecipe);
		Bukkit.getPluginManager().callEvent(tradeEvent);
		if (tradeEvent.isCancelled()) {
			assert event.isCancelled();
			Log.debug("Trade was cancelled by some other plugin.");
			return;
		}

		// let shopkeeper handle the purchase:
		this.onPurchaseClick(event, player, usedRecipe, item1, item2);

		// log purchase:
		if (Settings.enablePurchaseLogging && !event.isCancelled()) {
			// TODO maybe move this somewhere else:
			try {
				String owner = (shopkeeper instanceof PlayerShopkeeper) ? ((PlayerShopkeeper) shopkeeper).getOwnerAsString() : "[Admin]";
				File file = new File(ShopkeepersPlugin.getInstance().getDataFolder(), "purchases-" + new SimpleDateFormat("yyyy-MM-dd").format(new Date()) + ".csv");
				boolean isNew = !file.exists();
				BufferedWriter writer = new BufferedWriter(new FileWriter(file, true));
				if (isNew) writer.append("TIME,PLAYER,SHOP ID,SHOP TYPE,SHOP POS,OWNER,ITEM TYPE,DATA,QUANTITY,CURRENCY 1,CURRENCY 1 AMOUNT,CURRENCY 2,CURRENCY 2 AMOUNT\n");
				writer.append("\"" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "\",\"" + playerName + "\",\"" + shopkeeper.getUniqueId()
						+ "\",\"" + shopkeeper.getType().getIdentifier() + "\",\"" + shopkeeper.getPositionString() + "\",\"" + owner
						+ "\",\"" + resultItem.getType().name() + "\",\"" + resultItem.getDurability() + "\",\"" + resultItem.getAmount()
						+ "\",\"" + (item1 != null ? item1.getType().name() + ":" + item1.getDurability() : "")
						+ "\",\"" + (usedRecipe[0] != null ? usedRecipe[0].getAmount() : "")
						+ "\",\"" + (item2 != null ? item2.getType().name() + ":" + item2.getDurability() : "")
						+ "\",\"" + (usedRecipe[1] != null ? usedRecipe[1].getAmount() : "")
						+ "\"\n");
				writer.close();
			} catch (IOException e) {
				Log.severe("IO exception while trying to log purchase");
			}
		}

		// call trade-completed event:
		ShopkeeperTradeCompletedEvent tradeCompletedEvent = new ShopkeeperTradeCompletedEvent(tradeEvent);
		Bukkit.getPluginManager().callEvent(tradeCompletedEvent);
	}

	// whether or not the player can buy via shift click on the result slot:
	protected boolean isShiftTradeAllowed(InventoryClickEvent event) {
		return false; // not allowed by default, just in case
	}

	/**
	 * Called when a player is trying to trade.
	 * 
	 * <p>
	 * Note: The offered items are the items the trading player provided. They can slightly differ from the items from
	 * the trading recipe, depending on item comparison of minecraft and shopkeeper settings.
	 * </p>
	 * 
	 * @param event
	 * @param player
	 * @param usedRecipe
	 *            The recipe minecraft is using for the trade.
	 * @param offered1
	 *            The first offered item. If the first slot was empty, this is the item from the second slot.
	 * @param offered2
	 *            The second offered item. If the first slot was empty, this is null.
	 */
	protected void onPurchaseClick(InventoryClickEvent event, Player player, ItemStack[] usedRecipe, ItemStack offered1, ItemStack offered2) {
		// nothing to do by default
	}

	protected int getAmountAfterTaxes(int amount) {
		if (Settings.taxRate == 0) return amount;
		int taxes = 0;
		if (Settings.taxRoundUp) {
			taxes = (int) Math.ceil((double) amount * (Settings.taxRate / 100F));
		} else {
			taxes = (int) Math.floor((double) amount * (Settings.taxRate / 100F));
		}
		return amount - taxes;
	}

	private boolean isStrictMatchingRecipe(ItemStack[] recipe, ItemStack offered1, ItemStack offered2) {
		assert recipe != null;
		if (!Utils.isSimilar(recipe[0], offered1)) return false;
		if (!Utils.isSimilar(recipe[1], offered2)) return false;
		return true;
	}
}
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.Set;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import com.nisovin.shopkeepers.abstractTypes.SelectableTypeRegistry;
import com.nisovin.shopkeepers.compat.NMSManager;
import com.nisovin.shopkeepers.events.CreatePlayerShopkeeperEvent;
import com.nisovin.shopkeepers.events.ShopkeeperCreatedEvent;
import com.nisovin.shopkeepers.pluginhandlers.CitizensHandler;
import com.nisovin.shopkeepers.pluginhandlers.TownyHandler;
import com.nisovin.shopkeepers.pluginhandlers.WorldGuardHandler;
import com.nisovin.shopkeepers.shopobjects.CitizensShop;
import com.nisovin.shopkeepers.shopobjects.DefaultShopObjectTypes;
import com.nisovin.shopkeepers.shopobjects.SignShop;
import com.nisovin.shopkeepers.shopobjects.living.LivingEntityShop;
import com.nisovin.shopkeepers.shoptypes.DefaultShopTypes;
import com.nisovin.shopkeepers.shoptypes.PlayerShopkeeper;
import com.nisovin.shopkeepers.ui.UIManager;
import com.nisovin.shopkeepers.ui.defaults.DefaultUIs;
import com.nisovin.shopkeepers.ui.defaults.TradingHandler;
package com.nisovin.shopkeepers;




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();