package com.earth2me.essentials.craftbukkit;

import com.earth2me.essentials.utils.MaterialUtil;
import com.earth2me.essentials.utils.VersionUtil;
import org.bukkit.entity.Player;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

public final class Inventories {
    private static final int HELM_SLOT = 39;
    private static final int CHEST_SLOT = 38;
    private static final int LEG_SLOT = 37;
    private static final int BOOT_SLOT = 36;
    private static final boolean HAS_OFFHAND = VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_9_R01);
    private static final int INVENTORY_SIZE = HAS_OFFHAND ? 41 : 40;

    private Inventories() {
    }

    public static ItemStack getItemInHand(final Player player) {
        if (!HAS_OFFHAND) {
            //noinspection deprecation
            return player.getInventory().getItemInHand();
        }
        final PlayerInventory inventory = player.getInventory();
        final ItemStack main = inventory.getItemInMainHand();
        return !isEmpty(main) ? main : inventory.getItemInOffHand();
    }

    public static ItemStack getItemInMainHand(final Player player) {
        if (!HAS_OFFHAND) {
            //noinspection deprecation
            return player.getInventory().getItemInHand();
        }
        return player.getInventory().getItemInMainHand();
    }

    public static void setItemInMainHand(final Player player, final ItemStack stack) {
        if (HAS_OFFHAND) {
            player.getInventory().setItemInMainHand(stack);
        } else {
            //noinspection deprecation
            player.setItemInHand(stack);
        }
    }

    public static void setItemInMainHand(final EntityEquipment entityEquipment, final ItemStack stack) {
        if (HAS_OFFHAND) {
            entityEquipment.setItemInMainHand(stack);
        } else {
            //noinspection deprecation
            entityEquipment.setItemInHand(stack);
        }
    }

    public static void setItemInMainHandDropChance(final EntityEquipment entityEquipment, final float chance) {
        if (HAS_OFFHAND) {
            entityEquipment.setItemInMainHandDropChance(chance);
        } else {
            //noinspection deprecation
            entityEquipment.setItemInHandDropChance(chance);
        }
    }

    public static boolean containsAtLeast(final Player player, final ItemStack item, int amount) {
        for (final ItemStack invItem : player.getInventory().getContents()) {
            if (isEmpty(invItem)) {
                continue;
            }
            if (invItem.isSimilar(item)) {
                amount -= invItem.getAmount();
                if (amount <= 0) {
                    return true;
                }
            }
        }
        return false;
    }

    public static boolean hasSpace(final Player player, final int maxStack, final boolean includeArmor, ItemStack... items) {
        items = normalizeItems(cloneItems(items));
        final InventoryData inventoryData = parseInventoryData(player.getInventory(), items, maxStack, includeArmor);

        final List<Integer> emptySlots = inventoryData.getEmptySlots();
        for (final ItemStack item : items) {
            if (isEmpty(item)) {
                continue;
            }

            final int itemMax = Math.max(maxStack, item.getMaxStackSize());
            final List<Integer> partialSlots = inventoryData.getPartialSlots().get(item);
            while (true) {
                if (partialSlots == null || partialSlots.isEmpty()) {
                    if (emptySlots.isEmpty()) {
                        return false;
                    }

                    emptySlots.remove(0);
                    if (item.getAmount() > itemMax) {
                        item.setAmount(item.getAmount() - itemMax);
                    } else {
                        break;
                    }
                } else {
                    final int slot = partialSlots.remove(0);
                    ItemStack existing = player.getInventory().getItem(slot);
                    if (isEmpty(existing)) {
                        existing = item.clone();
                        existing.setAmount(0);
                    }

                    final int amount = item.getAmount();
                    final int existingAmount = existing.getAmount();

                    if (amount + existingAmount <= itemMax) {
                        break;
                    } else {
                        item.setAmount(amount + existingAmount - itemMax);
                    }
                }
            }
        }

        return true;
    }

    public static Map<Integer, ItemStack> addItem(final Player player, final ItemStack... items) {
        return addItem(player, 0, false, items);
    }

    public static Map<Integer, ItemStack> addItem(final Player player, final int maxStack, final ItemStack... items) {
        return addItem(player, maxStack, false, items);
    }

    public static Map<Integer, ItemStack> addItem(final Player player, final int maxStack, final boolean allowArmor, ItemStack... items) {
        items = normalizeItems(cloneItems(items));
        final Map<Integer, ItemStack> leftover = new HashMap<>();
        final InventoryData inventoryData = parseInventoryData(player.getInventory(), items, maxStack, allowArmor);

        final List<Integer> emptySlots = inventoryData.getEmptySlots();
        for (int i = 0; i < items.length; i++) {
            final ItemStack item = items[i];
            if (isEmpty(item)) {
                continue;
            }

            final int itemMax = Math.max(maxStack, item.getMaxStackSize());
            final List<Integer> partialSlots = inventoryData.getPartialSlots().get(item);
            while (true) {
                if (partialSlots == null || partialSlots.isEmpty()) {
                    if (emptySlots.isEmpty()) {
                        leftover.put(i, item);
                        break;
                    }

                    final int slot = emptySlots.remove(0);
                    if (item.getAmount() > itemMax) {
                        final ItemStack split = item.clone();
                        split.setAmount(itemMax);
                        player.getInventory().setItem(slot, split);
                        item.setAmount(item.getAmount() - itemMax);
                    } else {
                        player.getInventory().setItem(slot, item);
                        break;
                    }
                } else {
                    final int slot = partialSlots.remove(0);
                    ItemStack existing = player.getInventory().getItem(slot);
                    if (isEmpty(existing)) {
                        existing = item.clone();
                        existing.setAmount(0);
                    }

                    final int amount = item.getAmount();
                    final int existingAmount = existing.getAmount();

                    if (amount + existingAmount <= itemMax) {
                        existing.setAmount(amount + existingAmount);
                        player.getInventory().setItem(slot, existing);
                        break;
                    } else {
                        existing.setAmount(itemMax);
                        player.getInventory().setItem(slot, existing);
                        item.setAmount(amount + existingAmount - itemMax);
                    }
                }
            }
        }

        return leftover;
    }

    public static ItemStack[] getInventory(final Player player, final boolean includeArmor) {
        final ItemStack[] items = new ItemStack[INVENTORY_SIZE];
        for (int i = 0; i < items.length; i++) {
            if (!includeArmor && isArmorSlot(i)) {
                items[i] = null;
                continue;
            }

            items[i] = player.getInventory().getItem(i);
        }

        return items;
    }

    public static void removeItemExact(final Player player, final ItemStack toRemove, final boolean includeArmor) {
        removeItems(player, itemStack -> itemStack.equals(toRemove), includeArmor);
    }

    public static int removeItemSimilar(final Player player, final ItemStack toRemove, final boolean includeArmor) {
        return removeItems(player, itemStack -> itemStack.isSimilar(toRemove), includeArmor);
    }

    public static int removeItems(final Player player, final Predicate<ItemStack> removePredicate, final boolean includeArmor) {
        int removedAmount = 0;
        final ItemStack[] items = player.getInventory().getContents();
        for (int i = 0; i < items.length; i++) {
            if (!includeArmor && isArmorSlot(i)) {
                continue;
            }

            final ItemStack item = items[i];
            if (isEmpty(item)) {
                continue;
            }

            if (removePredicate.test(item)) {
                removedAmount += item.getAmount();
                item.setAmount(0);
                player.getInventory().setItem(i, item);
            }
        }
        return removedAmount;
    }

    public static boolean removeItemAmount(final Player player, final ItemStack toRemove, int amount) {
        final List<Integer> clearSlots = new ArrayList<>();
        final ItemStack[] items = player.getInventory().getContents();

        for (int i = 0; i < items.length; i++) {
            final ItemStack item = items[i];
            if (isEmpty(item)) {
                continue;
            }

            if (item.isSimilar(toRemove)) {
                if (item.getAmount() >= amount) {
                    item.setAmount(item.getAmount() - amount);
                    player.getInventory().setItem(i, item);
                    for (final int slot : clearSlots) {
                        clearSlot(player, slot);
                    }
                    return true;
                } else {
                    amount -= item.getAmount();
                    clearSlots.add(i);
                }

                if (amount == 0) {
                    for (final int slot : clearSlots) {
                        clearSlot(player, slot);
                    }
                    return true;
                }
            }
        }
        return false;
    }

    public static void clearSlot(final Player player, final int slot) {
        final ItemStack item = player.getInventory().getItem(slot);
        if (!isEmpty(item)) {
            item.setAmount(0);
            player.getInventory().setItem(slot, item);
        }
    }

    public static void setSlot(final Player inventory, final int slot, final ItemStack item) {
        inventory.getInventory().setItem(slot, item);
    }

    private static ItemStack[] normalizeItems(final ItemStack[] items) {
        if (items.length <= 1) {
            return items;
        }

        final ItemStack[] normalizedItems = new ItemStack[items.length];
        int nextNormalizedIndex = 0;
        inputLoop:
        for (final ItemStack item : items) {
            if (isEmpty(item)) {
                continue;
            }

            for (int j = 0; j < nextNormalizedIndex; j++) {
                final ItemStack normalizedItem = normalizedItems[j];
                if (isEmpty(normalizedItem)) {
                    continue;
                }

                if (item.isSimilar(normalizedItem)) {
                    normalizedItem.setAmount(normalizedItem.getAmount() + item.getAmount());
                    continue inputLoop;
                }
            }
            normalizedItems[nextNormalizedIndex++] = item;
        }

        return normalizedItems;
    }

    private static ItemStack[] cloneItems(final ItemStack[] items) {
        final ItemStack[] clonedItems = new ItemStack[items.length];
        for (int i = 0; i < items.length; i++) {
            final ItemStack item = items[i];
            if (isEmpty(item)) {
                continue;
            }

            clonedItems[i] = item.clone();
        }

        return clonedItems;
    }

    private static InventoryData parseInventoryData(final Inventory inventory, final ItemStack[] items, final int maxStack, final boolean includeArmor) {
        final ItemStack[] inventoryContents = inventory.getContents();
        final List<Integer> emptySlots = new ArrayList<>();
        final HashMap<ItemStack, List<Integer>> partialSlots = new HashMap<>();

        for (int i = 0; i < inventoryContents.length; i++) {
            if (!includeArmor && isArmorSlot(i)) {
                continue;
            }

            final ItemStack invItem = inventoryContents[i];
            if (isEmpty(invItem)) {
                emptySlots.add(i);
            } else {
                for (final ItemStack newItem : items) {
                    if (invItem.getAmount() < Math.max(maxStack, invItem.getMaxStackSize()) && invItem.isSimilar(newItem)) {
                        partialSlots.computeIfAbsent(newItem, k -> new ArrayList<>()).add(i);
                    }
                }
            }
        }

        // Convert empty armor slots to partial slots if we have armor items in the inventory, otherwise remove them from the empty slots.
        if (includeArmor) {
            ItemStack helm = null;
            ItemStack chest = null;
            ItemStack legs = null;
            ItemStack boots = null;
            for (final ItemStack item : items) {
                if (isEmpty(item)) {
                    continue;
                }
                if (helm == null && MaterialUtil.isHelmet(item.getType())) {
                    helm = item;
                    if (emptySlots.contains(HELM_SLOT)) {
                        partialSlots.computeIfAbsent(helm, k -> new ArrayList<>()).add(HELM_SLOT);
                    }
                } else if (chest == null && MaterialUtil.isChestplate(item.getType())) {
                    chest = item;
                    if (emptySlots.contains(CHEST_SLOT)) {
                        partialSlots.computeIfAbsent(chest, k -> new ArrayList<>()).add(CHEST_SLOT);
                    }
                } else if (legs == null && MaterialUtil.isLeggings(item.getType())) {
                    legs = item;
                    if (emptySlots.contains(LEG_SLOT)) {
                        partialSlots.computeIfAbsent(legs, k -> new ArrayList<>()).add(LEG_SLOT);
                    }
                } else if (boots == null && MaterialUtil.isBoots(item.getType())) {
                    boots = item;
                    if (emptySlots.contains(BOOT_SLOT)) {
                        partialSlots.computeIfAbsent(boots, k -> new ArrayList<>()).add(BOOT_SLOT);
                    }
                }
            }
            emptySlots.remove((Object) HELM_SLOT);
            emptySlots.remove((Object) CHEST_SLOT);
            emptySlots.remove((Object) LEG_SLOT);
            emptySlots.remove((Object) BOOT_SLOT);
        }

        return new InventoryData(emptySlots, partialSlots);
    }

    private static boolean isEmpty(final ItemStack stack) {
        return stack == null || MaterialUtil.isAir(stack.getType());
    }

    private static boolean isArmorSlot(final int slot) {
        return slot == HELM_SLOT || slot == CHEST_SLOT || slot == LEG_SLOT || slot == BOOT_SLOT;
    }
}
