package joshie.harvest.cooking.recipe;
import com.google.common.collect.Lists;
import gnu.trove.map.TObjectIntMap;
import gnu.trove.map.hash.TObjectIntHashMap;
import joshie.harvest.api.cooking.Ingredient;
import joshie.harvest.api.cooking.IngredientStack;
import joshie.harvest.api.cooking.Recipe;
import joshie.harvest.core.helpers.StackHelper;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import java.util.*;
import java.util.stream.Collectors;
import static joshie.harvest.cooking.HFCooking.COOKING_SELL_MODIFIER;
import static joshie.harvest.core.registry.ShippingRegistry.SELL_VALUE;
public class RecipeBuilder {
public static final String FOOD_LEVEL = "FoodLevel";
public static final String SATURATION_LEVEL = "FoodSaturation";
private List<IngredientStack> required;
private List<IngredientStack> optional;
private int hunger = 0;
private float saturation;
private int stackSize;
private long cost;
public List<ItemStack> build(Recipe recipe, List<IngredientStack> ingredients) {
//We can and will end up with recipes in the required and optional list
required = toRequired.toList(recipe, ingredients);
optional = toOptional.toList(recipe, ingredients);
stackSize = 1; //Always create one recipe at least
//We first always make one required item, and add all the optionals to it, as we already know we CAN make one item
Set<Ingredient> removed = new HashSet<>();
Set<IngredientStack> toRemove = new HashSet<>();
recipe.getRequired().stream().filter(stack -> !removed.contains(stack.getIngredient())).forEachOrdered(stack -> {
removed.add(stack.getIngredient());
toRemove.add(stack);
});
toRemove.forEach(this::removeFromList);
//The next step is calculate the additional hunger, and saturation from the optional list
calculateHungerSaturationAndRemoveOptionals(recipe, new ArrayList<>(optional));
//Now that we pretty much only have the spare items left in the required list
//We need to work out how many items total we can make, as well as calculating any extra
//To add to the last item in our stack
calculateStackSizeBasedOnRequiredListAndRemainingItems(recipe.getRequired());
calculateActualHungerAndSaturationValues(recipe);
calculateCostsBasedOnEverything(recipe);
//We now know exactly what the stack size will be, as well as exactly how many items we have left!
//We know this, as it will be the items remaining in the required
//If we have stuff to calculate then do so, otherwise return everything as is
if (required.size() == 0 || !recipe.supportsNBTData()) return build(recipe.getStack(), recipe.supportsNBTData());
else {
//Now we need to work out the additional stats for the last bit of food
List<ItemStack> ret = new ArrayList<>();
if (stackSize > 1) ret.add(setFoodStats(StackHelper.toStack(recipe.getStack(), stackSize - 1), hunger, saturation, cost));
//Added the basic recipes, now we need to work out the final oddball stack to add
float multiplier = 1F;
for (int i = 1; i <= required.size(); i++) {
multiplier += 0.5F / i;
}
//Now that we calulcated the multiplier let's adjust the values
hunger = (int)Math.floor((double)hunger * multiplier);
saturation = saturation * multiplier;
ret.add(setFoodStats(recipe.getStack(), hunger, saturation, cost));
return ret;
}
}
private List<ItemStack> build(ItemStack basicStack, boolean supportsNBTData) {
if (!supportsNBTData) return Lists.newArrayList(StackHelper.toStack(basicStack, basicStack.stackSize * stackSize));
else return Lists.newArrayList(setFoodStats(StackHelper.toStack(basicStack, stackSize), hunger, saturation, cost));
}
@SuppressWarnings("ConstantConditions")
public static ItemStack setFoodStats(ItemStack stack, int hunger, float saturation, long cost) {
if (!stack.hasTagCompound()) stack.setTagCompound(new NBTTagCompound());
NBTTagCompound tag = stack.getTagCompound();
tag.setInteger(FOOD_LEVEL, hunger);
tag.setFloat(SATURATION_LEVEL, saturation);
tag.setLong(SELL_VALUE, cost);
return stack;
}
private void calculateCostsBasedOnEverything(Recipe recipe) {
long sell = 0L;
for (IngredientStack stack: recipe.getRequired()) {
if (stack.getIngredient() == HFIngredients.BREAD) sell += 50L;
else {
long internal = stack.getIngredient().getSellValue();
if (internal == 0) {
for (Ingredient ingredient : stack.getIngredient().getEquivalents()) {
if (internal == 0 || (ingredient.getSellValue() != 0 && ingredient.getSellValue() < internal)) {
internal = ingredient.getSellValue();
}
}
sell += internal;
} else sell += stack.getIngredient().getSellValue();
}
}
this.cost = (long)(sell * COOKING_SELL_MODIFIER);
}
private void calculateActualHungerAndSaturationValues(Recipe recipe) {
//Add the leftover required ingredients
if (required.size() > 0) {
TObjectIntMap<Ingredient> added = new TObjectIntHashMap<>();
for (IngredientStack stack: required) {
Ingredient main = stack.getIngredient();
//If we haven't already added this ingredient, use the full value
if (!added.containsKey(main)) {
hunger += (main.getHunger() / 2);
saturation += (main.getSaturation() / 2);
added.put(main, 0);
} else {
//If we have already used this ingredient, let's get how many times we have
int used = added.adjustOrPutValue(main, 1, 1);
hunger += (((double)main.getHunger())/ (4 * used));
saturation += ((main.getSaturation())/ (4 * used));
//We're added less and less each time to hunger and saturation for each ingredient
}
}
}
this.hunger = recipe.getHunger() + (int)((double)this.hunger / stackSize);
this.saturation = recipe.getSaturation() + (this.saturation / stackSize);
}
private void calculateStackSizeBasedOnRequiredListAndRemainingItems(List<IngredientStack> required) {
int totalRecipesMade = 0;
for (int i = 0; i < 64; i++) {
if (areAllRequiredInRecipeAndRemove(required)) {
totalRecipesMade++;
} else break;
}
this.stackSize += totalRecipesMade;
}
private void removeFromList(IngredientStack required) {
//Search through the required set, and remove the first entry we find that matches
int removed = 0;
Iterator<IngredientStack> it = this.required.iterator();
while(it.hasNext()) {
if (required.isSame(it.next())) {
it.remove();
removed++;
if (removed >= required.getStackSize()) {
break;
}
}
}
}
private boolean areAllRequiredInRecipeAndRemove(List<IngredientStack> requiredSet) {
//We have looped through the set, so we should now go through and remove from the required again
for (IngredientStack required: requiredSet) {
if (!required.isSame(this.required)) return false;
}
//Removed and cleared up the set so we carry on
requiredSet.forEach(this::removeFromList);
return true;
}
private void calculateHungerSaturationAndRemoveOptionals(Recipe recipe, List<IngredientStack> optionals) {
float hunger = 0;
float saturation = 0F;
TObjectIntMap<Ingredient> added = new TObjectIntHashMap<>();
for (IngredientStack stack: optionals) {
Ingredient main = stack.getIngredient();
//If we haven't already added this ingredient, use the full value
if (!added.containsKey(main) || stack.isSame(required)) {
hunger += main.getHunger();
saturation += main.getSaturation();
added.put(main, 0);
} else {
//If we have already used this ingredient, let's get how many times we have
int used = added.adjustOrPutValue(main, 1, 1);
hunger += (((double)main.getHunger())/ (4 * used));
saturation += ((main.getSaturation())/ (4 * used));
//We're added less and less each time to hunger and saturation for each ingredient
}
if (added.size() >= recipe.getMaximumOptionalIngredients()) break;
}
this.optional.clear();
this.hunger = (int) Math.floor(hunger);
this.saturation = saturation;
}
private abstract class ListBuilder {
List<IngredientStack> toList(Recipe recipe, List<IngredientStack> ingredients) {
List<IngredientStack> list = new ArrayList<>();
for (IngredientStack required: getList(recipe)) {
//Search through the required set, and remove the first entry we find that matches
list.addAll(ingredients.stream().filter(required::isSame).collect(Collectors.toList()));
}
return list;
}
protected abstract List<IngredientStack> getList(Recipe recipe);
}
private final ListBuilder toRequired = new ListBuilder() {
@Override
protected List<IngredientStack> getList(Recipe recipe) {
return recipe.getRequired();
}
};
private final ListBuilder toOptional = new ListBuilder() {
@Override
protected List<IngredientStack> getList(Recipe recipe) {
return recipe.getOptional();
}
};
}