PersistentListener.java

package it.fulminazzo.yagl.listeners;

import it.fulminazzo.fulmicollection.objects.Refl;
import it.fulminazzo.fulmicollection.utils.ThreadUtils;
import it.fulminazzo.yagl.InstanceNotInitializedException;
import it.fulminazzo.yagl.SingleInstance;
import it.fulminazzo.yagl.items.DeathAction;
import it.fulminazzo.yagl.items.Mobility;
import it.fulminazzo.yagl.items.PersistentItem;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerItemConsumeEvent;
import org.bukkit.event.player.PlayerItemDamageEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * A listener for {@link PersistentItem}s.
 */
public class PersistentListener extends SingleInstance implements Listener {
    /**
     * Timeout, in milliseconds, to check before calling {@link #on(PlayerInteractEvent)}.
     * This is used to prevent double calls.
     */
    private static final long INTERACT_DELAY = 10;
    /**
     * The general sleep time used in many methods.
     */
    static final int SLEEP_TIME = 50;
    private final @NotNull Map<UUID, Long> lastUsed;

    /**
     * Instantiates a new Persistent listener.
     */
    public PersistentListener() {
        initialize();
        this.lastUsed = new HashMap<>();
    }

    @EventHandler
    protected void on(@NotNull PlayerDeathEvent event) {
        Player player = event.getEntity();
        ItemStack[] contents = player.getInventory().getContents();
        Map<Integer, PersistentItem> toRestore = parseDroppedItems(contents, event.getDrops());
        if (!toRestore.isEmpty())
            // Wait before restoring player contents.
            ThreadUtils.sleepAndThen(SLEEP_TIME, () -> toRestore.forEach((i, p) ->
                    player.getInventory().setItem(i, p.create())));
    }

    /**
     * Finds the corresponding {@link PersistentItem} from the given {@link ItemStack} array.
     * Saves the ones with {@link DeathAction#MAINTAIN} in the returning map.
     *
     * @param contents the contents
     * @param drops    the drops
     * @return the map
     */
    protected @NotNull Map<Integer, PersistentItem> parseDroppedItems(final @NotNull ItemStack[] contents,
                                                                      final @Nullable List<ItemStack> drops) {
        Map<Integer, PersistentItem> toRestore = new HashMap<>();
        for (int i = 0; i < contents.length; i++) {
            int finalI = i;
            final ItemStack item = contents[i];
            // Save every PersistentItem with the MAINTAIN action, remove if they have the DISAPPEAR one.
            findPersistentItem(p -> {
                DeathAction deathAction = p.getDeathAction();
                if (deathAction == null) return;
                if (deathAction == DeathAction.MAINTAIN) toRestore.put(finalI, p);
                if (drops != null) drops.remove(item);
            }, item);
        }
        return toRestore;
    }

    @EventHandler
    protected void on(@NotNull PlayerInteractEvent event) {
        Player player = event.getPlayer();
        long lastUsed = this.lastUsed.getOrDefault(player.getUniqueId(), 0L);
        long now = new Date().getTime();
        // Check that a double click is not happening.
        if (now - lastUsed < INTERACT_DELAY) return;
        this.lastUsed.put(player.getUniqueId(), now);
        interactPersistentItem(player, event.getAction(), cancelled(event), event.getItem());
    }

    @EventHandler
    protected void on(@NotNull PlayerItemConsumeEvent event) {
        findPersistentItem(cancelled(event), event.getItem());
    }

    @EventHandler
    protected void on(@NotNull PlayerItemDamageEvent event) {
        findPersistentItem(cancelled(event), event.getItem());
    }

    @EventHandler
    protected void on(@NotNull BlockPlaceEvent event) {
        PlayerInventory inventory = event.getPlayer().getInventory();
        findPersistentItem(cancelled(event), inventory.getItem(inventory.getHeldItemSlot()));
    }

    @EventHandler
    protected void on(@NotNull PlayerDropItemEvent event) {
        findPersistentItem(cancelled(event), event.getItemDrop().getItemStack());
    }

    @EventHandler
    protected void on(@NotNull InventoryClickEvent event) {
        Player player = (Player) event.getWhoClicked();
        ClickType type = event.getClick();
        ItemStack itemStack = event.getCurrentItem();
        Consumer<PersistentItem> ifPresent = clickConsumer(event, player);

        // Check the current item and the cursor.
        if (!clickPersistentItem(player, type, ifPresent, itemStack, event.getCursor()) && type.equals(ClickType.NUMBER_KEY)) {
            // Check if a number has been used from the keyboard to move the item.
            itemStack = player.getInventory().getItem(event.getHotbarButton());
            clickPersistentItem(player, type, ifPresent, itemStack);
        }
    }

    @EventHandler
    protected void on(@NotNull InventoryDragEvent event) {
        Player player = (Player) event.getWhoClicked();
        ClickType type = ClickType.LEFT;
        clickPersistentItem(player, type, cancelled(event), Stream.concat(Stream.of(
                event.getCursor(), event.getOldCursor()
        ), event.getNewItems().values().stream()).collect(Collectors.toList()));
    }

    /*
        API USAGE
     */

    /**
     * Returns a consumer that simply cancels the event.
     *
     * @param event the event
     * @return the consumer
     */
    protected @NotNull Consumer<PersistentItem> cancelled(@NotNull Cancellable event) {
        return p -> event.setCancelled(true);
    }

    /**
     * Returns the consumer used in the default {@link #on(InventoryClickEvent)}.
     * It checks if the {@link PersistentItem} is not {@link Mobility#INTERNAL}
     * or if the click is external to the player's inventory.
     * If so, it cancels the {@link InventoryClickEvent}.
     *
     * @param event  the event
     * @param player the player
     * @return the consumer
     */
    protected @NotNull Consumer<PersistentItem> clickConsumer(final @NotNull InventoryClickEvent event, final @NotNull Player player) {
        return e -> {
            // Reflections necessary for tests
            Inventory open = new Refl<>(player).invokeMethodRefl("getOpenInventory").invokeMethod("getTopInventory");
            int rawSlot = event.getRawSlot();
            if (e.getMobility() != Mobility.INTERNAL || (rawSlot < open.getSize() || event.getAction().equals(InventoryAction.MOVE_TO_OTHER_INVENTORY)))
                cancelled(event).accept(e);
        };
    }

    /**
     * Checks through every {@link ItemStack} provided for a match in {@link PersistentItem#getPersistentItem(ItemStack)}.
     * For every match, it executes an action.
     *
     * @param player         the player
     * @param interactAction the interact action
     * @param ifPresent      the consumer to run if the item is found
     * @param itemStacks     the item stacks
     * @return true if it was found
     */
    protected boolean interactPersistentItem(final @NotNull Player player,
                                             final @NotNull Action interactAction,
                                             final @Nullable Consumer<PersistentItem> ifPresent,
                                             final @NotNull Collection<ItemStack> itemStacks) {
        return interactPersistentItem(player, interactAction, ifPresent, itemStacks.toArray(new ItemStack[0]));
    }

    /**
     * Checks through every {@link ItemStack} provided for a match in {@link PersistentItem#getPersistentItem(ItemStack)}.
     * For every match, it executes an action.
     *
     * @param player         the player
     * @param interactAction the interact action
     * @param ifPresent      the consumer to run if the item is found
     * @param itemStacks     the item stacks
     * @return true if it was found
     */
    protected boolean interactPersistentItem(final @NotNull Player player,
                                             final @NotNull Action interactAction,
                                             final @Nullable Consumer<PersistentItem> ifPresent,
                                             final ItemStack @Nullable ... itemStacks) {
        return findPersistentItem((p, i) -> {
            if (ifPresent != null) ifPresent.accept(p);
            p.interact(player, i, interactAction);
        }, itemStacks);
    }

    /**
     * Checks through every {@link ItemStack} provided for a match in {@link PersistentItem#getPersistentItem(ItemStack)}.
     * For every match, it executes an action.
     *
     * @param player     the player
     * @param clickType  the click type
     * @param ifPresent  the consumer to run if the item is found
     * @param itemStacks the item stacks
     * @return true if it was found
     */
    protected boolean clickPersistentItem(final @NotNull Player player,
                                          final @NotNull ClickType clickType,
                                          final @Nullable Consumer<PersistentItem> ifPresent,
                                          final @NotNull Collection<ItemStack> itemStacks) {
        return clickPersistentItem(player, clickType, ifPresent, itemStacks.toArray(new ItemStack[0]));
    }

    /**
     * Checks through every {@link ItemStack} provided for a match in {@link PersistentItem#getPersistentItem(ItemStack)}.
     * For every match, it executes an action.
     *
     * @param player     the player
     * @param clickType  the click type
     * @param ifPresent  the consumer to run if the item is found
     * @param itemStacks the item stacks
     * @return true if it was found
     */
    protected boolean clickPersistentItem(final @NotNull Player player,
                                          final @NotNull ClickType clickType,
                                          final @Nullable Consumer<PersistentItem> ifPresent,
                                          final ItemStack @Nullable ... itemStacks) {
        return findPersistentItem((p, i) -> {
            if (ifPresent != null) ifPresent.accept(p);
            p.click(player, i, clickType);
        }, itemStacks);
    }

    /**
     * Finds {@link PersistentItem}s from the given {@link ItemStack} array.
     * For each one found, execute an action
     *
     * @param ifPresent  the action to execute
     * @param itemStacks the item stacks
     * @return true if at least one found
     */
    protected boolean findPersistentItem(final @Nullable Consumer<PersistentItem> ifPresent,
                                         final ItemStack @Nullable ... itemStacks) {
        return findPersistentItem(ifPresent == null ? null : (p, i) -> ifPresent.accept(p), itemStacks);
    }

    /**
     * Finds {@link PersistentItem}s from the given {@link ItemStack} array.
     * For each one found, execute an action
     *
     * @param ifPresent  the action to execute
     * @param itemStacks the item stacks
     * @return true if at least one found
     */
    protected boolean findPersistentItem(final @Nullable BiConsumer<PersistentItem, ItemStack> ifPresent,
                                         final ItemStack @Nullable ... itemStacks) {
        boolean found = false;
        if (itemStacks != null)
            for (final ItemStack itemStack : itemStacks) {
                PersistentItem persistentItem = PersistentItem.getPersistentItem(itemStack);
                if (persistentItem != null) {
                    if (ifPresent != null) ifPresent.accept(persistentItem, itemStack);
                    found = true;
                }
            }
        return found;
    }

    /**
     * Gets an instance of {@link PersistentListener}.
     * If none is currently loaded, it will be created.
     *
     * @return the instance
     */
    public static PersistentListener getInstance() {
        try {
            return getInstance(PersistentListener.class);
        } catch (InstanceNotInitializedException e) {
            PersistentListener listener = new PersistentListener();
            Bukkit.getPluginManager().registerEvents(listener, getProvidingPlugin());
            return listener;
        }
    }

    private static @NotNull JavaPlugin getProvidingPlugin() {
        return JavaPlugin.getProvidingPlugin(PersistentListener.class);
    }

    /**
     * Checks if the current listener has been initialized at least once.
     *
     * @return true if it is
     */
    public static boolean isInitialized() {
        try {
            getInstance(PersistentListener.class);
            return true;
        } catch (InstanceNotInitializedException e) {
            return false;
        }
    }

}