public class SerializableKey implements ConfigurationSerializable, Keyed {
    
    protected final NamespacedKey key;
    
    public SerializableKey(String namespace, String key) {
        this(new NamespacedKey(namespace, key));
    }
    
    public SerializableKey(NamespacedKey namespacedKey) {
        this.key = Objects.requireNonNull(namespacedKey);
    }

    @Override
    public Map<String, Object> serialize() {
        Map<String, Object> map = new HashMap<>();
        map.put("namespace", key.getNamespace());
        map.put("key", key.getKey());
        return map;
    }
    
    public static SerializableKey deserialize(Map<String, Object> map) {
        String namespace = String.valueOf(map.get("namespace"));
        String key = String.valueOf(map.get("key"));
        return new SerializableKey(namespace, key);
    }
    
    @Override
    public NamespacedKey getKey() {
        return key;
    }

} public interface CustomRecipesApi {

	/**
	 * Translate a bukkit shaped recipe to a CustomRecipes shaped recipe.
	 * 
	 * @param bukkitRecipe the bukkit recipe
	 * @return the CustomRecipes variant
	 */
	public ShapedRecipe asCustomRecipesMirror(org.bukkit.inventory.ShapedRecipe bukkitRecipe);

	/**
	 * Translate a bukkit shapeless recipe to a CustomRecipes shapeless recipe.
	 * 
	 * @param bukkitRecipe the bukkit recipe
	 * @return the CustomRecipes variant
	 */
	public ShapelessRecipe asCustomRecipesMirror(org.bukkit.inventory.ShapelessRecipe bukkitRecipe);
	
	/**
	 * Translate a bukkit furnace recipe to a CustomRecipes furnace recipe.
	 * 
	 * @param bukkitRecipe the bukkit recipe
	 * @return the CustomRecipes variant
	 */
	public FurnaceRecipe asCustomRecipesMirror(org.bukkit.inventory.FurnaceRecipe bukkitRecipe);

	/**
	 * Tests whether the crafting recipe is a vanilla kind.
	 * 
	 * @param recipe the recipe
	 * @return whether it's implementation is a vanilla implementation
	 * @deprecated weird method to put in the API, also, there is no equivalent for furnace recipes.
	 */
	@Deprecated
	public boolean isVanillaRecipeType(CraftingRecipe recipe);

	/**
	 * Get the furnace manager
	 * @return the furnace manager
	 */
	public FurnaceManager getFurnaceManager();

	/**
	 * Get the crafting manager
	 * @return the crafting manager
	 */
	public CraftingManager getCraftingManager();
	
	
} public final class InventoryUtils {
	
	private InventoryUtils() {}
	
	/**
	 * Checks whether the given ItemStack is empty.
	 * 
	 * @return true when the given ItemStack is empty, otherwise false
	 */
	public static boolean isEmptyStack(ItemStack itemStack) {
		return itemStack == null || itemStack.getType() == Material.AIR || itemStack.getAmount() < 1;
	}
	
	/**
	 * Get the name of an ItemStack
	 * 
	 * @param itemStack the itemStack
	 * @return the name if present, otherwise the name of the Material
	 */
	public static String getItemName(ItemStack itemStack) {
		if (itemStack == null) return Material.AIR.name();
		
		if (itemStack.hasItemMeta() && itemStack.getItemMeta().hasDisplayName()) {
			return itemStack.getItemMeta().getDisplayName();
		}
		
		return itemStack.getType().name();
	}
	
	/**
	 * Get 2d coordinates of an inventory slot.
	 * 
	 * @param rowLength the length of rows in the inventory
	 * @param index the slot number
	 * @return an array of two ints, the first being the row number, the second being the column number
	 */
	public static int[] inventoryRownumColnum(int rowLength, int index) {
		int rowNum = index / rowLength;
		int colNum = index - rowNum * rowLength;
		return new int[]{rowNum, colNum};
	}
	
	/**
	 * Get the inventory slot number from 2d coordinates.
	 * 
	 * @param rowLength the length of rows in the inventory
	 * @param rownumColnum an array of length 2, the first int being the row number, the second int being the column number
	 * @return the slot number
	 */
	public static int inventoryIndex(int rowLength, int[] rownumColnum) {
		int rowNum = rownumColnum[0];
		int colNum = rownumColnum[1];
		return rowNum * rowLength + colNum;
	}
	
	/**
	 * Checks whether the inventory is empty
	 * 
	 * @param inventory the inventory
	 * @return true if the inventory is empty, otherwise false
	 */
	public static boolean isEmpty(Inventory inventory) {
		return StreamSupport.stream(Spliterators.spliterator(inventory.iterator(),
				inventory.getSize(), Spliterator.SIZED), false)
			.allMatch(InventoryUtils::isEmptyStack);
	}
	
	/**
	 * Get the MaterialData that vanilla crafting recipes leave in a crafting inventory if the ingredient is of stack size 1.
	 * For most materials the returned data will have Material.AIR, but there are exceptions for filled buckets and dragons breath.
	 * 
	 * @param ingredient the material data used as a crafting ingredient.
	 * @return the material data that vanilla recipes would give back upon crafting
	 */
	public static MaterialData getSingleIngredientResult(MaterialData ingredient) {
		if (ingredient == null) return new MaterialData(Material.AIR);
		
		switch(ingredient.getItemType()) {
			case MILK_BUCKET:
			case WATER_BUCKET:
			case LAVA_BUCKET:
				return new MaterialData(Material.BUCKET);
				
			case DRAGONS_BREATH:
				return new MaterialData(Material.GLASS_BOTTLE);
				
			default: return new MaterialData(Material.AIR);
		}
	}

} public interface CraftingRecipe extends Representable, Recipe, ConfigurationSerializable {
	
	/**
	 * Tests whether the items in the crafting inventory match to this crafting recipe.
	 * 
	 * @param craftingInventory the crafting inventory - either a 3x3 workbench inventory, or the 2x2 hand crafting inventory
	 * @param world the world in which crafting takes place
	 * @return the recipe accepts the inventory and world as valid input for the result ItemStack
	 */
	public boolean matches(CraftingInventory craftingInventory, World world);
	
	/**
	 * Get the ItemStack that will be put in the result slot of the crafting inventory.
	 * 
	 * @param craftingInventory the crafting inventory - either a 3x3 workbench inventory, or the 2x2 hand crafting inventory
	 * @return the crafting result ItemStack
	 */
	public ItemStack craftItem(CraftingInventory craftingInventory);
	
	/**
	 * Get the result of this recipe. This is NOT the item that is used by the recipe when the player crafts an item.
	 * See {@link com.gmail.jannyboy11.customrecipes.api.crafting.CraftingRecipe#craftItem}
	 * 
	 * @return the result ItemStack
	 */
	public ItemStack getResult();
	
	/**
	 * Get the ingredients of this recipe. The ordef or the ingredients may or may not be important depending the recipe type.
	 * 
	 * @return the list of ingredients.
	 */
    List<? extends CraftingIngredient> getIngredients();
	
	/**
	 * Get the list of ItemStacks that remain in the crafting table after crafting.
	 * The size of the list is the same as the the size of the ingredient inventory.
	 * This method also takes out the items from the crafting inventory.
	 * 
	 * @param craftingInventory the crafting inventory - either a 3x3 workbench inventory, or the 2x2 hand crafting inventory
	 * @return the ItemStacks that are left over after crafting completed - can contain null or AIR ItemStacks
	 */
	public List<? extends ItemStack> getLeftOverItems(CraftingInventory craftingInventory);

	/**
	 * Tests whether the recipe is a special recipe that have multiple ingredient patterns.
	 * These special recipes are not shown in the Recipe Book.
	 * 
	 * Vanilla examples include
	 * 		- ArmorDyeRecipe
	 * 		- BannerAddPatternRecipe
	 * 		- BannerDuplicateRecipe
	 * 		- BookCloneRecipe
	 * 		- FireworksRecipe
	 * 		- MapCloneRecipe
	 * 		- MapExtendRecipe
	 * 		- RepairRecipe
	 * 		- ShieldDecorationRecipe
	 * 		- ShulkerBoxDyeRecipe
	 * 		- TippedArowRecipe
	 * 
	 * @return whether the recipe is special
	 */
	public boolean isHidden();	
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public default ItemStack getRepresentation() {
		ItemStack result = getResult();
		
		ItemStack representation = (result == null || result.getType() == Material.AIR) ? new ItemStack(Material.AIR) : result.clone();
		if (representation.getType() == Material.AIR) {
			representation = new ItemStack(Material.STRUCTURE_BLOCK);
			ItemMeta meta = representation.getItemMeta();
			meta.setDisplayName(InventoryUtils.getItemName(getResult()));
			meta.setLore(Arrays.asList("Result: UNKNOWN"));
			representation.setItemMeta(meta);
			return representation;
		}
		
		ItemMeta meta = representation.getItemMeta();

		List<String> lore = new ArrayList<>();
		lore.add(ChatColor.DARK_GRAY + "Hidden: " + isHidden());
		
		meta.setDisplayName(ChatColor.GRAY + InventoryUtils.getItemName(getResult()));
		meta.setLore(lore);
		
		representation.setItemMeta(meta);
		return representation;
	}
	
	/**
	 * Get the group of the recipe. Groups are used for grouping recipes together in the Recipe Book.
	 * Examples of vanilla recipes with a group are recipes that take different kinds of wood, or different colors of wool.
	 * The recipe book display mechanic is client side only, <a href="https://twitter.com/dinnerbone/status/856505341479145472">but is subject to change in Minecraft 1.13</a>.
	 * 
	 * @return the group identifier, or the empty string if the recipe has no group
	 */
	public default String getGroup() {
		return "";
	}
	
	/**
	 * Check whether this recipe has a group.
	 * 
	 * @return true if the shaped recipe has a group, otherwise false
	 */
	public default boolean hasGroup() {
		String group = getGroup();
		return !(group == null || group.isEmpty());
	}

} public class SimpleChoiceIngredient implements ChoiceIngredient {
	
	private static class SimpleEmptyIngredient extends SimpleChoiceIngredient implements EmptyIngredient {
		private SimpleEmptyIngredient() {
			super(Collections.emptyList());
		}
		/**
		 * Check if the ItemStack is accepted by this ingredient.
		 * @return true if the itemStack is null or air, otherwise false
		 */
		@Override
		public boolean isIngredient(ItemStack itemStack) {
			return InventoryUtils.isEmptyStack(itemStack);
		}
		/**
		 * Get the choices.
		 * @return an empty list
		 */
		@Override
		public List<? extends ItemStack> getChoices() {
			return Collections.emptyList();
		}
	}
	
	/**
	 * The ChoiceIngredient that accepts empty stacks
	 */
	public static final SimpleEmptyIngredient ACCEPTING_EMPTY = new SimpleEmptyIngredient();
	
	private final List<? extends ItemStack> choices;
	
	/**
	 * Instantiate the class using the given choices
	 * @param choices the list of choices
	 */
	public SimpleChoiceIngredient(List<? extends ItemStack> choices) {
		this.choices = Objects.requireNonNull(choices);
	}
	
	/**
	 * Static method for deserialization.
	 * 
	 * @param choicesMap the map containing the choices list for key "choices"
	 * @return a new SimpleChoicesIngredient if there are choices in the map, otherwise an empty ingredient
	 */
	@SuppressWarnings("unchecked")
	public static SimpleChoiceIngredient deserialize(Map<String, Object> choicesMap) {
		List<? extends ItemStack> choices = (List<? extends ItemStack>) choicesMap.get("choices");
		if (choices == null) return ACCEPTING_EMPTY;
		return new SimpleChoiceIngredient(choices);
	}
	
	/**
	 * Static method
	 * 
	 * @param choices the choices
	 * @return a new SimpleChoiceIngredient
	 */
	public static ChoiceIngredient fromChoices(ItemStack... choices) {
		if (choices == null || choices.length == 0) return ACCEPTING_EMPTY;
		return new SimpleChoiceIngredient(Arrays.asList(choices));
	}

	/**
	 * Checks if the item is accepted by this ingredient.
	 * 
	 * @param itemStack the item stack to test
	 * @return true if the itemStack matches any choice, otherwise false
	 */
	@Override
	public boolean isIngredient(ItemStack itemStack) {
		return choices.stream().anyMatch(choice -> {
			if (choice == null) {
				return itemStack == null;
			} else {
				Material choiceMaterial = choice.getType();
				Material stackMaterial = itemStack.getType();
				if (choiceMaterial != stackMaterial) return false;
				
				return choice.getDurability() == -1 || choice.getDurability() == Short.MAX_VALUE || choice.getDurability() == itemStack.getDurability();
			}
		});
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<? extends ItemStack> getChoices() {
		return choices;
	}
	
	@Override
	public boolean equals(Object o) {
		if (!(o instanceof ChoiceIngredient)) return false;
		ChoiceIngredient that = (ChoiceIngredient) o;
		return Objects.equals(this.getChoices(), that.getChoices());
	}
	
	@Override
	public int hashCode() {
		return Objects.hashCode(getChoices());
	}
	
	@Override
	public String toString() {
		return getClass().getName() + "{" +
				"choices=" + choices +
				"}";
	}

} public final class SimpleShapedRecipe extends SimpleCraftingRecipe implements ShapedRecipe {
	
	protected int width = 3;
	protected int heigth = 3;
	@SuppressWarnings("serial")
	protected List<ChoiceIngredient> ingredients = new ArrayList<ChoiceIngredient>(width * heigth) {
		{
			for (int i = 0; i < width * heigth; i++) {
				add(SimpleChoiceIngredient.ACCEPTING_EMPTY);
			}
		}
	};
	
	/**
	 * Construct a shaped recipe with the given ItemStack as the result
	 * 
	 * @param result the result of the recipe
	 */
	public SimpleShapedRecipe(ItemStack result) {
		super(result);
	}
	
	/**
	 * Construct a shaped recipe with given result, with, height and ingredients.
	 * 
	 * @param result the result of the recipe
	 * @param width the width (1-3)
	 * @param heigth the height (1-3)
	 * @param ingredients the ingredients
	 */
	public SimpleShapedRecipe(ItemStack result, int width, int heigth, List<? extends ChoiceIngredient> ingredients) {
		this(result);
		setIngredients(width, heigth, ingredients);
	}
	
	/**
	 * Constructor for deserialization.
	 * 
	 * @param map the serialized fields
	 */
	@SuppressWarnings("unchecked")
    public SimpleShapedRecipe(Map<String, Object> map) {
		super(map);
		this.width = Integer.valueOf(map.get("width").toString());
		this.heigth = Integer.valueOf(map.get("heigth").toString());
		this.ingredients = (List<ChoiceIngredient>) map.get("ingredients");
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public Map<String, Object> serialize() {
		Map<String, Object> map = super.serialize();
		map.put("width", getWidth());
		map.put("height", getHeight());
		map.put("ingredients", getIngredients());
		return map;
	}
	
	/**
	 * Set the ingredients.
	 * 
	 * @param width the width of the shape
	 * @param heigth the heigth of the shape
	 * @param ingredients the ingredients. The list size should be equal to width * heigth
	 */
	public void setIngredients(int width, int heigth, List<? extends ChoiceIngredient> ingredients) {
		if (width <= 0) throw new IllegalArgumentException("width cannot be smaller than zero!");
		if (heigth <= 0) throw new IllegalArgumentException("heigth cannot be smaller than zero!");
		
		this.width = width;
		this.heigth = heigth;
		
		Objects.requireNonNull(ingredients);
		if (ingredients.stream().anyMatch(Objects::isNull)) throw new IllegalArgumentException("ingredients cannot be null");
		this.ingredients = new ArrayList<>(ingredients);
		for (int i = ingredients.size(); i < width * heigth; i++) {
			this.ingredients.add(SimpleChoiceIngredient.ACCEPTING_EMPTY);
		}
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean matches(CraftingInventory craftingInventory, World world) {
		int width, heigth;
		
		//check boundaries for the crafting inventory
		InventoryType type = craftingInventory.getType();
		switch(type) {
			case CRAFTING:
				width = heigth = 2;
				break;
			case WORKBENCH:
				width = heigth = 3;
				break;
			default: return false; //unknown crafting inventory type.
		}
		
		for (int w = 0; w <= width - this.width; w++) {
			for (int h = 0; h <= heigth - this.heigth; h++) {
				if (matrixMatch(craftingInventory, w, h, true)) {
					return true;
				}
				if (matrixMatch(craftingInventory, w, h, false)) {
					return true;
				}
			}
		}
				
        return false;
	}
	
	
	private boolean matrixMatch(CraftingInventory craftingInventory, int maxColumns, int maxRows, boolean mirrored) {
		for (int c = 0; c < 3; c++) {
			for (int r = 0; r < 3; r++) {
				int colNum = c - maxColumns;
				int rowNum = r - maxRows;
				CraftingIngredient choiceIngredient = InventoryUtils::isEmptyStack;
				if (colNum >= 0 && rowNum >= 0 && colNum < width && rowNum < heigth) {
					if (mirrored) {
						choiceIngredient = ingredients.get(width - 1 - colNum + rowNum * width);
					} else {
						choiceIngredient = ingredients.get(colNum + rowNum * width);
					}
				}
				
				if (!choiceIngredient.isIngredient(craftingInventory.getItem(colNum + rowNum * width))) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int getWidth() {
		return width;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public int getHeight() {
		return heigth;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<ChoiceIngredient> getIngredients() {
		return Collections.unmodifiableList(ingredients);
	}
	
	@Override
	public boolean equals(Object o) {
		if (o == this) return true;
		if (!(o instanceof ShapedRecipe)) return false;
		ShapedRecipe that = (ShapedRecipe) o;
		
		return Objects.equals(this.getResult(), that.getResult()) && Objects.equals(this.getIngredients(), that.getIngredients()) &&
				Objects.equals(this.getWidth(), that.getWidth()) && Objects.equals(this.getHeight(), that.getHeight()) &&
				Objects.equals(this.isHidden(), that.isHidden()) &&	Objects.equals(this.getGroup(), that.getGroup());
	}
	
	@Override
	public int hashCode() {
		return Objects.hash(result, width, heigth, ingredients, hidden, group);
	}
	
	@Override
	public String toString() {
		return getClass().getName() + "{" + 
			"result=" + result +
			",width=" + width +
			",heigth=" + heigth +
			",ingredients=" + ingredients +
			",hidden=" + hidden +
			",group=" + group +				
			"}";
	}

} public final class SimpleShapelessRecipe extends SimpleCraftingRecipe implements ShapelessRecipe {
	
	protected List<ChoiceIngredient> ingredients = new ArrayList<>();
	
	/**
	 * Construct a shapeless recipe with the given ItemStack as the result.
	 * @param result the result of the recipe
	 */
	public SimpleShapelessRecipe(ItemStack result) {
		super(result);
	}
	
	/**
	 * Construct a shapeless recipe with the given result and ingredients.
	 * 
	 * @param result the result of the recipe
	 * @param ingredients the ingredients list
	 */
	public SimpleShapelessRecipe(ItemStack result, List<? extends ChoiceIngredient> ingredients) {
		this(result);
		setIngredients(ingredients);
	}
	
	/**
	 * Constructor for deserialization.
	 * 
	 * @param map the serialized fields
	 */
	@SuppressWarnings("unchecked")
    public SimpleShapelessRecipe(Map<String, Object> map) {
		super(map);
		this.ingredients = (List<ChoiceIngredient>) map.get("ingredients");
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public Map<String, Object> serialize() {
		Map<String, Object> map = super.serialize();
		map.put("ingredients", getIngredients());
		return map;
	}
	
	/**
	 * Set the ingredients list. None of the ingredients in the list can be null.
	 * 
	 * @param ingredients the ingredients list
	 */
	public void setIngredients(List<? extends ChoiceIngredient> ingredients) {
		Objects.requireNonNull(ingredients);
		if (ingredients.stream().anyMatch(Objects::isNull)) throw new IllegalArgumentException("ingredients cannot be null");
		
		this.ingredients = new ArrayList<>(ingredients);
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean matches(CraftingInventory craftingInventory, World world) {
		final List<ChoiceIngredient> ingredients = new ArrayList<>(this.ingredients);
		final List<ItemStack> contents = Arrays.asList(craftingInventory.getMatrix())
				.stream().filter(i -> !InventoryUtils.isEmptyStack(i))
				.collect(Collectors.toList());
		
		for (ItemStack stack : contents) {
			boolean match = false;
			for (int ingredientIndex = 0; ingredientIndex < ingredients.size(); ingredientIndex++) {
				ChoiceIngredient ingredient = ingredients.get(ingredientIndex);
				if (ingredient.isIngredient(stack)) {
					ingredients.remove(ingredientIndex);
					match = true;
					break;
				}
			}
			
			//there was no matching ingredient for the current ItemStack
			if (!match) return false;
		}
		
		//return true if there are no unused ingredients left over
		return ingredients.isEmpty();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<? extends ChoiceIngredient> getIngredients() {
		return Collections.unmodifiableList(ingredients);
	}
	
	@Override
	public boolean equals(Object o) {
		if (o == this) return true;
		if (!(o instanceof ShapelessRecipe)) return false;
		ShapelessRecipe that = (ShapelessRecipe) o;
		
		return Objects.equals(this.getResult(), that.getResult()) && Objects.equals(this.getIngredients(), that.getIngredients()) &&
				Objects.equals(this.isHidden(), that.isHidden()) && Objects.equals(this.getGroup(), that.getGroup());
	}
	
	@Override
	public int hashCode() {
		return Objects.hash(getResult(), getIngredients(), isHidden(), getGroup());
	}
	
	@Override
	public String toString() {
		return getClass().getName() + "{" + 
			"result=" + result +
			",ingredients=" + ingredients +
			",hidden=" + hidden +
			",group=" + group +				
			"}";
	}

} public class SimilarIngredient implements CraftingIngredient {
	
	private final ItemStack itemStack;
	
	/**
	 * Constructs the SimilarIngredient with the given ItemStack as similarity checker.
	 * 
	 * @param itemStack the ItemStack that is used for similarity checks, can be null
	 */
	public SimilarIngredient(ItemStack itemStack) {
		this.itemStack = itemStack == null ? null : itemStack.clone();
	}
	
	/**
	 * Constructor for deserialization.
	 * @param map the map with contents
	 */
	public SimilarIngredient(Map<String, Object> map) {
		this.itemStack = (ItemStack) map.get("similarTo");
	}
	
	/**
	 * Serialize method for the ConfigurationSerializable interface.
	 * @return a singleton map with key "similarTo" and value the corresponding ItemStack
	 */
	@Override
	public Map<String, Object> serialize() {
		return Collections.singletonMap("similarTo", itemStack);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean isIngredient(ItemStack itemStack) {
		return this.itemStack == null ? itemStack == null : this.itemStack.isSimilar(itemStack);
	}
	
	/**
	 * Gets the item that is used for similarity checks.
	 * 
	 * @return the checker ItemStack, or null if no ItemStack was present
	 */
	public ItemStack getCheckingItem() {
		return itemStack == null ? null : itemStack.clone();
	}

} public final class WildcardIngredient implements CraftingIngredient {
	
	private static final WildcardIngredient INSTANCE = new WildcardIngredient();
	
	private WildcardIngredient() {
	}
	
	/**
	 * Get the singleton instance.
	 * 
	 * @return the singleton instance
	 */
	public static WildcardIngredient getInstance() {
		return INSTANCE;
	}
	

	/**
	 * Tests whether the ItemStack is an ingredient.
	 * 
	 * @return true
	 */
	@Override
	public boolean isIngredient(ItemStack test) {
		return true;
	}
	
	/**
	 * Deserialization method for the ConfigurationSerializable interface.
	 * @return the singleton instance
	 */
	public static WildcardIngredient deserialize(Map<String, Object> map) {
		return getInstance();
	}

} public interface CountRecipe extends ShapedRecipe {

} public interface NBTRecipe extends ShapedRecipe {

} public interface PermissionRecipe extends ShapedRecipe {
	
	/**
	 * Get the permission that is needed for this recipe to work
	 * 
	 * @return the permission string
	 */
	public String getPermission();
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public default ItemStack getRepresentation() {
		ItemStack result = getResult();
		
		ItemStack representation = (result == null || result.getType() == Material.AIR) ? new ItemStack(Material.AIR) : result.clone();
		if (representation.getType() == Material.AIR) {
			representation = new ItemStack(Material.STRUCTURE_BLOCK);
			ItemMeta meta = representation.getItemMeta();
			meta.setLore(Arrays.asList("Result: UNKNOWN"));
			representation.setItemMeta(meta);
			return representation;
		}
		
		ItemMeta meta = representation.getItemMeta();

		List<String> lore = new ArrayList<>();
		lore.add(ChatColor.DARK_GRAY + "Permission: " + getPermission());
		lore.add(ChatColor.DARK_GRAY + "Width: " + getWidth());
		lore.add(ChatColor.DARK_GRAY + "Height: " + getHeight());
		
		List<? extends ChoiceIngredient> ingredients = getIngredients();
		if (!ingredients.isEmpty()) {
			lore.add(ChatColor.DARK_GRAY + "Ingredients:");
			for (ChoiceIngredient ingredient : ingredients) {
				lore.add(ChatColor.DARK_GRAY + ingredient.getChoices().stream().map(InventoryUtils::getItemName)
						.collect(Collectors.joining(ChatColor.DARK_GRAY + ", ")));
			}
		}
		
		lore.add(ChatColor.DARK_GRAY + "Hidden: " + isHidden());
		if (hasGroup()) lore.add(ChatColor.DARK_GRAY + "Group: " + getGroup());
		
		meta.setLore(lore);
		
		representation.setItemMeta(meta);
		return representation;
	}

} public interface WorldRecipe extends ShapedRecipe {
	
	/**
	 * Get the unique universal identifier of the world that this recipe works in.
	 * 
	 * @return the uuid
	 */
	public UUID getWorld();

} public interface ChoiceIngredient extends CraftingIngredient {
	
	/**
	 * Get the list of choices.
	 * @return the list of choices
	 */
	public List<? extends ItemStack> getChoices();
	
	
	/**
	 * Serialize method for ConfigurationSerializable interface.
	 * The default implementation returns a singleton map, the key is "choices" and the value is the list of ItemStacks. 
	 */
	public default Map<String, Object> serialize() {
		return Collections.singletonMap("choices", getChoices());
	}

} public interface BannerDuplicateRecipe extends ShapelessRecipe {

	/**
	 * {@inheritDoc}
	 */
	@Override
	public default ItemStack getRepresentation() {
		ItemStack stack = new ItemStack(Material.BANNER);
		ItemMeta meta = stack.getItemMeta();
		meta.setDisplayName(ChatColor.GRAY + "Banner Duplicate Pattern");
		stack.setItemMeta(meta);
		return stack;
	}
	
} public interface ShapedRecipe extends CraftingRecipe {
	
	/**
	 * Get the width of the recipe
	 * @return
	 */
	public int getWidth();
	
	/**
	 * Get the height of the recipe
	 * @return
	 */
	public int getHeight();
	
	/**
	 * Get the ingredients of the recipe.
	 * The size of the list must be equal to getWidth() * getHeight();
	 * 
	 * @return the ingredients
	 */
	public List<? extends ChoiceIngredient> getIngredients();
	
	/**
	 * Get the group of the recipe. Groups are used for grouping recipes together in the Recipe Book.
	 * Examples of vanilla recipes with a group are recipes that take different kinds of wood, or different colors of wool.
	 * The recipe book display mechanic is client side only, <a href="https://twitter.com/dinnerbone/status/856505341479145472">but is subject to change in Minecraft 1.13</a>.
	 * 
	 * @return the group identifier, or the empty string if the recipe has no group
	 */
	public String getGroup();
	
	/**
	 * Check whether this recipe has a group.
	 * 
	 * @return true if the shaped recipe has a group, otherwise false
	 */
	public default boolean hasGroup() {
		String group = getGroup();
		return !(group == null || group.isEmpty());
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public default ItemStack getRepresentation() {
		ItemStack result = getResult();
		
		ItemStack representation = (result == null || result.getType() == Material.AIR) ? new ItemStack(Material.AIR) : result.clone();
		if (representation.getType() == Material.AIR) {
			representation = new ItemStack(Material.STRUCTURE_BLOCK);
			ItemMeta meta = representation.getItemMeta();
			meta.setLore(Arrays.asList("Result: UNKNOWN"));
			representation.setItemMeta(meta);
			return representation;
		}
		
		ItemMeta meta = representation.getItemMeta();

		List<String> lore = new ArrayList<>();
		lore.add(ChatColor.DARK_GRAY + "Width: " + getWidth());
		lore.add(ChatColor.DARK_GRAY + "Height: " + getHeight());
		
		List<? extends ChoiceIngredient> ingredients = getIngredients();
		if (!ingredients.isEmpty()) {
			lore.add(ChatColor.DARK_GRAY + "Ingredients:");
			for (ChoiceIngredient ingredient : ingredients) {
				lore.add(ChatColor.DARK_GRAY + ingredient.getChoices().stream().map(InventoryUtils::getItemName)
						.collect(Collectors.joining(ChatColor.DARK_GRAY + ", ")));
			}
		}
		
		lore.add(ChatColor.DARK_GRAY + "Hidden: " + isHidden());
		if (hasGroup()) lore.add(ChatColor.DARK_GRAY + "Group: " + getGroup());
		
		meta.setLore(lore);
		
		representation.setItemMeta(meta);
		return representation;
	}

} public interface ShapelessRecipe extends CraftingRecipe {
	
	/**
	 * Get the ingredients of the recipe.
	 * 
	 * @return the ingredients
	 */
	public List<? extends ChoiceIngredient> getIngredients();
	
	/**
	 * Get the group of the recipe. Groups are used for grouping recipes together in the Recipe Book.
	 * Examples of vanilla recipes with a group are recipes that take different kinds of wood, or different colors of wool.
	 * The recipe book display mechanic is client side only, <a href="https://twitter.com/dinnerbone/status/856505341479145472">but is subject to change in Minecraft 1.13</a>.
	 * 
	 * @return the group identifier, or the empty string if the recipe has no group
	 */
	public String getGroup();

	/**
	 * Check whether this recipe has a group.
	 * 
	 * @return true if the shapeless recipe has a group, otherwise false
	 */
	public default boolean hasGroup() {
		String group = getGroup();
		return !(group == null || group.isEmpty());
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public default ItemStack getRepresentation() {
		ItemStack result = getResult();
		
		ItemStack representation = (result == null || result.getType() == Material.AIR) ? new ItemStack(Material.AIR) : result.clone();
		if (representation.getType() == Material.AIR) {
			representation = new ItemStack(Material.STRUCTURE_BLOCK);
			ItemMeta meta = representation.getItemMeta();
			meta.setLore(Arrays.asList("Result: UNKNOWN"));
			representation.setItemMeta(meta);
			return representation;
		}
		
		ItemMeta meta = representation.getItemMeta();

		List<String> lore = new ArrayList<>();
		
		List<? extends ChoiceIngredient> ingredients = getIngredients();
		if (!ingredients.isEmpty()) {
			lore.add(ChatColor.DARK_GRAY + "Ingredients:");
			for (ChoiceIngredient ingredient : ingredients) {
				lore.add(ChatColor.DARK_GRAY + ingredient.getChoices().stream().map(InventoryUtils::getItemName)
						.collect(Collectors.joining(ChatColor.DARK_GRAY + ", ")));
			}
		}
		
		lore.add(ChatColor.DARK_GRAY + "Hidden: " + isHidden());
		if (hasGroup()) lore.add(ChatColor.DARK_GRAY + "Group: " + getGroup());
		
		meta.setLore(lore);
		
		representation.setItemMeta(meta);
		return representation;
	}

} public interface FurnaceRecipe extends Recipe, Keyed, ConfigurationSerializable, Representable {

    /**
     * Get the ingredient of this furnace recipe.
     * 
     * @return the ingredient
     */
    public CraftingIngredient getIngredient();

    /**
     * Get the output of this furnace recipe.
     * 
     * @return the output
     */
    public ItemStack getResult();

    /**
     * Get the experience that is granted for using this furnace recipe.
     * 
     * @return the experienced
     */
    public float getXp();

    /**
     * Set the ingredient of this furnace recipe.
     * 
     * @param ingredient
     */
    public void setIngredient(CraftingIngredient ingredient);

    /**
     * Set the output of this furnace recipe.
     * 
     * @param result the output
     */
    public void setResult(ItemStack result);

    /**
     * Set the experience of this furnace recipe.
     * 
     * @param xp the experience
     */
    public void setXp(float xp);
    
    /**
     * Gets the representation. The default implementation returns the result ItemStack.
     * 
     * @return the representation - same as {@link FurnaceRecipe#getResult()}
     */
    public default ItemStack getRepresentation() {
        return getResult();
    }
    
} public final class SimpleFurnaceRecipe implements FurnaceRecipe {
	
    private final NamespacedKey key;
	private CraftingIngredient ingredient;
	private ItemStack result;
	private float xp;
	
	public SimpleFurnaceRecipe(NamespacedKey key, CraftingIngredient ingredient, ItemStack result) {
        this.key = Objects.requireNonNull(key, "key cannot be null.");
        this.ingredient = Objects.requireNonNull(ingredient, "ingredient cannot be null.");
        this.result = Objects.requireNonNull(result, "result cannot be null.");
    }
	
	public SimpleFurnaceRecipe(NamespacedKey key, CraftingIngredient ingredient, ItemStack result, float xp) {
	    this(key, ingredient, result);
	    this.xp = xp;
	}
	
	public SimpleFurnaceRecipe(Map<String, Object> map) {
	    this.key = ((SerializableKey) map.get("key")).getKey();
		this.ingredient = (CraftingIngredient) map.get("ingredient");
		this.result = (ItemStack) map.get("result");
		if (map.containsKey("xp")) xp = Float.valueOf(map.get("xp").toString());
	}
	
    @Override
    public Map<String, Object> serialize() {
        Map<String, Object> map = new HashMap<>();
        map.put("key", new SerializableKey(key));
        map.put("ingredient", ingredient);
        map.put("result", result);
        if (xp > 0F) map.put("xp", xp);
        return map;
    }
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public CraftingIngredient getIngredient() {
		return ingredient;
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public ItemStack getResult() {
		return result;
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public float getXp() {
		return xp;
	}
	
	/**
	 * Set the ingredient.
	 * 
	 * @param ingredient the new ingredient
	 */
	@Override
	public void setIngredient(CraftingIngredient ingredient) {
		this.ingredient = ingredient;
	}
	
	/**
	 * Set the result
	 * 
	 * @param result the new result.
	 */
	@Override
	public void setResult(ItemStack result) {
		this.result = result;
	}
	
	/**
	 * Set the experience reward.
	 * 
	 * @param xp the new experience reward
	 */
	@Override
	public void setXp(float xp) {
		this.xp = xp;
	}
	
	/**
	 * Builder method
	 * 
	 * @param ingredient the ingredient of the new furnace recipe
	 * @return a new SimpleFurnaceRecipe instance
	 */
	public SimpleFurnaceRecipe ingredient(CraftingIngredient ingredient) {
		return new SimpleFurnaceRecipe(key, ingredient, result, xp);
	}
	
	/**
	 * Builder method
	 * 
	 * @param result the result of the new furnace recipe
	 * @return a new SimpleFurnaceRecipe instance
	 */
	public SimpleFurnaceRecipe result(ItemStack result) {
		return new SimpleFurnaceRecipe(key, ingredient, result, xp);
	}
	
	/**
	 * Builder method
	 * 
	 * @param xp the experience reward of the new furnace recipe
	 * @return a new SimpleFurnaceRecipe instance
	 */
	public SimpleFurnaceRecipe xp(float xp) {
		return new SimpleFurnaceRecipe(key, ingredient, result, xp);
	}
	

	@Override
	public boolean equals(Object o) {
		if (!(o instanceof FurnaceRecipe)) return false;
		
		FurnaceRecipe that = (FurnaceRecipe) o;
		return this.key.equals(that.getKey());
	}
	
	@Override
	public int hashCode() {
		return Objects.hash(getIngredient(), getResult(), getXp());
	}
	
	@Override
	public String toString() {
		return getClass().getName() + "{" +
		        "key=" + key +
				",ingredient=" + ingredient +
				",output=" + result +
				",xp=" + xp +
				"}";
	}

    @Override
    public NamespacedKey getKey() {
        return key;
    }

} public class AddRecipeCommandExecutor implements CommandExecutor {
	
	private final Map<String, ? extends BiConsumer<? super Player, ? super List<String>>> recipeAdders;
	
	public AddRecipeCommandExecutor(Map<String, ? extends BiConsumer<? super Player, ? super List<String>>> recipeAdders) {
		this.recipeAdders = recipeAdders;
	}
	
	@Override
	public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
		if (!(sender instanceof Player)) {
			sender.sendMessage(ChatColor.RED + "You can only use this command as a player.");
			return true;
		}
		
		if (args.length == 0) return false;
		Player player = (Player) sender;
		
		String type = args[0];		
		BiConsumer<? super Player, ? super List<String>> adder = recipeAdders.get(type);
		if (adder == null) {
			sender.sendMessage(ChatColor.RED + "Unrecognized recipe type " + ChatColor.WHITE + type + ChatColor.RED + ".");
			sender.sendMessage(ChatColor.RED + "Please choose from " + ChatColor.WHITE + recipeAdders.keySet().toString() + ChatColor.RED + ".");
			return true;
		}
		
		try {
			adder.accept(player, Arrays.asList(Arrays.copyOfRange(args, 1, args.length)));
		} catch (Throwable throwable) {
			throwable.printStackTrace();
			player.sendMessage(ChatColor.RED + "Something went wrong when trying to create a new recipe. Check the console for errors.");
		}
		
		return true;
	}

} public class ListRecipesCommandExecutor implements CommandExecutor {
	
	private final Function<String, ? extends List<? extends Recipe>> recipesByTypeMapper;
	private final Map<String, Function<? super Recipe, ? extends ItemStack>> recipeToItemMap;
	private final Map<String, BiConsumer<? super Recipe, ? super CommandSender>> recipeToCommandSenderDiplayMap;
	
	public ListRecipesCommandExecutor(Function<String, ? extends List<? extends Recipe>> recipesByTypeMapper,
			Map<String, Function<? super Recipe, ? extends ItemStack>> recipeToItemMap,
			Map<String, BiConsumer<? super Recipe, ? super CommandSender>> recipeToCommandSenderDiplayMap) {
		this.recipesByTypeMapper = recipesByTypeMapper;
		this.recipeToItemMap = recipeToItemMap;
		this.recipeToCommandSenderDiplayMap = recipeToCommandSenderDiplayMap;
	}
	
	@Override
	public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
		if (args.length == 0) return false;
		
		String recipeType = args[0];
		List<? extends Recipe> recipes = recipesByTypeMapper.apply(recipeType);
		
		if (recipes == null) {
			sender.sendMessage(ChatColor.RED + "Unknown recipe type: " + recipeType);
			return true;
		} else if (recipes.isEmpty()) {
			sender.sendMessage(ChatColor.RED + "No recipes found for type: " + recipeType);
			return true;
		}
		
		return sender instanceof Player ? listPlayer((Player) sender, recipeType, recipes) : listSender(sender, recipeType, recipes);
	}
	
	
	private boolean listPlayer(Player player, String recipeType, List<? extends Recipe> recipes) {
		Function<? super Recipe, ? extends ItemStack> representationFunction = recipeToItemMap.get(recipeType);
		if (representationFunction == null) {
			player.sendMessage(ChatColor.RED + "No representation function found for this type of recipe. Trying chat messages..");
			return listSender(player, recipeType, recipes);
		}
		
		List<? extends ItemStack> representations = recipes.stream().map(representationFunction).collect(Collectors.toList());
		player.openInventory(new ListRecipesInventoryHolder(recipeType, representations).getInventory());
		
		return true;
	}
	
	private boolean listSender(CommandSender sender, String recipeType, List<? extends Recipe> recipes) {
		BiConsumer<? super Recipe, ? super CommandSender> recipeDisplayFunction = recipeToCommandSenderDiplayMap.get(recipeType);
		if (recipeDisplayFunction == null) {
			sender.sendMessage(ChatColor.RED + "No display function found for this type of recpie.");
			return true;
		}
		
		recipes.forEach(recipe -> recipeDisplayFunction.accept(recipe, sender));
		
		return true;
	}

} public class MigrateRecipesCommandExecutor implements CommandExecutor {

	private Random random = new Random();
	private int incr = random.nextInt();
	
	private final CustomRecipesPlugin plugin;

	public MigrateRecipesCommandExecutor(CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}
	
	private MinecraftKey nextCustomKey() {
		return new MinecraftKey("customrecipes", "migrated_" + String.valueOf(incr++));
	}

	@Override
	public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
		File pluginDataFolder = plugin.getDataFolder();

		if (!pluginDataFolder.exists()) {
			sender.sendMessage(ChatColor.GREEN + "Plugin data folder does not exists, no recipes were migrated.");
			return true;
		}

		File extraRecipesFolder = new File(pluginDataFolder, "extra_recipes");
		if (extraRecipesFolder.exists() && extraRecipesFolder.isDirectory()) {
			migrateExtraRecipes(sender, extraRecipesFolder);
		} else {
			sender.sendMessage(ChatColor.RED + "Could not migrate extra recipes, the extra_recipes file does not exist or is not a directory.");
		}

		File disabledRecipesFolder = new File(pluginDataFolder, "disabled_recipes");
		if (disabledRecipesFolder.exists() && disabledRecipesFolder.isDirectory()) {
			migrateDisabledRecipes(sender, disabledRecipesFolder);
		} else {
			sender.sendMessage(ChatColor.RED + "Could not migrate disabled recipes, the disabled_recipes file does not exist or is not a directory.");
		}

		sender.sendMessage(ChatColor.GREEN + "Migration complete! Restart your server for the migrated recipes to take effect.");

		return true;
	}


	private void migrateExtraRecipes(CommandSender sender, File extraFolder) {
		sender.sendMessage(ChatColor.YELLOW + "Migrating shaped recipes");
		//shaped
		File folder = new File(extraFolder, "shaped_recipes");
		if (folder.exists() && folder.isDirectory()) {
			for (File file : folder.listFiles()) {
				String name = file.getName();

				try {
					NBTTagCompound readTag = NBTUtil.readNBTTagCompound(file);
					NBTTagCompound resultTag = readTag.getCompound("Result");
					ItemStack result = resultTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(resultTag);

					int width = readTag.getInt("Width");
					int height = readTag.getInt("Height");
					NBTTagList nbtIngredients = readTag.getList("Ingredients", NBTUtil.COMPOUND);
					NonNullList<RecipeItemStack> ingredients = NonNullList.a(width * height, RecipeItemStack.a);
					for (int i = 0; i < nbtIngredients.size(); i++) {
						NBTTagCompound ingredientTag = nbtIngredients.get(i);
						ItemStack ingredientStack = ingredientTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(ingredientTag);
						RecipeItemStack ingredient = RecipeItemStack.a(new ItemStack[] {ingredientStack});
						ingredients.set(i, ingredient);
					}

					ShapedRecipes shaped = new ShapedRecipes("", height, height, ingredients, result);
					MinecraftKey key = nextCustomKey();
					shaped.setKey(key);

					CRShapedRecipe<ShapedRecipes> cr = new CRShapedRecipe<>(shaped);
					plugin.saveCraftingRecipeFile("shaped", cr);

				} catch (IOException e) {
					e.printStackTrace();
					sender.sendMessage(ChatColor.RED + "Something went wrong when migrating shaped recipe " + name + ", please check your console for errors.");
				}
			}
		}

		sender.sendMessage(ChatColor.YELLOW + "Migrating shapeless recipes");
		//shapeless
		folder = new File(extraFolder, "shapeless_recipes");
		if (folder.exists() && folder.isDirectory()) {
			for (File file : folder.listFiles()) {
				String name = file.getName();

				try {
					NBTTagCompound readTag = NBTUtil.readNBTTagCompound(file);
					NBTTagCompound resultTag = readTag.getCompound("Result");
					ItemStack result = resultTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(resultTag);

					NBTTagList nbtIngredients = readTag.getList("Ingredients", NBTUtil.COMPOUND);
					NonNullList<RecipeItemStack> ingredients = NonNullList.a(nbtIngredients.size(), RecipeItemStack.a);
					for (int i = 0; i < nbtIngredients.size(); i++) {
						NBTTagCompound ingredientTag = nbtIngredients.get(i);
						ItemStack ingredientStack = ingredientTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(ingredientTag);
						RecipeItemStack ingredient = RecipeItemStack.a(new ItemStack[] {ingredientStack});
						ingredients.set(i, ingredient);
					}

					ShapelessRecipes shaped = new ShapelessRecipes("", result, ingredients);
					MinecraftKey key = nextCustomKey();
					shaped.setKey(key);

					CRShapelessRecipe<ShapelessRecipes> cr = new CRShapelessRecipe<>(shaped);
					plugin.saveCraftingRecipeFile("shapeless", cr);

				} catch (IOException e) {
					e.printStackTrace();
					sender.sendMessage(ChatColor.RED + "Something went wrong when migrating shapeless recipe " + name + ", please check your console for errors.");
				}
			}
		}

		sender.sendMessage(ChatColor.YELLOW + "Migrating permission recipes");
		//permission
		folder = new File(extraFolder, "permission_recipes");
		if (folder.exists() && folder.isDirectory()) {
			for (File file : folder.listFiles()) {
				String name = file.getName();

				try {
					NBTTagCompound readTag = NBTUtil.readNBTTagCompound(file);
					NBTTagCompound resultTag = readTag.getCompound("Result");
					ItemStack result = resultTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(resultTag);

					int width = readTag.getInt("Width");
					int height = readTag.getInt("Height");
					NBTTagList nbtIngredients = readTag.getList("Ingredients", NBTUtil.COMPOUND);
					NonNullList<RecipeItemStack> ingredients = NonNullList.a(width * height, RecipeItemStack.a);
					for (int i = 0; i < nbtIngredients.size(); i++) {
						NBTTagCompound ingredientTag = nbtIngredients.get(i);
						ItemStack ingredientStack = ingredientTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(ingredientTag);
						RecipeItemStack ingredient = RecipeItemStack.a(new ItemStack[] {ingredientStack});
						ingredients.set(i, ingredient);
					}
					String permission = readTag.getString("Permission");

					PermissionRecipe shaped = new PermissionRecipe("", height, height, ingredients, result, permission);
					MinecraftKey key = nextCustomKey();
					shaped.setKey(key);

					CRPermissionRecipe cr = new CRPermissionRecipe(shaped);
					plugin.saveCraftingRecipeFile("permission", cr);

				} catch (IOException e) {
					e.printStackTrace();
					sender.sendMessage(ChatColor.RED + "Something went wrong when migrating permission recipe " + name + ", please check your console for errors.");
				}
			}
		}

		sender.sendMessage(ChatColor.YELLOW + "Migrating furnace recipes");
		//furnace
		folder = new File(extraFolder, "furnace_recipes");
		if (folder.exists() && folder.isDirectory()) {

			RecipesFurnace recipesFurnace = new RecipesFurnace();
			for (File file : folder.listFiles()) {
				String name = file.getName();

				try {
					NBTTagCompound readTag = NBTUtil.readNBTTagCompound(file);
					NBTTagCompound resultTag = readTag.getCompound("Result");
					ItemStack result = resultTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(resultTag);
					NBTTagCompound ingredientTag = readTag.getCompound("Ingredient");
					ItemStack ingredient = ingredientTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(ingredientTag);
					float xp = readTag.hasKeyOfType("Experience", NBTUtil.FLOAT) ? readTag.getFloat("Experience") : 0F;
					RecipeItemStack recipeItemStack = RecipeItemStack.a(new ItemStack[]{ingredient});
					
					NMSFurnaceRecipe furnaceRecipe = new NMSFurnaceRecipe(nextCustomKey(), recipeItemStack, result, xp);
					CRFurnaceRecipe cr = new CRFurnaceRecipe(furnaceRecipe);

					plugin.saveFurnaceRecipeFile(cr);
				} catch (IOException e) {
					e.printStackTrace();
					sender.sendMessage(ChatColor.RED + "Something went wrong when migrating furnace recipe " + name + ", please check your console for errors.");
				}
			}
		}

	}

	private void migrateDisabledRecipes(CommandSender sender, File disabledFolder) {	
		
		sender.sendMessage(ChatColor.YELLOW + "Migrating disabled shaped recipes");
		//shaped
		File folder = new File(disabledFolder, "shaped_recipes");
		if (folder.exists() && folder.isDirectory()) {
			for (File file : folder.listFiles()) {
				String name = file.getName();

				try {
					NBTTagCompound readTag = NBTUtil.readNBTTagCompound(file);
					NBTTagCompound resultTag = readTag.getCompound("Result");
					ItemStack result = resultTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(resultTag);

					int width = readTag.getInt("Width");
					int height = readTag.getInt("Height");
					NBTTagList nbtIngredients = readTag.getList("Ingredients", NBTUtil.COMPOUND);
					NonNullList<ItemStack> ingredients = NonNullList.a(width * height, ItemStack.a);
					for (int i = 0; i < nbtIngredients.size(); i++) {
						NBTTagCompound ingredientTag = nbtIngredients.get(i);
						ItemStack ingredientStack = ingredientTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(ingredientTag);
						ingredients.set(i, ingredientStack);
					}

					LegacyShapedRecipe legacyShaped = new LegacyShapedRecipe(width, height, result, ingredients);
					
					ShapedRecipes match = null;
					Iterator<IRecipe> iterator = CraftingManager.recipes.iterator();
					
					//loop until a match was found
					while (iterator.hasNext() && match == null) {
						IRecipe serverRecipe = iterator.next();
						if (serverRecipe instanceof ShapedRecipes) {
							ShapedRecipes shaped = (ShapedRecipes) serverRecipe;
							if (legacyShaped.equalsRecipe(shaped)) {
								match = shaped;
							}
						}
					}

					if (match != null) {
						
						//very nice! great success!
						CRShapedRecipe<ShapedRecipes> crShapedRecipe = new CRShapedRecipe<>(match);
						plugin.disableCraftingRecipeFile("shaped", crShapedRecipe);
						
					} else {
						sender.sendMessage(ChatColor.RED + "Couln't migrate disabled shaped recipe \"" + name + "\", no match was found in the crafting manager. Is it already disabled?");
					}

				} catch (IOException e) {
					e.printStackTrace();
					sender.sendMessage(ChatColor.RED + "Something went wrong when migrating disabled shaped recipe " + name + ", please check your console for errors.");
				}
			}
		}

		sender.sendMessage(ChatColor.YELLOW + "Migrating disabled shapeless recipes");
		//shapeless
		folder = new File(disabledFolder, "shapeless_recipes");
		if (folder.exists() && folder.isDirectory()) {
			for (File file : folder.listFiles()) {
				String name = file.getName();

				try {
					NBTTagCompound readTag = NBTUtil.readNBTTagCompound(file);
					NBTTagCompound resultTag = readTag.getCompound("Result");
					ItemStack result = resultTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(resultTag);

					NBTTagList nbtIngredients = readTag.getList("Ingredients", NBTUtil.COMPOUND);
					NonNullList<ItemStack> ingredients = NonNullList.a(nbtIngredients.size(), ItemStack.a);
					for (int i = 0; i < nbtIngredients.size(); i++) {
						NBTTagCompound ingredientTag = nbtIngredients.get(i);
						ItemStack ingredientStack = ingredientTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(ingredientTag);
						ingredients.set(i, ingredientStack);
					}

					LegacyShapelessRecipe legacyShapeless = new LegacyShapelessRecipe(result, ingredients);

					ShapelessRecipes match = null;
					Iterator<IRecipe> iterator = CraftingManager.recipes.iterator();
					
					//loop until a match was found
					while (iterator.hasNext() && match == null) {
						IRecipe serverRecipe = iterator.next();
						if (serverRecipe instanceof ShapelessRecipes) {
							ShapelessRecipes shapeless = (ShapelessRecipes) serverRecipe;
							if (legacyShapeless.equalsRecipe(shapeless)) {
								match = shapeless;
							}
						}
					}

					if (match != null) {
						
						//very nice! great success!
						CRShapelessRecipe<ShapelessRecipes> crShapedRecipe = new CRShapelessRecipe<>(match);
						plugin.disableCraftingRecipeFile("shapeless", crShapedRecipe);
						
					} else {
						sender.sendMessage(ChatColor.RED + "Couln't migrate disabled shapeless recipe \"" + name + "\", no match was found in the crafting manager. Is it already disabled?");
					}

				} catch (IOException e) {
					e.printStackTrace();
					sender.sendMessage(ChatColor.RED + "Something went wrong when migrating disabled shapeless recipe " + name + ", please check your console for errors.");
				}
			}
		}

		//no disabled permission recipes
		
		sender.sendMessage(ChatColor.YELLOW + "Migrating disabled furnace recipes");
		//furnace
		folder = new File(disabledFolder, "furnace_recipes");
		if (folder.exists() && folder.isDirectory()) {

			RecipesFurnace recipesFurnace = new RecipesFurnace();
			for (File file : folder.listFiles()) {
				String name = file.getName();

				try {
					NBTTagCompound readTag = NBTUtil.readNBTTagCompound(file);
					NBTTagCompound resultTag = readTag.getCompound("Result");
					ItemStack result = resultTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(resultTag);
					NBTTagCompound ingredientTag = readTag.getCompound("Ingredient");
					ItemStack ingredient = ingredientTag.isEmpty() ? ItemStack.a : NBTUtil.deserializeItemStack(ingredientTag);
					float xp = readTag.hasKeyOfType("Experience", NBTUtil.FLOAT) ? readTag.getFloat("Experience") : 0F;

					RecipeItemStack recipeItemStack = RecipeItemStack.a(new ItemStack[]{ingredient});
					NMSFurnaceRecipe furnaceRecipe = new NMSFurnaceRecipe(nextCustomKey(), recipeItemStack, result, xp);

					CRFurnaceRecipe cr = new CRFurnaceRecipe(furnaceRecipe);
					plugin.disableFurnaceRecipeFile(cr);
				} catch (IOException e) {
					e.printStackTrace();
					sender.sendMessage(ChatColor.RED + "Something went wrong when migrating disabled furnace recipe " + name + ", please check your console for errors.");
				}
			}
		}

	}
	
	
	//shaped recipe as in minecraft 1.11 and earlier versions. ingredients was just a list of itemstacks
	private static class LegacyShapedRecipe {
		private int width;
		private int heigth;
		private ItemStack result;
		private NonNullList<ItemStack> ingredients;
		
		public LegacyShapedRecipe(int width, int heigth, ItemStack result, NonNullList<ItemStack> ingredients) {
			this.width = width;
			this.heigth = heigth;
			this.result = result;
			this.ingredients = ingredients;
		}
		
		public boolean equalsRecipe(ShapedRecipes oneTwelveVersion) {
			int width = (int) ReflectionUtil.getDeclaredFieldValue(oneTwelveVersion, "width");
			if (width != this.width) return false;
			
			int heigth = (int) ReflectionUtil.getDeclaredFieldValue(oneTwelveVersion, "height");
			if (heigth != this.heigth) return false;
			
			ItemStack result = (ItemStack) ReflectionUtil.getDeclaredFieldValue(oneTwelveVersion, "result");
			if (!ItemStack.fastMatches(result, this.result)) return false;
			
			NonNullList<RecipeItemStack> ingredients = (NonNullList<RecipeItemStack>) ReflectionUtil.getDeclaredFieldValue(oneTwelveVersion, "items");
			for (int i = 0; i < ingredients.size(); i++) {
				RecipeItemStack ingredient = ingredients.get(i);
				if (!ingredient.a(this.ingredients.get(i))) return false; //legacy ingredient was not accepted by the 1.12 ingredient
			}
			
			return true;
		}
	}
	
	//shapeless recipe as in minecraft 1.11 and earlier versions
	private static class LegacyShapelessRecipe {
		private ItemStack result;
		private NonNullList<ItemStack> ingredients;
		
		public LegacyShapelessRecipe(ItemStack result, NonNullList<ItemStack> ingredients) {
			this.result = result;
			this.ingredients = ingredients;
		}
		
		public boolean equalsRecipe(ShapelessRecipes oneTwelveVersion) {
			ItemStack result = (ItemStack) ReflectionUtil.getDeclaredFieldValue(oneTwelveVersion, "result");
			if (!ItemStack.fastMatches(result, this.result)) return false;
			
			NonNullList<RecipeItemStack> ingredients = (NonNullList<RecipeItemStack>) ReflectionUtil.getDeclaredFieldValue(oneTwelveVersion, "ingredients");
			for (int i = 0; i < ingredients.size(); i++) {
				RecipeItemStack ingredient = ingredients.get(i);
				if (!ingredient.a(this.ingredients.get(i))) return false; //legacy ingredient was not accepted by the 1.12 ingredient
			}
			
			return true;
		}
	}

} public class RemoveRecipeCommandExecutor implements CommandExecutor {
	
	private final Map<String, ? extends BiConsumer<? super Player, ? super List<String>>> recipeRemovers;
	
	public RemoveRecipeCommandExecutor(Map<String, ? extends BiConsumer<? super Player, ? super List<String>>> recipeRemovers) {
		this.recipeRemovers = recipeRemovers;
	}
	
	@Override
	public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
		if (!(sender instanceof Player)) {
			sender.sendMessage(ChatColor.RED + "You can only use this command as a player.");
			return true;
		}
		
		if (args.length == 0) return false;
		Player player = (Player) sender;
		
		String type = args[0];		
		BiConsumer<? super Player, ? super List<String>> remover = recipeRemovers.get(type);
		if (remover == null) {
			sender.sendMessage(ChatColor.RED + "Unrecognized recipe type " + ChatColor.WHITE + args[0] + ChatColor.RED + ".");
			sender.sendMessage(ChatColor.RED + "Please choose from " + ChatColor.WHITE + recipeRemovers.keySet().toString() + ChatColor.RED + ".");
			return true;
		}
		
		try {
			remover.accept(player, Arrays.asList(Arrays.copyOfRange(args, 1, args.length)));
		} catch (Throwable throwable) {
			throwable.printStackTrace();
			player.sendMessage(ChatColor.RED + "Something went wrong when trying to remove a recipe. Check the console for errors.");
		}
		
		return true;
	}

} public class ListRecipesListener implements Listener {

	private static final ItemStack NEXT = new ItemStack(Material.MAGENTA_GLAZED_TERRACOTTA);
	private static final ItemStack PREVIOUS = new ItemStack(Material.MAGENTA_GLAZED_TERRACOTTA);
	static {
		ItemMeta nextMeta = NEXT.getItemMeta();
		nextMeta.setDisplayName("NEXT");
		NEXT.setItemMeta(nextMeta);

		ItemMeta previousMeta = PREVIOUS.getItemMeta();
		previousMeta.setDisplayName("PREVIOUS");
		PREVIOUS.setItemMeta(previousMeta);
	}

	public static ItemStack nextItem() {
		return NEXT.clone();
	}

	public static ItemStack previousItem() {
		return PREVIOUS.clone();
	}

	@EventHandler
	public void onInventoryClick(InventoryClickEvent event) {
		if (event.getView().getTopInventory().getHolder() instanceof ListRecipesInventoryHolder) {
			event.setCancelled(true);
			ListRecipesInventoryHolder pageHolder = (ListRecipesInventoryHolder) event.getView().getTopInventory().getHolder();

			//https://hub.spigotmc.org/javadocs/spigot/org/bukkit/event/inventory/InventoryClickEvent.html
			//as per the docs, the close and open methods cannot be called in the same game tick.
			if (NEXT.isSimilar(event.getCurrentItem())) {
				Bukkit.getScheduler().runTask(CustomRecipesPlugin.getInstance(), () -> {
					event.getWhoClicked().closeInventory();
					pageHolder.nextPage();
					event.getWhoClicked().openInventory(pageHolder.getInventory());
				});
			} else if (PREVIOUS.isSimilar(event.getCurrentItem())) {
				Bukkit.getScheduler().runTask(CustomRecipesPlugin.getInstance(), () -> {
					event.getWhoClicked().closeInventory();
					pageHolder.previousPage();
					event.getWhoClicked().openInventory(pageHolder.getInventory());
				});
			}

		}
	}

} public class CRCraftingManager implements com.gmail.jannyboy11.customrecipes.api.crafting.CraftingManager {

	protected final BiMap<IRecipe, CraftingRecipe> nms2cr = HashBiMap.create();
	protected final BiMap<CraftingRecipe, IRecipe> cr2nms = nms2cr.inverse();

	protected CRRecipeRegistry recipeRegistry;

	public CRCraftingManager() {
		ensurePatchedRegistry();
	}

	/**
	 * Add a crafting recipe.
	 * 
	 * @deprecated internal use only. Use {@link CRCraftingManager#addRecipe(MinecraftKey, IRecipe, CraftingRecipe)} instead.
	 * @param key
	 * @param recipe
	 * @return whether the recipe was registered.
	 */
	@Deprecated
	protected boolean putRecipe(MinecraftKey key, CraftingRecipe recipe) {
		if (recipe == null || key == null) return false;
		
		//should I care about implementations that don't extend CRCraftingRecipe?

		if (recipe instanceof CRCraftingRecipe) {
			@SuppressWarnings("rawtypes")
			CRCraftingRecipe wrapper = (CRCraftingRecipe) recipe;
			return addRecipe(key, wrapper.getHandle(), wrapper);
		} else {
			Bukkit2NMSRecipe nmsifiedRecipe = new Bukkit2NMSRecipe(recipe);
			return addRecipe(key, nmsifiedRecipe, recipe);
		}
		
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean addRecipe(NamespacedKey key, CraftingRecipe recipe) {
		if (key == null || recipe == null) return false;
		
		MinecraftKey minecraftKey = CraftNamespacedKey.toMinecraft(key);

		return putRecipe(minecraftKey, recipe);
	}

	/**
	 * Register a recipe
	 * 
	 * @param key the key of the recipe
	 * @param nmsRecipe the Minecraft variant of the recipe
	 * @param crRecipe the CustomRecipes variant of the recipe
	 * @return whether the recipe was registered
	 */
	public boolean addRecipe(MinecraftKey key, IRecipe nmsRecipe, CraftingRecipe crRecipe) {
		if (key == null || nmsRecipe == null || crRecipe == null) return false;
		
		IRecipe existing = CraftingManager.a(key);
		if (existing != null) return false; //duplicate key
					
		nms2cr.forcePut(nmsRecipe, crRecipe);
		CraftingManager.a(key, nmsRecipe);
		nmsRecipe.setKey(key);
		return true;
	}
	
	
	/**
	 * Get the mirror or handle from a registered recipe
	 * @param nms the nms variant
	 * @return the custom recipes variant
	 */
	public CraftingRecipe mirrorOrHandle(IRecipe nms) {
		return nms2cr.get(nms);
	}
	
	/**
	 * Get the mirror or handle from a registered recipe
	 * @param cr the custom recipes variant
	 * @return the nms variant
	 */
	public IRecipe mirrorOrHandle(CraftingRecipe cr) {
		return cr2nms.get(cr);
	}
	

	/**
	 * {@inheritDoc}
	 */
	@Override
	public boolean reset() {
		clear();
		return CraftingManager.init(); //the CraftingManager#init() method can only be called when the registry is empty!
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void clear() {
		nms2cr.clear();
		CraftingManager.recipes = recipeRegistry = new CRRecipeRegistry();
	}

	/**
	 * Obtain the CustomRecipes variant of a Minecraft recipe.
	 * 
	 * @param recipe the minecraft recipe
	 * @return the custom recipes mirror
	 */
	public CraftingRecipe fromNMSRecipe(IRecipe recipe) {
		if (recipe == null) return null;

		//the recipe could be a custom implementation. return that one if present
		CraftingRecipe fromNMS = nms2cr.get(recipe);
		if (fromNMS != null) return fromNMS;

		//guaranteed to be absent
		return nms2cr.computeIfAbsent(recipe, r -> {
			//if not not custom, check which vanilla implementation. order is important.
			//register it as well to ensure reference equality when this method is called again
			if (r instanceof RecipeArmorDye) {
				return new CRArmorDyeRecipe((RecipeArmorDye) r);
			} else if (r instanceof RecipesBanner.AddRecipe) {
				return new CRBannerAddPatternRecipe((RecipesBanner.AddRecipe) r);
			} else if (r instanceof RecipesBanner.DuplicateRecipe){
				return new CRBannerDuplicateRecipe((RecipesBanner.DuplicateRecipe) r);
			} else if (r instanceof RecipeBookClone) {
				return new CRBookCloneRecipe((RecipeBookClone) r);
			} else if (r instanceof RecipeFireworks) {
				return new CRFireworksRecipe((RecipeFireworks) r);
			} else if (r instanceof RecipeMapClone) {
				return new CRMapCloneRecipe((RecipeMapClone) r);
			} else if (r instanceof RecipeMapExtend) {
				return new CRMapExtendRecipe((RecipeMapExtend) r);
			} else if (r instanceof RecipeRepair) {
				return new CRRepairRecipe((RecipeRepair) r);
			} else if (r instanceof RecipiesShield.Decoration) {
				return new CRShieldDecorationRecipe((RecipiesShield.Decoration) r);
			} else if (r instanceof RecipeShulkerBox.Dye) {
				return new CRShulkerBoxDyeRecipe((RecipeShulkerBox.Dye) r);
			} else if (r instanceof RecipeTippedArrow) {
				return new CRTippedArrowRecipe((RecipeTippedArrow) r);
			} else if (r instanceof ShapedRecipes) {
				return new CRShapedRecipe<>((ShapedRecipes) r);
			} else if (r instanceof ShapelessRecipes) {
				return new CRShapelessRecipe<>((ShapelessRecipes) r);
			} 

			//unknown type of recipe.
			//best we can do is return a generic crafting recipe 
			return new CRCraftingRecipe<>(recipe);
		});
	}
	
	/**
	 * Obtain the Minecraft variant of a CustomRecipes CraftingRecipe.
	 * 
	 * @param craftingRecipe the recipe
	 * @return the minecraft handle or mirror
	 */
	public IRecipe toNMSRecipe(CraftingRecipe craftingRecipe) {
		if (craftingRecipe == null) return null;
		
		IRecipe fromCR = cr2nms.get(craftingRecipe);
		if (fromCR != null) return fromCR;
		
		if (craftingRecipe instanceof CRCraftingRecipe) {
			//best we can do - try to get the nms handle
			fromCR = ((CRCraftingRecipe) craftingRecipe).getHandle();
		} else {
			//fallback - use a bukkit2nms mirror
			fromCR = new Bukkit2NMSRecipe(craftingRecipe);
		}
		
		cr2nms.put(craftingRecipe, fromCR);
		return fromCR;
	}

	/**
	 * Get a Minecraft recipe by its key.
	 * 
	 * @param minecraftKey the key
	 * @return the recipe if one was registered for the key, or null if no recipe was registered for the key
	 */
	public IRecipe getNMSRecipe(MinecraftKey minecraftKey) {
		if (minecraftKey == null) return null;
		
		return CraftingManager.a(minecraftKey);
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public CraftingRecipe getRecipe(NamespacedKey key) {
		if (key == null) return null;
		
		MinecraftKey mcKey = CraftNamespacedKey.toMinecraft(key);
		IRecipe mcRecipe = getNMSRecipe(mcKey);
		if (mcRecipe == null) return null;
		return fromNMSRecipe(mcRecipe);
	}
	
	/**
	 * Remove a Minecraft recipe by its key.
	 * 
	 * @param key the key
	 * @return the recipe what was registered for the key, or null if the key was not registered
	 */
	public IRecipe removeRecipe(MinecraftKey key) {
		ensurePatchedRegistry();

		IRecipe nmsRecipe = recipeRegistry.removeRecipe(key);
		if (nmsRecipe != null) nms2cr.remove(nmsRecipe);
		return nmsRecipe;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public CraftingRecipe removeRecipe(NamespacedKey key) {
		if (key == null) return null;
		
		MinecraftKey mcKey = CraftNamespacedKey.toMinecraft(key);
		IRecipe removed = removeRecipe(mcKey);
		if (removed == null) return null;
		
		CraftingRecipe recipe = fromNMSRecipe(removed);
		nms2cr.remove(removed);
		
		return recipe;
	}

	/**
	 * Get the iterator that iterates over all registered crafting recipes
	 * 
	 * @return the iterator
	 */
	@Override
	public Iterator<CraftingRecipe> iterator() {
		return new MapIterator<>(CraftingManager.recipes.iterator(), this::fromNMSRecipe);
	}

	/**
	 * Get the key for a recipe.
	 * 
	 * @param recipe the recipe
	 * @return the associated key, or null of the recipe was not registered
	 */
	public MinecraftKey getKey(IRecipe recipe) {
		if (recipe == null) return null;
		
		return CraftingManager.recipes.b(recipe);
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public NamespacedKey getKey(CraftingRecipe recipe) {
		if (recipe == null) return null;
		
		IRecipe nmsRecipe = toNMSRecipe(recipe);
		MinecraftKey key = getKey(nmsRecipe);
		return key == null ? null : CraftNamespacedKey.fromMinecraft(key);
	}

	/**
	 * Remove a Minecraft recipe
	 * 
	 * @param recipe the recipe
	 * @return the key that was associated with the recipe, or null if the recipe was not registered
	 */
	public MinecraftKey removeRecipe(IRecipe recipe) {
		if (recipe == null) return null;
		ensurePatchedRegistry();

		nms2cr.remove(recipe);
		return recipeRegistry.removeRecipe(recipe);
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public NamespacedKey removeRecipe(CraftingRecipe recipe) {
		if (recipe == null) return null;
		
		IRecipe nmsRecipe = toNMSRecipe(recipe);
		if (nmsRecipe == null) return null;
		
		MinecraftKey key = removeRecipe(nmsRecipe);
		if (key == null) return null;
		
		return CraftNamespacedKey.fromMinecraft(key);
	}

	/**
	 * Get the id for a recipe
	 * 
	 * @param recipe the recipe
	 * @return the id if the recipe was registered, otherwise -1
	 */
	public int getRecipeId(IRecipe recipe) {
		if (recipe == null) return -1;
		
		return CraftingManager.a(recipe);
	}
	
	/**
	 * Get the id for a crafting recipe
	 * 
	 * @param recipe the crafting recipe
	 * @return the id if the recipe is registered, otherwise -1
	 */
	//not in the api because the id system is likely to change in 1.13, and the method is not really needed since we already have the getKey() method
	public int getId(CraftingRecipe recipe) {
		if (recipe == null) return -1;
		
		IRecipe nmsRecipe = toNMSRecipe(recipe);
		if (nmsRecipe == null) return -1;
		
		return getRecipeId(nmsRecipe);
	}

	/**
	 * Get a recipe by its id
	 * @param id the id
	 * @return the recipe if one was registered for the id, otherwise null
	 */
	public IRecipe getNmsById(int id) {
		return CraftingManager.a(id);
	}
	
	/**
	 * Get the crafting recipe for its id
	 * 
	 * @param id the id 
	 * @return the crafting recipe if one was registered for this id, otherwise null
	 */
	//not in the api because the id system is likely to change in 1.13, and the method is not really needed since we already have the getKey() method
	public CraftingRecipe getById(int id) {
		IRecipe nmsRecipe = getNmsById(id);
		
		if (nmsRecipe == null) return null;
		
		return fromNMSRecipe(nmsRecipe);
	}
	
	
	/**
	 * This method is used to patch the RegistryMaterials in the CraftingManager to allow recipes to be removed.
	 * 
	 * This method must be called every time before the crafting manager tries to remove a recipe,
	 * because other plugins may set the recipe registry to the vanilla implementation by calling {@link org.bukkit.Server#resetRecipes()} or {@link org.bukkit.Server#clearRecipes()}. 
	 */
	protected void ensurePatchedRegistry() {
		if (CraftingManager.recipes != recipeRegistry) {
			CraftingManager.recipes = recipeRegistry = new CRRecipeRegistry(CraftingManager.recipes);
		}
	}

} public class CRCraftingRecipe<R extends IRecipe> implements CraftingRecipe, NBTSerializable {
	
	protected final R nmsRecipe;
	
	protected CRCraftingRecipe(R nmsRecipe) {		
		this.nmsRecipe = Objects.requireNonNull(nmsRecipe);
	}

	@Override
	public boolean matches(CraftingInventory craftingInventory, World world) {
		CraftWorld cWorld = (CraftWorld) world;
		
		WorldServer nmsWorld = cWorld.getHandle();
		InventoryCrafting nmsCraftingInventory = getNmsCraftingInventory(craftingInventory);

		return nmsRecipe.a(nmsCraftingInventory, nmsWorld);
	}

	@Override
	public CraftItemStack craftItem(CraftingInventory craftingInventory) {
		InventoryCrafting nmsCraftingInventory = getNmsCraftingInventory(craftingInventory);
		return CraftItemStack.asCraftMirror(nmsRecipe.craftItem(nmsCraftingInventory));
	}

	@Override
	public CraftItemStack getResult() {
		return CraftItemStack.asCraftMirror(nmsRecipe.b());
	}
	
	@Override
	public boolean isHidden() {
		return nmsRecipe.c();
	}
	
	@Override
	public List<CRChoiceIngredient> getIngredients() {
	    return nmsRecipe.d().stream()
	            .map(CRCraftingIngredient::asBukkitIngredient)
	            .collect(Collectors.toList());
	}

	@Override
	public List<CraftItemStack> getLeftOverItems(CraftingInventory craftingInventory) {
		InventoryCrafting nmsCraftingInventory = getNmsCraftingInventory(craftingInventory);
		return nmsRecipe.b(nmsCraftingInventory).stream()
				.map(CraftItemStack::asCraftMirror)
				.collect(Collectors.toList());
	}
	
	public static InventoryCrafting getNmsCraftingInventory(CraftingInventory bukkitInventory) {
		return (InventoryCrafting) ReflectionUtil.getDeclaredFieldValue(bukkitInventory, "inventory");
	}
	
	public IRecipe getHandle() {
		return nmsRecipe;
	}
	
	/**
	 * Get the key of this recipe
	 * 
	 * @return the key if this recipe is registered, otherwise null
	 */
	public final NamespacedKey getKey() {
		MinecraftKey mcKey = getMinecraftKey();
		return mcKey == null ? null : CraftNamespacedKey.fromMinecraft(mcKey);
	}
	
	public MinecraftKey getMinecraftKey() {
		return CraftingManager.recipes.b(nmsRecipe);
	}
	
	@Override
	public ItemStack getRepresentation() {
		ItemStack result = getResult();
		
		ItemStack representation = (result == null || result.getType() == Material.AIR) ? new ItemStack(Material.AIR) : result.clone();
		if (representation.getType() == Material.AIR) {
			representation = new ItemStack(Material.STRUCTURE_BLOCK);
			ItemMeta meta = representation.getItemMeta();
			meta.setDisplayName(InventoryUtils.getItemName(getResult()));
			meta.setLore(Arrays.asList("Result: UNKNOWN", ChatColor.DARK_GRAY + "Key: " + getMinecraftKey()));
			representation.setItemMeta(meta);
			return representation;
		}
		
		ItemMeta meta = representation.getItemMeta();

		List<String> lore = new ArrayList<>();
		lore.add(ChatColor.DARK_GRAY + "Hidden: " + isHidden());
		lore.add(ChatColor.DARK_GRAY + "Key: " + getMinecraftKey());
		
		meta.setDisplayName(ChatColor.GRAY + InventoryUtils.getItemName(getResult()));
		meta.setLore(lore);
		
		representation.setItemMeta(meta);
		return representation;
	}
	
	public NBTTagCompound serializeToNbt() {
		NBTTagCompound compound = new NBTTagCompound();
		compound.set("result", nmsRecipe.b().save(new NBTTagCompound()));
		if (hasGroup()) compound.setString("group", getGroup());
		if (isHidden()) compound.setBoolean("hidden", isHidden());
		MinecraftKey key = getMinecraftKey();
		if (key != null) compound.set("key", NBTUtil.serializeKey(key));
		return compound;
	}
	

	@Override
	public boolean equals(Object object) {
		if (object == null) return false;
		if (!(object instanceof CRCraftingRecipe)) return false;
		@SuppressWarnings("rawtypes")
		CRCraftingRecipe that = (CRCraftingRecipe) object;
		return this.nmsRecipe.equals(that.nmsRecipe);
	}
	
	@Override
	public int hashCode() {
		return nmsRecipe.hashCode();
	}
	
	@Override
	public String toString() {
		return getClass().getName() + "{" +
				"key()=" + getKey() +
				",result()=" + InventoryUtils.getItemName(getResult()) +
				",hidden()=" + isHidden() +
				"}";
	}
	
} public class CountAdder implements BiConsumer<Player, List<String>> {

	private final CustomRecipesPlugin plugin;

	public CountAdder(CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}

	@Override
	public void accept(Player player, List<String> args) {
		org.bukkit.inventory.ItemStack itemInMainHand = player.getInventory().getItemInMainHand();
		if (itemInMainHand == null) {
			player.sendMessage(ChatColor.RED + "Put the result of the recipe in your main hand when executing this command.");
			return;
		}

		if (args.isEmpty()) {
			player.sendMessage(ChatColor.RED + "Usage: /addrecipe count <key> [<group>]");
			return;
		}

		String keyString = args.get(0);
		String group = args.size() >= 2 ? args.get(1) : "";
		NamespacedKey bukkitKey = plugin.getKey(keyString);

		ItemStack result = CraftItemStack.asNMSCopy(itemInMainHand);
		MinecraftKey key = CraftNamespacedKey.toMinecraft(bukkitKey);

		player.openInventory(new CountRecipeHolder(plugin, result, key, group, player).getInventory());
	}


	private static final class CountRecipeHolder implements InventoryHolder, Listener {

		private final Inventory dispenserInventory;
		private final CustomRecipesPlugin plugin;
		private final ItemStack result;
		private final MinecraftKey key;
		private final String group;
		private final Player callbackPlayer;

		public CountRecipeHolder(CustomRecipesPlugin plugin, ItemStack result, MinecraftKey key, String group, Player callbackPlayer) {
			this.plugin = plugin;
			this.result = result;
			this.key = key;
			this.group = group;
			this.callbackPlayer = callbackPlayer;
			this.dispenserInventory = plugin.getServer().createInventory(this, InventoryType.DISPENSER, "Create a count recipe!");
			plugin.getServer().getPluginManager().registerEvents(this, plugin);
		}

		@Override
		public Inventory getInventory() {
			return dispenserInventory;
		}


		@EventHandler
		public void onInventoryClose(InventoryCloseEvent event) {
			if (event.getInventory().getHolder() instanceof CountRecipeHolder) {
				CountRecipeHolder holder = (CountRecipeHolder) event.getInventory().getHolder();
				if (holder != this) return;

				Inventory inventory = event.getInventory();
				if (InventoryUtils.isEmpty(inventory)) {
					holder.callbackPlayer.sendMessage(ChatColor.RED + "Do you seriously want to create a recipe without ingredients?");
					return;
				}

				CountRecipe nmsRecipe = holder.toRecipe();
				CRCountRecipe countRecipe = new CRCountRecipe(nmsRecipe);
				List<List<String>> recipeIngredients = countRecipe.getIngredients().stream()
						.map((CRChoiceIngredient ingr) -> ingr.getChoices().stream()
								.map(InventoryUtils::getItemName).collect(Collectors.toList()))
						.collect(Collectors.toList());
				String recipeString = recipeIngredients + "" +
						ChatColor.RESET + " -> " +
						InventoryUtils.getItemName(countRecipe.getResult());

				boolean success = holder.plugin.getCraftingManager().addRecipe(holder.key, nmsRecipe, countRecipe);
				if (success) {
					holder.callbackPlayer.sendMessage(String.format("%sAdded count recipe: %s%s%s!",
							ChatColor.GREEN, ChatColor.WHITE, recipeString, ChatColor.WHITE));
					plugin.saveCraftingRecipeFile("count", countRecipe);
				} else {
					holder.callbackPlayer.sendMessage(ChatColor.RED + "Couldn't create a count recipe. Possibly a duplicate key.");
				}

				HandlerList.unregisterAll(holder);
			}
		}


		private CountRecipe toRecipe() {
			CraftInventoryCustom dispenserInventory = (CraftInventoryCustom) this.dispenserInventory;
			IInventory minecraftInventory = (IInventory) ReflectionUtil.getDeclaredFieldValue(dispenserInventory, "inventory");
			NonNullList<ItemStack> dispenserInventoryContents = (NonNullList<ItemStack>) ReflectionUtil.getDeclaredFieldValue(minecraftInventory, "items");

			int minNonEmptyRownum = 2;
			int minNonEmptyColnum = 2;
			int maxNonEmptyRownum = 0;
			int maxNonEmptyColnum = 0;

			for (int index = 0; index < dispenserInventoryContents.size(); index++) {
				ItemStack stack = dispenserInventoryContents.get(index);
				if (!stack.isEmpty()) {
					int[] rownumColnum = InventoryUtils.inventoryRownumColnum(3, index);
					int rowNum = rownumColnum[0];
					int colNum = rownumColnum[1];

					minNonEmptyRownum = Math.min(minNonEmptyRownum, rowNum);
					minNonEmptyColnum = Math.min(minNonEmptyColnum, colNum);
					maxNonEmptyRownum = Math.max(maxNonEmptyRownum, rowNum);
					maxNonEmptyColnum = Math.max(maxNonEmptyColnum, colNum);
				}
			}

			int height = maxNonEmptyRownum - minNonEmptyRownum + 1;
			int width = maxNonEmptyColnum - minNonEmptyColnum + 1;

			NonNullList<RecipeItemStack> ingredients = NonNullList.a(height * width, RecipeItemStack.a);
			for (int h = 0; h < height; h++) {
				for (int w = 0; w < width; w++) {
					int index = InventoryUtils.inventoryIndex(width, new int[] {h, w});

					ItemStack ingredientStack = dispenserInventoryContents.get(InventoryUtils.inventoryIndex(3,
							new int[] {h + minNonEmptyRownum, w + minNonEmptyColnum}));

					RecipeItemStack vanillaIngredient = RecipeItemStack.a(new ItemStack[] {ingredientStack});

					ingredients.set(index, vanillaIngredient);
				}
			}

			CountRecipe countRecipe = new CountRecipe(group, width, height, ingredients, result);
			countRecipe.setKey(key);
			return countRecipe;
		}

	}

} public class CountRemover implements BiConsumer<Player, List<String>> {

	private final CustomRecipesPlugin plugin;

	public CountRemover(CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}

	@Override
	public void accept(Player player, List<String> args) {
		CRCraftingManager craftingManager = plugin.getCraftingManager();

		CRCountRecipe toBeRemoved = null;

		if (!args.isEmpty()) {

			String firstArg = args.get(0);
			NamespacedKey key = plugin.getKey(firstArg);

			CraftingRecipe recipe = craftingManager.getRecipe(key);
			
			if (recipe instanceof CRCountRecipe) {
				toBeRemoved = (CRCountRecipe) recipe;
			} else {
				player.sendMessage(ChatColor.RED + "Couldn't remove recipe for key " + ChatColor.WHITE + key + ChatColor.RED + ", it is not a native count recipe.");
				return;
			}

		} else {
			ItemStack itemInHand = player.getInventory().getItemInMainHand();
			if (InventoryUtils.isEmptyStack(itemInHand)) itemInHand = player.getInventory().getItemInOffHand();
			if (InventoryUtils.isEmptyStack(itemInHand)) {
				player.sendMessage(ChatColor.RED + "Usage: /removerecipe count <key>");
				player.sendMessage(ChatColor.RED + "Or '/removerecipe count' with an item in your hand.");
				return;
			}

			//loop until match
			for (CraftingRecipe recipe : craftingManager) {
				if (!(recipe instanceof CRCountRecipe)) continue;
				if (itemInHand.equals(recipe.getResult())) {
					toBeRemoved = (CRCountRecipe) recipe;
					break;
				}
			}

			//no match
			if (toBeRemoved == null) {
				player.sendMessage(ChatColor.RED + "No count recipe found for resultstack " +
						ChatColor.WHITE + InventoryUtils.getItemName(itemInHand) +
						ChatColor.RED + ".");
				return;
			}

		}

		if (toBeRemoved != null) {
			//match
			NamespacedKey key = craftingManager.removeRecipe(toBeRemoved);
			player.sendMessage(ChatColor.GREEN + "Removed recipe with key " +
					ChatColor.WHITE + key +
					ChatColor.GREEN + " for item " +
					ChatColor.WHITE + InventoryUtils.getItemName(toBeRemoved.getResult()) +
					ChatColor.GREEN + ".");
			plugin.disableCraftingRecipeFile("count", toBeRemoved);
		}
	}

} public class NBTAdder implements BiConsumer<Player, List<String>> {

	private final CustomRecipesPlugin plugin;

	public NBTAdder(CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}

	@Override
	public void accept(Player player, List<String> args) {
		org.bukkit.inventory.ItemStack itemInMainHand = player.getInventory().getItemInMainHand();
		if (itemInMainHand == null) {
			player.sendMessage(ChatColor.RED + "Put the result of the recipe in your main hand when executing this command.");
			return;
		}

		if (args.isEmpty()) {
			player.sendMessage(ChatColor.RED + "Usage: /addrecipe NBT <key> [<group>]");
			return;
		}

		String keyString = args.get(0);
		String group = args.size() >= 2 ? args.get(1) : "";
		NamespacedKey bukkitKey = plugin.getKey(keyString);

		ItemStack result = CraftItemStack.asNMSCopy(itemInMainHand);
		MinecraftKey key = CraftNamespacedKey.toMinecraft(bukkitKey);

		player.openInventory(new NBTRecipeHolder(plugin, result, key, group, player).getInventory());
	}


	private static final class NBTRecipeHolder implements InventoryHolder, Listener {

		private final Inventory dispenserInventory;
		private final CustomRecipesPlugin plugin;
		private final ItemStack result;
		private final MinecraftKey key;
		private final String group;
		private final Player callbackPlayer;

		public NBTRecipeHolder(CustomRecipesPlugin plugin, ItemStack result, MinecraftKey key, String group, Player callbackPlayer) {
			this.plugin = plugin;
			this.result = result;
			this.key = key;
			this.group = group;
			this.callbackPlayer = callbackPlayer;
			this.dispenserInventory = plugin.getServer().createInventory(this, InventoryType.DISPENSER, "Create an NBT recipe!");
			plugin.getServer().getPluginManager().registerEvents(this, plugin);
		}

		@Override
		public Inventory getInventory() {
			return dispenserInventory;
		}


		@EventHandler
		public void onInventoryClose(InventoryCloseEvent event) {
			if (event.getInventory().getHolder() instanceof NBTRecipeHolder) {
				NBTRecipeHolder holder = (NBTRecipeHolder) event.getInventory().getHolder();
				if (holder != this) return;

				Inventory inventory = event.getInventory();
				if (InventoryUtils.isEmpty(inventory)) {
					holder.callbackPlayer.sendMessage(ChatColor.RED + "Do you seriously want to create a recipe without ingredients?");
					return;
				}

				NBTRecipe nmsRecipe = holder.toRecipe();
				CRNBTRecipe nbtRecipe = new CRNBTRecipe(nmsRecipe);
				List<List<String>> recipeIngredients = nbtRecipe.getIngredients().stream()
						.map((CRChoiceIngredient ingr) -> ingr.getChoices().stream()
								.map(InventoryUtils::getItemName).collect(Collectors.toList()))
						.collect(Collectors.toList());
				String recipeString = recipeIngredients + "" +
						ChatColor.RESET + " -> " +
						InventoryUtils.getItemName(nbtRecipe.getResult());

				boolean success = holder.plugin.getCraftingManager().addRecipe(holder.key, nmsRecipe, nbtRecipe);
				if (success) {
					holder.callbackPlayer.sendMessage(String.format("%sAdded NBT recipe: %s%s%s!",
							ChatColor.GREEN, ChatColor.WHITE, recipeString, ChatColor.WHITE));
					plugin.saveCraftingRecipeFile("nbt", nbtRecipe);
				} else {
					holder.callbackPlayer.sendMessage(ChatColor.RED + "Couldn't create an NBT recipe. Possibly a duplicate key.");
				}

				HandlerList.unregisterAll(holder);
			}
		}


		private NBTRecipe toRecipe() {
			CraftInventoryCustom dispenserInventory = (CraftInventoryCustom) this.dispenserInventory;
			IInventory minecraftInventory = (IInventory) ReflectionUtil.getDeclaredFieldValue(dispenserInventory, "inventory");
			NonNullList<ItemStack> dispenserInventoryContents = (NonNullList<ItemStack>) ReflectionUtil.getDeclaredFieldValue(minecraftInventory, "items");

			int minNonEmptyRownum = 2;
			int minNonEmptyColnum = 2;
			int maxNonEmptyRownum = 0;
			int maxNonEmptyColnum = 0;

			for (int index = 0; index < dispenserInventoryContents.size(); index++) {
				ItemStack stack = dispenserInventoryContents.get(index);
				if (!stack.isEmpty()) {
					int[] rownumColnum = InventoryUtils.inventoryRownumColnum(3, index);
					int rowNum = rownumColnum[0];
					int colNum = rownumColnum[1];

					minNonEmptyRownum = Math.min(minNonEmptyRownum, rowNum);
					minNonEmptyColnum = Math.min(minNonEmptyColnum, colNum);
					maxNonEmptyRownum = Math.max(maxNonEmptyRownum, rowNum);
					maxNonEmptyColnum = Math.max(maxNonEmptyColnum, colNum);
				}
			}

			int height = maxNonEmptyRownum - minNonEmptyRownum + 1;
			int width = maxNonEmptyColnum - minNonEmptyColnum + 1;

			NonNullList<RecipeItemStack> ingredients = NonNullList.a(height * width, RecipeItemStack.a);
			for (int h = 0; h < height; h++) {
				for (int w = 0; w < width; w++) {
					int index = InventoryUtils.inventoryIndex(width, new int[] {h, w});

					ItemStack ingredientStack = dispenserInventoryContents.get(InventoryUtils.inventoryIndex(3,
							new int[] {h + minNonEmptyRownum, w + minNonEmptyColnum}));

					RecipeItemStack vanillaIngredient = RecipeItemStack.a(new ItemStack[] {ingredientStack});

					ingredients.set(index, vanillaIngredient);
				}
			}

			NBTRecipe nbtRecipe = new NBTRecipe(group, width, height, ingredients, result);
			nbtRecipe.setKey(key);
			return nbtRecipe;
		}

	}

} public class NBTRemover implements BiConsumer<Player, List<String>> {

	private final CustomRecipesPlugin plugin;

	public NBTRemover(CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}

	@Override
	public void accept(Player player, List<String> args) {
		CRCraftingManager craftingManager = plugin.getCraftingManager();

		CRNBTRecipe toBeRemoved = null;

		if (!args.isEmpty()) {

			String firstArg = args.get(0);
			NamespacedKey key = plugin.getKey(firstArg);

			CraftingRecipe recipe = craftingManager.getRecipe(key);

			if (recipe instanceof CRNBTRecipe) {
				toBeRemoved = (CRNBTRecipe) recipe;
			} else {
				player.sendMessage(ChatColor.RED + "Couldn't remove recipe for key " + ChatColor.WHITE + key + ChatColor.RED + ", it is not a native nbt recipe.");
				return;
			}


		} else {
			ItemStack itemInHand = player.getInventory().getItemInMainHand();
			if (InventoryUtils.isEmptyStack(itemInHand)) itemInHand = player.getInventory().getItemInOffHand();
			if (InventoryUtils.isEmptyStack(itemInHand)) {
				player.sendMessage(ChatColor.RED + "Usage: /removerecipe nbt <key>");
				player.sendMessage(ChatColor.RED + "Or '/removerecipe nbt' with an item in your hand.");
				return;
			}

			//loop until match
			net.minecraft.server.v1_12_R1.ItemStack nmsItemInHand = CraftItemStack.asNMSCopy(itemInHand);
			for (Iterator<IRecipe> recipeIterator = CraftingManager.recipes.iterator(); recipeIterator.hasNext();) {
				IRecipe recipe = recipeIterator.next();
				if (!(recipe instanceof NBTRecipe)) continue;
				if (net.minecraft.server.v1_12_R1.ItemStack.fastMatches(nmsItemInHand, recipe.b()) &&
						Objects.equals(nmsItemInHand.getTag(), recipe.b().getTag())) {
					toBeRemoved = (CRNBTRecipe) craftingManager.mirrorOrHandle(recipe);
					break;
				}
			}

			if (toBeRemoved == null) {
				//no match
				player.sendMessage(ChatColor.RED + "No nbt recipe found for resultstack " +
						ChatColor.WHITE + InventoryUtils.getItemName(itemInHand) +
						ChatColor.RED + ".");
				return;
			}
		}


		if (toBeRemoved != null) {
			NamespacedKey key = craftingManager.removeRecipe(toBeRemoved);
			player.sendMessage(ChatColor.GREEN + "Removed recipe with key " +
					ChatColor.WHITE + key +
					ChatColor.GREEN + " for item " +
					ChatColor.WHITE + InventoryUtils.getItemName(toBeRemoved.getResult()) +
					ChatColor.GREEN + ".");
			plugin.disableCraftingRecipeFile("nbt", toBeRemoved);
			return;
		}

	}

} public class PermissionAdder implements BiConsumer<Player, List<String>> {

	private final CustomRecipesPlugin plugin;

	public PermissionAdder(CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}

	@Override
	public void accept(Player player, List<String> args) {
		org.bukkit.inventory.ItemStack itemInMainHand = player.getInventory().getItemInMainHand();
		if (itemInMainHand == null) {
			player.sendMessage(ChatColor.RED + "Put the result of the recipe in your main hand when executing this command.");
			return;
		}

		if (args.size() < 2) {
			player.sendMessage(ChatColor.RED + "Usage: /addrecipe permission <key> <permission> [<group>]");
			return;
		}

		String keyString = args.get(0);
		String permission = args.get(1);
		String group = args.size() >= 3 ? args.get(2) : "";
		NamespacedKey bukkitKey = plugin.getKey(keyString);

		ItemStack result = CraftItemStack.asNMSCopy(itemInMainHand);
		MinecraftKey key = CraftNamespacedKey.toMinecraft(bukkitKey);

		player.openInventory(new PermissionRecipeHolder(plugin, result, key, group, player, permission).getInventory());
	}


	public static class PermissionRecipeHolder implements InventoryHolder, Listener {

		private final CustomRecipesPlugin plugin;
		private final ItemStack result;
		private final MinecraftKey key;
		private final String group;
		private final Player callbackPlayer;
		private final String permission;
		private final Inventory dispenserInventory;

		public PermissionRecipeHolder(CustomRecipesPlugin plugin, ItemStack result,
				MinecraftKey key, String group, Player player, String permission) {
			this.plugin = plugin;
			this.result = result;
			this.key = key;
			this.group = group;
			this.callbackPlayer = player;
			this.permission = permission;
			this.dispenserInventory = plugin.getServer().createInventory(this, InventoryType.DISPENSER, "Create a permission recipe!");
			plugin.getServer().getPluginManager().registerEvents(this, plugin);
		}

		@Override
		public Inventory getInventory() {
			return dispenserInventory;
		}

		@EventHandler
		public void onInventoryClick(InventoryCloseEvent event) {
			if (event.getInventory().getHolder() instanceof PermissionRecipeHolder) {
				PermissionRecipeHolder holder = (PermissionRecipeHolder) event.getView().getTopInventory().getHolder();
				if (holder != this) return;

				Inventory inventory = event.getInventory();
				if (InventoryUtils.isEmpty(inventory)) {
					holder.callbackPlayer.sendMessage(ChatColor.RED + "Do you seriously want to create a recipe without ingredients?");
					return;
				}

				PermissionRecipe nmsRecipe = holder.toRecipe();
				CRPermissionRecipe permissionRecipe = new CRPermissionRecipe(nmsRecipe);

				List<List<String>> recipeIngredients = permissionRecipe.getIngredients().stream()
						.map((CRChoiceIngredient ingr) -> ingr.getChoices().stream()
								.map(InventoryUtils::getItemName).collect(Collectors.toList()))
						.collect(Collectors.toList());
				String recipeString = recipeIngredients + "" +
						ChatColor.RESET + " -> " +
						InventoryUtils.getItemName(permissionRecipe.getResult());

				boolean success = holder.plugin.getCraftingManager().addRecipe(holder.key, nmsRecipe, permissionRecipe);
				if (success) {
					holder.callbackPlayer.sendMessage(String.format("%sAdded a permission recipe: %s%s%s!",
							ChatColor.GREEN, ChatColor.WHITE, recipeString, ChatColor.WHITE));
					plugin.saveCraftingRecipeFile("permission", permissionRecipe);
				} else {
					holder.callbackPlayer.sendMessage(ChatColor.RED + "Couldn't create a permission recipe. Possibly a duplicate key.");
				}

				HandlerList.unregisterAll(holder);
			}
		}

		private PermissionRecipe toRecipe() {
			CraftInventoryCustom dispenserInventory = (CraftInventoryCustom) this.dispenserInventory;
			IInventory minecraftInventory = (IInventory) ReflectionUtil.getDeclaredFieldValue(dispenserInventory, "inventory");
			NonNullList<ItemStack> itemStacks = (NonNullList<ItemStack>) ReflectionUtil.getDeclaredFieldValue(minecraftInventory, "items");

			int minNonEmptyRownum = 2;
			int minNonEmptyColnum = 2;
			int maxNonEmptyRownum = 0;
			int maxNonEmptyColnum = 0;

			for (int index = 0; index < itemStacks.size(); index++) {
				ItemStack stack = itemStacks.get(index);
				if (!stack.isEmpty()) {
					int[] rownumColnum = InventoryUtils.inventoryRownumColnum(3, index);
					int rowNum = rownumColnum[0];
					int colNum = rownumColnum[1];

					minNonEmptyRownum = Math.min(minNonEmptyRownum, rowNum);
					minNonEmptyColnum = Math.min(minNonEmptyColnum, colNum);
					maxNonEmptyRownum = Math.max(maxNonEmptyRownum, rowNum);
					maxNonEmptyColnum = Math.max(maxNonEmptyColnum, colNum);
				}
			}

			int height = maxNonEmptyRownum - minNonEmptyRownum + 1;
			int width = maxNonEmptyColnum - minNonEmptyColnum + 1;

			NonNullList<RecipeItemStack> ingredients = NonNullList.a(height * width, RecipeItemStack.a);
			for (int h = 0; h < height; h++) {
				for (int w = 0; w < width; w++) {
					int index = InventoryUtils.inventoryIndex(width, new int[] {h, w});

					ItemStack ingredientStack = itemStacks.get(InventoryUtils.inventoryIndex(3,
							new int[] {h + minNonEmptyRownum, w + minNonEmptyColnum}));
					RecipeItemStack ingredient = RecipeItemStack.a(new ItemStack[] {ingredientStack});

					ingredients.set(index, ingredient);
				}
			}

			PermissionRecipe recipe = new PermissionRecipe(group, width, height, ingredients, result, permission);
			recipe.setKey(key);
			return recipe;
		}

	}

} public class PermissionRemover implements BiConsumer<Player, List<String>> {

	private final CustomRecipesPlugin plugin;

	public PermissionRemover(CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}

	@Override
	public void accept(Player player, List<String> args) {
		CRCraftingManager craftingManager = plugin.getCraftingManager();

		CRPermissionRecipe toBeRemoved = null;

		if (!args.isEmpty()) {

			String firstArg = args.get(0);
			NamespacedKey key = plugin.getKey(firstArg);

			CraftingRecipe recipe = craftingManager.getRecipe(key);
			
			if (recipe instanceof CRPermissionRecipe) {
				toBeRemoved = (CRPermissionRecipe) recipe;
			} else {
				player.sendMessage(ChatColor.RED + "Couldn't remove recipe for key " + ChatColor.WHITE + key + ChatColor.RED + ", it is not a native permission recipe.");
				return;
			}

		} else {
			ItemStack itemInHand = player.getInventory().getItemInMainHand();
			if (InventoryUtils.isEmptyStack(itemInHand)) itemInHand = player.getInventory().getItemInOffHand();
			if (InventoryUtils.isEmptyStack(itemInHand)) {
				player.sendMessage(ChatColor.RED + "Usage: /removerecipe permssion <key>");
				player.sendMessage(ChatColor.RED + "Or '/removerecipe permssion' with an item in your hand.");
				return;
			}

			//loop until match
			for (CraftingRecipe recipe : craftingManager) {
				if (!(recipe instanceof CRPermissionRecipe)) continue;
				if (itemInHand.equals(recipe.getResult())) {
					toBeRemoved = (CRPermissionRecipe) recipe;
					break;
				}
			}

			//no match
			if (toBeRemoved == null) {
				player.sendMessage(ChatColor.RED + "No permssion recipe found for resultstack " +
						ChatColor.WHITE + InventoryUtils.getItemName(itemInHand) +
						ChatColor.RED + ".");
				return;
			}

		}

		if (toBeRemoved != null) {
			//match
			NamespacedKey key = craftingManager.removeRecipe(toBeRemoved);
			player.sendMessage(ChatColor.GREEN + "Removed recipe with key " +
					ChatColor.WHITE + key +
					ChatColor.GREEN + " for item " +
					ChatColor.WHITE + InventoryUtils.getItemName(toBeRemoved.getResult()) +
					ChatColor.GREEN + ".");
			plugin.disableCraftingRecipeFile("permission", toBeRemoved);
		}
	}

} public class WorldAdder implements BiConsumer<Player, List<String>> {

	private final CustomRecipesPlugin plugin;

	public WorldAdder(CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}

	@Override
	public void accept(Player player, List<String> args) {
		org.bukkit.inventory.ItemStack itemInMainHand = player.getInventory().getItemInMainHand();
		if (itemInMainHand == null) {
			player.sendMessage(ChatColor.RED + "Put the result of the recipe in your main hand when executing this command.");
			return;
		}

		if (args.isEmpty()) {
			player.sendMessage(ChatColor.RED + "Usage: /addrecipe world <key> [<world name>] [<group>]");
			return;
		}

		UUID world = player.getWorld().getUID();
		if (args.size() >= 2) {
			World bukkitWorld = plugin.getServer().getWorld(args.get(1));
			if (bukkitWorld != null) world = bukkitWorld.getUID();
		}
		
		String keyString = args.get(0);	
		String group = args.size() >= 3 ? args.get(2) : "";
		NamespacedKey bukkitKey = plugin.getKey(keyString);

		ItemStack result = CraftItemStack.asNMSCopy(itemInMainHand);
		MinecraftKey key = CraftNamespacedKey.toMinecraft(bukkitKey);

		player.openInventory(new WorldRecipeHolder(plugin, result, key, group, player, world).getInventory());
	}


	private static final class WorldRecipeHolder implements InventoryHolder, Listener {

		private final Inventory dispenserInventory;
		private final CustomRecipesPlugin plugin;
		private final ItemStack result;
		private final MinecraftKey key;
		private final String group;
		private final UUID world;
		private final Player callbackPlayer;

		public WorldRecipeHolder(CustomRecipesPlugin plugin, ItemStack result, MinecraftKey key, String group, Player callbackPlayer, UUID world) {
			this.plugin = plugin;
			this.result = result;
			this.key = key;
			this.group = group;
			this.world = world;
			this.callbackPlayer = callbackPlayer;
			this.dispenserInventory = plugin.getServer().createInventory(this, InventoryType.DISPENSER, "Create a world recipe!");
			plugin.getServer().getPluginManager().registerEvents(this, plugin);
		}

		@Override
		public Inventory getInventory() {
			return dispenserInventory;
		}


		@EventHandler
		public void onInventoryClose(InventoryCloseEvent event) {
			if (event.getInventory().getHolder() instanceof WorldRecipeHolder) {
				WorldRecipeHolder holder = (WorldRecipeHolder) event.getInventory().getHolder();
				if (holder != this) return;

				Inventory inventory = event.getInventory();
				if (InventoryUtils.isEmpty(inventory)) {
					holder.callbackPlayer.sendMessage(ChatColor.RED + "Do you seriously want to create a recipe without ingredients?");
					return;
				}

				WorldRecipe nmsRecipe = holder.toRecipe();
				CRWorldRecipe worldRecipe = new CRWorldRecipe(nmsRecipe);
				List<List<String>> recipeIngredients = worldRecipe.getIngredients().stream()
						.map((CRChoiceIngredient ingr) -> ingr.getChoices().stream()
								.map(InventoryUtils::getItemName).collect(Collectors.toList()))
						.collect(Collectors.toList());
				String recipeString = recipeIngredients + "" +
						ChatColor.RESET + " -> " +
						InventoryUtils.getItemName(worldRecipe.getResult());

				boolean success = holder.plugin.getCraftingManager().addRecipe(holder.key, nmsRecipe, worldRecipe);
				if (success) {
					holder.callbackPlayer.sendMessage(String.format("%sAdded world recipe: %s%s%s!",
							ChatColor.GREEN, ChatColor.WHITE, recipeString, ChatColor.WHITE));
					plugin.saveCraftingRecipeFile("world", worldRecipe);
				} else {
					holder.callbackPlayer.sendMessage(ChatColor.RED + "Couldn't create a world recipe. Possibly a duplicate key.");
				}

				HandlerList.unregisterAll(holder);
			}
		}


		private WorldRecipe toRecipe() {
			CraftInventoryCustom dispenserInventory = (CraftInventoryCustom) this.dispenserInventory;
			IInventory minecraftInventory = (IInventory) ReflectionUtil.getDeclaredFieldValue(dispenserInventory, "inventory");
			NonNullList<ItemStack> dispenserInventoryContents = (NonNullList<ItemStack>) ReflectionUtil.getDeclaredFieldValue(minecraftInventory, "items");

			int minNonEmptyRownum = 2;
			int minNonEmptyColnum = 2;
			int maxNonEmptyRownum = 0;
			int maxNonEmptyColnum = 0;

			for (int index = 0; index < dispenserInventoryContents.size(); index++) {
				ItemStack stack = dispenserInventoryContents.get(index);
				if (!stack.isEmpty()) {
					int[] rownumColnum = InventoryUtils.inventoryRownumColnum(3, index);
					int rowNum = rownumColnum[0];
					int colNum = rownumColnum[1];

					minNonEmptyRownum = Math.min(minNonEmptyRownum, rowNum);
					minNonEmptyColnum = Math.min(minNonEmptyColnum, colNum);
					maxNonEmptyRownum = Math.max(maxNonEmptyRownum, rowNum);
					maxNonEmptyColnum = Math.max(maxNonEmptyColnum, colNum);
				}
			}

			int height = maxNonEmptyRownum - minNonEmptyRownum + 1;
			int width = maxNonEmptyColnum - minNonEmptyColnum + 1;

			NonNullList<RecipeItemStack> ingredients = NonNullList.a(height * width, RecipeItemStack.a);
			for (int h = 0; h < height; h++) {
				for (int w = 0; w < width; w++) {
					int index = InventoryUtils.inventoryIndex(width, new int[] {h, w});

					ItemStack ingredientStack = dispenserInventoryContents.get(InventoryUtils.inventoryIndex(3,
							new int[] {h + minNonEmptyRownum, w + minNonEmptyColnum}));

					RecipeItemStack vanillaIngredient = RecipeItemStack.a(new ItemStack[] {ingredientStack});

					ingredients.set(index, vanillaIngredient);
				}
			}

			WorldRecipe worldRecipe = new WorldRecipe(group, width, height, ingredients, result, world);
			worldRecipe.setKey(key);
			return worldRecipe;
		}

	}

} public class WorldRemover implements BiConsumer<Player, List<String>> {

	private final CustomRecipesPlugin plugin;

	public WorldRemover(CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}

	@Override
	public void accept(Player player, List<String> args) {
		CRCraftingManager craftingManager = plugin.getCraftingManager();

		CRWorldRecipe toBeRemoved = null;

		if (!args.isEmpty()) {

			String firstArg = args.get(0);
			NamespacedKey key = plugin.getKey(firstArg);

			CraftingRecipe recipe = craftingManager.getRecipe(key);
			
			if (recipe instanceof CRWorldRecipe) {
				toBeRemoved = (CRWorldRecipe) recipe;
			} else {
				player.sendMessage(ChatColor.RED + "Couldn't remove recipe for key " + ChatColor.WHITE + key + ChatColor.RED + ", it is not a native world recipe.");
				return;
			}

		} else {
			ItemStack itemInHand = player.getInventory().getItemInMainHand();
			if (InventoryUtils.isEmptyStack(itemInHand)) itemInHand = player.getInventory().getItemInOffHand();
			if (InventoryUtils.isEmptyStack(itemInHand)) {
				player.sendMessage(ChatColor.RED + "Usage: /removerecipe world <key>");
				player.sendMessage(ChatColor.RED + "Or '/removerecipe world' with an item in your hand.");
				return;
			}

			//loop until match
			for (CraftingRecipe recipe : craftingManager) {
				if (!(recipe instanceof CRWorldRecipe)) continue;
				if (itemInHand.equals(recipe.getResult())) {
					toBeRemoved = (CRWorldRecipe) recipe;
					break;
				}
			}

			//no match
			if (toBeRemoved == null) {
				player.sendMessage(ChatColor.RED + "No permssion recipe found for resultstack " +
						ChatColor.WHITE + InventoryUtils.getItemName(itemInHand) +
						ChatColor.RED + ".");
				return;
			}

		}

		if (toBeRemoved != null) {
			//match
			NamespacedKey key = craftingManager.removeRecipe(toBeRemoved);
			player.sendMessage(ChatColor.GREEN + "Removed recipe with key " +
					ChatColor.WHITE + key +
					ChatColor.GREEN + " for item " +
					ChatColor.WHITE + InventoryUtils.getItemName(toBeRemoved.getResult()) +
					ChatColor.GREEN + ".");
			plugin.disableCraftingRecipeFile("world", toBeRemoved);
		}
	}

} public class Bukkit2NMSIngredient extends InjectedIngredient {
	
	private final CraftingIngredient bukkitIngredient;

	public Bukkit2NMSIngredient(CraftingIngredient bukkitIngredient) {
		super (nmsStack -> bukkitIngredient.test(CraftItemStack.asCraftMirror(nmsStack)));
		this.bukkitIngredient = bukkitIngredient;
	}
	
	public static Bukkit2NMSIngredient deserialize(Map<String, Object> map) {
		CraftingIngredient bukkitIngredient = (CraftingIngredient) map.get("bukkitIngredient");
		return new Bukkit2NMSIngredient(bukkitIngredient);
	}
	
	@Override
	public Map<String, Object> serialize() {
		return Collections.singletonMap("bukkitIngredient", bukkitIngredient);
	}
	
	@Override
	public NBTTagCompound serializeToNbt() {
		return NBTUtil.fromMap(serialize());
	}

} public class InjectedIngredient implements Predicate<ItemStack>, NBTSerializable {

	private static Class<? extends RecipeItemStack> recipeItemStackInjectedClass;
	
	@SuppressWarnings("unchecked")
    public static void inject() {
		RecipeItemStackClassLoader loader = new RecipeItemStackClassLoader();
		try {
			recipeItemStackInjectedClass = (Class<? extends RecipeItemStack>) loader
					.defineClass("net.minecraft.server.v1_12_R1.RecipeItemStackInjected", RecipeItemStackInjectedDump.dump());
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private final Predicate<? super ItemStack> tester;

	public InjectedIngredient(Predicate<? super ItemStack> predicate) {
		this.tester = predicate;
	}

	@Override
	public boolean test(ItemStack itemStack) {
		return tester.test(itemStack);
	}


	public RecipeItemStack asNMSIngredient() {
		try {
		    //not usable with java 9
//			RecipeItemStack recipeItemStackInjected = (RecipeItemStack) unsafe.allocateInstance(recipeItemStackInjectedClass);
//			ReflectionUtil.setDeclaredFieldValue(recipeItemStackInjected, "predicate", tester);
//			ReflectionUtil.setFinalFieldValue(recipeItemStackInjected, "choices", new ItemStack[0]);
//			return recipeItemStackInjected;

		    //not usable to to illegal access exception
//			Constructor<? extends RecipeItemStack> constructor = recipeItemStackInjectedClass.getConstructor(Predicate.class);
//			RecipeItemStack recipeItemStackInjected = constructor.newInstance(tester);
//			return recipeItemstackInjected;
		    
		    Constructor<? extends RecipeItemStack> constructor = (Constructor<? extends RecipeItemStack>) ReflectionFactory.getReflectionFactory()
		            .newConstructorForSerialization(recipeItemStackInjectedClass, Object.class.getConstructor()); //TODO generates NPE?
		    RecipeItemStack recipeItemStackInjected = constructor.newInstance();
		    ReflectionUtil.setDeclaredFieldValue(recipeItemStackInjected, "predicate", tester);
		    ReflectionUtil.setFinalFieldValue(recipeItemStackInjected, "choices", new ItemStack[0]);
		    return recipeItemStackInjected;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	

	@Override
	public NBTTagCompound serializeToNbt() {
		return NBTUtil.serializeRecipeItemStack(asNMSIngredient());
	}


	/*
	 * The class loader to define the RecipeItemStackInjected class
	 */

	private static class RecipeItemStackClassLoader extends ClassLoader {
		public RecipeItemStackClassLoader() {
			super(RecipeItemStack.class.getClassLoader());
		}

		public Class<?> defineClass(String name, byte[] bytecodeBytes) {
			return defineClass(name, bytecodeBytes, 0, bytecodeBytes.length);
		}
	}

	/*
	 * 
	 * 	Java source code of the class:
	 * 
	 * 	package net.minecraft.server.v1_12_R1;
	 * 
	 * 	import java.util.Objects;
	 * 	import java.util.function.Predicate;
	 * 
	 * 	public class RecipeItemStackInjected extends RecipeItemStack implements Predicate<ItemStack> {
	 *
	 * 		private Predicate<? super ItemStack> predicate;
	 *
	 * 		public RecipeItemStackInjected(Predicate<? super ItemStack> injection) {
	 *     		super(new ItemStack[0], null);
	 *     		this.predicate = Objects.requireNonNull(injection);
	 * 		}
	 *
	 *		@Override
	 *		public boolean a(ItemStack itemStack) {
	 * 			return predicate.test(itemStack);
	 *		}
	 *
	 * 		@Override
	 * 		public boolean apply(ItemStack itemStack) {
	 *			return predicate.test(itemStack);
	 *		}
	 *
	 *		@Override
	 *		public void test(ItemStack itemStack) {
	 *			return predicate.test(itemStack) {
	 *		}
	 *
	 *	}
	 *
	 *	THANKS ASM!
	 */

	private static final class RecipeItemStackInjectedDump implements Opcodes {

		public static byte[] dump () throws Exception {

			ClassWriter cw = new ClassWriter(0);
			FieldVisitor fv;
			MethodVisitor mv;

			cw.visit(/*version*/ 52,
					/*access*/ ACC_PUBLIC + ACC_SUPER,
					/*name*/ "net/minecraft/server/v1_12_R1/RecipeItemStackInjected",
					/*signature*/ "Lnet/minecraft/server/v1_12_R1/RecipeItemStack;Ljava/util/function/Predicate<Lnet/minecraft/server/v1_12_R1/ItemStack;>;",
					/*super name*/ "net/minecraft/server/v1_12_R1/RecipeItemStack",
					/*interfaces*/ new String[] { "java/util/function/Predicate" });

			cw.visitSource("RecipeItemStackInjected.java", null);

			{
				fv = cw.visitField(ACC_PRIVATE, "predicate", "Ljava/util/function/Predicate;", "Ljava/util/function/Predicate<-Lnet/minecraft/server/v1_12_R1/ItemStack;>;", null);
				fv.visitEnd();
			}
			{
				mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/util/function/Predicate;)V", "(Ljava/util/function/Predicate<-Lnet/minecraft/server/v1_12_R1/ItemStack;>;)V", null);
				mv.visitCode();				
				Label l0 = new Label();
				mv.visitLabel(l0);
				mv.visitLineNumber(11, l0);
				mv.visitVarInsn(ALOAD, 0);
				mv.visitInsn(ICONST_0);
				mv.visitTypeInsn(ANEWARRAY, "net/minecraft/server/v1_12_R1/ItemStack");
				mv.visitInsn(ACONST_NULL);
				mv.visitMethodInsn(INVOKESPECIAL, "net/minecraft/server/v1_12_R1/RecipeItemStack", "<init>", "([Lnet/minecraft/server/v1_12_R1/ItemStack;Lnet/minecraft/server/v1_12_R1/RecipeItemStack$1;)V", false);
				Label l1 = new Label();
				mv.visitLabel(l1);
				mv.visitLineNumber(12, l1);
				mv.visitVarInsn(ALOAD, 0);
				mv.visitVarInsn(ALOAD, 1);
				mv.visitMethodInsn(INVOKESTATIC, "java/util/Objects", "requireNonNull", "(Ljava/lang/Object;)Ljava/lang/Object;", false);
				mv.visitTypeInsn(CHECKCAST, "java/util/function/Predicate");
				mv.visitFieldInsn(PUTFIELD, "net/minecraft/server/v1_12_R1/RecipeItemStackInjected", "predicate", "Ljava/util/function/Predicate;");
				Label l2 = new Label();
				mv.visitLabel(l2);
				mv.visitLineNumber(13, l2);
				mv.visitInsn(RETURN);
				Label l3 = new Label();
				mv.visitLabel(l3);
				mv.visitLocalVariable("this", "Lnet/minecraft/server/v1_12_R1/RecipeItemStackInjected;", null, l0, l3, 0);
				mv.visitLocalVariable("injection", "Ljava/util/function/Predicate;", "Ljava/util/function/Predicate<-Lnet/minecraft/server/v1_12_R1/ItemStack;>;", l0, l3, 1);
				mv.visitMaxs(3, 2);
				mv.visitEnd();
			}
			{
				mv = cw.visitMethod(ACC_PUBLIC, "a", "(Lnet/minecraft/server/v1_12_R1/ItemStack;)Z", null, null);
				mv.visitCode();
				Label l0 = new Label();
				mv.visitLabel(l0);
				mv.visitLineNumber(16, l0);
				mv.visitVarInsn(ALOAD, 0);
				mv.visitFieldInsn(GETFIELD, "net/minecraft/server/v1_12_R1/RecipeItemStackInjected", "predicate", "Ljava/util/function/Predicate;");
				mv.visitVarInsn(ALOAD, 1);
				mv.visitMethodInsn(INVOKEINTERFACE, "java/util/function/Predicate", "test", "(Ljava/lang/Object;)Z", true);
				mv.visitInsn(IRETURN);
				Label l1 = new Label();
				mv.visitLabel(l1);
				mv.visitLocalVariable("this", "Lnet/minecraft/server/v1_12_R1/RecipeItemStackInjected;", null, l0, l1, 0);
				mv.visitLocalVariable("itemStack", "Lnet/minecraft/server/v1_12_R1/ItemStack;", null, l0, l1, 1);
				mv.visitMaxs(2, 2);
				mv.visitEnd();
			}
			{
				mv = cw.visitMethod(ACC_PUBLIC, "apply", "(Lnet/minecraft/server/v1_12_R1/ItemStack;)Z", null, null);
				mv.visitCode();
				Label l0 = new Label();
				mv.visitLabel(l0);
				mv.visitLineNumber(20, l0);
				mv.visitVarInsn(ALOAD, 0);
				mv.visitFieldInsn(GETFIELD, "net/minecraft/server/v1_12_R1/RecipeItemStackInjected", "predicate", "Ljava/util/function/Predicate;");
				mv.visitVarInsn(ALOAD, 1);
				mv.visitMethodInsn(INVOKEINTERFACE, "java/util/function/Predicate", "test", "(Ljava/lang/Object;)Z", true);
				mv.visitInsn(IRETURN);
				Label l1 = new Label();
				mv.visitLabel(l1);
				mv.visitLocalVariable("this", "Lnet/minecraft/server/v1_12_R1/RecipeItemStackInjected;", null, l0, l1, 0);
				mv.visitLocalVariable("itemStack", "Lnet/minecraft/server/v1_12_R1/ItemStack;", null, l0, l1, 1);
				mv.visitMaxs(2, 2);
				mv.visitEnd();
			}
			{
				mv = cw.visitMethod(ACC_PUBLIC, "test", "(Lnet/minecraft/server/v1_12_R1/ItemStack;)Z", null, null);
				mv.visitCode();
				Label l0 = new Label();
				mv.visitLabel(l0);
				mv.visitLineNumber(24, l0);
				mv.visitVarInsn(ALOAD, 0);
				mv.visitFieldInsn(GETFIELD, "net/minecraft/server/v1_12_R1/RecipeItemStackInjected", "predicate", "Ljava/util/function/Predicate;");
				mv.visitVarInsn(ALOAD, 1);
				mv.visitMethodInsn(INVOKEINTERFACE, "java/util/function/Predicate", "test", "(Ljava/lang/Object;)Z", true);
				mv.visitInsn(IRETURN);
				Label l1 = new Label();
				mv.visitLabel(l1);
				mv.visitLocalVariable("this", "Lnet/minecraft/server/v1_12_R1/RecipeItemStackInjected;", null, l0, l1, 0);
				mv.visitLocalVariable("itemStack", "Lnet/minecraft/server/v1_12_R1/ItemStack;", null, l0, l1, 1);
				mv.visitMaxs(2, 2);
				mv.visitEnd();
			}
			{
				mv = cw.visitMethod(ACC_PUBLIC + ACC_BRIDGE + ACC_SYNTHETIC, "test", "(Ljava/lang/Object;)Z", null, null);
				mv.visitCode();
				Label l0 = new Label();
				mv.visitLabel(l0);
				mv.visitLineNumber(1, l0);
				mv.visitVarInsn(ALOAD, 0);
				mv.visitVarInsn(ALOAD, 1);
				mv.visitTypeInsn(CHECKCAST, "net/minecraft/server/v1_12_R1/ItemStack");
				mv.visitMethodInsn(INVOKEVIRTUAL, "net/minecraft/server/v1_12_R1/RecipeItemStackInjected", "test", "(Lnet/minecraft/server/v1_12_R1/ItemStack;)Z", false);
				mv.visitInsn(IRETURN);
				mv.visitMaxs(2, 2);
				mv.visitEnd();
			}
			cw.visitEnd();

			return cw.toByteArray();
		}
	}

} public class Bukkit2NMSRecipe implements IRecipe, NBTSerializable {
	
	//dirty hacks!
	private static final String CUSTOM_RECIPES_BUKKITRECIPE_KEY = "cr-bukkit2nmsrecipe-";
	private static final AtomicInteger keyCount = new AtomicInteger(0);
	
	private static NamespacedKey newNamespacedKey() {
		CustomRecipesPlugin plugin = CustomRecipesPlugin.getInstance();
		return new NamespacedKey(plugin, CUSTOM_RECIPES_BUKKITRECIPE_KEY + keyCount.getAndIncrement() + UUID.randomUUID().toString());
	}
	
	
	private CraftingRecipe crRecipe;

	public Bukkit2NMSRecipe(CraftingRecipe bukkitRecipe) {
		this.crRecipe = Objects.requireNonNull(bukkitRecipe);
	}
	
	@Override
	public Map<String, Object> serialize() {
		return Collections.singletonMap("crRecipe", crRecipe);
	}
	
	@Override
	public NBTTagCompound serializeToNbt() {
		return NBTUtil.fromMap(serialize());
	}
	
	
	public static CraftInventoryCrafting getBukkitCraftingInventory(InventoryCrafting inventoryCrafting) {
		IInventory resultInventory = inventoryCrafting.resultInventory;
		return new CraftInventoryCrafting(inventoryCrafting, resultInventory);
	}

	@Override
	public boolean a(InventoryCrafting inventoryCrafting, World world) {
		CraftInventoryCrafting bukkitInventory = getBukkitCraftingInventory(inventoryCrafting);
		CraftWorld bukkitWorld = world.getWorld();		
		
		return crRecipe.matches(bukkitInventory, bukkitWorld);
	}

	@Override
	public ItemStack b() {
		return CraftItemStack.asNMSCopy(crRecipe.getResult());
	}
	
	@Override
	public NonNullList<RecipeItemStack> d() {
	    return crRecipe.getIngredients().stream()
	            .map(CRCraftingIngredient::asNMSIngredient)
	            .collect(Collectors.toCollection(NonNullList::a));
	}

	@Override
	public NonNullList<ItemStack> b(InventoryCrafting inventoryCrafting) {
		CraftInventoryCrafting bukkitInventory = getBukkitCraftingInventory(inventoryCrafting);

		List<? extends org.bukkit.inventory.ItemStack> bukkitLeftovers = crRecipe.getLeftOverItems(bukkitInventory);
		ItemStack[] nmsLeftovers = bukkitLeftovers.stream().map(CraftItemStack::asNMSCopy).toArray(size -> new ItemStack[size]);
		return NonNullList.a(ItemStack.a, nmsLeftovers);
	}

	@Override
	public ItemStack craftItem(InventoryCrafting inventoryCrafting) {
		CraftInventoryCrafting bukkitInventory = getBukkitCraftingInventory(inventoryCrafting);
		
		return CraftItemStack.asNMSCopy(crRecipe.craftItem(bukkitInventory));
	}
	
	@Override
	public boolean c() {
		return crRecipe.isHidden();
	}
	
	@Override
	public boolean equals(Object object) {
		if (object == null) return false;
		if (!(object instanceof Bukkit2NMSRecipe)) return false;
		Bukkit2NMSRecipe that = (Bukkit2NMSRecipe) object;
		return this.crRecipe.equals(that.crRecipe);
	}
	
	@Override
	public int hashCode() {
		return crRecipe.hashCode();
	}
	

	@SuppressWarnings("rawtypes")
	@Override
	public Recipe toBukkitRecipe() {
		//we try the best we can to do something meaningful.
		
		if (crRecipe instanceof CRShapedRecipe) {
			return ((CRShapedRecipe) crRecipe).getHandle().toBukkitRecipe();
			
		} else if (crRecipe instanceof CRShapelessRecipe) {
			return ((CRShapelessRecipe) crRecipe).getHandle().toBukkitRecipe();
			
			
		} else if (crRecipe instanceof ShapedRecipe) {
			//try our very best to do something useful
			ShapedRecipe shapedRecipe = (ShapedRecipe) crRecipe;
			NamespacedKey namespacedKey = CustomRecipesPlugin.getInstance().getCraftingManager().getKey(shapedRecipe);
			if (namespacedKey == null) namespacedKey = newNamespacedKey();
			
			org.bukkit.inventory.ShapedRecipe bukkitShapedRecipe = new org.bukkit.inventory.ShapedRecipe(namespacedKey, crRecipe.getResult());
			
			AtomicInteger character = new AtomicInteger('a');
			List<StringBuilder> shapeBuilder = new ArrayList<>();
			for (int i = 0; i < shapedRecipe.getHeight(); i++) {
				shapeBuilder.add(new StringBuilder());
				for (int j = 0; j < shapedRecipe.getWidth(); j++) {
					shapeBuilder.get(i).append((char) character.getAndIncrement());
				}
			}
			
			String[] shape = shapeBuilder.stream().map(StringBuilder::toString).toArray(size -> new String[size]);
			bukkitShapedRecipe.shape(shape);
			// for clarity. arrayNum goes from top to bottom, charNum goes from left to right
			/* [a,b,c],
			 * [d,e,f],
			 * [g,h,i]
			 */
			
			int arrayNum = 0;
			int charNum = 0;
			for (ChoiceIngredient ingr : shapedRecipe.getIngredients()) {
				Iterator<? extends org.bukkit.inventory.ItemStack> choiceIterator = ingr.getChoices().iterator();
				if (choiceIterator.hasNext()) {
					char key = shape[arrayNum].charAt(charNum);
					bukkitShapedRecipe.setIngredient(key, choiceIterator.next().getType());
				}
				//skip the other choices since they would take up the same item slot.
				//instead, the materialdata is ignored for this recipe, such that multiple data values would still work.
				//this seems like the most reasonable thing to do as we don't know what the choices are.
				
				if (++charNum > shape[arrayNum].length()) {
					charNum = 0;
					arrayNum++;
				}
			}
			
			return bukkitShapedRecipe;
			
		} else if (crRecipe instanceof ShapelessRecipe) {
			//try our very best to do something useful
			ShapelessRecipe shapelessRecipe = (ShapelessRecipe) crRecipe;
			NamespacedKey namespacedKey = CustomRecipesPlugin.getInstance().getCraftingManager().getKey(shapelessRecipe);
			if (namespacedKey == null) namespacedKey = newNamespacedKey();
			
			org.bukkit.inventory.ShapelessRecipe bukkitShapelessRecipe = new org.bukkit.inventory.ShapelessRecipe(namespacedKey, crRecipe.getResult());
			
			for (ChoiceIngredient ingr : shapelessRecipe.getIngredients()) {
				Iterator<? extends org.bukkit.inventory.ItemStack> choiceIterator = ingr.getChoices().iterator();
				if (choiceIterator.hasNext()) {
					bukkitShapelessRecipe.addIngredient(choiceIterator.next().getType());
				}
				//skip the other choices since they would take up the same item slot.
				//instead, the materialdata is ignored for this recipe, such that multiple data values would still work.
				//this seems like the most reasonable thing to do as we don't know what the choices are.
			}
			
			return bukkitShapelessRecipe;
		}
		
		//Recipe was neither a Shaped nor a Shapeless recipe, return the generic crafting recipe.
		//Bukkit and other plugins will not like this, but it's the best we can do instead of returning null.
		return crRecipe;
	}


	
	@Override
	public void setKey(MinecraftKey key) {		
		//no spigot pls. fuck you. the key is not part of the recipe itself dangit!
	}
	
	public MinecraftKey getKey() {
		CRCraftingManager craftingManager = CustomRecipesPlugin.getInstance().getCraftingManager();
		return craftingManager.getKey(this);
	}

} public class CRCountRecipe extends CRShapedRecipe<CountRecipe> implements com.gmail.jannyboy11.customrecipes.api.crafting.custom.recipe.CountRecipe {

	public CRCountRecipe(CountRecipe nmsRecipe) {
		super(nmsRecipe);
	}
	
	public CRCountRecipe(NBTTagCompound recipeCompound) {
		this(deserializeNmsRecipe(recipeCompound));
	}
	
	public CRCountRecipe(Map<String, Object> map) {
		this(NBTUtil.fromMap(map));
	}
	
	protected static CountRecipe deserializeNmsRecipe(NBTTagCompound recipeCompound) {
		String group = recipeCompound.hasKeyOfType("group", NBTUtil.STRING) ? recipeCompound.getString("group") : "";
		int width = recipeCompound.getInt("width");
		int height = recipeCompound.getInt("height");
		NonNullList<RecipeItemStack> ingredients = NonNullList.a();
		NBTTagList nbtIngredients = recipeCompound.getList("ingredients", NBTUtil.COMPOUND);
		for (int i = 0; i < nbtIngredients.size(); i++) {
			NBTTagCompound ingredientTag = nbtIngredients.get(i);
			RecipeItemStack recipeItemStack = NBTUtil.deserializeRecipeItemStack(ingredientTag);
			ingredients.add(recipeItemStack);
		}
		NBTTagCompound resultCompound = (NBTTagCompound) recipeCompound.get("result");
		ItemStack result = new ItemStack(resultCompound);
		//ingredients are made nbt specific in NBTRecipe constructor
		CountRecipe countRecipe = new CountRecipe(group, width, height, ingredients, result);
		if (recipeCompound.hasKey("key")) {
			countRecipe.setKey(NBTUtil.deserializeKey(recipeCompound.getCompound("key")));
		}
		return countRecipe;
	}

} public class CRNBTRecipe extends CRShapedRecipe<NBTRecipe> implements com.gmail.jannyboy11.customrecipes.api.crafting.custom.recipe.NBTRecipe {

	public CRNBTRecipe(NBTRecipe nbtRecipe) {
		super(nbtRecipe);
	}

	public CRNBTRecipe(NBTTagCompound recipeCompound) {
		this(deserializeNmsRecipe(recipeCompound));
	}
	
	public CRNBTRecipe(Map<String, Object> map) {
		this(NBTUtil.fromMap(map));
	}
	
	protected static NBTRecipe deserializeNmsRecipe(NBTTagCompound recipeCompound) {
		String group = recipeCompound.hasKeyOfType("group", NBTUtil.STRING) ? recipeCompound.getString("group") : "";
		int width = recipeCompound.getInt("width");
		int height = recipeCompound.getInt("height");
		NonNullList<RecipeItemStack> ingredients = NonNullList.a();
		NBTTagList nbtIngredients = recipeCompound.getList("ingredients", NBTUtil.COMPOUND);
		for (int i = 0; i < nbtIngredients.size(); i++) {
			NBTTagCompound ingredientTag = nbtIngredients.get(i);
			RecipeItemStack recipeItemStack = NBTUtil.deserializeRecipeItemStack(ingredientTag);
			ingredients.add(recipeItemStack);
		}
		NBTTagCompound resultCompound = (NBTTagCompound) recipeCompound.get("result");
		ItemStack result = new ItemStack(resultCompound);
		//ingredients are made nbt specific in NBTRecipe constructor
		NBTRecipe nbtRecipe = new NBTRecipe(group, width, height, ingredients, result);
		if (recipeCompound.hasKey("key")) {
			nbtRecipe.setKey(NBTUtil.deserializeKey(recipeCompound.getCompound("key")));
		}
		return nbtRecipe;
	}
	
} public class CRPermissionRecipe extends CRShapedRecipe<PermissionRecipe> implements com.gmail.jannyboy11.customrecipes.api.crafting.custom.recipe.PermissionRecipe {

	public CRPermissionRecipe(PermissionRecipe nmsRecipe) {
		super(nmsRecipe);
	}
	
	public CRPermissionRecipe(NBTTagCompound recipeCompound) {
		this(deserializeNmsRecipe(recipeCompound));
	}
	
	public CRPermissionRecipe(Map<String, Object> map) {
		this(NBTUtil.fromMap(map));
	}
	
	protected static PermissionRecipe deserializeNmsRecipe(NBTTagCompound recipeCompound) {
		String group = recipeCompound.hasKeyOfType("group", NBTUtil.STRING) ? recipeCompound.getString("group") : "";
		int width = recipeCompound.getInt("width");
		int height = recipeCompound.getInt("height");
		NonNullList<RecipeItemStack> ingredients = NonNullList.a();
		NBTTagList nbtIngredients = recipeCompound.getList("ingredients", NBTUtil.COMPOUND);
		for (int i = 0; i < nbtIngredients.size(); i++) {
			NBTTagCompound ingredientTag = nbtIngredients.get(i);
			RecipeItemStack recipeItemStack = NBTUtil.deserializeRecipeItemStack(ingredientTag);
			ingredients.add(recipeItemStack);
		}
		NBTTagCompound resultCompound = (NBTTagCompound) recipeCompound.get("result");
		ItemStack result = new ItemStack(resultCompound);
		String permission = recipeCompound.getString("permission");
		PermissionRecipe permissionRecipe = new PermissionRecipe(group, width, height, ingredients, result, permission);
		if (recipeCompound.hasKey("key")) {
			permissionRecipe.setKey(NBTUtil.deserializeKey(recipeCompound.getCompound("key")));
		}		
		return permissionRecipe;
	}
	
	@Override
	public NBTTagCompound serializeToNbt() {
		NBTTagCompound serialized = super.serializeToNbt();
		serialized.setString("permission", getPermission());
		return serialized;
	}
	

	@Override
	public String getPermission() {
		return nmsRecipe.getPermission();
	}
	

} public class CRWorldRecipe extends CRShapedRecipe<WorldRecipe> implements com.gmail.jannyboy11.customrecipes.api.crafting.custom.recipe.WorldRecipe {

	public CRWorldRecipe(WorldRecipe nmsRecipe) {
		super(nmsRecipe);
	}

	public CRWorldRecipe(NBTTagCompound recipeCompound) {
		this(deserializeNmsRecipe(recipeCompound));
	}

	public CRWorldRecipe(Map<String, ?> map) {
		this(NBTUtil.fromMap(map));
	}
	
	protected static WorldRecipe deserializeNmsRecipe(NBTTagCompound recipeCompound) {
		String group = recipeCompound.hasKeyOfType("group", NBTUtil.STRING) ? recipeCompound.getString("group") : "";
		int width = recipeCompound.getInt("width");
		int height = recipeCompound.getInt("height");
		NonNullList<RecipeItemStack> ingredients = NonNullList.a();
		NBTTagList nbtIngredients = recipeCompound.getList("ingredients", NBTUtil.COMPOUND);
		for (int i = 0; i < nbtIngredients.size(); i++) {
			NBTTagCompound ingredientTag = nbtIngredients.get(i);
			RecipeItemStack recipeItemStack = NBTUtil.deserializeRecipeItemStack(ingredientTag);
			ingredients.add(recipeItemStack);
		}
		NBTTagCompound resultCompound = (NBTTagCompound) recipeCompound.get("result");
		ItemStack result = new ItemStack(resultCompound);
		UUID world = recipeCompound.a("world");
		WorldRecipe permissionRecipe = new WorldRecipe(group, width, height, ingredients, result, world);
		if (recipeCompound.hasKey("key")) {
			permissionRecipe.setKey(NBTUtil.deserializeKey(recipeCompound.getCompound("key")));
		}		
		return permissionRecipe;
	}
	
	@Override
	public NBTTagCompound serializeToNbt() {
		NBTTagCompound serialized = super.serializeToNbt();
		serialized.a("world", getWorld());
		return serialized;
	}

	@Override
	public UUID getWorld() {
		return nmsRecipe.getWorld();
	}

} public class ShapedAdder implements BiConsumer<Player, List<String>> {

	private final CustomRecipesPlugin plugin;

	public ShapedAdder (CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}

	@Override
	public void accept(Player player, List<String> args) {
		org.bukkit.inventory.ItemStack itemInMainHand = player.getInventory().getItemInMainHand();
		if (itemInMainHand == null) {
			player.sendMessage(ChatColor.RED + "Put the result of the recipe in your main hand when executing this command.");
			return;
		}

		if (args.isEmpty()) {
			player.sendMessage(ChatColor.RED + "Usage: /addrecipe shaped <key> [<group>]");
			return;
		}

		String keyString = args.get(0);
		String group = args.size() >= 2 ? args.get(1) : "";
		NamespacedKey bukkitKey = plugin.getKey(keyString);

		ItemStack result = CraftItemStack.asNMSCopy(itemInMainHand);
		MinecraftKey key = CraftNamespacedKey.toMinecraft(bukkitKey); 

		player.openInventory(new ShapedRecipeHolder(player, plugin, result, key, group).getInventory());
	}

	private static final class ShapedRecipeHolder implements InventoryHolder, Listener {

		private final Inventory dispenserInventory;
		private final CustomRecipesPlugin plugin;
		private final ItemStack result;
		private final Player callbackPlayer;
		private final String group;
		private final MinecraftKey key;

		public ShapedRecipeHolder(Player callbackPlayer, CustomRecipesPlugin plugin, ItemStack result, MinecraftKey key, String group) {
			this.plugin = plugin;
			this.dispenserInventory = plugin.getServer().createInventory(this, InventoryType.DISPENSER, "Create a shaped recipe!");
			this.result = result;
			this.callbackPlayer = callbackPlayer;
			this.group = group;
			this.key = key;
			plugin.getServer().getPluginManager().registerEvents(this, plugin);
		}

		@Override
		public Inventory getInventory() {
			return dispenserInventory;
		}

		@EventHandler
		public void onInventoryClose(InventoryCloseEvent event) {
			if (event.getInventory().getHolder() instanceof ShapedRecipeHolder) {
				ShapedRecipeHolder holder = (ShapedRecipeHolder) event.getInventory().getHolder();
				if (holder != this) return;

				Inventory inventory = event.getInventory();
				if (InventoryUtils.isEmpty(inventory)) {
					holder.callbackPlayer.sendMessage(ChatColor.RED + "Do you seriously want to create a recipe without ingredients?");
					return;
				}

				ShapedRecipes nmsRecipe = holder.toRecipe();
				CRShapedRecipe<ShapedRecipes> shapedRecipe = new CRShapedRecipe<>(nmsRecipe);
				List<List<String>> recipeIngredients = shapedRecipe.getIngredients().stream()
						.map((CRChoiceIngredient ingr) -> ingr.getChoices().stream()
								.map(InventoryUtils::getItemName).collect(Collectors.toList()))
						.collect(Collectors.toList());
				String recipeString = recipeIngredients + "" +
						ChatColor.RESET + " -> " +
						InventoryUtils.getItemName(shapedRecipe.getResult());

				boolean success = holder.plugin.getCraftingManager().addRecipe(holder.key, nmsRecipe, shapedRecipe);
				if (success) {
					holder.callbackPlayer.sendMessage(String.format("%sAdded shaped recipe: %s%s%s!",
							ChatColor.GREEN, ChatColor.WHITE, recipeString, ChatColor.WHITE));
					plugin.saveCraftingRecipeFile("shaped", shapedRecipe);
				} else {
					holder.callbackPlayer.sendMessage(ChatColor.RED + "Couldn't create a shaped recipe. Possibly a duplicate key.");
				}

				HandlerList.unregisterAll(holder);
			}
		}

		private ShapedRecipes toRecipe() {
			CraftInventoryCustom dispenserInventory = (CraftInventoryCustom) this.dispenserInventory;
			IInventory minecraftInventory = (IInventory) ReflectionUtil.getDeclaredFieldValue(dispenserInventory, "inventory");
			NonNullList<ItemStack> itemStacks = (NonNullList<ItemStack>) ReflectionUtil.getDeclaredFieldValue(minecraftInventory, "items");

			int minNonEmptyRownum = 2;
			int minNonEmptyColnum = 2;
			int maxNonEmptyRownum = 0;
			int maxNonEmptyColnum = 0;

			for (int index = 0; index < itemStacks.size(); index++) {
				ItemStack stack = itemStacks.get(index);
				if (!stack.isEmpty()) {
					int[] rownumColnum = InventoryUtils.inventoryRownumColnum(3, index);
					int rowNum = rownumColnum[0];
					int colNum = rownumColnum[1];

					minNonEmptyRownum = Math.min(minNonEmptyRownum, rowNum);
					minNonEmptyColnum = Math.min(minNonEmptyColnum, colNum);
					maxNonEmptyRownum = Math.max(maxNonEmptyRownum, rowNum);
					maxNonEmptyColnum = Math.max(maxNonEmptyColnum, colNum);
				}
			}

			int height = maxNonEmptyRownum - minNonEmptyRownum + 1;
			int width = maxNonEmptyColnum - minNonEmptyColnum + 1;

			NonNullList<RecipeItemStack> ingredients = NonNullList.a(height * width, RecipeItemStack.a);
			for (int h = 0; h < height; h++) {
				for (int w = 0; w < width; w++) {
					int index = InventoryUtils.inventoryIndex(width, new int[] {h, w});

					ItemStack ingredientStack = itemStacks.get(InventoryUtils.inventoryIndex(3,
							new int[] {h + minNonEmptyRownum, w + minNonEmptyColnum}));
					RecipeItemStack ingredient = RecipeItemStack.a(new ItemStack[] {ingredientStack});

					ingredients.set(index, ingredient);
				}
			}

			ShapedRecipes recipe = new ShapedRecipes(group, width, height, ingredients, result);
			recipe.setKey(key);
			return recipe;
		}

	}

} public class ShapedRemover implements BiConsumer<Player, List<String>> {

	private final CustomRecipesPlugin plugin;

	public ShapedRemover(CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}

	@Override
	public void accept(Player player, List<String> args) {
		CRCraftingManager craftingManager = plugin.getCraftingManager();

		CRShapedRecipe toBeRemoved = null;

		if (!args.isEmpty()) {

			String firstArg = args.get(0);
			NamespacedKey key = plugin.getKey(firstArg);

			CraftingRecipe recipe = craftingManager.getRecipe(key);
			if (recipe == null) {
				player.sendMessage(ChatColor.RED + "No recipe found for key " + ChatColor.WHITE + key + ChatColor.RED + ".");
				return;
			}

			if (recipe instanceof CRShapedRecipe) {
				toBeRemoved = (CRShapedRecipe) recipe;
			} else {
				player.sendMessage(ChatColor.RED + "Couldn't remove recipe for key " + ChatColor.WHITE + key + ChatColor.RED + ", it is not a native shaped recipe.");
				return;
			}

		} else {
			ItemStack itemInHand = player.getInventory().getItemInMainHand();
			if (InventoryUtils.isEmptyStack(itemInHand)) itemInHand = player.getInventory().getItemInOffHand();
			if (InventoryUtils.isEmptyStack(itemInHand)) {
				player.sendMessage(ChatColor.RED + "Usage: /removerecipe shaped <key>");
				player.sendMessage(ChatColor.RED + "Or '/removerecipe shaped' with an item in your hand.");
				return;
			}

			//loop until match
			for (CraftingRecipe recipe : craftingManager) {
				if (!(recipe instanceof CRShapedRecipe)) continue;
				if (itemInHand.equals(recipe.getResult())) {
					toBeRemoved = (CRShapedRecipe) recipe;
					break;
				}
			}

			//no match
			if (toBeRemoved == null) {
				player.sendMessage(ChatColor.RED + "No shaped recipe found for resultstack " +
						ChatColor.WHITE + InventoryUtils.getItemName(itemInHand) +
						ChatColor.RED + ".");
				return;
			}
		}

		if (toBeRemoved != null) {
			//match
			NamespacedKey key = craftingManager.removeRecipe(toBeRemoved);
			player.sendMessage(ChatColor.GREEN + "Removed recipe with key " +
					ChatColor.WHITE + key +
					ChatColor.GREEN + " for item " +
					ChatColor.WHITE + InventoryUtils.getItemName(toBeRemoved.getResult()) +
					ChatColor.GREEN + ".");
			plugin.disableCraftingRecipeFile("shaped", toBeRemoved);
		}

	}

} public class ShapelessAdder implements BiConsumer<Player, List<String>> {

	private final CustomRecipesPlugin plugin;

	public ShapelessAdder(CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}

	@Override
	public void accept(Player player, List<String> args) {
		org.bukkit.inventory.ItemStack itemInMainHand = player.getInventory().getItemInMainHand();
		if (itemInMainHand == null) {
			player.sendMessage(ChatColor.RED + "Put the result of the recipe in your main hand when executing this command.");
			return;
		}

		if (args.isEmpty()) {
			player.sendMessage(ChatColor.RED + "Usage: /addrecipe shapeless <key> [<group>]");
			return;
		}

		String keyString = args.get(0);
		String group = args.size() >= 2 ? args.get(1) : "";
		NamespacedKey bukkitKey = plugin.getKey(keyString);

		ItemStack result = CraftItemStack.asNMSCopy(itemInMainHand);
		MinecraftKey key = CraftNamespacedKey.toMinecraft(bukkitKey);

		player.openInventory(new ShapelessRecipeHolder(plugin, result, key, group, player).getInventory());
	}

	private static final class ShapelessRecipeHolder implements InventoryHolder, Listener {

		private final Inventory dispenserInventory;
		private final CustomRecipesPlugin plugin;
		private final ItemStack result;
		private final String group;
		private final MinecraftKey key;
		private final Player callbackPlayer;

		public ShapelessRecipeHolder(CustomRecipesPlugin plugin, ItemStack result, MinecraftKey key, String group,
				Player callbackPlayer) {
			this.plugin = plugin;
			this.callbackPlayer = callbackPlayer;
			this.result = result;
			this.key = key;
			this.group = group;
			this.dispenserInventory = plugin.getServer().createInventory(this, InventoryType.DISPENSER, "Create a shapeless recipe!");
			plugin.getServer().getPluginManager().registerEvents(this, plugin);
		}

		@Override
		public Inventory getInventory() {
			return dispenserInventory;
		}

		@EventHandler
		public void onInventoryClose(InventoryCloseEvent event) {
			if (event.getInventory().getHolder() instanceof ShapelessRecipeHolder) {
				ShapelessRecipeHolder holder = (ShapelessRecipeHolder) event.getInventory().getHolder();
				if (holder != this) return;

				ShapelessRecipes nmsRecipe = holder.toRecipe();
				CRShapelessRecipe<ShapelessRecipes> shapelessRecipe = new CRShapelessRecipe<>(nmsRecipe);

				List<List<String>> recipeIngredients = shapelessRecipe
						.getIngredients().stream().map((CRChoiceIngredient ingr) -> ingr.getChoices().stream()
								.map(InventoryUtils::getItemName)
								.collect(Collectors.toList()))
						.collect(Collectors.toList());
				String recipeString = recipeIngredients + "" + ChatColor.RESET + " -> "
						+ InventoryUtils.getItemName(shapelessRecipe.getResult());

				boolean success = holder.plugin.getCraftingManager().addRecipe(holder.key, nmsRecipe, shapelessRecipe);
				if (success) {
					holder.callbackPlayer.sendMessage(String.format("%sAdded shapeless recipe: %s%s%s!",
							ChatColor.GREEN, ChatColor.WHITE, recipeString, ChatColor.WHITE));
					plugin.saveCraftingRecipeFile("shapeless", shapelessRecipe);
				} else {
					holder.callbackPlayer.sendMessage(ChatColor.RED + "Couldn't create a permission recipe. Possibly a duplicate key.");
				}

				HandlerList.unregisterAll(holder);
			}
		}

		private ShapelessRecipes toRecipe() {
			CraftInventoryCustom dispenserInventory = (CraftInventoryCustom) this.dispenserInventory;
			IInventory minecraftInventory = (IInventory) ReflectionUtil.getDeclaredFieldValue(dispenserInventory, "inventory");
			NonNullList<ItemStack> itemStacks = (NonNullList<ItemStack>) ReflectionUtil.getDeclaredFieldValue(minecraftInventory, "items");

			NonNullList<RecipeItemStack> ingredients = itemStacks.stream()
					.filter(is -> !is.isEmpty())
					.map(is -> RecipeItemStack.a(new ItemStack[] {is}))
					.collect(Collectors.toCollection(NonNullList::a));

			ShapelessRecipes recipe = new ShapelessRecipes(group, result, ingredients);
			recipe.setKey(key);
			return recipe;
		}

	}

} public class ShapelessRemover implements BiConsumer<Player, List<String>> {

	private final CustomRecipesPlugin plugin;

	public ShapelessRemover(CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}

	@Override
	public void accept(Player player, List<String> args) {
		CRCraftingManager craftingManager = plugin.getCraftingManager();
		
		CRShapelessRecipe toBeRemoved = null;

		if (!args.isEmpty()) {

			String firstArg = args.get(0);
			NamespacedKey key = plugin.getKey(firstArg);

			CraftingRecipe recipe = craftingManager.getRecipe(key);
			if (recipe == null) {
				player.sendMessage(ChatColor.RED + "No recipe found for key " + ChatColor.WHITE + key + ChatColor.RED + ".");
				return;
			}

			if (recipe instanceof CRShapelessRecipe) {
				toBeRemoved = (CRShapelessRecipe) recipe;
			} else {
				player.sendMessage(ChatColor.RED + "Couldn't remove recipe for key " + ChatColor.WHITE + key + ChatColor.RED + ", it is not a native shapeless recipe.");
				return;
			}

		} else {
			ItemStack itemInHand = player.getInventory().getItemInMainHand();
			if (InventoryUtils.isEmptyStack(itemInHand)) itemInHand = player.getInventory().getItemInOffHand();
			if (InventoryUtils.isEmptyStack(itemInHand)) {
				player.sendMessage(ChatColor.RED + "Usage: /removerecipe shapeless <key>");
				player.sendMessage(ChatColor.RED + "Or '/removerecipe shapeless' with an item in your hand.");
				return;
			}

			//loop until match
			for (CraftingRecipe recipe : craftingManager) {
				if (!(recipe instanceof CRShapelessRecipe)) continue;
				if (itemInHand.equals(recipe.getResult())) {
					toBeRemoved = (CRShapelessRecipe) recipe;
					break;
				}
			}
			
			//no match
			if (toBeRemoved == null) {
				player.sendMessage(ChatColor.RED + "No shapeless recipe found for resultstack " +
						ChatColor.WHITE + InventoryUtils.getItemName(itemInHand) +
						ChatColor.RED + ".");
				return;
			}
		}
		
		if (toBeRemoved != null) {
			//match
			NamespacedKey key = craftingManager.removeRecipe(toBeRemoved);
			player.sendMessage(ChatColor.GREEN + "Removed recipe with key " +
					ChatColor.WHITE + key +
					ChatColor.GREEN + " for item " +
					ChatColor.WHITE + InventoryUtils.getItemName(toBeRemoved.getResult()) +
					ChatColor.GREEN + ".");
			plugin.disableCraftingRecipeFile("shapeless", toBeRemoved);
		}

	}

} public class CRChoiceIngredient extends CRCraftingIngredient<RecipeItemStack> implements ChoiceIngredient, NBTSerializable {

	public CRChoiceIngredient(RecipeItemStack nmsIngredient) {
		super(nmsIngredient);
	}
	
	public static CRChoiceIngredient deserialize(Map<String, Object> map) {
		RecipeItemStack nmsIngredient = NBTUtil.deserializeRecipeItemStack(NBTUtil.fromMap(map));
		return nmsIngredient == RecipeItemStack.a ? CREmptyIngredient.INSTANCE : new CRChoiceIngredient(nmsIngredient);
	}

	@Override
	public List<? extends ItemStack> getChoices() {
		return Arrays.stream(nmsIngredient.choices).map(CraftItemStack::asCraftMirror).collect(Collectors.toList());
	}
	
	@Override
	public boolean equals(Object o) {
		if (o == this) return true;
		if (o instanceof CRChoiceIngredient) {
			CRChoiceIngredient that = (CRChoiceIngredient) o;
			net.minecraft.server.v1_12_R1.ItemStack[] thisChoices = nmsIngredient.choices;
			net.minecraft.server.v1_12_R1.ItemStack[] thatChoices = that.nmsIngredient.choices;
			
			if (thisChoices.length != thatChoices.length) return false;
			
			for (int i = 0; i < thisChoices.length; i++) {
				net.minecraft.server.v1_12_R1.ItemStack thisChoice = thisChoices[i];
				net.minecraft.server.v1_12_R1.ItemStack thatChoice = thatChoices[i];
				if (!net.minecraft.server.v1_12_R1.ItemStack.fastMatches(thisChoice, thatChoice)) return false;
			}
			
			return true;
			
		} else if (o instanceof ChoiceIngredient) {
			return Objects.deepEquals(this.getChoices(), ((ChoiceIngredient) o).getChoices());
			
		}
	
		return false;
	}
	
	@Override
	public int hashCode() {
		return Objects.hashCode(getChoices());
	}

} public class CREmptyIngredient extends CRChoiceIngredient implements EmptyIngredient {
	
	public static final CREmptyIngredient INSTANCE = new CREmptyIngredient();

	private CREmptyIngredient() {
		super(RecipeItemStack.a);
	}
	
	public static CREmptyIngredient deserialize(Map<String, Object> map) {
		return INSTANCE;
	}

} public class CRArmorDyeRecipe extends CRShapelessRecipe<RecipeArmorDye> {

	public CRArmorDyeRecipe(RecipeArmorDye nmsRecipe) {
		super(nmsRecipe);
	}
	
	public CRArmorDyeRecipe(java.util.Map<String, ?> map) {
		super(map);
	}

} public class CRBannerAddPatternRecipe extends CRShapelessRecipe<RecipesBanner.AddRecipe> implements BannerAddPatternRecipe {

	public CRBannerAddPatternRecipe(AddRecipe nmsRecipe) {
		super(nmsRecipe);
	}

	public CRBannerAddPatternRecipe(java.util.Map<String, ?> map) {
		super(map);
	}
} public class CRBookCloneRecipe extends CRShapelessRecipe<RecipeBookClone> implements BookCloneRecipe {

	public CRBookCloneRecipe(RecipeBookClone nmsRecipe) {
		super(nmsRecipe);
	}
	
	public CRBookCloneRecipe(java.util.Map<String, ?> map) {
		super(map);
	}

} public class CRFireworksRecipe extends CRShapelessRecipe<RecipeFireworks> implements FireworksRecipe {

	public CRFireworksRecipe(RecipeFireworks nmsRecipe) {
		super(nmsRecipe);
	}
	
	public CRFireworksRecipe(java.util.Map<String, ?> map) {
		super(map);
	}

} public class CRMapCloneRecipe extends CRShapelessRecipe<RecipeMapClone> implements MapCloneRecipe {

	public CRMapCloneRecipe(RecipeMapClone nmsRecipe) {
		super(nmsRecipe);
	}

	public CRMapCloneRecipe(java.util.Map<String, ?> map) {
		super(map);
	}
} public class CRMapExtendRecipe extends CRShapedRecipe<RecipeMapExtend> implements MapExtendRecipe {

	public CRMapExtendRecipe(RecipeMapExtend nmsRecipe) {
		super(nmsRecipe);
	}
	
	public CRMapExtendRecipe(java.util.Map<String, ?> map) {
		super(map);
	}

} public class CRRepairRecipe extends CRShapelessRecipe<RecipeRepair> implements RepairRecipe {

	public CRRepairRecipe(RecipeRepair nmsRecipe) {
		super(nmsRecipe);
	}
	
	public CRRepairRecipe(java.util.Map<String, ?> map) {
		super(map);
	}

} public class CRShapedRecipe<R extends ShapedRecipes> extends CRVanillaRecipe<R> implements ShapedRecipe {

	public CRShapedRecipe(R nmsRecipe) {
		super(nmsRecipe);
	}
	
	public CRShapedRecipe(NBTTagCompound recipeCompound) {
		this((R) deserializeNmsRecipe(recipeCompound));
	}
	
	public CRShapedRecipe(Map<String, ?> map) {
		this(NBTUtil.fromMap(map));
	}
	
	@Override
	public NBTTagCompound serializeToNbt() {
		NBTTagCompound serialized = super.serializeToNbt();
		serialized.setInt("width", getWidth());
		serialized.setInt("height", getHeight());
		NBTTagList ingredients = new NBTTagList();
		for (RecipeItemStack ingr : nmsIngredients()) {
			ingredients.add(NBTUtil.serializeRecipeItemStack(ingr));
		}
		serialized.set("ingredients", ingredients);
		return serialized;
	}
	
	protected static ShapedRecipes deserializeNmsRecipe(NBTTagCompound recipeCompound) {
		String group = recipeCompound.hasKeyOfType("group", NBTUtil.STRING) ? recipeCompound.getString("group") : "";
		int width = recipeCompound.getInt("width");
		int height = recipeCompound.getInt("height");
		NonNullList<RecipeItemStack> ingredients = NonNullList.a();
		NBTTagList nbtIngredients = recipeCompound.getList("ingredients", NBTUtil.COMPOUND);
		for (int i = 0; i < nbtIngredients.size(); i++) {
			NBTTagCompound ingredientTag = nbtIngredients.get(i);
			RecipeItemStack recipeItemStack = NBTUtil.deserializeRecipeItemStack(ingredientTag);
			ingredients.add(recipeItemStack);
		}
		NBTTagCompound resultCompound = (NBTTagCompound) recipeCompound.get("result");
		ItemStack result = new ItemStack(resultCompound);
		ShapedRecipes shapedRecipes = new ShapedRecipes(group, width, height, ingredients, result);
		if (recipeCompound.hasKey("key")) {
			shapedRecipes.setKey(NBTUtil.deserializeKey(recipeCompound.getCompound("key")));
		}
		return shapedRecipes;
	}
	
	@Override
	public int getWidth() {
		return (int) ReflectionUtil.getDeclaredFieldValue(nmsRecipe, "width");
	}
	
	@Override
	public int getHeight() {
		return (int) ReflectionUtil.getDeclaredFieldValue(nmsRecipe, "height");
	}
	
	@Override
	public List<CRChoiceIngredient> getIngredients() {
		return nmsIngredients().stream().map(CRCraftingIngredient::asBukkitIngredient).collect(Collectors.toList());
	}
	
	@SuppressWarnings("unchecked")
	protected NonNullList<RecipeItemStack> nmsIngredients() {
		return (NonNullList<RecipeItemStack>) ReflectionUtil.getDeclaredFieldValue(nmsRecipe, "items");
	}

	@Override
	public String getGroup() {
		return (String) ReflectionUtil.getDeclaredFieldValue(nmsRecipe, "e");
	}
	
	@Override
	public MinecraftKey getMinecraftKey() {
		return nmsRecipe.key;
	}
	
	
	@Override
	public boolean equals(Object o) {
		if (o == this) return true;
		if (!(o instanceof ShapedRecipe)) return false;
		if (o instanceof CRShapedRecipe) return Objects.equals(this.nmsRecipe, ((CRShapedRecipe<?>) o).nmsRecipe);
		
		ShapedRecipe that = (ShapedRecipe) o;
		
		return Objects.equals(this.getResult(), that.getResult()) && Objects.equals(this.getIngredients(), that.getIngredients()) &&
				Objects.equals(this.getWidth(), that.getWidth()) && Objects.equals(this.getHeight(), that.getHeight()) &&
				Objects.equals(this.isHidden(), that.isHidden()) && Objects.equals(this.getGroup(), that.getGroup());
	}
	
	@Override
	public int hashCode() {
		return Objects.hash(getResult(), getWidth(), getHeight(), getIngredients(), isHidden(), getGroup());
	}
	
	@Override
	public String toString() {
		return getClass().getName() + "{" + 
			"result()=" + getResult() +
			",width()=" + getWidth() +
			",heigth()=" + getHeight() +
			",ingredients()=" + getIngredients() +
			",hidden()=" + isHidden() +
			",group()=" + getGroup() +				
			"}";
	}

} public class CRShapelessRecipe<R extends ShapelessRecipes> extends CRVanillaRecipe<R> implements ShapelessRecipe {

	public CRShapelessRecipe(R nmsRecipe) {
		super(nmsRecipe);
	}
	
	public CRShapelessRecipe(NBTTagCompound recipeCompound) {
		this((R) deserializeNmsRecipe(recipeCompound));
	}
	
	public CRShapelessRecipe(Map<String, ?> map) {
		this(NBTUtil.fromMap(map));
	}
	
	@Override
	public NBTTagCompound serializeToNbt() {
		NBTTagCompound serialized = super.serializeToNbt();
		NBTTagList ingredients = new NBTTagList();
		for (RecipeItemStack ingr : nmsIngredients()) {
			ingredients.add(NBTUtil.serializeRecipeItemStack(ingr));
		}
		serialized.set("ingredients", ingredients);
		return serialized;
	}
	
	protected static ShapelessRecipes deserializeNmsRecipe(NBTTagCompound recipeCompound) {
		String group = recipeCompound.hasKeyOfType("group", NBTUtil.STRING) ? recipeCompound.getString("group") : "";
		NonNullList<RecipeItemStack> ingredients = NonNullList.a();
		NBTTagList nbtIngredients = recipeCompound.getList("ingredients", NBTUtil.COMPOUND);
		for (int i = 0; i < nbtIngredients.size(); i++) {
			NBTTagCompound ingredientTag = nbtIngredients.get(i);
			RecipeItemStack recipeItemStack = NBTUtil.deserializeRecipeItemStack(ingredientTag);
			ingredients.add(recipeItemStack);
		}
		NBTTagCompound resultCompound = (NBTTagCompound) recipeCompound.get("result");
		ItemStack result = new ItemStack(resultCompound);
		ShapelessRecipes shapelessRecipes = new ShapelessRecipes(group, result, ingredients);
		if (recipeCompound.hasKey("key")) {
			shapelessRecipes.setKey(NBTUtil.deserializeKey(recipeCompound.getCompound("key")));
		}
		return shapelessRecipes;
	}
	
	@Override
	public List<CRChoiceIngredient> getIngredients() {
		return nmsIngredients().stream().map(CRCraftingIngredient::asBukkitIngredient).collect(Collectors.toList());
	}
	
	@SuppressWarnings("unchecked")
	private NonNullList<RecipeItemStack> nmsIngredients() {
		return (NonNullList<RecipeItemStack>) ReflectionUtil.getDeclaredFieldValue(nmsRecipe, "ingredients");
	}
	
	@Override
	public MinecraftKey getMinecraftKey() {
		return nmsRecipe.key;
	}
	
	@Override
	public String getGroup() {
		return (String) ReflectionUtil.getDeclaredFieldValue(nmsRecipe, "c");
	}
	
	@Override
	public boolean equals(Object o) {
		if (o == this) return true;
		if (!(o instanceof ShapelessRecipe)) return false;
		if (o instanceof CRShapelessRecipe) return Objects.equals(this.nmsRecipe, ((CRShapelessRecipe<?>) o).nmsRecipe);
		
		ShapelessRecipe that = (ShapelessRecipe) o;
		
		return Objects.equals(this.getResult(), that.getResult()) && Objects.equals(this.getIngredients(), that.getIngredients()) &&
				Objects.equals(this.isHidden(), that.isHidden()) && Objects.equals(this.getGroup(), that.getGroup());
	}
	
	@Override
	public int hashCode() {
		return Objects.hash(getResult(), getIngredients(), isHidden(), getGroup());
	}
	
	@Override
	public String toString() {
		return getClass().getName() + "{" + 
			"result()=" + getResult() +
			",ingredients()=" + getIngredients() +
			",hidden()=" + isHidden() +
			",group()=" + getGroup() +				
			"}";
	}

} public class CRShieldDecorationRecipe extends CRShapelessRecipe<RecipiesShield.Decoration> implements ShieldDecorationRecipe {

	public CRShieldDecorationRecipe(Decoration nmsRecipe) {
		super(nmsRecipe);
	}
	
	public CRShieldDecorationRecipe(java.util.Map<String, ?> map) {
		super(map);
	}

} public class CRShulkerBoxDyeRecipe extends CRShapelessRecipe<RecipeShulkerBox.Dye> implements ShulkerBoxDyeRecipe {

	public CRShulkerBoxDyeRecipe(Dye nmsRecipe) {
		super(nmsRecipe);
	}
	
	public CRShulkerBoxDyeRecipe(java.util.Map<String, ?> map) {
		super(map);
	}

} public class CRTippedArrowRecipe extends CRShapedRecipe<RecipeTippedArrow> implements TippedArrowRecipe {

	public CRTippedArrowRecipe(RecipeTippedArrow nmsRecipe) {
		super(nmsRecipe);
	}
	
	public CRTippedArrowRecipe(java.util.Map<String, ?> map) {
		super(map);
	}

} @Deprecated
public class CRVanillaRecipe<R extends IRecipe> extends CRCraftingRecipe<R> implements CraftingRecipe {

	public CRVanillaRecipe(R nmsRecipe) {
		super(nmsRecipe);
	}

} public class CRFurnaceManager implements FurnaceManager {
    
    private final NMSFurnaceManager nmsManager;
    
    public CRFurnaceManager(NMSFurnaceManager nmsManager) {
        this.nmsManager = Objects.requireNonNull(nmsManager);
    }

    @Override
    public Iterator<com.gmail.jannyboy11.customrecipes.api.furnace.FurnaceRecipe> iterator() {
        return new MapIterator<>(nmsManager.iterator(), NMSFurnaceRecipe::getBukkitRecipe);
    }

    @Override
    public FurnaceRecipe addCustomRecipe(FurnaceRecipe furnaceRecipe) {
        NMSFurnaceRecipe nms = CRFurnaceRecipe.getNMSRecipe(furnaceRecipe);
        nmsManager.addCustomRecipe(nms);
        return nms.getBukkitRecipe();
    }

    @Override
    public FurnaceRecipe addVanillaRecipe(FurnaceRecipe furnaceRecipe) {
        NMSFurnaceRecipe nms = CRFurnaceRecipe.getNMSRecipe(furnaceRecipe);
        nmsManager.addVanillaRecipe(nms);
        return nms.getBukkitRecipe();
    }

    @Override
    public Iterator<? extends FurnaceRecipe> customIterator() {
        return new MapIterator<>(nmsManager.customIterator(), NMSFurnaceRecipe::getBukkitRecipe);
    }

    @Override
    public Iterator<? extends FurnaceRecipe> vanillaIterator() {
        return new MapIterator<>(nmsManager.vanillaIterator(), NMSFurnaceRecipe::getBukkitRecipe);
    }

    @Override
    public FurnaceRecipe getRecipe(ItemStack ingredient) {
        NMSFurnaceRecipe result = nmsManager.getRecipe(CraftItemStack.asNMSCopy(ingredient));
        return result == null ? null : result.getBukkitRecipe();
    }

    @Override
    public FurnaceRecipe getCustomRecipe(ItemStack ingredient) {
        NMSFurnaceRecipe result = nmsManager.getCustomRecipe(CraftItemStack.asNMSCopy(ingredient));
        return result == null ? null : result.getBukkitRecipe();
    }

    @Override
    public FurnaceRecipe getVanillaRecipe(ItemStack ingredient) {
        NMSFurnaceRecipe result = nmsManager.getCustomRecipe(CraftItemStack.asNMSCopy(ingredient));
        return result == null ? null : result.getBukkitRecipe();
    }

    @Override
    public void reset() {
        nmsManager.reset();
    }

    @Override
    public void clearVanilla() {
        nmsManager.clearVanilla();
    }

    @Override
    public void clearCustom() {
        nmsManager.clearCustom();
    }
    
    public void clear() {
        nmsManager.clear();
    }

    @Override
    public FurnaceRecipe removeRecipe(ItemStack ingredient) {
        NMSFurnaceRecipe recipe = nmsManager.removeRecipe(CraftItemStack.asNMSCopy(ingredient));
        return recipe == null ? null : recipe.getBukkitRecipe();
    }

    @Override
    public FurnaceRecipe removeVanillaRecipe(ItemStack ingredient) {
        NMSFurnaceRecipe recipe = nmsManager.removeVanillaRecipe(CraftItemStack.asNMSCopy(ingredient));
        return recipe == null ? null : recipe.getBukkitRecipe();
    }

    @Override
    public FurnaceRecipe removeCustomRecipe(ItemStack ingredient) {
        NMSFurnaceRecipe recipe = nmsManager.removeCustomRecipe(CraftItemStack.asNMSCopy(ingredient));
        return recipe == null ? null : recipe.getBukkitRecipe();
    }
    
    
    @Override
    public FurnaceRecipe removeRecipe(NamespacedKey key) {
        NMSFurnaceRecipe recipe = nmsManager.removeRecipe(CraftNamespacedKey.toMinecraft(key));
        return recipe == null ? null : recipe.getBukkitRecipe();
    }
    
    @Override
    public FurnaceRecipe removeVanillaRecipe(NamespacedKey key) {
        NMSFurnaceRecipe recipe = nmsManager.removeVanillaRecipe(CraftNamespacedKey.toMinecraft(key));
        return recipe == null ? null : recipe.getBukkitRecipe();
    }

    @Override
    public FurnaceRecipe removeCustomRecipe(NamespacedKey key) {
        NMSFurnaceRecipe recipe = nmsManager.removeCustomRecipe(CraftNamespacedKey.toMinecraft(key));
        return recipe == null ? null : recipe.getBukkitRecipe();
    }
    
    
    @Override
    public FurnaceRecipe getRecipe(NamespacedKey key) {
        NMSFurnaceRecipe recipe = nmsManager.getRecipe(CraftNamespacedKey.toMinecraft(key));
        return recipe == null ? null : recipe.getBukkitRecipe();
    }
    
    @Override
    public FurnaceRecipe getCustomRecipe(NamespacedKey key) {
        NMSFurnaceRecipe recipe = nmsManager.getCustomRecipe(CraftNamespacedKey.toMinecraft(key));
        return recipe == null ? null : recipe.getBukkitRecipe();
    }

    @Override
    public FurnaceRecipe getVanillaRecipe(NamespacedKey key) {
        NMSFurnaceRecipe recipe = nmsManager.getVanillaRecipe(CraftNamespacedKey.toMinecraft(key));
        return recipe == null ? null : recipe.getBukkitRecipe();
    }

} public class CRFurnaceRecipe implements FurnaceRecipe, NBTSerializable {

    private final NMSFurnaceRecipe nmsRecipe;
    
    public CRFurnaceRecipe(NMSFurnaceRecipe injectedFurnaceRecipe) {
        this.nmsRecipe = injectedFurnaceRecipe;
    }
    
    public CRFurnaceRecipe(NBTTagCompound serialized) {
        this(new NMSFurnaceRecipe(serialized));
    }

    
    public static NMSFurnaceRecipe getNMSRecipe(FurnaceRecipe bukkit) {
        if (bukkit instanceof CRFurnaceRecipe) {
            return ((CRFurnaceRecipe) bukkit).nmsRecipe;
        } else {
            return new Bukkit2NMSFurnaceRecipe(bukkit);
        }
    }
    
    public NMSFurnaceRecipe getHandle() {
        return nmsRecipe;
    }
    
    @Override
    public NamespacedKey getKey() {
        return CraftNamespacedKey.fromMinecraft(nmsRecipe.getKey());
    }

    @Override
    public NBTTagCompound serializeToNbt() {
        return nmsRecipe.serializeToNbt();
    }

    @Override
    public CraftingIngredient getIngredient() {
        return CRCraftingIngredient.asBukkitIngredient(nmsRecipe.getIngredient());
    }

    @Override
    public CraftItemStack getResult() {
        return CraftItemStack.asCraftMirror(nmsRecipe.getResult());
    }

    @Override
    public float getXp() {
        return nmsRecipe.getExperience();
    }

    @Override
    public void setIngredient(CraftingIngredient ingredient) {
        nmsRecipe.setIngredient(CRCraftingIngredient.asNMSIngredient(ingredient));
    }

    @Override
    public void setResult(org.bukkit.inventory.ItemStack result) {
        nmsRecipe.setResult(CraftItemStack.asNMSCopy(result));
    }

    @Override
    public void setXp(float xp) {
        nmsRecipe.setExperience(xp);
    }
} public class NMSFurnaceManager extends RecipesFurnace implements Iterable<NMSFurnaceRecipe> {
    
    private final Map<MinecraftKey, NMSFurnaceRecipe> vanillaRecipes;
    private final Map<MinecraftKey, NMSFurnaceRecipe> customRecipes;
    
    private static NMSFurnaceManager instance;
    
    protected NMSFurnaceManager() {
        super();
        
        this.vanillaRecipes = new HashMap<>();
        this.customRecipes = new HashMap<>();
    
        setInstance(this);        
        
        copyVanillaRecipes(this);
        copyCustomRecipes(this);
    }
    
    public static NMSFurnaceManager getInstance() {
        return instance == null ? instance = new NMSFurnaceManager() : instance;
    }
    
    protected static void setInstance(NMSFurnaceManager recipesFurnace) {
        ReflectionUtil.setStaticFinalFieldValue(RecipesFurnace.class, "a", recipesFurnace);
        instance = recipesFurnace;
    }
    
    
    private void copyVanillaRecipes(RecipesFurnace instance) {
        instance.recipes.forEach((ingredient, result) -> {                
            Item item = ingredient.getItem();
            MinecraftKey key = new MinecraftKey(item.getName());
            RecipeItemStack newIngredient = makeVanillaIngredient(ingredient);
            float xp = super.b(ingredient);
            
            NMSFurnaceRecipe injectedFurnaceRecipe = new NMSFurnaceRecipe(key, newIngredient, result, xp);
            vanillaRecipes.put(key, injectedFurnaceRecipe);
        });
    }
    
    private void copyCustomRecipes(RecipesFurnace instance) {
        instance.customRecipes.forEach((ingredient, result) -> {
            Item item = ingredient.getItem();
            MinecraftKey key = CraftNamespacedKey.toMinecraft(CustomRecipesPlugin.getInstance().getKey(item.getName()));
            RecipeItemStack newIngredient = makeVanillaIngredient(ingredient);
            float xp = super.b(ingredient);
            
            NMSFurnaceRecipe injectedFurnaceRecipe = new NMSFurnaceRecipe(key, newIngredient, result, xp);
            customRecipes.put(key, injectedFurnaceRecipe);
        });
    }
    
    protected final RecipeItemStack makeVanillaIngredient(ItemStack referenceStack) {
        return new InjectedIngredient(itemStack -> furnaceEqualsVanilla(this, itemStack, referenceStack)).asNMSIngredient();
    }
    
    public static boolean furnaceEqualsVanilla(RecipesFurnace recipesFurnace, ItemStack test, ItemStack referenceStack) {
        return (boolean) ReflectionUtil.invokeInstanceMethod(recipesFurnace, "a", test, referenceStack);
    }

    @Override
    public Iterator<NMSFurnaceRecipe> iterator() {
        return Iterators.concat(customRecipes.values().iterator(), vanillaRecipes.values().iterator());
    }
    
    @Override
    public void a(ItemStack input, ItemStack result, float xp) {
        super.a(input, result, xp);
        if (instance == null) return; //if we are called from the super constructor, return. ugly hack!        
        
        MinecraftKey key = new MinecraftKey(input.getItem().getName());
        RecipeItemStack ingredient = makeVanillaIngredient(input);
        NMSFurnaceRecipe recipe = new NMSFurnaceRecipe(key, ingredient, result, xp);
        vanillaRecipes.put(key, recipe);
    }
    
    @Override
    public void registerRecipe(ItemStack input, ItemStack result, float xp) {
        super.a(input, result, xp);
        if (instance == null) return; //if we are called from the super constructor, return. ugly hack!
        
        MinecraftKey key = CraftNamespacedKey.toMinecraft(CustomRecipesPlugin.getInstance().getKey(input.getItem().getName()));
        RecipeItemStack ingredient = makeVanillaIngredient(input);
        NMSFurnaceRecipe recipe = new NMSFurnaceRecipe(key, ingredient, result, xp);
        customRecipes.put(key, recipe);
    }
    
    @Override
    public ItemStack getResult(ItemStack itemstack) {
        for (NMSFurnaceRecipe recipe : this) {
           if (recipe.getIngredient().a(itemstack)) {
               return recipe.getResult();
           }
        }
        return ItemStack.a;
    }
    
    @Override
    public float b(ItemStack itemstack) {
        for (NMSFurnaceRecipe recipe : this) {
            if (recipe.getIngredient().a(itemstack)) {
                return recipe.getExperience();
            }
        }
        return 0F;
    }
    
    
    public void clear() {
        clearVanilla();
        clearCustom();
    }
    
    public void clearVanilla() {
        super.recipes = new HashMap<>();
        
        vanillaRecipes.clear();
    }
    
    public void clearCustom() {
        super.customRecipes.clear();
        super.customExperience.clear();
        
        customRecipes.clear();
    }
    
    public void reset() {
        clear();
        copyVanillaRecipes(new RecipesFurnace());
    }
    
    public NMSFurnaceRecipe getRecipe(MinecraftKey key) {
        NMSFurnaceRecipe recipe = getCustomRecipe(key);
        if (recipe == null) recipe = getVanillaRecipe(key);
        return recipe;
    }
    
    public NMSFurnaceRecipe getVanillaRecipe(MinecraftKey key) {
        return vanillaRecipes.get(key);
    }
    
    public NMSFurnaceRecipe getCustomRecipe(MinecraftKey key) {
        return customRecipes.get(key);
    }
    
    public void addVanillaRecipe(NMSFurnaceRecipe nms) {
        vanillaRecipes.put(nms.getKey(), nms);
    }
    
    public void addCustomRecipe(NMSFurnaceRecipe nms) {
        customRecipes.put(nms.getKey(), nms);
    }
    
    public Iterator<NMSFurnaceRecipe> vanillaIterator() {
        return vanillaRecipes.values().iterator();
    }
    
    public Iterator<NMSFurnaceRecipe> customIterator() {
        return customRecipes.values().iterator();
    }
    
    protected static NMSFurnaceRecipe getRecipe(ItemStack ingredientTest, Iterator<? extends NMSFurnaceRecipe> iterator) {
        while (iterator.hasNext()) {
            NMSFurnaceRecipe recipe = iterator.next();
            if (recipe.getIngredient().a(ingredientTest)) {
                return recipe;
            }
        }
        return null;
    }
    
    public NMSFurnaceRecipe getRecipe(ItemStack itemStack) {
        return getRecipe(itemStack, iterator());
    }
    
    public NMSFurnaceRecipe getCustomRecipe(ItemStack itemStack) {
        return getRecipe(itemStack, customIterator());
    }
    
    public NMSFurnaceRecipe getVanillaRecipe(ItemStack itemStack) {
        return getRecipe(itemStack, vanillaIterator());
    }

    protected static NMSFurnaceRecipe removeRecipe(ItemStack ingredient, Iterator<? extends NMSFurnaceRecipe> iterator) {
        while (iterator.hasNext()) {
            NMSFurnaceRecipe recipe = iterator.next();
            if (recipe.getIngredient().a(ingredient)) {
                iterator.remove();
                return recipe;
            }
        }
        return null;
    }
    
    public NMSFurnaceRecipe removeRecipe(ItemStack ingredient) {
        return removeRecipe(ingredient, iterator());
    }

    public NMSFurnaceRecipe removeVanillaRecipe(ItemStack ingredient) {
        return removeRecipe(ingredient, vanillaIterator());
    }
    
    public NMSFurnaceRecipe removeCustomRecipe(ItemStack ingredient) {
        return removeRecipe(ingredient, customIterator());
    }
    
    public NMSFurnaceRecipe removeVanillaRecipe(MinecraftKey key) {
        return vanillaRecipes.remove(key);
    }
    
    public NMSFurnaceRecipe removeCustomRecipe(MinecraftKey key) {
        return customRecipes.remove(key);
    }
    
    public NMSFurnaceRecipe removeRecipe(MinecraftKey key) {
        NMSFurnaceRecipe removedRecipe = removeCustomRecipe(key);
        return removedRecipe != null ? removedRecipe : removeVanillaRecipe(key);
    }
    
} public class NMSFurnaceRecipe implements NBTSerializable {
    private final MinecraftKey key;
    private RecipeItemStack ingredient;
    private ItemStack result;
    private float experience;
    
    protected FurnaceRecipe bukkit;
    
    protected NMSFurnaceRecipe() {
        this.key = null;
    }
    
    public NMSFurnaceRecipe(MinecraftKey key, ItemStack ingredient, ItemStack result, float experience) {
        this(key, RecipeItemStack.a(new ItemStack[] {ingredient}), result, experience);
    }
    
    public NMSFurnaceRecipe(MinecraftKey key, RecipeItemStack ingredient, ItemStack result, float experience) {
        this.key = key;
        this.ingredient = ingredient;
        this.result = result;
        this.experience = experience;
    }
    
    public NMSFurnaceRecipe(NBTTagCompound serialized) {
        this.key = NBTUtil.deserializeKey(serialized.getCompound("key"));
        this.ingredient = NBTUtil.deserializeRecipeItemStack(serialized.getCompound("ingredient"));
        this.result = NBTUtil.deserializeItemStack(serialized.getCompound("result"));
        this.experience = serialized.getFloat("experience");
    }
    
    @Override
    public NBTTagCompound serializeToNbt() {
        NBTTagCompound compound = new NBTTagCompound();
        compound.set("ingredient", NBTUtil.serializeRecipeItemStack(getIngredient()));
        compound.set("result", NBTUtil.serializeItemStack(getResult()));
        compound.setFloat("experience", getExperience());
        compound.set("key", NBTUtil.serializeKey(getKey()));
        return compound;
    }
    
    public MinecraftKey getKey() {
        return key;
    }
    
    public RecipeItemStack getIngredient() {
        return ingredient;
    }
    
    public ItemStack getResult() {
        return result;
    }
    
    public float getExperience() {
        return experience;
    }
    
    public NMSFurnaceRecipe setIngredient(RecipeItemStack ingredient) {
        this.ingredient = ingredient;
        return this;
    }
    
    public NMSFurnaceRecipe setResult(ItemStack result) {
        this.result = result;
        return this;
    }
    
    public NMSFurnaceRecipe setExperience(float experience) {
        this.experience = experience;
        return this;
    }
    
    public FurnaceRecipe getBukkitRecipe() {
        return bukkit == null ? bukkit = new CRFurnaceRecipe(this) : bukkit;
    }
    
    @Override
    public boolean equals(Object o) {
        //TODO can we do better?
        if (o == this) return true;
        return (o instanceof NMSFurnaceRecipe) && ((Objects.equals(this.key, ((NMSFurnaceRecipe) o).getKey())));
    }
    
    @Override
    public int hashCode() {
        //TODO can we do better?
        return Objects.hashCode(key);
    }
    
} public class FurnaceAdder implements BiConsumer<Player, List<String>> {

    private final CustomRecipesPlugin plugin;

    public FurnaceAdder(CustomRecipesPlugin plugin) {
        this.plugin = plugin;
    }

    @Override
    public void accept(Player player, List<String> args) {
        org.bukkit.inventory.ItemStack itemInMainHand = player.getInventory().getItemInMainHand();

        if (InventoryUtils.isEmptyStack(itemInMainHand)) {
            player.sendMessage(ChatColor.RED + "Put the result of the recipe in your main hand when executing this command.");
            return;
        }

        if (args.isEmpty()) {
            player.sendMessage(ChatColor.RED + "Usage: /addrecipe furnace <key> [<xp>] [vanilla]");
            return;
        }

        String keyString = args.get(0);
        NamespacedKey bukkitKey = plugin.getKey(keyString);
        float xp = 0;
        if (args.size() >= 2) {
            try {
                xp = Float.parseFloat(args.get(1));
            } catch (NumberFormatException ignored) {
                //xp stays 0
            }
        }
        boolean vanilla = args.size() >= 3 ? "vanilla".equalsIgnoreCase(args.get(2)) : false;

        ItemStack result = CraftItemStack.asNMSCopy(itemInMainHand);

        player.openInventory(new FurnaceRecipeHolder(plugin, player, result, bukkitKey, xp, vanilla).getInventory());
    }

    public static class FurnaceRecipeHolder implements InventoryHolder, Listener {

        private final CustomRecipesPlugin plugin;
        private final Inventory hopperInventory;
        private final Player callbackPlayer;
        private final MinecraftKey key;
        private final ItemStack result;
        private final boolean vanilla;
        private final float xp;

        public FurnaceRecipeHolder(CustomRecipesPlugin plugin, Player player, ItemStack result, NamespacedKey key, float xp, boolean vanilla) {
            this.plugin = plugin;
            this.callbackPlayer = player;
            this.hopperInventory = plugin.getServer().createInventory(this, InventoryType.HOPPER, "Create a furnace recipe!");
            this.key = CraftNamespacedKey.toMinecraft(key);
            this.result = result;
            this.vanilla = vanilla;
            this.xp = xp;
            plugin.getServer().getPluginManager().registerEvents(this, plugin);
        }

        @Override
        public Inventory getInventory() {
            return hopperInventory;
        }

        @EventHandler
        public void onInventoryClose(InventoryCloseEvent event) {
            if (event.getInventory().getHolder() instanceof FurnaceRecipeHolder) {
                FurnaceRecipeHolder holder = (FurnaceRecipeHolder) event.getInventory().getHolder();
                if (holder != this) return;

                Inventory inventory = event.getInventory();
                if (InventoryUtils.isEmpty(inventory)) {
                    holder.callbackPlayer.sendMessage(ChatColor.RED + "please put an ingredient int he inventory.");
                    HandlerList.unregisterAll(holder);
                    return;
                }

                org.bukkit.inventory.ItemStack ingredient = null;
                for (org.bukkit.inventory.ItemStack first : inventory) {
                    if (!InventoryUtils.isEmptyStack(first)) {
                        ingredient = first;
                        break;
                    }
                }

                CRFurnaceRecipe furnaceRecipe = new CRFurnaceRecipe(registerRecipe());
                String ingredientString = InventoryUtils.getItemName(ingredient);

                String recipeString = ingredientString + "" + ChatColor.RESET + " -> "
                        + InventoryUtils.getItemName(furnaceRecipe.getResult());
                if (holder.xp > 0) {
                    DecimalFormat decimalFormat = new DecimalFormat("##.##");
                    String xpString = decimalFormat.format(holder.xp);
                    recipeString += ChatColor.RESET + " (" + xpString + " xp)";
                }

                holder.callbackPlayer.sendMessage(String.format("%sAdded furnace recipe: %s%s%s!",
                        ChatColor.GREEN, ChatColor.WHITE, recipeString, ChatColor.WHITE));
                plugin.saveFurnaceRecipeFile(furnaceRecipe);

                HandlerList.unregisterAll(holder);
            }
        }

        private NMSFurnaceRecipe registerRecipe() {
            CraftInventoryCustom dispenserInventory = (CraftInventoryCustom) this.hopperInventory;
            IInventory minecraftInventory = (IInventory) ReflectionUtil.getDeclaredFieldValue(dispenserInventory, "inventory");
            NonNullList<ItemStack> itemStacks = (NonNullList<ItemStack>) ReflectionUtil.getDeclaredFieldValue(minecraftInventory, "items");
            NMSFurnaceManager recipesFurnace = NMSFurnaceManager.getInstance();

            ItemStack ingredientStack = itemStacks.stream().filter(nmsStack -> !nmsStack.isEmpty()).findFirst().get();
            RecipeItemStack ingredient = RecipeItemStack.a(new ItemStack[] {ingredientStack});            
            NMSFurnaceRecipe furnaceRecipe = new NMSFurnaceRecipe(key, ingredient, result, xp);

            if (vanilla) {
                recipesFurnace.addVanillaRecipe(furnaceRecipe);
            } else {
                recipesFurnace.addCustomRecipe(furnaceRecipe);
            }
            
            return furnaceRecipe;
        }

    }

} public class FurnaceRemover implements BiConsumer<Player, List<String>> {
	
	private final CustomRecipesPlugin plugin;

	public FurnaceRemover(CustomRecipesPlugin plugin) {
		this.plugin = plugin;
	}

	@Override
	public void accept(Player player, List<String> args) {		
		ItemStack itemStack = player.getInventory().getItemInMainHand();
		if (itemStack == null) itemStack = player.getInventory().getItemInOffHand();
		if (itemStack == null) {
			player.sendMessage(ChatColor.RED + "Please use this comming while holding an item in your hand.");
			return;
		}
		
		CRFurnaceManager furnaceManager = plugin.getFurnaceManager();
		
		boolean vanilla = args.size() > 0 ? "vanilla".equalsIgnoreCase(args.get(0)) : false;
		FurnaceRecipe removed = vanilla ? furnaceManager.removeVanillaRecipe(itemStack) : furnaceManager.removeRecipe(itemStack);
		if (removed != null) {		
			player.sendMessage(ChatColor.GREEN + "Removed furnace recipe with ingredient " + 
					ChatColor.WHITE + InventoryUtils.getItemName(itemStack) +
					ChatColor.GREEN + ".");
			
			if (removed instanceof CRFurnaceRecipe) {
			    plugin.disableFurnaceRecipeFile((CRFurnaceRecipe) removed);
			}
		} else {
			player.sendMessage(ChatColor.RED + "No " + (vanilla ? "vanilla" : "custom") + " furnace recipe found for ingredient " + 
					ChatColor.WHITE + InventoryUtils.getItemName(itemStack) +
					ChatColor.RED + ".");
		}
	}

} public class ConfigurationSerializableByteArray implements ConfigurationSerializable {
	
	private final byte[] bytes;

	public ConfigurationSerializableByteArray(byte[] bytes) {
		this.bytes = bytes;
	}
	
	public ConfigurationSerializableByteArray(Map<String, Object> map) {
		List<Number> byteList = (List) map.get("bytes");
		bytes = new byte[byteList.size()];
		for (int i = 0; i < byteList.size(); i++) {
			bytes[i] = byteList.get(i).byteValue();
		}
	}

	@Override
	public Map<String, Object> serialize() {
		return Collections.singletonMap("bytes", byteList());
	}

	private List<Byte> byteList() {
		List<Byte> list = new ArrayList<>(bytes.length);
		for (int i = 0; i < bytes.length; i++) {
			list.add(bytes[i]);
		}
		return list;
	}
	
	public byte[] getBytes() {
		return bytes;
	}

} public class ConfigurationSerializableIntArray implements ConfigurationSerializable {
	
	private final int[] ints;
	
	public ConfigurationSerializableIntArray(int[] ints) {
		this.ints = ints;
	}
	
	public ConfigurationSerializableIntArray(Map<String, Object> map) {
		List<Number> intList = (List) map.get("ints");
		ints = new int[intList.size()];
		for (int i = 0; i < intList.size(); i++) {
			ints[i] = intList.get(i).intValue();
		}
	}

	@Override
	public Map<String, Object> serialize() {
		return Collections.singletonMap("ints", intList());
	}

	private List<Integer> intList() {
		List<Integer> list = new ArrayList<>(ints.length);
		for (int i = 0; i < ints.length; i++) {
			list.add(ints[i]);
		}
		return list;
	}

	public int[] getInts() {
		return ints;
	}
} public class ConfigurationSerializableLongArray implements ConfigurationSerializable {
	
	private final long[] longs;
	
	public ConfigurationSerializableLongArray(long[] longs) {
		this.longs = longs;
	}
	
	public ConfigurationSerializableLongArray(Map<String, Object> map) {
		List<Number> longList = (List) map.get("longs");
		longs = new long[longList.size()];
		for (int i = 0; i < longList.size(); i++) {
			longs[i] = longList.get(i).longValue();
		}
	}

	@Override
	public Map<String, Object> serialize() {
		return Collections.singletonMap("longs", longList());
	}

	private List<Long> longList() {
		List<Long> list = new ArrayList<>(longs.length);
		for (int i = 0; i < longs.length; i++) {
			list.add(longs[i]);
		}
		return list;
	}

	public long[] getLongs() {
		return longs;
	}
	
} public interface NBTSerializable extends ConfigurationSerializable {
	
	public NBTTagCompound serializeToNbt();
	
	public default Map<String, Object> serialize() {
		return NBTUtil.toMap(serializeToNbt());
	}

} public class NBTUtil {
	
	public static final int END = 0;
	public static final int BYTE = 1;
	public static final int SHORT = 2;
	public static final int INT = 3;
	public static final int LONG = 4;
	public static final int FLOAT = 5;
	public static final int DOUBLE = 6;
	public static final int BYTE_ARRAY = 7;
	public static final int STRING = 8;
	public static final int LIST = 9;
	public static final int COMPOUND = 10;
	public static final int INT_ARRAY = 11;
	public static final int LONG_ARRAY = 12;

	protected NBTUtil() {}
	
	
	// ============== IO ==============
	
	public static void writeNBTTagCompound(File saveFile, NBTTagCompound compound) throws IOException {
		File tempFile = new File(saveFile.getAbsolutePath() + ".tmp");
		if (tempFile.exists()) tempFile.delete();
		tempFile.createNewFile();
		
		NBTCompressedStreamTools.a(compound, new FileOutputStream(tempFile));
		tempFile.renameTo(saveFile);
	}
	
	public static NBTTagCompound readNBTTagCompound(File saveFile) throws IOException {
		NBTTagCompound compound = NBTCompressedStreamTools.a(new FileInputStream(saveFile));
		return compound;
	}
	
	
	// ============== MinecraftKey ==============
	
	public static NBTTagCompound serializeKey(MinecraftKey key) {
		NBTTagCompound compound = new NBTTagCompound();
		compound.setString("namespace", key.b());
		compound.setString("key", key.getKey());
		return compound;
	}
	
	public static MinecraftKey deserializeKey(NBTTagCompound compound) {
		return new MinecraftKey(compound.getString("namespace"), compound.getString("key"));
	}
	
	
	// ============== ItemStack ==============
	
	public static NBTTagCompound serializeItemStack(ItemStack stack) {
		NBTTagCompound tag = new NBTTagCompound();
		stack.save(tag);
		return tag;
	}
	
	public static ItemStack deserializeItemStack(NBTTagCompound tag) {
		return new ItemStack(tag);
	}
	
	
	// ============== RecipeItemStack ==============
	
	public static NBTTagCompound serializeRecipeItemStack(RecipeItemStack recipeItemStack) {
		if (recipeItemStack == RecipeItemStack.a) return new NBTTagCompound(); //empty ingredient -> empty compound
		
		NBTTagCompound compound = new NBTTagCompound();
		if (recipeItemStack.choices.length == 0) return compound; //not the empty ingredient, but no choices
		
		NBTTagList nbtChoices = new NBTTagList();
		for (ItemStack choice : recipeItemStack.choices) {
			NBTTagCompound choiceCompound = new NBTTagCompound();
			choice.save(choiceCompound);
			nbtChoices.add(choiceCompound);
		}
		compound.set("choices", nbtChoices);
		
		return compound;
	}
	
	public static RecipeItemStack deserializeRecipeItemStack(NBTTagCompound compound) {
		if (compound.isEmpty()) return RecipeItemStack.a; //empty compound -> empty ingredient
		
		if (compound.hasKeyOfType("choices", LIST)) {
			NBTTagList choicesList = compound.getList("choices", COMPOUND);
			List<ItemStack> itemsList = new ArrayList<>();
			for (int i = 0; i < choicesList.size(); i++) {
				NBTTagCompound choiceCompound = choicesList.get(i);
				ItemStack stack = new ItemStack(choiceCompound);
				itemsList.add(stack);
			}
			
			ItemStack[] choices = itemsList.toArray(new ItemStack[itemsList.size()]);
			return RecipeItemStack.a(choices);
		}
		
		return RecipeItemStack.a(new ItemStack[0]); //not an empty compound, but no choices.
	}
	
	
	
	// ============== ConfigurationSerializable Conversion ==============
	
	public static Map<String, Object> toMap(NBTTagCompound compound) {
		Map<String, Object> map = new HashMap<>();
		for (String key : compound.c()) { //c() => keyset
			NBTBase value = compound.get(key);
			map.put(key, toObject(value));
		}
		return map;
	}
	
	public static NBTTagCompound fromMap(Map<String, ?> map) {
		NBTTagCompound compound = new NBTTagCompound();
		map.forEach((key, value) -> compound.set(key, fromObject(value)));
		return compound;
	}
	
	public static NBTTagList fromList(List<?> list) {
		NBTTagList nbtList = new NBTTagList();
		list.forEach(o -> nbtList.add(fromObject(o)));
		return nbtList;
	}
	
	public static List toList(NBTTagList nbtList) {
		List list = new ArrayList();
		for (int i = 0; i < nbtList.size(); i++) {
			list.add(toObject(nbtList.i(i)));
		}
		return list;
	}
	
	public static Object toObject(NBTBase nbtBase) {
		if (nbtBase instanceof NBTTagEnd) {
			return null;
		} else if (nbtBase instanceof NBTTagByte) {
			return ((NBTTagByte) nbtBase).g();
		} else if (nbtBase instanceof NBTTagShort) {
			return ((NBTTagShort) nbtBase).f();
		} else if (nbtBase instanceof NBTTagInt) {
			return ((NBTTagInt) nbtBase).e();
		} else if (nbtBase instanceof NBTTagLong) {
			return ((NBTTagLong) nbtBase).d();
		} else if (nbtBase instanceof NBTTagFloat) {
			return ((NBTTagFloat) nbtBase).i();
		} else if (nbtBase instanceof NBTTagDouble) {
			return ((NBTTagDouble) nbtBase).asDouble();
		} else if (nbtBase instanceof NBTTagByteArray) {
			byte[] bytes = ((NBTTagByteArray) nbtBase).c();
			return new ConfigurationSerializableByteArray(bytes);
		} else if (nbtBase instanceof NBTTagIntArray) {
			int[] ints = ((NBTTagIntArray) nbtBase).d();
			return new ConfigurationSerializableIntArray(ints);
		} else if (nbtBase instanceof NBTTagLongArray) {
			NBTTagLongArray array = (NBTTagLongArray) nbtBase;
			long[] longs = (long[]) ReflectionUtil.getDeclaredFieldValue(array, "b");
			return new ConfigurationSerializableLongArray(longs);
		} else if (nbtBase instanceof NBTTagString) {
			return ((NBTTagString) nbtBase).c_();
		} else if (nbtBase instanceof NBTTagList) {
			return toList((NBTTagList) nbtBase);
		} else if (nbtBase instanceof NBTTagCompound) {
			return toMap((NBTTagCompound) nbtBase);
		}
		
		throw new IllegalArgumentException("Unrecognized NBT type: " + nbtBase);
	}
	
	public static NBTBase fromObject(Object o) {

		if (o instanceof NBTTagEnd) {
			return null;
		} else if (o instanceof Byte) {
			return new NBTTagByte((Byte) o);
		} else if (o instanceof Short) {
			return new NBTTagShort((Short) o);
		} else if (o instanceof Integer) {
			return new NBTTagInt((Integer) o);
		} else if (o instanceof Long) {
			return new NBTTagLong((Long) o);
		} else if (o instanceof Float) {
			return new NBTTagFloat((Float) o);
		} else if (o instanceof Double) {
			return new NBTTagDouble((Double) o);
		} else if (o instanceof byte[]) {
			return new NBTTagByteArray((byte[]) o);
		} else if (o instanceof ConfigurationSerializableByteArray) {
			return new NBTTagByteArray(((ConfigurationSerializableByteArray) o).getBytes());
		} else if (o instanceof int[]) {
			return new NBTTagIntArray((int[]) o);
		} else if (o instanceof ConfigurationSerializableIntArray) {
			return new NBTTagIntArray(((ConfigurationSerializableIntArray) o).getInts());
		} else if (o instanceof long[]) {
			return new NBTTagLongArray((long[]) o);
		} else if (o instanceof ConfigurationSerializableLongArray) {
			return new NBTTagLongArray(((ConfigurationSerializableLongArray) o).getLongs());
		} else if (o instanceof String) {
			return new NBTTagString((String) o);
		} else if (o instanceof List) {
			return fromList((List) o);
		} else if (o instanceof Map) {
			return fromMap((Map) o);
		}

		throw new IllegalArgumentException("Object not nbt deserializable: " + o);
	}

	
	//dirty hack to get an NBTTagEnd
	protected static class NBTDummy extends NBTTagCompound {
		
		public static final NBTDummy INSTANCE = new NBTDummy();
		
		public final NBTTagEnd end() {
			return (NBTTagEnd) createTag((byte) 0);
		}
		
	}
	
}
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.bukkit.NamespacedKey;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.craftbukkit.v1_12_R1.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_12_R1.util.CraftNamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Recipe;
import org.bukkit.plugin.java.JavaPlugin;
import com.gmail.jannyboy11.customrecipes.api.SerializableKey;
import com.gmail.jannyboy11.customrecipes.api.CustomRecipesApi;
import com.gmail.jannyboy11.customrecipes.api.InventoryUtils;
import com.gmail.jannyboy11.customrecipes.api.crafting.CraftingRecipe;
import com.gmail.jannyboy11.customrecipes.api.crafting.SimpleChoiceIngredient;
import com.gmail.jannyboy11.customrecipes.api.crafting.SimpleShapedRecipe;
import com.gmail.jannyboy11.customrecipes.api.crafting.SimpleShapelessRecipe;
import com.gmail.jannyboy11.customrecipes.api.crafting.custom.ingredient.SimilarIngredient;
import com.gmail.jannyboy11.customrecipes.api.crafting.custom.ingredient.WildcardIngredient;
import com.gmail.jannyboy11.customrecipes.api.crafting.custom.recipe.CountRecipe;
import com.gmail.jannyboy11.customrecipes.api.crafting.custom.recipe.NBTRecipe;
import com.gmail.jannyboy11.customrecipes.api.crafting.custom.recipe.PermissionRecipe;
import com.gmail.jannyboy11.customrecipes.api.crafting.custom.recipe.WorldRecipe;
import com.gmail.jannyboy11.customrecipes.api.crafting.vanilla.ingredient.ChoiceIngredient;
import com.gmail.jannyboy11.customrecipes.api.crafting.vanilla.recipe.BannerDuplicateRecipe;
import com.gmail.jannyboy11.customrecipes.api.crafting.vanilla.recipe.ShapedRecipe;
import com.gmail.jannyboy11.customrecipes.api.crafting.vanilla.recipe.ShapelessRecipe;
import com.gmail.jannyboy11.customrecipes.api.furnace.FurnaceRecipe;
import com.gmail.jannyboy11.customrecipes.api.furnace.SimpleFurnaceRecipe;
import com.gmail.jannyboy11.customrecipes.commands.AddRecipeCommandExecutor;
import com.gmail.jannyboy11.customrecipes.commands.ListRecipesCommandExecutor;
import com.gmail.jannyboy11.customrecipes.commands.MigrateRecipesCommandExecutor;
import com.gmail.jannyboy11.customrecipes.commands.RemoveRecipeCommandExecutor;
import com.gmail.jannyboy11.customrecipes.gui.ListRecipesListener;
import com.gmail.jannyboy11.customrecipes.impl.crafting.CRCraftingManager;
import com.gmail.jannyboy11.customrecipes.impl.crafting.CRCraftingRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.addremove.CountAdder;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.addremove.CountRemover;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.addremove.NBTAdder;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.addremove.NBTRemover;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.addremove.PermissionAdder;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.addremove.PermissionRemover;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.addremove.WorldAdder;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.addremove.WorldRemover;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.ingredient.Bukkit2NMSIngredient;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.ingredient.InjectedIngredient;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.recipe.Bukkit2NMSRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.recipe.tobukkit.CRCountRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.recipe.tobukkit.CRNBTRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.recipe.tobukkit.CRPermissionRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.custom.recipe.tobukkit.CRWorldRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.addremove.ShapedAdder;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.addremove.ShapedRemover;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.addremove.ShapelessAdder;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.addremove.ShapelessRemover;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.ingredient.CRChoiceIngredient;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.ingredient.CREmptyIngredient;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.recipe.CRArmorDyeRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.recipe.CRBannerAddPatternRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.recipe.CRBookCloneRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.recipe.CRFireworksRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.recipe.CRMapCloneRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.recipe.CRMapExtendRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.recipe.CRRepairRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.recipe.CRShapedRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.recipe.CRShapelessRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.recipe.CRShieldDecorationRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.recipe.CRShulkerBoxDyeRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.recipe.CRTippedArrowRecipe;
import com.gmail.jannyboy11.customrecipes.impl.crafting.vanilla.recipe.CRVanillaRecipe;
import com.gmail.jannyboy11.customrecipes.impl.furnace.CRFurnaceManager;
import com.gmail.jannyboy11.customrecipes.impl.furnace.CRFurnaceRecipe;
import com.gmail.jannyboy11.customrecipes.impl.furnace.custom.NMSFurnaceManager;
import com.gmail.jannyboy11.customrecipes.impl.furnace.custom.NMSFurnaceRecipe;
import com.gmail.jannyboy11.customrecipes.impl.furnace.vanilla.addremove.FurnaceAdder;
import com.gmail.jannyboy11.customrecipes.impl.furnace.vanilla.addremove.FurnaceRemover;
import com.gmail.jannyboy11.customrecipes.serialize.ConfigurationSerializableByteArray;
import com.gmail.jannyboy11.customrecipes.serialize.ConfigurationSerializableIntArray;
import com.gmail.jannyboy11.customrecipes.serialize.ConfigurationSerializableLongArray;
import com.gmail.jannyboy11.customrecipes.serialize.NBTSerializable;
import com.gmail.jannyboy11.customrecipes.util.NBTUtil;
import net.minecraft.server.v1_12_R1.CraftingManager;
import net.minecraft.server.v1_12_R1.IRecipe;
import net.minecraft.server.v1_12_R1.MinecraftKey;
import net.minecraft.server.v1_12_R1.NBTTagCompound;
import net.minecraft.server.v1_12_R1.RecipeItemStack;
import net.minecraft.server.v1_12_R1.RecipesFurnace;
import net.minecraft.server.v1_12_R1.ShapedRecipes;
import net.minecraft.server.v1_12_R1.ShapelessRecipes;
package com.gmail.jannyboy11.customrecipes;





/**
 * The CustomRecipes Plugin's main class. Internal use only!
 * 
 * @author Jan
 */
public class CustomRecipesPlugin extends JavaPlugin implements CustomRecipesApi {

	private final NavigableMap<String, BiConsumer<? super Player, ? super List<String>>> adders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
	private final NavigableMap<String, BiConsumer<? super Player, ? super List<String>>> removers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

	private final Map<String, Supplier<? extends List<? extends Recipe>>> recipeSuppliers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
	private final Map<String, Function<? super Recipe, ? extends ItemStack>> recipeToItemMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
	private final Map<String, BiConsumer<? super Recipe, ? super CommandSender>> recipeToCommandSenderDiplayMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

	private final Map<String, BiConsumer<? super Recipe, ? super File>> writers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
	private final Map<String, Function<? super File, ? extends Recipe>> readers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

	private CRCraftingManager craftingManager;
	private CRFurnaceManager furnaceManager;

	private Set<MinecraftKey> vanillaCraftingRecipes;
	private Map<net.minecraft.server.v1_12_R1.ItemStack, net.minecraft.server.v1_12_R1.ItemStack> vanillaFurnaceRecipes;

	private void recordVanillaRecipes() {
		getServer().resetRecipes(); //Is called before onEnable of other plugins, so shouldn't cause trouble.
		vanillaCraftingRecipes = new HashSet<>(CraftingManager.recipes.keySet());
		vanillaFurnaceRecipes = new HashMap<>(RecipesFurnace.getInstance().recipes);
	}

	public boolean isVanillaCraftingRecipe(MinecraftKey key) {
		if (key == null) return false;
		return vanillaCraftingRecipes.contains(key);
	}

	public boolean isVanillaFurnaceRecipe(net.minecraft.server.v1_12_R1.ItemStack ingredient, net.minecraft.server.v1_12_R1.ItemStack result) {
		if (ingredient == null) return false;
		return vanillaFurnaceRecipes.entrySet().stream().anyMatch(entry -> 
			NMSFurnaceManager.furnaceEqualsVanilla(RecipesFurnace.getInstance(), entry.getKey(), ingredient) &&
			NMSFurnaceManager.furnaceEqualsVanilla(RecipesFurnace.getInstance(), entry.getValue(), result));
	}


	@Override
	public void onLoad() {
		//define RecipeItemStackInjected subclass
		InjectedIngredient.inject();		

		//let's hope no other plugins have added crafting recipes here
		recordVanillaRecipes();
		
		craftingManager = new CRCraftingManager();
		furnaceManager = new CRFurnaceManager(NMSFurnaceManager.getInstance());

		getLogger().info("DEUBG: adding adders");
		
		//adders
		addAdder("shaped", new ShapedAdder(this));
		addAdder("shapeless", new ShapelessAdder(this));
		addAdder("nbt", new NBTAdder(this));
		addAdder("permission", new PermissionAdder(this));
		addAdder("count", new CountAdder(this));
		addAdder("world", new WorldAdder(this));
		addAdder("furnace", new FurnaceAdder(this));

		getLogger().info("DEBUG: adding removers");
		
		//removers
		addRemover("shaped", new ShapedRemover(this));
		addRemover("shapeless", new ShapelessRemover(this));
		addRemover("nbt", new NBTRemover(this));
		addRemover("permission", new PermissionRemover(this));
		addRemover("count", new CountRemover(this));
		addRemover("world", new WorldRemover(this));
		addRemover("furnace", new FurnaceRemover(this));

		getLogger().info("DEBUG: adding writers");
		
		//readers and writers only used in commands, thats why casting to CR variants is allowed
		BiConsumer<Recipe, File> nbtSaver = (recipe, file) -> {
			NBTSerializable cr = (NBTSerializable) recipe;
			try {
				NBTUtil.writeNBTTagCompound(file, cr.serializeToNbt());
			} catch (IOException e) {
				e.printStackTrace();
			}
		};
		addWriter("shaped", nbtSaver);
		addWriter("shapeless", nbtSaver);
		addWriter("nbt", nbtSaver);
		addWriter("permission", nbtSaver);
		addWriter("count", nbtSaver);
		addWriter("world", nbtSaver);
		addWriter("furnace", nbtSaver);
		
		getLogger().info("DEBUG: adding readers");
		
		Function<File, NBTTagCompound> nbtReader = file -> {
			try {
				return NBTUtil.readNBTTagCompound(file);
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		};
		addReader("shaped", nbtReader.andThen(CRShapedRecipe::new));
		addReader("shapeless", nbtReader.andThen(CRShapelessRecipe::new));
		addReader("nbt", nbtReader.andThen(CRNBTRecipe::new));
		addReader("permission", nbtReader.andThen(CRPermissionRecipe::new));
		addReader("count", nbtReader.andThen(CRCountRecipe::new));