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));
		addReader("world", nbtReader.andThen(CRWorldRecipe::new));
		addReader("furnace", nbtReader.andThen(CRFurnaceRecipe::new));
		
		getLogger().info("DEBUG: adding listers");
		
		//representations for the listrecipes menu
		recipeToItemMap.put("shaped", recipe -> ((ShapedRecipe) recipe).getRepresentation());
		recipeToItemMap.put("shapeless", recipe -> ((ShapelessRecipe) recipe).getRepresentation());
		recipeToItemMap.put("furnace", recipe -> ((FurnaceRecipe) recipe).getRepresentation());
		recipeToItemMap.put("nbt", recipe -> ((NBTRecipe) recipe).getRepresentation());
		recipeToItemMap.put("permission", recipe -> ((PermissionRecipe) recipe).getRepresentation());
		recipeToItemMap.put("count", recipe -> ((CountRecipe) recipe).getRepresentation());
		recipeToItemMap.put("world", recipe -> ((WorldRecipe) recipe).getRepresentation());

		//recipe displayers
		recipeToCommandSenderDiplayMap.put("shaped", (recipe, commandSender) -> {
			ShapedRecipe shapedRecipe = (ShapedRecipe) recipe;
			commandSender.sendMessage("Key: " + craftingManager.getKey(shapedRecipe));
			commandSender.sendMessage("Result: " + InventoryUtils.getItemName(shapedRecipe.getResult()));
			commandSender.sendMessage("Width: " + shapedRecipe.getWidth());
			commandSender.sendMessage("Height: " + shapedRecipe.getHeight());
			commandSender.sendMessage("Ingredients: " + shapedRecipe.getIngredients().stream()
					.map(ingr -> ingr.getChoices().stream().map(InventoryUtils::getItemName).collect(Collectors.toList()))
					.collect(Collectors.toList()));
			if (shapedRecipe.hasGroup()) commandSender.sendMessage("Group: " + shapedRecipe.getGroup());
			if (shapedRecipe.isHidden()) commandSender.sendMessage("Hidden: true");
			commandSender.sendMessage("");
		});
		recipeToCommandSenderDiplayMap.put("shapeless", (recipe, commandSender) -> {
			ShapelessRecipe shapelessRecipe = (ShapelessRecipe) recipe;
			commandSender.sendMessage("Key: " + craftingManager.getKey(shapelessRecipe));
			commandSender.sendMessage("Result: " + InventoryUtils.getItemName(shapelessRecipe.getResult()));
			commandSender.sendMessage("Ingredients: " + shapelessRecipe.getIngredients().stream()
					.map(ingr -> ingr.getChoices().stream().map(InventoryUtils::getItemName).collect(Collectors.toList()))
					.collect(Collectors.toList()));
			if (shapelessRecipe.hasGroup()) commandSender.sendMessage("Group: " + shapelessRecipe.getGroup());
			if (shapelessRecipe.isHidden()) commandSender.sendMessage("Hidden: true");
			commandSender.sendMessage("");
		});
		recipeToCommandSenderDiplayMap.put("furnace", (recipe, commandSender) -> {
			FurnaceRecipe furnaceRecipe = (FurnaceRecipe) recipe;
			if (furnaceRecipe.getIngredient() instanceof ChoiceIngredient) {
			    ChoiceIngredient choiceIngredient = (ChoiceIngredient) furnaceRecipe.getIngredient();
			    if (!choiceIngredient.getChoices().isEmpty()) {
			        commandSender.sendMessage("Ingredient: " + InventoryUtils.getItemName(choiceIngredient.getChoices().get(0)));
			    }
			}
			commandSender.sendMessage("Result: " + InventoryUtils.getItemName(furnaceRecipe.getResult()));
			if (furnaceRecipe.getXp() > 0) commandSender.sendMessage("XP: " + furnaceRecipe.getXp());
			commandSender.sendMessage("");
		});
		recipeToCommandSenderDiplayMap.put("nbt", (recipe, commandSender) -> {
			NBTRecipe nbtRecipe = (NBTRecipe) recipe;
			commandSender.sendMessage("Key: " + craftingManager.getKey(nbtRecipe));
			commandSender.sendMessage("Result: " + InventoryUtils.getItemName(nbtRecipe.getResult()));
			commandSender.sendMessage("Width: " + nbtRecipe.getWidth());
			commandSender.sendMessage("Height: " + nbtRecipe.getHeight());
			commandSender.sendMessage("Ingredients: " + nbtRecipe.getIngredients().stream()
					.map(ingr -> ingr.getChoices().stream().map(InventoryUtils::getItemName).collect(Collectors.toList()))
					.collect(Collectors.toList()));
			if (nbtRecipe.hasGroup()) commandSender.sendMessage("Group: " + nbtRecipe.getGroup());
			if (nbtRecipe.isHidden()) commandSender.sendMessage("Hidden: true");
			commandSender.sendMessage("NBT specific");
			commandSender.sendMessage("");
		});
		recipeToCommandSenderDiplayMap.put("permission", (recipe, commandSender) -> {
			PermissionRecipe permissionRecipe = (PermissionRecipe) recipe;
			commandSender.sendMessage("Key: " + craftingManager.getKey(permissionRecipe));
			commandSender.sendMessage("Result: " + InventoryUtils.getItemName(permissionRecipe.getResult()));
			commandSender.sendMessage("Width: " + permissionRecipe.getWidth());
			commandSender.sendMessage("Height: " + permissionRecipe.getHeight());
			commandSender.sendMessage("Ingredients: " + permissionRecipe.getIngredients().stream()
					.map(ingr -> ingr.getChoices().stream().map(InventoryUtils::getItemName).collect(Collectors.toList()))
					.collect(Collectors.toList()));
			if (permissionRecipe.hasGroup()) commandSender.sendMessage("Group: " + permissionRecipe.getGroup());
			if (permissionRecipe.isHidden()) commandSender.sendMessage("Hidden: true");
			commandSender.sendMessage("Permission: " + permissionRecipe.getPermission());
			commandSender.sendMessage("");
		});
		recipeToCommandSenderDiplayMap.put("count", (recipe, commandSender) -> {
			CountRecipe countRecipe = (CountRecipe) recipe;
			commandSender.sendMessage("Key: " + craftingManager.getKey(countRecipe));
			commandSender.sendMessage("Result: " + InventoryUtils.getItemName(countRecipe.getResult()));
			commandSender.sendMessage("Width: " + countRecipe.getWidth());
			commandSender.sendMessage("Height: " + countRecipe.getHeight());
			commandSender.sendMessage("Ingredients: " + countRecipe.getIngredients().stream()
					.map(ingr -> ingr.getChoices().stream().map(InventoryUtils::getItemName).collect(Collectors.toList()))
					.collect(Collectors.toList()));
			if (countRecipe.hasGroup()) commandSender.sendMessage("Group: " + countRecipe.getGroup());
			if (countRecipe.isHidden()) commandSender.sendMessage("Hidden: true");
			commandSender.sendMessage("");
		});
		recipeToCommandSenderDiplayMap.put("world", (recipe, commandSender) -> {
			WorldRecipe worldRecipe = (WorldRecipe) recipe;
			commandSender.sendMessage("Key: " + craftingManager.getKey(worldRecipe));
			commandSender.sendMessage("Result: " + InventoryUtils.getItemName(worldRecipe.getResult()));
			commandSender.sendMessage("Width: " + worldRecipe.getWidth());
			commandSender.sendMessage("Height: " + worldRecipe.getHeight());
			commandSender.sendMessage("Ingredients: " + worldRecipe.getIngredients().stream()
					.map(ingr -> ingr.getChoices().stream().map(InventoryUtils::getItemName).collect(Collectors.toList()))
					.collect(Collectors.toList()));
			if (worldRecipe.hasGroup()) commandSender.sendMessage("Group: " + worldRecipe.getGroup());
			if (worldRecipe.isHidden()) commandSender.sendMessage("Hidden: true");
			commandSender.sendMessage("World: " + worldRecipe.getWorld());
			commandSender.sendMessage("");
		});

		getLogger().info("DEBUG: adding providers");
		
		//recipe providers
		recipeSuppliers.put("shaped", () -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(craftingManager.iterator(),
				Spliterator.NONNULL), false)
				.filter(recipe -> recipe instanceof ShapedRecipe)
				.collect(Collectors.toList()));
		recipeSuppliers.put("shapeless", () -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(craftingManager.iterator(),
				Spliterator.NONNULL), false)
				.filter(recipe -> recipe instanceof ShapelessRecipe)
				.collect(Collectors.toList()));
		recipeSuppliers.put("furnace", () -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(furnaceManager.iterator(),
				Spliterator.NONNULL), false)
				.collect(Collectors.toList()));
		recipeSuppliers.put("nbt", () -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(craftingManager.iterator(),
				Spliterator.NONNULL), false)
				.filter(recipe -> recipe instanceof NBTRecipe)
				.collect(Collectors.toList()));
		recipeSuppliers.put("permission", () -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(craftingManager.iterator(),
				Spliterator.NONNULL), false)
				.filter(recipe -> recipe instanceof PermissionRecipe)
				.collect(Collectors.toList()));
		recipeSuppliers.put("count", () -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(craftingManager.iterator(),
				Spliterator.NONNULL), false)
				.filter(recipe -> recipe instanceof CountRecipe)
				.collect(Collectors.toList()));
		recipeSuppliers.put("world", () -> StreamSupport.stream(Spliterators.spliteratorUnknownSize(craftingManager.iterator(),
				Spliterator.NONNULL), false)
				.filter(recipe -> recipe instanceof WorldRecipe)
				.collect(Collectors.toList()));
	}


	public boolean addAdder(String recipeType, BiConsumer<? super Player, ? super List<String>> adder) {
		return adders.putIfAbsent(recipeType, adder) == null;
	}

	public boolean addRemover(String recipeType, BiConsumer<? super Player, ? super List<String>> remover) {
		return removers.putIfAbsent(recipeType, remover) == null;
	}

	public boolean addWriter(String recipeType, BiConsumer<? super Recipe, ? super File> saver) {
		return writers.putIfAbsent(recipeType, saver) == null;
	}

	public boolean addReader(String recipeType, Function<? super File, ? extends Recipe> loader) {
		return readers.putIfAbsent(recipeType, loader) == null;
	}



	@Override
	public void onEnable() {
		//serializable stuff

		//nbt arrays
		ConfigurationSerialization.registerClass(ConfigurationSerializableByteArray.class);
		ConfigurationSerialization.registerClass(ConfigurationSerializableIntArray.class);
		ConfigurationSerialization.registerClass(ConfigurationSerializableLongArray.class);
		
		//bukkit namespaced key
		ConfigurationSerialization.registerClass(SerializableKey.class);

		//bukkit ingredients
		ConfigurationSerialization.registerClass(SimilarIngredient.class);
		ConfigurationSerialization.registerClass(WildcardIngredient.class);
		ConfigurationSerialization.registerClass(SimpleChoiceIngredient.class);

		//bukkit recipes
		ConfigurationSerialization.registerClass(SimpleShapedRecipe.class);
		ConfigurationSerialization.registerClass(SimpleShapelessRecipe.class);

		//nms mirrors
		ConfigurationSerialization.registerClass(Bukkit2NMSIngredient.class);
		ConfigurationSerialization.registerClass(Bukkit2NMSRecipe.class);

		//nms ingredient wrappers
		ConfigurationSerialization.registerClass(CRChoiceIngredient.class);
		ConfigurationSerialization.registerClass(CREmptyIngredient.class);

		//nms recipe wrappers
		ConfigurationSerialization.registerClass(CRShapedRecipe.class);
		ConfigurationSerialization.registerClass(CRShapelessRecipe.class);
		ConfigurationSerialization.registerClass(CRNBTRecipe.class);
		ConfigurationSerialization.registerClass(CRPermissionRecipe.class);
		ConfigurationSerialization.registerClass(CRArmorDyeRecipe.class);
		ConfigurationSerialization.registerClass(CRBannerAddPatternRecipe.class);
		ConfigurationSerialization.registerClass(BannerDuplicateRecipe.class);
		ConfigurationSerialization.registerClass(CRBookCloneRecipe.class);
		ConfigurationSerialization.registerClass(CRFireworksRecipe.class);
		ConfigurationSerialization.registerClass(CRMapCloneRecipe.class);
		ConfigurationSerialization.registerClass(CRMapExtendRecipe.class);
		ConfigurationSerialization.registerClass(CRRepairRecipe.class);
		ConfigurationSerialization.registerClass(CRShieldDecorationRecipe.class);
		ConfigurationSerialization.registerClass(CRShulkerBoxDyeRecipe.class);
		ConfigurationSerialization.registerClass(CRTippedArrowRecipe.class);
		
		//furnace
		ConfigurationSerialization.registerClass(SimpleFurnaceRecipe.class);
		ConfigurationSerialization.registerClass(CRFurnaceRecipe.class);

		getCommand("migrateRecipes").setExecutor(new MigrateRecipesCommandExecutor(this));
		getCommand("addrecipe").setExecutor(new AddRecipeCommandExecutor(Collections.unmodifiableNavigableMap(adders)));
		getCommand("removerecipe").setExecutor(new RemoveRecipeCommandExecutor(Collections.unmodifiableNavigableMap(removers)));
		getCommand("listrecipes").setExecutor(new ListRecipesCommandExecutor(this::getRecipes,
				Collections.unmodifiableMap(recipeToItemMap),
				Collections.unmodifiableMap(recipeToCommandSenderDiplayMap)));

		getServer().getPluginManager().registerEvents(new ListRecipesListener(), this);

		
		//remove disabled vanilla recipes. only vanilla recipe types are shaped, shapeless and furnace
		for (File file : disabledFolder("furnace").listFiles()) {
			if (file.isDirectory()) continue;
			try {
				CRFurnaceRecipe furnaceRecipe = new CRFurnaceRecipe(NBTUtil.readNBTTagCompound(file));
				furnaceManager.removeVanillaRecipe(furnaceRecipe.getKey());
			} catch (IOException e) {
				getLogger().log(Level.SEVERE, "Could not disable vanilla furnace recipe!", e);
			}
		}
		for (File file : disabledFolder("shaped").listFiles()) {
			if (file.isDirectory()) continue;
			try {
				CRShapedRecipe<ShapedRecipes> shapedRecipe = new CRShapedRecipe<>(NBTUtil.readNBTTagCompound(file));
				craftingManager.removeRecipe(shapedRecipe.getKey());
			} catch (IOException e) {
				getLogger().log(Level.SEVERE, "Could not disable vanilla shaped recipe!", e);
			}
		}
		for (File file : disabledFolder("shapeless").listFiles()) {
			if (file.isDirectory()) continue;
			try {
				CRShapelessRecipe<ShapelessRecipes> shapelessRecipe = new CRShapelessRecipe<>(NBTUtil.readNBTTagCompound(file));
				craftingManager.removeRecipe(shapelessRecipe);
			} catch (IOException e) {
				getLogger().log(Level.SEVERE, "Could not disable vanilla shapeless recipe!", e);
			}
		}
		
		//load custom recipes
		readers.forEach((type, reader) -> {
			File saveFolder = saveFolder(type);
			for (File recipeFile : saveFolder.listFiles()) {
				if (recipeFile.isDirectory()) continue;
				Recipe recipe = reader.apply(recipeFile);
				if (recipe instanceof CRCraftingRecipe) {
					@SuppressWarnings({ "rawtypes", "unchecked" })
					CRCraftingRecipe<IRecipe> craftingRecipe = (CRCraftingRecipe) recipe;
					craftingManager.addRecipe(craftingRecipe.getMinecraftKey(), craftingRecipe.getHandle(), craftingRecipe);
				} else if (recipe instanceof CRFurnaceRecipe) {
					CRFurnaceRecipe furnaceRecipe = (CRFurnaceRecipe) recipe;
					boolean isVanilla = furnaceManager.getVanillaRecipe(furnaceRecipe.getKey()) != null;
					if (isVanilla) {
						furnaceManager.addVanillaRecipe(furnaceRecipe);
					} else {
						furnaceManager.addCustomRecipe(furnaceRecipe);
					}
				} else {
					getLogger().warning("Tried to read non-CR recipe: " + recipe);
				}
			}
		});
	}


	public static CustomRecipesPlugin getInstance() {
		return JavaPlugin.getPlugin(CustomRecipesPlugin.class);
	}


	@Override
	public CRCraftingManager getCraftingManager() {
		return craftingManager;
	}

	@Override
	public CRFurnaceManager getFurnaceManager() {
		return furnaceManager;
	}


	@Override
	@Deprecated
	public boolean isVanillaRecipeType(CraftingRecipe recipe) {
		if (!(recipe instanceof CRVanillaRecipe)) return false;

		CRVanillaRecipe<? extends IRecipe> vanillaWrapper = (CRVanillaRecipe<? extends IRecipe>) recipe;
		return vanillaWrapper.getHandle().getClass().getName().startsWith("net.minecraft.server.");
	}


	@Override
	public ShapedRecipe asCustomRecipesMirror(org.bukkit.inventory.ShapedRecipe bukkitRecipe) {
		String[] shape = bukkitRecipe.getShape();
		Map<Character, ItemStack> map = bukkitRecipe.getIngredientMap();
		List<? extends ChoiceIngredient> ingredients = Arrays.stream(shape)
				.flatMapToInt(s -> s.chars())
				.mapToObj(i -> map.getOrDefault((char) i, null))
				.map(SimpleChoiceIngredient::fromChoices)
				.collect(Collectors.toList());

		int width = shape[0].length();
		int height = shape.length;

		SimpleShapedRecipe simple = new SimpleShapedRecipe(bukkitRecipe.getResult(), width, height, ingredients);
		CraftingRecipe byKey = craftingManager.getRecipe(bukkitRecipe.getKey()); //can we do better? get by result and by ingredients?
		return simple.equals(byKey) ? (ShapedRecipe) byKey : simple;
	}

	@Override
	public ShapelessRecipe asCustomRecipesMirror(org.bukkit.inventory.ShapelessRecipe bukkitRecipe) {
		List<? extends ChoiceIngredient> ingredients = bukkitRecipe.getIngredientList().stream()
				.map(SimpleChoiceIngredient::fromChoices)
				.collect(Collectors.toList());

		SimpleShapelessRecipe simple = new SimpleShapelessRecipe(bukkitRecipe.getResult(), ingredients);
		CraftingRecipe byKey = craftingManager.getRecipe(bukkitRecipe.getKey()); //can we do better? get by result and by ingredients?
		return simple.equals(byKey) ? (ShapelessRecipe) byKey : simple;
	}

	@Override
	public FurnaceRecipe asCustomRecipesMirror(org.bukkit.inventory.FurnaceRecipe bukkitRecipe) {
		FurnaceRecipe recipe = furnaceManager.getRecipe(bukkitRecipe.getInput());
		if (recipe == null ||
		        !Objects.equals(recipe.getResult(), bukkitRecipe.getResult()) ||
		        !Objects.equals(Float.floatToIntBits(recipe.getXp()), Float.floatToIntBits(bukkitRecipe.getExperience()))) {
		    MinecraftKey key = CraftNamespacedKey.toMinecraft(getKey(InventoryUtils.getItemName(bukkitRecipe.getInput())));
	        NMSFurnaceRecipe nmsFurnaceRecipe  = new NMSFurnaceRecipe(key,
	                CraftItemStack.asNMSCopy(bukkitRecipe.getInput()),
	                CraftItemStack.asNMSCopy(bukkitRecipe.getResult()),
	                bukkitRecipe.getExperience());
	        recipe = new CRFurnaceRecipe(nmsFurnaceRecipe);
		}
		return recipe;
	}


	public List<? extends Recipe> getRecipes(String type) {
		return recipeSuppliers.getOrDefault(type, Collections::emptyList).get();
	}

	public void save(String recipeType, String fileName, Recipe recipe) {
		File saveFolder = saveFolder(recipeType);

		File saveFile = new File(saveFolder, fileName);
		writers.get(recipeType).accept(recipe, saveFile);
	}

	public Recipe load(String recipeType, File saveFile) {
		return readers.get(recipeType).apply(saveFile);
	}

	public File saveFolder(String recipeType) {
		File folder = new File(getDataFolder(), recipeType);
		if (!folder.exists()) folder.mkdirs();
		return folder;
	}

	public File disabledFolder(String recipeType) {
		File saveFolder = saveFolder(recipeType);
		File disabledFolder = new File(saveFolder, "disabled");
		disabledFolder.mkdirs();
		return disabledFolder;
	}
	
	public void saveCraftingRecipeFile(String recipeType, CRCraftingRecipe<? extends IRecipe> recipe) {
		String fileName = craftingRecipeFileName(recipe);
		if (isVanillaCraftingRecipe(recipe.getMinecraftKey())) {
			File disabledFolder = disabledFolder(recipeType);
			File disabledRecipeFile = new File(disabledFolder, fileName);
			disabledRecipeFile.delete();
		} else {
			save(recipeType, fileName, recipe);
		}
	}
	
	public void saveFurnaceRecipeFile(CRFurnaceRecipe recipe) {
		String fileName = furnaceRecipeFileName(recipe);
		boolean vanilla = false;
		
		NMSFurnaceRecipe nmsRecipe = recipe.getHandle();
		RecipeItemStack nmsIngredient = nmsRecipe.getIngredient();
		if (nmsIngredient.choices.length > 0) {
		    vanilla = isVanillaFurnaceRecipe(nmsIngredient.choices[0], nmsRecipe.getResult());
		}
		
		if (vanilla) {
			File disabledFolder = disabledFolder("furnace");
			File disabledRecipeFile = new File(disabledFolder, fileName);
			disabledRecipeFile.delete();
		} else {
			save("furnace", fileName, recipe);
		}
	}
	
	
	/**
	 * Delete recipe or disable vanilla recipe
	 * 
	 * @param recipeType shaped or shapeless
	 * @param recipe the vanilla recipe
	 */
	public void disableCraftingRecipeFile(String recipeType, CRCraftingRecipe recipe) {
		String fileName = craftingRecipeFileName(recipe);
		if (isVanillaCraftingRecipe(recipe.getMinecraftKey())) {
			
			//disable
			File disabledFolder = disabledFolder(recipeType);
			File disabledFile = new File(disabledFolder, fileName);
			try {
				NBTUtil.writeNBTTagCompound(disabledFile, recipe.serializeToNbt());
			} catch (IOException e) {
				getLogger().log(Level.SEVERE, "Could not disable vanilla crafting recipe!", e);
			}
		} else {

			//delete
			File saveFolder = saveFolder(recipeType);
			File saveFile = new File(saveFolder, fileName);
			saveFile.delete();
		}
	}
	
	/**
	 * Delete recipe or disable vanilla recipe
	 * 
	 * @param recipe the furnace recipe
	 */
	public void disableFurnaceRecipeFile(CRFurnaceRecipe recipe) {
		String fileName = furnaceRecipeFileName(recipe);
		
		boolean vanilla = false;
        NMSFurnaceRecipe nmsRecipe = recipe.getHandle();
        RecipeItemStack nmsIngredient = nmsRecipe.getIngredient();
        if (nmsIngredient.choices.length > 0) {
            vanilla = isVanillaFurnaceRecipe(nmsIngredient.choices[0], nmsRecipe.getResult());
        }
		
		if (vanilla) {
			//disable
			File disabledFolder = disabledFolder("furnace");
			File disabledFile = new File(disabledFolder, fileName);
			try {
				NBTUtil.writeNBTTagCompound(disabledFile, recipe.serializeToNbt());
			} catch (IOException e) {
				getLogger().log(Level.SEVERE, "Could not disable vanilla crafting recipe!", e);
			}
		} else {
			//delete
			File saveFolder = saveFolder("furnace");
			File saveFile = new File(saveFolder, fileName);
			saveFile.delete();
		}
	}
	
	public static String craftingRecipeFileName(CRCraftingRecipe<? extends IRecipe> recipe) {
		NamespacedKey key = recipe.getKey();
		return key.toString().replace(':', '_') + ".dat";
	}
	
	public static String furnaceRecipeFileName(CRFurnaceRecipe recipe) {
	    NamespacedKey key = recipe.getKey();
        return key.toString().replace(':', '_') + ".dat";
	}


	public void setCraftingManager(CRCraftingManager craftingManager) {
		this.craftingManager = Objects.requireNonNull(craftingManager);
	}

	public void setFurnaceManager(CRFurnaceManager furnaceManager) {
		this.furnaceManager = Objects.requireNonNull(furnaceManager);
	}

	@SuppressWarnings("deprecation")
	public NamespacedKey getKey(String string) {
		String[] split = string.split(":");
		return split.length == 1 ? new NamespacedKey(this, string) : new NamespacedKey(split[0], split[1]);
	}

} 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 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 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 class CRCraftingIngredient<I extends RecipeItemStack> implements CraftingIngredient, NBTSerializable {

	protected final I nmsIngredient;

	public CRCraftingIngredient(I nmsIngredient) {
		this.nmsIngredient = nmsIngredient;
	}

	@Override
	public boolean isIngredient(ItemStack itemStack) {
		return this.nmsIngredient.a(CraftItemStack.asNMSCopy(itemStack));
	}

	@Override
	public NBTTagCompound serializeToNbt() {
		return NBTUtil.serializeRecipeItemStack(nmsIngredient);
	}
	
	@Override
	public Map<String, Object> serialize() {
		return NBTSerializable.super.serialize();
	}

	public static CRChoiceIngredient asBukkitIngredient(RecipeItemStack ingredient) {
	    //TODO can we handle subclasses of RecipeItemStack?
		return ingredient == RecipeItemStack.a ? CREmptyIngredient.INSTANCE : new CRChoiceIngredient(ingredient);
	}
	
	public static RecipeItemStack asNMSIngredient(ChoiceIngredient ingredient) {
		if (ingredient instanceof CRChoiceIngredient) {
			CRChoiceIngredient crIngredient = (CRChoiceIngredient) ingredient;
			return crIngredient.nmsIngredient;
		}
		
		RecipeItemStack nmsIngredient = asNMSIngredient((CraftingIngredient) ingredient);
		ItemStack[] choices = ingredient.getChoices().stream().map(CraftItemStack::asNMSCopy).toArray(size -> new ItemStack[size]);
		ReflectionUtil.setFinalFieldValue(nmsIngredient, "choices", choices);
		
		return nmsIngredient;
	}

	public static RecipeItemStack asNMSIngredient(CraftingIngredient ingredient) {
		if (ingredient instanceof CRCraftingIngredient) {
			@SuppressWarnings("unchecked")
			CRCraftingIngredient<RecipeItemStack> crIngredient = (CRCraftingIngredient<RecipeItemStack>) ingredient;
			return crIngredient.nmsIngredient;
		}
		
		//fallback
		return new Bukkit2NMSIngredient(ingredient).asNMSIngredient();
	}

} 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 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 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.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.bukkit.NamespacedKey;
import org.bukkit.craftbukkit.v1_12_R1.CraftWorld;
import org.bukkit.craftbukkit.v1_12_R1.inventory.CraftInventoryCrafting;
import org.bukkit.craftbukkit.v1_12_R1.inventory.CraftItemStack;
import org.bukkit.inventory.Recipe;
import com.gmail.jannyboy11.customrecipes.CustomRecipesPlugin;
import com.gmail.jannyboy11.customrecipes.api.crafting.CraftingRecipe;
import com.gmail.jannyboy11.customrecipes.api.crafting.vanilla.ingredient.ChoiceIngredient;
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.impl.crafting.CRCraftingIngredient;
import com.gmail.jannyboy11.customrecipes.impl.crafting.CRCraftingManager;
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.serialize.NBTSerializable;
import com.gmail.jannyboy11.customrecipes.util.NBTUtil;
import net.minecraft.server.v1_12_R1.IInventory;
import net.minecraft.server.v1_12_R1.IRecipe;
import net.minecraft.server.v1_12_R1.InventoryCrafting;
import net.minecraft.server.v1_12_R1.ItemStack;
import net.minecraft.server.v1_12_R1.MinecraftKey;
import net.minecraft.server.v1_12_R1.NBTTagCompound;
import net.minecraft.server.v1_12_R1.NonNullList;
import net.minecraft.server.v1_12_R1.RecipeItemStack;
import net.minecraft.server.v1_12_R1.World;
		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();