package net.knarcraft.blacksmith.util;

import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Recipe;
import org.bukkit.inventory.ShapedRecipe;
import org.bukkit.inventory.ShapelessRecipe;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

/**
 * A helper class for deciding the salvage returned if salvaging an item
 */
public final class SalvageHelper {

    private static final Random random = new Random();

    /**
     * Gets the sum of all enchantment levels for the given item
     *
     * @param item <p>The item to check</p>
     * @return <p>The total amount of enchantment levels on the item</p>
     */
    public static int getTotalEnchantmentLevels(@NotNull ItemStack item) {
        int expLevels = 0;
        for (Enchantment enchantment : item.getEnchantments().keySet()) {
            expLevels += item.getEnchantmentLevel(enchantment);
        }
        return expLevels;
    }

    /**
     * Gets the items to return if salvaging the given item stack
     *
     * <p>Note: Only items craft-able in a crafting table are salvageable. Netherite gear is therefore not salvageable.</p>
     *
     * @param server         <p>The server to get recipes from</p>
     * @param salvagedItem   <p>The item stack to salvage</p>
     * @param ignoredSalvage <p>Any material which should not be returned as part of the salvage.</p>
     * @param extended       <p>Whether to enable extended salvage, ignoring the repairable restriction</p>
     * @return <p>The items to return to the user, or null if not salvageable</p>
     */
    public static @Nullable List<ItemStack> getSalvage(@NotNull Server server, @Nullable ItemStack salvagedItem,
                                                       @NotNull Collection<Material> ignoredSalvage, boolean extended) {
        if (salvagedItem == null || salvagedItem.getAmount() < 1 ||
                (!extended && !ItemHelper.isRepairable(salvagedItem))) {
            return null;
        }

        for (Recipe recipe : server.getRecipesFor(new ItemStack(salvagedItem.getType(), salvagedItem.getAmount()))) {
            if (recipe instanceof ShapedRecipe || recipe instanceof ShapelessRecipe) {
                List<ItemStack> salvage = getRecipeSalvage(recipe, salvagedItem, ignoredSalvage);
                if (salvage != null && !salvage.isEmpty()) {
                    return salvage;
                }
            }
        }

        return null;
    }

    /**
     * Gets the salvage resulting from the given recipe and the given item
     *
     * @param recipe         <p>The recipe to get salvage for</p>
     * @param salvagedItem   <p>The item to be salvaged</p>
     * @param ignoredSalvage <p>Any material which should not be returned as part of the salvage.</p>
     * @return <p>A list of items, or null if not a valid type of recipe</p>
     */
    private static @Nullable List<ItemStack> getRecipeSalvage(@NotNull Recipe recipe, @NotNull ItemStack salvagedItem,
                                                              @NotNull Collection<Material> ignoredSalvage) {
        List<ItemStack> ingredients;
        if (recipe instanceof ShapedRecipe shapedRecipe) {
            ingredients = getIngredients(shapedRecipe);
        } else if (recipe instanceof ShapelessRecipe shapelessRecipe) {
            ingredients = shapelessRecipe.getIngredientList();
        } else {
            //Recipes other than crafting shouldn't be considered for salvaging
            return null;
        }
        //Make things easier by eliminating identical stacks
        ingredients = combineStacks(ingredients);

        //Purge any ignored salvage to only calculate salvage using the remaining items
        ingredients.removeIf((item) -> ignoredSalvage.contains(item.getType()));

        return combineStacks(getSalvage(copyItems(ingredients), salvagedItem));
    }

    /**
     * Copies a list of items
     *
     * <p>Note: This does not copy any metadata. It only copies the item type and the amount.</p>
     *
     * @param itemsToCopy <p>The items to make a copy of</p>
     * @return <p>A copy of the given items</p>
     */
    private static @NotNull List<ItemStack> copyItems(@NotNull List<ItemStack> itemsToCopy) {
        List<ItemStack> copies = new ArrayList<>(itemsToCopy.size());
        for (ItemStack itemStack : itemsToCopy) {
            copies.add(new ItemStack(itemStack.getType(), itemStack.getAmount()));
        }
        return copies;
    }

    /**
     * Gets the salvage resulting from the given item, and the given recipe items
     *
     * @param recipeItems  <p>All items required for crafting the item to salvage</p>
     * @param salvagedItem <p>The item to be salvaged</p>
     * @return <p>The items to be returned to the user as salvage</p>
     */
    @NotNull
    private static List<ItemStack> getSalvage(@NotNull List<ItemStack> recipeItems,
                                              @NotNull ItemStack salvagedItem) {
        int durability = ItemHelper.getDurability(salvagedItem);
        int maxDurability = ItemHelper.getMaxDurability(salvagedItem);

        // Prevent divide by zero for items that don't have a set max durability
        if (maxDurability == 0) {
            maxDurability = 1;
            durability = 1;
        }

        double percentageRemaining = (double) durability / maxDurability;

        int totalItems = totalItemAmount(recipeItems);
        //Get the amount of recipe items to be returned
        int itemsToReturn = (int) Math.floor(percentageRemaining * totalItems);
        int bound = recipeItems.size();

        List<ItemStack> salvage = new ArrayList<>();
        for (int i = 0; i < itemsToReturn; i++) {
            int itemIndex = SalvageHelper.random.nextInt(bound);
            ItemStack itemStack = recipeItems.get(itemIndex);

            //Make sure to never give more of one item than the amount which exists in the recipe
            if (itemStack.getAmount() <= 0) {
                i--;
                continue;
            }
            itemStack.setAmount(itemStack.getAmount() - 1);

            salvage.add(new ItemStack(itemStack.getType(), 1));
        }
        return salvage;
    }

    /**
     * Gets the total sum of items in the given list of items
     *
     * @param items <p>The items to get the sum of</p>
     * @return <p>The total number of items</p>
     */
    private static int totalItemAmount(@NotNull List<ItemStack> items) {
        int sum = 0;
        for (ItemStack itemStack : items) {
            sum += itemStack.getAmount();
        }
        return sum;
    }

    /**
     * Combines all items of the same type in the given list
     *
     * <p>Basically, if the input is two item stacks containing one diamond each, the output will be an item stack with
     * two diamonds instead.</p>
     *
     * @param items <p>The items to combine</p>
     * @return <p>The given items, but combined</p>
     */
    private static @NotNull List<ItemStack> combineStacks(@NotNull List<ItemStack> items) {
        Map<Material, Integer> itemAmounts = new HashMap<>();
        for (ItemStack item : items) {
            Material itemType = item.getType();
            itemAmounts.put(itemType, itemAmounts.getOrDefault(itemType, 0) + 1);
        }

        List<ItemStack> combined = new ArrayList<>();
        for (Material material : itemAmounts.keySet()) {
            combined.add(new ItemStack(material, itemAmounts.get(material)));
        }
        return combined;
    }

    /**
     * Gets all ingredients contained in the given shaped recipe
     *
     * @param shapedRecipe <p>The shaped recipe to get ingredients for</p>
     * @return <p>The items contained in the recipe</p>
     */
    @NotNull
    private static List<ItemStack> getIngredients(@NotNull ShapedRecipe shapedRecipe) {
        List<ItemStack> ingredients = new ArrayList<>();
        Map<Character, ItemStack> ingredientMap = shapedRecipe.getIngredientMap();
        //The shape is a list of the three rows' strings. Each string contains 3 characters.
        String[] shape = shapedRecipe.getShape();
        for (String row : shape) {
            for (int column = 0; column < row.length(); column++) {
                ItemStack item = ingredientMap.get(row.charAt(column));
                if (item != null && item.getType() != Material.AIR && item.getAmount() > 0) {
                    ingredients.add(item);
                }
            }
        }
        return ingredients;
    }

}