GUIAdapter.java

package it.fulminazzo.yagl;

import it.fulminazzo.fulmicollection.objects.Refl;
import it.fulminazzo.yagl.content.GUIContent;
import it.fulminazzo.yagl.event.FullscreenGUIUpdateEvent;
import it.fulminazzo.yagl.gui.FullscreenGUI;
import it.fulminazzo.yagl.gui.GUI;
import it.fulminazzo.yagl.gui.GUIType;
import it.fulminazzo.yagl.gui.TypeGUI;
import it.fulminazzo.yagl.inventory.InventoryWrapper;
import it.fulminazzo.yagl.item.BukkitItem;
import it.fulminazzo.yagl.metadatable.PAPIParser;
import it.fulminazzo.yagl.scheduler.Scheduler;
import it.fulminazzo.yagl.util.MessageUtils;
import it.fulminazzo.yagl.util.NMSUtils;
import it.fulminazzo.yagl.viewer.PlayerOfflineException;
import it.fulminazzo.yagl.viewer.Viewer;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
 * A collection of utilities for handling with {@link GUI}s and Bukkit.
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class GUIAdapter {

    /**
     * Opens the given {@link GUI} for the specified {@link Viewer}.
     *
     * @param gui    the gui
     * @param viewer the viewer
     */
    public static void openGUI(final @NotNull GUI gui,
                               final @NotNull Viewer viewer) {
        openGUI(gui, viewer, null, null);
    }


    /**
     * Opens the given {@link GUI} for the specified {@link Viewer}.
     * Uses the given {@link ItemMeta} function to {@link BukkitItem#create(Class, Consumer)} the contents.
     *
     * @param gui          the gui
     * @param viewer       the viewer
     * @param metaFunction the meta function
     */
    public static void openGUI(final @NotNull GUI gui,
                               final @NotNull Viewer viewer,
                               final @NotNull Consumer<ItemMeta> metaFunction) {
        openGUI(gui, viewer, ItemMeta.class, metaFunction);
    }

    /**
     * Opens the given {@link GUI} for the specified {@link Viewer}.
     * Uses the given {@link ItemMeta} class and function to {@link BukkitItem#create(Class, Consumer)} the contents.
     *
     * @param <M>           the type of the item meta
     * @param gui           the gui
     * @param viewer        the viewer
     * @param itemMetaClass the ItemMeta class
     * @param metaFunction  the meta function
     */
    public static <M extends ItemMeta> void openGUI(final @NotNull GUI gui,
                                                    final @NotNull Viewer viewer,
                                                    final @Nullable Class<M> itemMetaClass,
                                                    final @Nullable Consumer<M> metaFunction) {
        openGUIHelper(gui, viewer, (p, v) -> {
            // Open inventory
            final InventoryWrapper inventory;
            // Check if GUI is Fullscreen
            if (gui instanceof FullscreenGUI) {
                FullscreenGUI fullscreenGUI = (FullscreenGUI) gui;
                GUI upperGUI = fullscreenGUI.getUpperGUI();
                GUI lowerGUI = fullscreenGUI.getLowerGUI();
                upperGUI.apply(upperGUI);
                lowerGUI.apply(lowerGUI);

                inventory = guiToInventory(p, upperGUI);
                fillInventoryWithGUIContents(upperGUI, v, itemMetaClass, metaFunction, inventory.getActualInventory(), upperGUI.size());
                inventory.open(p);

                PlayersInventoryCache inventoryCache = GUIManager.getInstance().getInventoryCache();
                if (v.getNextGUI() == null) {
                    inventoryCache.storePlayerContents(p);
                    inventoryCache.clearPlayerStorage(p, lowerGUI.size());
                }
                int upperGUISize = upperGUI.size();
                int lowerGUISize = lowerGUI.size();
                setGUIContentsToPlayerInventory(gui, itemMetaClass, metaFunction, p, lowerGUISize, upperGUISize);
            } else {
                inventory = guiToInventory(p, gui);
                fillInventoryWithGUIContents(gui, v, itemMetaClass, metaFunction, inventory.getActualInventory(), gui.size());
                inventory.open(p);
            }
        });
    }

    /**
     * Updates the player's open {@link FullscreenGUI} by setting the updated contents in
     * the player's inventory.
     *
     * @param gui    the gui
     * @param viewer the viewer
     */
    public static void updatePlayerGUI(final @NotNull GUI gui,
                                       final @NotNull Viewer viewer) {
        updatePlayerGUI(gui, viewer, null, null);
    }

    /**
     * Updates the player's open {@link FullscreenGUI} by setting the updated contents in
     * the player's inventory.
     * Uses the given {@link ItemMeta} class and function to {@link BukkitItem#create(Class, Consumer)} the contents.
     *
     * @param <M>           the type of the item meta
     * @param gui           the gui
     * @param viewer        the viewer
     * @param itemMetaClass the ItemMeta class
     * @param metaFunction  the meta function
     */
    public static <M extends ItemMeta> void updatePlayerGUI(final @NotNull GUI gui,
                                                            final @NotNull Viewer viewer,
                                                            final @Nullable Class<M> itemMetaClass,
                                                            final @Nullable Consumer<M> metaFunction) {
        final UUID uuid = viewer.getUniqueId();
        final Player player = Bukkit.getPlayer(uuid);
        if (player == null) throw new PlayerOfflineException(viewer.getName());

        FullscreenGUIUpdateEvent event = new FullscreenGUIUpdateEvent(player.getOpenInventory());
        Bukkit.getPluginManager().callEvent(event);
        if (event.isCancelled()) return;

        if (!(gui instanceof FullscreenGUI))
            throw new IllegalArgumentException("updatePlayerGUI can only be used with FullscreenGUI");

        GUIManager.executeUnsafeEvent(event, player, () ->
                openGUIHelper(gui, viewer, (p, v) -> {
                    FullscreenGUI fullscreenGUI = (FullscreenGUI) gui;
                    GUI upperGUI = fullscreenGUI.getUpperGUI();
                    GUI lowerGUI = fullscreenGUI.getLowerGUI();
                    upperGUI.apply(upperGUI);
                    lowerGUI.apply(lowerGUI);

                    InventoryView inventoryView = p.getOpenInventory();
                    String title = MessageUtils.color(gui.getTitle());

                    if (!inventoryView.getTitle().equals(title)) {
                        fillInventoryWithGUIContents(upperGUI, v,
                                itemMetaClass, metaFunction,
                                inventoryView.getTopInventory(), upperGUI.size());

                        if (title != null) NMSUtils.updateInventoryTitle(p, title);
                    }

                    int upperGUISize = upperGUI.size();
                    int lowerGUISize = lowerGUI.size();
                    setGUIContentsToPlayerInventory(gui, itemMetaClass, metaFunction, p, lowerGUISize, upperGUISize);
                })
        );
    }

    /**
     * Support function for {@link #openGUI(GUI, Viewer, Class, Consumer)}.
     *
     * @param gui    the gui
     * @param viewer the viewer
     * @param action the action to execute
     */
    static void openGUIHelper(final @NotNull GUI gui,
                              final @NotNull Viewer viewer,
                              final @NotNull BiConsumer<Player, Viewer> action) {
        Consumer<Viewer> runnable = v -> {
            final UUID uuid = v.getUniqueId();
            final Player player = Bukkit.getPlayer(uuid);
            if (player == null) throw new PlayerOfflineException(v.getName());
            final Refl<Viewer> reflViewer = new Refl<>(v);
            // Add to GUIManager if not present
            v = GUIManager.getViewer(player);
            // Save previous GUI, if present
            GUIManager.getOpenGUIViewer(uuid).ifPresent((vi, g) -> {
                reflViewer.setFieldObject("previousGUI", g).setFieldObject("openGUI", null);
                reflViewer.setFieldObject("nextGUI", gui);
                g.changeGUIAction().ifPresent(a -> a.execute(vi, g, gui));
            });
            // Set global variables
            for (final @NotNull BukkitVariable variable : BukkitVariable.DEFAULT_VARIABLES)
                gui.setVariable(variable.getName(), variable.getValue(player));
            gui.apply(gui);
            if (isPlaceholderAPIEnabled()) PAPIParser.parse(player, gui);
            action.accept(player, v);
            // Set new GUI
            reflViewer.setFieldObject("openGUI", gui);
            reflViewer.setFieldObject("nextGUI", null);
            // Execute action if present
            gui.openGUIAction().ifPresent(a -> a.execute(reflViewer.getObject(), gui));
        };
        // Check if context is Async and synchronize
        if (Bukkit.isPrimaryThread()) runnable.accept(viewer);
        else
            Scheduler.getScheduler().run(JavaPlugin.getProvidingPlugin(GUIAdapter.class), () -> runnable.accept(viewer));
    }

    /**
     * Sets the given {@link GUI} contents to the {@link Player}'s inventory.
     *
     * @param <M>            the type of the item meta
     * @param gui            the gui
     * @param itemMetaClass  the ItemMeta class
     * @param metaFunction   the meta function
     * @param player         the player
     * @param contentsSize   the amount of contents to set
     * @param contentsOffset the offset upon which to start getting the contents
     */
    static <M extends ItemMeta> void setGUIContentsToPlayerInventory(final @NotNull GUI gui,
                                                                     final @Nullable Class<M> itemMetaClass,
                                                                     final @Nullable Consumer<M> metaFunction,
                                                                     final @NotNull Player player,
                                                                     final int contentsSize, final int contentsOffset) {
        Viewer viewer = GUIManager.getViewer(player);
        PlayerInventory playerInventory = player.getInventory();

        // Since Minecraft handles player inventory in a "particular" way,
        // it is necessary to manually set each item.
        List<ItemStack> itemStacks = new ArrayList<>(Arrays.asList(playerInventory.getContents()));

        // Hotbar contents
        for (int i = 27; i < contentsSize; i++) {
            GUIContent content = gui.getContent(viewer, i + contentsOffset);
            int slot = i - 27;
            if (content == null) itemStacks.set(slot, null);
            else itemStacks.set(slot, convertContentToItemStack(gui, itemMetaClass, metaFunction, content));
        }

        // Storage contents
        for (int i = 0; i < Math.min(contentsSize, 27); i++) {
            GUIContent content = gui.getContent(viewer, i + contentsOffset);
            int slot = i + 9;
            if (content == null) itemStacks.set(slot, null);
            else itemStacks.set(slot, convertContentToItemStack(gui, itemMetaClass, metaFunction, content));
        }

        playerInventory.setContents(itemStacks.toArray(new ItemStack[0]));
    }

    /**
     * Fills the given inventory with the gui contents.
     *
     * @param <M>           the type of the item meta
     * @param gui           the gui
     * @param viewer        the viewer
     * @param itemMetaClass the ItemMeta class
     * @param metaFunction  the meta function
     * @param inventory     the inventory
     * @param size          the size of the inventory
     */
    static <M extends ItemMeta> void fillInventoryWithGUIContents(
            final @NotNull GUI gui, final @NotNull Viewer viewer,
            final @Nullable Class<M> itemMetaClass, final @Nullable Consumer<M> metaFunction,
            final @NotNull Inventory inventory,
            final int size
    ) {
        for (int i = 0; i < size; i++) {
            GUIContent content = gui.getContent(viewer, i);
            if (content != null) {
                final ItemStack o = convertContentToItemStack(gui, itemMetaClass, metaFunction, content);
                inventory.setItem(i, o);
            }
        }
    }

    /**
     * Converts the given content to a {@link ItemStack}, using the given data.
     *
     * @param <M>           the type of the item meta
     * @param gui           the gui
     * @param itemMetaClass the ItemMeta class
     * @param metaFunction  the meta function
     * @param content       the content
     * @return the item stack
     */
    static <M extends ItemMeta> @NotNull ItemStack convertContentToItemStack(final @NotNull GUI gui,
                                                                             final @Nullable Class<M> itemMetaClass,
                                                                             final @Nullable Consumer<M> metaFunction,
                                                                             final @NotNull GUIContent content) {
        content.copyFrom(gui, false);
        BukkitItem render = content.apply(content).render().copy(BukkitItem.class);
        final ItemStack itemStack;
        if (itemMetaClass == null || metaFunction == null) itemStack = render.create();
        else itemStack = render.create(itemMetaClass, metaFunction);
        return itemStack;
    }

    /**
     * Closes the currently open {@link GUI} for the specified {@link Viewer}, if present.
     *
     * @param viewer the viewer
     */
    public static void closeGUI(final @NotNull Viewer viewer) {
        final UUID uuid = viewer.getUniqueId();
        final Player player = Bukkit.getPlayer(uuid);
        if (player == null) throw new PlayerOfflineException(viewer.getName());
        final Refl<Viewer> reflViewer = new Refl<>(viewer);
        // Save previous GUI, if present
        GUIManager.getOpenGUIViewer(uuid).ifPresent((v, g) -> {
            reflViewer.setFieldObject("previousGUI", g).setFieldObject("openGUI", null);
            player.closeInventory();
            g.closeGUIAction().ifPresent(a ->
                    Scheduler.getScheduler().run(JavaPlugin.getProvidingPlugin(GUIAdapter.class), () -> a.execute(v, g))
            );
        });
    }

    /**
     * Converts the given {@link GUI} to a {@link Inventory}.
     *
     * @param gui   the gui
     * @param owner the owner of the inventory
     * @return the inventory
     */
    public static @NotNull InventoryWrapper guiToInventory(final @NotNull Player owner,
                                                           final @NotNull GUI gui) {
        String title = MessageUtils.color(gui.getTitle());
        final InventoryWrapper inventory;
        if (title == null) {
            if (gui instanceof TypeGUI) {
                InventoryType type = guiToInventoryType(((TypeGUI) gui).getInventoryType());
                inventory = InventoryWrapper.createInventory(owner, type);
            } else inventory = InventoryWrapper.createInventory(owner, gui.size());
        } else {
            if (gui instanceof TypeGUI) {
                InventoryType type = guiToInventoryType(((TypeGUI) gui).getInventoryType());
                inventory = InventoryWrapper.createInventory(owner, type, title);
            } else inventory = InventoryWrapper.createInventory(owner, gui.size(), title);
        }
        return inventory;
    }

    /**
     * Converts the given {@link GUIType} to a {@link InventoryType}.
     *
     * @param guiType the gui type
     * @return the inventory type
     */
    public static @NotNull InventoryType guiToInventoryType(final @NotNull GUIType guiType) {
        return InventoryType.valueOf(guiType.name());
    }

    /**
     * Checks if PlaceholderAPI is enabled.
     *
     * @return true if it is
     */
    public static boolean isPlaceholderAPIEnabled() {
        return Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI");
    }


    /**
     * Gets the plugin associated with the YAGL library.
     *
     * @return the plugin
     */
    public static @NotNull JavaPlugin getProvidingPlugin() {
        return JavaPlugin.getProvidingPlugin(GUIAdapter.class);
    }

}