package com.ryanmichela.trees.cost;

import java.util.*;
import java.util.Map.Entry;

import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;

public class ItemCost extends Cost {
    private final ItemStack toMatch;
    private final boolean matchData;
    private final boolean matchMeta;
    private int dropped;
    private List<ItemStack> taken = new ArrayList<ItemStack>();

    public ItemCost(Material mat, double quantity) {
        super(quantity);
        this.matchData = false;
        this.matchMeta = false;
        this.toMatch = new ItemStack(mat, (int) quantity, (short) 0);
    }

    public ItemCost(Material mat, short data, double quantity) {
        super(quantity);
        this.matchData = true;
        this.matchMeta = false;
        this.toMatch = new ItemStack(mat, (int) quantity, data);
    }

    public ItemCost(ItemStack stack) {
        super(stack.getAmount());
        this.matchData = stack.getDurability() != 32767;
        this.matchMeta = true;
        this.toMatch = stack.clone();
    }

    public Material getMaterial() {
        return toMatch.getType();
    }

    public short getData() {
        return toMatch.getData().getData();
    }

    @Override
    public boolean isAffordable(Player player) {
        int remainingCheck = (int) getQuantity();
        return getRemaining(remainingCheck, player.getInventory()) <= 0;
    }

    protected int getRemaining(int remainingCheck, Inventory inv) {
        HashMap<Integer, ? extends ItemStack> matchingInvSlots = inv.all(getMaterial());
        for (Entry<Integer, ? extends ItemStack> entry : matchingInvSlots.entrySet()) {
            if (matches(entry.getValue())) {
                remainingCheck -= entry.getValue().getAmount();
                if (remainingCheck <= 0)
                    break;
            }
        }
        return remainingCheck;
    }

    @Override
    public void apply(Player player) {
        if (getQuantity() > 0) {
            chargeItems(player.getInventory());
        } else {
            dropped = addItems(player.getInventory());
            dropExcess(player, dropped);
        }
        player.updateInventory();
    }

    /**
     * Apply this cost to the given player plus zero or more supplementary inventories.
     *
     * @param player the player to give or take items from
     * @param playerFirst if true, then the player's inventory will be modified first
     * @param extraInventories zero or more supplementary inventories to give or take items from
     */
    public void apply(Player player, boolean playerFirst, Inventory... extraInventories) {
        Inventory[] invs = new Inventory[extraInventories.length + 1];
        if (playerFirst) {
            invs[0] = player.getInventory();
            System.arraycopy(extraInventories, 0, invs, 1, extraInventories.length);
        } else {
            System.arraycopy(extraInventories, 0, invs, 0, extraInventories.length);
            invs[extraInventories.length] = player.getInventory();
        }

        if (getQuantity() > 0) {
            chargeItems(invs);
        } else {
            dropped = addItems(invs);
            dropExcess(player, dropped);
        }
    }

    private int addItems(Inventory... inventories) {
        int remaining = (int) -getQuantity();
        for (Inventory inv : inventories) {
            remaining = addToOneInventory(inv, remaining);
            if (remaining == 0) {
                break;
            }
        }
        return remaining;
    }

    private int chargeItems(Inventory... inventories) {
        taken.clear();

        int remaining = (int) getQuantity();

        for (Inventory inv : inventories) {
            remaining = takeFromOneInventory(inv, remaining);
            if (remaining == 0) {
                break;
            }
        }
        return remaining;
    }

    private int addToOneInventory(Inventory inventory, int quantity) {
        int maxStackSize = inventory.getMaxStackSize();

        while (quantity > maxStackSize) {
            Map<Integer, ItemStack> toDrop = inventory.addItem(new ItemStack(getMaterial(), maxStackSize, getData()));
            if (!toDrop.isEmpty()) {
                // this inventory is full; return the number of items that could not be added
                return toDrop.get(0).getAmount() + (quantity - maxStackSize);
            }
            quantity -= maxStackSize;
        }
        Map<Integer, ItemStack> toDrop = inventory.addItem(new ItemStack(getMaterial(), quantity, getData()));
        return toDrop.isEmpty() ? 0 : toDrop.get(0).getAmount();
    }

    private int takeFromOneInventory(Inventory inventory, int quantity) {
        HashMap<Integer, ? extends ItemStack> matchingInvSlots = inventory.all(getMaterial());

        for (Entry<Integer, ? extends ItemStack> entry : matchingInvSlots.entrySet()) {
            if (matches(entry.getValue())) {
                quantity -= entry.getValue().getAmount();
                if (quantity < 0) {
                    entry.getValue().setAmount(-quantity);
                    taken.add(entry.getValue().clone());
                    break;
                } else {
                    inventory.removeItem(entry.getValue());
                    taken.add(entry.getValue().clone());
                }
                if (quantity == 0) {
                    break;
                }
            }
        }
        return quantity;
    }

    protected boolean matches(ItemStack stack) {
        if (toMatch.getType() != stack.getType()) {
            return false;
        } else if (matchData && toMatch.getData().getData() != stack.getData().getData()) {
            return false;
        } else if (matchMeta) {
            String d1 = stack.hasItemMeta() ? stack.getItemMeta().getDisplayName() : null;
            String d2 = toMatch.hasItemMeta() ? toMatch.getItemMeta().getDisplayName() : null;
            if (d1 != null && !d1.equals(d2)) {
                return false;
            } else if (d2 != null && !d2.equals(d1)) {
                return false;
            }
            return true;
        } else {
            return true;
        }
    }

    private void dropExcess(Player player, int nToDrop) {
        while (nToDrop > 0) {
            ItemStack stack = new ItemStack(getMaterial(), Math.min(nToDrop, getMaterial().getMaxStackSize()), getData());
            player.getWorld().dropItemNaturally(player.getLocation(), stack);
            nToDrop -= getMaterial().getMaxStackSize();
        }
    }
}