GUIManager.java

package it.fulminazzo.yagl;

import it.fulminazzo.fulmicollection.objects.Refl;
import it.fulminazzo.fulmicollection.structures.tuples.Tuple;
import it.fulminazzo.fulmicollection.utils.ReflectionUtils;
import it.fulminazzo.yagl.content.GUIContent;
import it.fulminazzo.yagl.exception.InstanceNotInitializedException;
import it.fulminazzo.yagl.gui.GUI;
import it.fulminazzo.yagl.gui.SearchGUI;
import it.fulminazzo.yagl.handler.AnvilRenameHandler;
import it.fulminazzo.yagl.viewer.Viewer;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.inventory.InventoryEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.server.PluginDisableEvent;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.logging.Level;

/**
 * A general manager class that controls most {@link GUI} features.
 * It is completely independent and not required from the end user to be loaded or registered.
 */
public class GUIManager extends SingleInstance implements Listener {
    private final @NotNull List<Viewer> viewers;
    private final @NotNull List<AnvilRenameHandler> anvilRenameHandlers;
    @Getter
    private final @NotNull PlayersInventoryCache inventoryCache;

    /**
     * Instantiates a new GUI manager.
     */
    public GUIManager() {
        initialize();
        this.viewers = new ArrayList<>();
        this.anvilRenameHandlers = new ArrayList<>();
        this.inventoryCache = new PlayersInventoryCache();

        Bukkit.getOnlinePlayers().forEach(this::addNewAnvilRenameHandler);
    }

    @EventHandler
    void on(final @NotNull PlayerJoinEvent event) {
        addNewAnvilRenameHandler(event.getPlayer());
    }

    @EventHandler
    void on(final @NotNull PlayerQuitEvent event) {
        removeAnvilRenameHandler(event.getPlayer());
    }

    @SuppressWarnings("Convert2Lambda")
    @EventHandler
    void on(final @NotNull InventoryClickEvent event) {
        Player player = (Player) event.getWhoClicked();
        // Necessary explicit declaration for Groovy errors
        executeUnsafeEvent(event, player, new Runnable() {

            @Override
            public void run() {
                getOpenGUIViewer(player).ifPresent((v, g) -> {
                    int slot = event.getRawSlot();
                    if (slot < 0) g.clickOutsideAction().ifPresent(a -> a.execute(v, g));
                    else if (slot < g.size()) {
                        if (!g.isMovable(slot)) event.setCancelled(true);
                        @Nullable GUIContent content = g.getContent(v, slot);
                        if (content != null) content.clickItemAction().ifPresent(a -> a.execute(v, g, content));
                    }
                });
            }

        });
    }

    @EventHandler
    void on(final @NotNull InventoryDragEvent event) {
        Player player = (Player) event.getWhoClicked();
        executeUnsafeEvent(event, player, () ->
                getOpenGUIViewer(player).ifPresent((v, g) -> event.setCancelled(true))
        );
    }

    @EventHandler
    void on(final @NotNull InventoryCloseEvent event) {
        Player player = (Player) event.getPlayer();
        executeUnsafeEvent(event, player, () -> {
            Viewer viewer = getViewer(player);
            GUIAdapter.closeGUI(viewer);
            restorePlayerContents(player, false);
        });
    }

    @EventHandler
    void on(final @NotNull PluginDisableEvent event) {
        JavaPlugin plugin = GUIAdapter.getProvidingPlugin();
        Plugin disablingPlugin = event.getPlugin();
        if (plugin.equals(disablingPlugin)) {
            this.viewers.stream()
                    .filter(Viewer::hasOpenGUI)
                    .map(Viewer::getUniqueId)
                    .map(Bukkit::getPlayer)
                    .filter(Objects::nonNull)
                    .forEach(HumanEntity::closeInventory);
            Bukkit.getOnlinePlayers().forEach(this::removeAnvilRenameHandler);
            terminate();
        }
    }

    /**
     * Adds a new {@link AnvilRenameHandler} for the given player.
     *
     * @param player the player
     */
    void addNewAnvilRenameHandler(final @NotNull Player player) {
        AnvilRenameHandler handler = new AnvilRenameHandler(
                player.getUniqueId(),
                (p, n) -> getOpenGUIViewer(p).ifPresent((v, g) -> {
                    Class<?> clazz = g.getClass();
                    Class<?> expectedClass = SearchGUI.class.getDeclaredClasses()[0];
                    if (expectedClass.equals(clazz)) {
                        SearchGUI<?> searchGUI = new Refl<>(g).invokeMethod("getSearchGui");
                        if (n.equals(searchGUI.getQuery())) return;
                        searchGUI.setQuery(n);
                        GUIAdapter.updatePlayerGUI(searchGUI.getFirstPage(), v);
                    }
                })
        );
        handler.inject();
        this.anvilRenameHandlers.add(handler);
    }

    /**
     * Removes the {@link AnvilRenameHandler} of the given player.
     *
     * @param player the player
     */
    void removeAnvilRenameHandler(final @NotNull Player player) {
        AnvilRenameHandler handler = this.anvilRenameHandlers.stream()
                .filter(h -> h.belongsTo(player))
                .findFirst().orElse(null);
        if (handler != null) {
            handler.remove();
            this.anvilRenameHandlers.remove(handler);
        }
    }

    /**
     * Tries to execute the specified function with the associated event.
     * If an exception occurs,
     * <ul>
     *     <li>if {@link Viewer#getOpenGUI()} is not null, it means that the exception
     *     was not that bad to break the GUI functioning. Therefore, only a log
     *     message will be shown with the stacktrace;</li>
     *     <li>if {@link Viewer#getOpenGUI()} is null, it means that the exception
     *     broke the normal functioning and the player must be forced to close the inventory
     *     to avoid any more problems.</li>
     * </ul>
     *
     * @param event  the event
     * @param target the target of the event
     * @param action the function to execute
     */
    static void executeUnsafeEvent(final @NotNull InventoryEvent event,
                                   final @NotNull Player target,
                                   final @NotNull Runnable action) {
        try {
            action.run();
        } catch (Exception e) {
            // Normally, catching Exception is bad.
            // However, in this case we want to avoid any possible glitch or
            // inconsistency with GUIs (for example non-responsive contents).
            Viewer viewer = getViewer(target);
            Level level = viewer.getOpenGUI() == null ? Level.SEVERE : Level.WARNING;

            GUIAdapter.getProvidingPlugin().getLogger().log(
                    level,
                    "An error occurred while handling event " + event.getClass().getSimpleName(),
                    e
            );

            if (level == Level.SEVERE) {
                target.closeInventory();
                restorePlayerContents(target, true);
            }
        }
    }

    /**
     * Restores the specified player contents if present.
     *
     * @param player the player
     * @param force  true to force the restore (if contents are present)
     */
    static void restorePlayerContents(final @NotNull Player player,
                                      final boolean force) {
        GUIManager guiManager = getInstance();
        @NotNull PlayersInventoryCache inventoryCache = guiManager.inventoryCache;
        Viewer viewer = getViewer(player);
        if (inventoryCache.areContentsStored(player) && (viewer.getNextGUI() == null || force)) {
            inventoryCache.restorePlayerContents(player);
            inventoryCache.clearPlayerContents(player);
        }
    }

    /**
     * Gets a {@link Tuple} with the corresponding {@link Viewer} and open {@link GUI} if present.
     *
     * @param player the player
     * @return the Tuple
     */
    public static @NotNull Tuple<Viewer, GUI> getOpenGUIViewer(final @NotNull HumanEntity player) {
        Viewer viewer = getViewer(player);
        if (viewer.hasOpenGUI()) return new Tuple<>(viewer, viewer.getOpenGUI());
        else return new Tuple<>();
    }

    /**
     * Gets a {@link Tuple} with the corresponding {@link Viewer} and open {@link GUI} if present.
     *
     * @param uuid the uuid
     * @return the Tuple
     */
    public static @NotNull Tuple<Viewer, GUI> getOpenGUIViewer(final @NotNull UUID uuid) {
        Viewer viewer = getViewer(uuid);
        if (viewer != null && viewer.hasOpenGUI()) return new Tuple<>(viewer, viewer.getOpenGUI());
        else return new Tuple<>();
    }

    /**
     * Gets the corresponding {@link Viewer} to the provided player.
     * If it is not present, it will be created.
     *
     * @param player the player
     * @return the viewer
     */
    public static @NotNull Viewer getViewer(final @NotNull HumanEntity player) {
        Viewer viewer = getViewer(player.getUniqueId());
        if (viewer == null) {
            viewer = newViewer(player);
            getInstance().viewers.add(viewer);
        }
        return viewer;
    }

    private static @NotNull Viewer newViewer(final @NotNull HumanEntity player) {
        final String packageName = Viewer.class.getPackage().getName();
        final Class<?> bukkitViewer = ReflectionUtils.getClass(packageName + ".BukkitViewer");
        Viewer viewer = new Refl<>(bukkitViewer).invokeMethod("newViewer", player);
        return Objects.requireNonNull(viewer);
    }

    /**
     * Gets the corresponding {@link Viewer} to the provided player.
     * If it is not present, it will be returned null.
     *
     * @param uuid the uuid
     * @return the viewer
     */
    public static @Nullable Viewer getViewer(final @NotNull UUID uuid) {
        return getInstance().viewers.stream()
                .filter(v -> v.getUniqueId().equals(uuid))
                .findFirst().orElse(null);
    }

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

}