package tc.oc.pgm.listeners;

import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.inventory.InventoryPickupItemEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerPickupItemEvent;
import org.bukkit.inventory.*;
import tc.oc.commons.bukkit.inventory.InventorySlot;
import tc.oc.pgm.PGM;
import tc.oc.pgm.events.ItemTransferEvent;
import tc.oc.pgm.events.PlayerItemTransferEvent;

import java.util.Map;
import java.util.Optional;

public class ItemTransferListener implements Listener {
    // Track players dropping an item stack from within an inventory GUI
    private boolean ignoreNextDropEvent;
    private boolean collectToCursor;

    private Inventory getLocalInventory(InventoryView view, int rawSlot) {
        int cookedSlot = view.convertSlot(rawSlot);
        if(cookedSlot == rawSlot) {
            return view.getTopInventory();
        } else {
            return view.getBottomInventory();
        }
    }

    private static Inventory getOtherInventory(InventoryView view, Inventory inventory) {
        if(view.getTopInventory() == inventory) {
            return view.getBottomInventory();
        } else {
            return view.getTopInventory();
        }
    }

    private static int getQuantityPlaceable(ItemStack stack, Inventory inventory) {
        int transferrable = 0;
        for(ItemStack slotStack : inventory.contents()) {
            if(slotStack == null) {
                return stack.getAmount();
            } else if(slotStack.isSimilar(stack)) {
                transferrable += stack.getMaxStackSize() - slotStack.getAmount();
                if(transferrable >= stack.getAmount()) {
                    return stack.getAmount();
                }
            }
        }
        return transferrable;
    }

    private void dropFromPlayer(Player player, ItemStack stack) {
        Item entity = player.getWorld().dropItem(player.getEyeLocation(), stack);
        entity.setVelocity(player.getLocation().getDirection().multiply(0.3));
    }

    private void callEvent(ItemTransferEvent event) {
        Bukkit.getPluginManager().callEvent(event);
    }

    @EventHandler
    public void onPlayerPickupItem(PlayerPickupItemEvent event) {
        // When this event is fired, the ItemStack in the Item being picked up is temporarily
        // set to the amount that will actually be picked up, while the difference from the
        // actual amount in the stack is available from getRemaining(). When the event returns,
        // the original amount is restored to the stack, meaning that we can't change the amount
        // from inside the event, so instead we replace the entire stack.

        int initialQuantity = event.getItem().getItemStack().getAmount();
        PlayerItemTransferEvent transferEvent = new PlayerItemTransferEvent(
            event, ItemTransferEvent.Type.PICKUP, event.getPlayer(),
            Optional.empty(),
            Optional.of(new InventorySlot<>(event.getPlayer().getInventory())),
            event.getItem().getItemStack(), event.getItem(),
            initialQuantity, event.getPlayer().getOpenInventory().getCursor()
        );

        this.callEvent(transferEvent);

        int quantity = Math.min(transferEvent.getQuantity(), initialQuantity);

        if(!event.isCancelled() && quantity < initialQuantity) {
            event.setCancelled(true);
            if(quantity > 0) {
                ItemStack stack = event.getItem().getItemStack().clone();
                stack.setAmount(stack.getAmount() - quantity);
                event.getItem().setItemStack(stack);

                stack = stack.clone();
                stack.setAmount(quantity);
                event.getPlayer().getInventory().addItem(stack);
                event.getPlayer().playSound(event.getPlayer().getLocation(), Sound.ENTITY_ITEM_PICKUP, 1, 1);
            }
        }
    }

    @EventHandler
    public void onBlockPickupItem(InventoryPickupItemEvent event) {
        int initialQuantity = getQuantityPlaceable(event.getItem().getItemStack(), event.getInventory());
        ItemTransferEvent transferEvent = new ItemTransferEvent(
            event, ItemTransferEvent.Type.PICKUP,
            Optional.empty(),
            Optional.of(new InventorySlot<>(event.getInventory())),
            event.getItem().getItemStack(), event.getItem(), initialQuantity
        );

        this.callEvent(transferEvent);

        if(initialQuantity != transferEvent.getQuantity() && !event.isCancelled()) {
            event.setCancelled(true);
            ItemStack stack = event.getItem().getItemStack();
            stack.setAmount(stack.getAmount() - transferEvent.getQuantity());
            stack = stack.clone();
            stack.setAmount(transferEvent.getQuantity());
            event.getInventory().addItem(stack);
        }
    }

    @EventHandler
    public void onPlayerClickInventory(InventoryClickEvent event) {
        // Ignored actions
        switch(event.getAction()) {
            case CLONE_STACK:         // Out of scope
            case COLLECT_TO_CURSOR:   // Handled by InventoryClickedEvent
            case NOTHING:
            case UNKNOWN:
                return;
        }

        // Get the player who clicked
        if(!(event.getWhoClicked() instanceof Player)) {
            // Can this happen?
            return;
        }
        Player player = (Player) event.getWhoClicked();

        // In a dual-inventory view, InventoryClickEvent.getInventory() always returns the top inventory,
        // so to figure out which one was actually clicked, we compare the view slot with the inv slot.
        // If they are the same, the click is in the top inv, because it is always mapped to view slot 0.
        // Otherwise, the click is in the bottom inv. This is the Bukkit recommended way to do this.
        Inventory inventory = getLocalInventory(event.getView(), event.getRawSlot());

        if(event.getAction() == InventoryAction.SWAP_WITH_CURSOR) {
            // Click on a stack while already holding a stack, fire two events for the stacks being swapped
            if(inventory.getHolder() == player) {
                // Swap with own inventory is not a transfer
                return;
            }
            boolean cancelled = event.isCancelled();
            int quantity = event.getCurrentItem().getAmount();

            // The take event has no items on the cursor, because those will be placed by the second event
            PlayerItemTransferEvent transferEvent = new PlayerItemTransferEvent(
                event, ItemTransferEvent.Type.TAKE, player,
                Optional.of(InventorySlot.fromEvent(event)),
                Optional.empty(),
                event.getCurrentItem(), null,
                quantity, null
            );
            this.callEvent(transferEvent);
            cancelled = cancelled | event.isCancelled() | quantity != transferEvent.getQuantity();

            // Remove the item from the inventory so handlers of the second event can see that it is gone
            ItemStack oldInvStack = event.getCurrentItem();
            event.setCurrentItem(null);

            quantity = event.getCursor().getAmount();
            transferEvent = new PlayerItemTransferEvent(
                event, ItemTransferEvent.Type.PLACE, player,
                Optional.empty(),
                Optional.of(InventorySlot.fromEvent(event)),
                event.getCursor(), null,
                event.getCursor().getAmount(), event.getCursor()
            );
            event.setCancelled(cancelled | event.isCancelled() | quantity != transferEvent.getQuantity());

            // Replace the old item in the inventory
            event.setCurrentItem(oldInvStack);

            return;
        }

        // The remaining actions will generate one event at most
        ItemTransferEvent.Type type;
        Inventory fromInventory = null;
        Integer fromSlot = null;
        Inventory toInventory = null;
        Integer toSlot = null;
        ItemStack itemStack;

        // Determine inv, slot, and stack
        switch(event.getAction()) {
            case PICKUP_ALL:
            case PICKUP_SOME:
            case PICKUP_HALF:
            case PICKUP_ONE:
                if(inventory.getHolder() == player) {
                    // Taking from own inventory is not a transfer
                    return;
                }
                type = ItemTransferEvent.Type.TAKE;
                itemStack = event.getCurrentItem();
                fromInventory = inventory;
                fromSlot = event.getSlot();
                break;

            case PLACE_ALL:
            case PLACE_SOME:
            case PLACE_ONE:
                if(inventory.getHolder() == player) {
                    // Placing in own inventory is not a transfer
                    return;
                }
                type = ItemTransferEvent.Type.PLACE;
                itemStack = event.getCursor();
                toInventory = inventory;
                toSlot = event.getSlot();
                break;

            case DROP_ONE_SLOT:
            case DROP_ALL_SLOT:
                type = ItemTransferEvent.Type.DROP;
                itemStack = event.getCurrentItem();
                fromInventory = inventory;
                fromSlot = event.getSlot();
                break;

            case DROP_ONE_CURSOR:
            case DROP_ALL_CURSOR:
                type = ItemTransferEvent.Type.DROP;
                itemStack = event.getCursor();
                break;

            case MOVE_TO_OTHER_INVENTORY:
                itemStack = event.getCurrentItem();
                fromInventory = inventory;
                fromSlot = event.getSlot();
                toInventory = getOtherInventory(event.getView(), fromInventory);

                if(toInventory == null || fromInventory.getHolder() == toInventory.getHolder()) {
                    // shift-click to hotbar/armor slots
                    return;
                }

                if(fromInventory.getHolder() == player && toInventory.getHolder() != player) {
                    type = ItemTransferEvent.Type.PLACE;
                } else if (fromInventory.getHolder() != player && toInventory.getHolder() == player) {
                    type = ItemTransferEvent.Type.TAKE;
                } else {
                    type = ItemTransferEvent.Type.TRANSFER;
                }
                break;

            case HOTBAR_SWAP:
            case HOTBAR_MOVE_AND_READD:
                // Use a hotkey to move a stack in or out of a hotbar slot. If moving a stack into a
                // hotbar slot that is already occupied by an incompatible stack, that stack will be
                // moved to the slot under the cursor, if that slot is in the player's inventory,
                // otherwise it will be moved to the first available slot in the player's inventory.
                if(inventory.getHolder() == player) {
                    // Ignore intra-inventory swap
                    return;
                }
                if(event.getCurrentItem() != null && event.getCurrentItem().getType() != Material.AIR) {
                    // Moving an item onto the hotbar
                    type = ItemTransferEvent.Type.TAKE;
                    itemStack = event.getCurrentItem();
                    fromInventory = inventory;
                    fromSlot = event.getSlot();
                    toInventory = player.getInventory();
                    toSlot = event.getHotbarButton();
                } else {
                    itemStack = player.getInventory().getItem(event.getHotbarButton());
                    if(itemStack == null || itemStack.getType() == Material.AIR) {
                        return;
                    }
                    // Moving an item out of the hotbar
                    type = ItemTransferEvent.Type.PLACE;
                    fromInventory = player.getInventory();
                    fromSlot = event.getHotbarButton();
                    toInventory = inventory;
                    toSlot = event.getSlot();
                }
                break;

            default:
                PGM.get().getLogger().warning("ItemTransferListener.onPlayerClickItem: Unhandled action " + event.getAction());
                return;
        }

        int initialQuantity = 0;

        // Determine quantity
        switch(event.getAction()) {
            case PICKUP_ALL: // left-click stack with empty cursor
            case DROP_ALL_SLOT: // press control-drop key while hovering over stack
            case HOTBAR_SWAP:
            case HOTBAR_MOVE_AND_READD:
                initialQuantity = event.getCurrentItem().getAmount();
                break;

            case PLACE_ALL: // left-click with cursor stack on empty slot or matching stack with enough space
            case DROP_ALL_CURSOR: // left-click outside of window with cursor stack
                initialQuantity = event.getCursor().getAmount();
                break;

            case PICKUP_SOME: // left/right-click oversized stack with empty cursor
                initialQuantity = Math.min(event.getCurrentItem().getAmount(), event.getCurrentItem().getMaxStackSize());
                break;

            case PLACE_SOME: // left-click with cursor stack on undersized-slot (e.g. beacon) or matching stack without enough space
                initialQuantity = Math.min(event.getCursor().getAmount(), Math.min(event.getCursor().getMaxStackSize(),
                                                                            event.getInventory().getMaxStackSize()));
                ItemStack existingStack = event.getCurrentItem();
                if(existingStack != null) {
                    initialQuantity -= existingStack.getAmount();
                }
                break;

            case PICKUP_HALF: // right-click stack with empty cursor (rounds up)
                initialQuantity = (event.getCurrentItem().getAmount() + 1) / 2;
                break;

            case PICKUP_ONE: // same cause as PICKUP_SOME
            case PLACE_ONE: // right-click with cursor stack on slot/stack with space
            case DROP_ONE_CURSOR: // right-click outside of window with cursor stack
            case DROP_ONE_SLOT: // press drop key while hovering over stack
                initialQuantity = 1;
                break;

            case MOVE_TO_OTHER_INVENTORY: // shift-click in a dual-inventory view
                initialQuantity = getQuantityPlaceable(event.getCurrentItem(), toInventory);
                break;
        }

        if(initialQuantity <= 0) {
            return;
        }

        final Integer finalFromSlot = fromSlot, finalToSlot = toSlot;
        PlayerItemTransferEvent transferEvent = new PlayerItemTransferEvent(event, type, player,
                                                                            Optional.ofNullable(fromInventory).map(inv -> InventorySlot.fromInventoryIndex(inv, finalFromSlot)),
                                                                            Optional.ofNullable(toInventory).map(inv -> InventorySlot.fromInventoryIndex(inv, finalToSlot)),
                                                                            itemStack, null, initialQuantity,
                                                                            event.getCursor());
        this.callEvent(transferEvent);
        int quantity = Math.min(transferEvent.getQuantity(), initialQuantity);

        if(quantity < initialQuantity) {
            event.setCancelled(true);
            if(quantity > 0) {
                ItemStack item;
                ItemStack otherItem;

                switch(event.getAction()) {
                    case PICKUP_ALL:
                    case PICKUP_SOME:
                    case PICKUP_HALF:
                    case PICKUP_ONE:
                        item = event.getCurrentItem();
                        item.setAmount(item.getAmount() - quantity);

                        otherItem = item.clone();
                        otherItem.setAmount(quantity);
                        event.getView().setCursor(otherItem);
                        break;

                    case PLACE_ALL:
                    case PLACE_SOME:
                    case PLACE_ONE:
                        otherItem = event.getCursor();
                        otherItem.setAmount(otherItem.getAmount() - quantity);
                        event.getView().setCursor(otherItem);

                        item = event.getCurrentItem();
                        if(item == null || item.getType() == Material.AIR) {
                            item = otherItem.clone();
                            item.setAmount(quantity);
                            event.setCurrentItem(item);
                            PGM.get().getLogger().info("Placing " + item + " in slot " + event.getRawSlot());
                        } else {
                            item.setAmount(item.getAmount() + quantity);
                        }
                        break;

                    case DROP_ALL_CURSOR:
                    case DROP_ONE_CURSOR:
                        otherItem = event.getCursor();
                        otherItem.setAmount(otherItem.getAmount() - quantity);
                        event.getView().setCursor(otherItem);

                        item = otherItem.clone();
                        item.setAmount(quantity);
                        this.dropFromPlayer(player, item);
                        break;

                    case DROP_ALL_SLOT:
                    case DROP_ONE_SLOT:
                        item = event.getCurrentItem();
                        item.setAmount(item.getAmount() - quantity);

                        item = item.clone();
                        item.setAmount(quantity);
                        this.dropFromPlayer(player, item);
                        break;

                    case MOVE_TO_OTHER_INVENTORY:
                        if(toInventory != null) {
                            item = event.getCurrentItem();
                            item.setAmount(item.getAmount() - quantity);

                            item = item.clone();
                            item.setAmount(quantity);
                            toInventory.addItem(item);
                        }
                        break;

                    case HOTBAR_SWAP:
                    case HOTBAR_MOVE_AND_READD:
                        otherItem = player.getInventory().getItem(event.getHotbarButton());

                        item = event.getCurrentItem();
                        if(item != null && item.getType() != Material.AIR) {
                            // Move item onto hotbar
                            item.setAmount(item.getAmount() - quantity);

                            item = item.clone();
                            item.setAmount(quantity);
                            player.getInventory().setItem(event.getHotbarButton(), item);

                            if(otherItem != null) {
                                player.getInventory().addItem(otherItem);
                            }
                        } else if(otherItem != null && otherItem.getType() != Material.AIR) {
                            // Move item off of hotbar
                            otherItem.setAmount(otherItem.getAmount() - quantity);
                            otherItem = otherItem.clone();
                            otherItem.setAmount(quantity);
                            event.setCurrentItem(otherItem);
                        }
                        break;
                }
            }
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void setIgnoreDropFlag(InventoryClickEvent event) {
        switch(event.getAction()) {
            case DROP_ALL_CURSOR:
            case DROP_ONE_CURSOR:
            case DROP_ALL_SLOT:
            case DROP_ONE_SLOT:
                // Make a note to ignore the PlayerDropItemEvent that will follow this one
                this.ignoreNextDropEvent = true;
                break;
        }
    }

    @EventHandler
    public void onPlayerDropItem(PlayerDropItemEvent event) {
        if(this.ignoreNextDropEvent) {
            this.ignoreNextDropEvent = false;
        } else {
            // If the ignore flag is clear, this drop was caused by something other than
            // an inventory click (e.g. drop key, death, etc), so an event has not yet been fired
            int initialQuantity = event.getItemDrop().getItemStack().getAmount();
            ItemStack stack = event.getItemDrop().getItemStack();
            PlayerItemTransferEvent transferEvent = new PlayerItemTransferEvent(event, ItemTransferEvent.Type.DROP, event.getPlayer(),
                                                                                Optional.of(new InventorySlot(event.getPlayer().getInventory())),
                                                                                Optional.empty(),
                                                                                stack, event.getItemDrop(), initialQuantity,
                                                                                event.getPlayer().getOpenInventory().getCursor());
            this.callEvent(transferEvent);

            if(!transferEvent.isCancelled() && transferEvent.getQuantity() < initialQuantity) {
                int diff = initialQuantity - transferEvent.getQuantity();
                stack.setAmount(stack.getAmount() - diff);
                stack = stack.clone();
                stack.setAmount(diff);
                event.getPlayer().getInventory().addItem(stack);
            }
        }
    }

    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
    public void collectToCursor(InventoryClickEvent event) {
        // If this hasn't been cancelled yet, cancel it so our implementation can take over
        if(event.getAction() == InventoryAction.COLLECT_TO_CURSOR) {
            event.setCancelled(true);
            this.collectToCursor = true;
        }
    }

    @EventHandler
    public void onPlayerInventoryClicked(InventoryClickedEvent event) {
        // Control-double-click on a stack, all similar stacks are moved to the cursor, up to the max stack size
        // We cancel all of these and redo them ourselves. We have to do it from a InventoryClickedEvent because
        // we can't make the necessary changes from inside a InventoryClickEvent.
        if(this.collectToCursor) {
            this.collectToCursor = false;

            if(!(event.getWhoClicked() instanceof Player)) {
                return;
            }
            Player player = (Player) event.getWhoClicked();

            ItemStack cursor = event.getCursor().clone();

            for(int pass = 0; pass < 2; pass++) {
                for(int rawSlot = 0; rawSlot < event.getView().countSlots(); rawSlot++) {
                    if(cursor.getAmount() >= cursor.getMaxStackSize()) {
                        // If the gathered stack is full, we're done
                        break;
                    }

                    ItemStack stack = event.getView().getItem(rawSlot);
                    // First pass takes incomplete stacks, second pass takes complete ones
                    if(cursor.isSimilar(stack) && ((pass == 0 && stack.getAmount() < stack.getMaxStackSize()) ||
                                                        (pass == 1 && stack.getAmount() >= stack.getMaxStackSize()))) {
                        // Calculate how much can be collected from this stack
                        // If it is the output slot of a transaction preview, 0
                        int quantity =
                                event.getView().getTopInventory() instanceof CraftingInventory && event.getView().convertSlot(rawSlot) == 0 ||
                                event.getView().getTopInventory() instanceof MerchantInventory && event.getView().convertSlot(rawSlot) == 2
                            ? 0
                            : Math.min(stack.getAmount(), cursor.getMaxStackSize() - cursor.getAmount());
                        Inventory localInventory = getLocalInventory(event.getView(), rawSlot);
                        if(localInventory.getHolder() != player) {
                            // If stack comes from an external inventory, fire a transfer event
                            PlayerItemTransferEvent transferEvent = new PlayerItemTransferEvent(event, ItemTransferEvent.Type.TAKE, player,
                                                                                                Optional.of(InventorySlot.fromInventoryIndex(localInventory, event.getView().convertSlot(rawSlot))),
                                                                                                Optional.empty(),
                                                                                                stack, null, quantity, cursor);
                            this.callEvent(transferEvent);
                            if(transferEvent.isCancelled()) {
                                // If the event is cancelled, don't transfer from this slot
                                quantity = 0;
                            } else {
                                quantity = transferEvent.getQuantity();
                            }
                        }

                        if(quantity > 0) {
                            // Collect items from this stack to the cursor
                            cursor.setAmount(cursor.getAmount() + quantity);
                            if(quantity == stack.getAmount()) {
                                event.getView().setItem(rawSlot, null);
                            } else {
                                stack.setAmount(stack.getAmount() - quantity);
                            }
                        }
                    }
                }
            }

            event.getView().setCursor(cursor);
            player.updateInventory();
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onPlayerDragInventory(InventoryDragEvent event) {
        // This is when you spread items evenly across slots by dragging
        if(!(event.getWhoClicked() instanceof Player)) {
            return;
        }
        Player player = (Player) event.getWhoClicked();

        ItemStack transferred = event.getOldCursor().clone();
        transferred.setAmount(0);
        Inventory externalInventory = null;

        for(Map.Entry<Integer, ItemStack> entry : event.getNewItems().entrySet()) {
            Inventory inventory = getLocalInventory(event.getView(), entry.getKey());
            if(inventory.getHolder() != player) {
                // Add stacks to the total if they are dragged over an external inventory
                externalInventory = inventory;
                transferred.setAmount(transferred.getAmount() + entry.getValue().getAmount());
            }
        }

        if(externalInventory != null) {
            int initialQuantity = transferred.getAmount();
            PlayerItemTransferEvent transferEvent = new PlayerItemTransferEvent(
                event, ItemTransferEvent.Type.PLACE, player,
                Optional.empty(),
                Optional.of(new InventorySlot<>(externalInventory)),
                transferred, null, initialQuantity,
                event.getOldCursor()
            );

            this.callEvent(transferEvent);

            if(initialQuantity != transferEvent.getQuantity()) {
                // If the quantity changes, we have to cancel the entire drag,
                // because bukkit does not let us modify the dragged stacks.
                event.setCancelled(true);
            }
        }
    }
}
