GUI.java

package it.fulminazzo.yagl.guis;

import it.fulminazzo.yagl.Metadatable;
import it.fulminazzo.yagl.actions.BiGUIAction;
import it.fulminazzo.yagl.actions.BiGUICommand;
import it.fulminazzo.yagl.actions.GUIAction;
import it.fulminazzo.yagl.actions.GUICommand;
import it.fulminazzo.yagl.contents.GUIContent;
import it.fulminazzo.yagl.contents.ItemGUIContent;
import it.fulminazzo.yagl.items.Item;
import it.fulminazzo.yagl.utils.ObjectUtils;
import it.fulminazzo.yagl.viewers.Viewer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

/**
 * The general interface to represent a GUI.
 */
public interface GUI extends Metadatable {

    /**
     * Opens the current GUI for the given {@link Viewer}.
     *
     * @param viewer the viewer
     */
    void open(final @NotNull Viewer viewer);

    /**
     * Sets title.
     *
     * @param title the title
     * @return this gui
     */
    @NotNull GUI setTitle(final @Nullable String title);

    /**
     * Gets title.
     *
     * @return the title
     */
    @Nullable String getTitle();

    /**
     * Gets size.
     *
     * @return the size
     */
    int size();

    /**
     * Checks if all the returned {@link #getContents()} are null.
     *
     * @return true if they are, or if it is empty
     */
    default boolean isEmpty() {
        return getContents().stream().allMatch(Objects::isNull);
    }

    /**
     * Checks if the content at the given slot is movable.
     *
     * @param slot the slot
     * @return true if it is
     */
    boolean isMovable(int slot);

    /**
     * Sets all movable.
     *
     * @return this gui
     */
    default @NotNull GUI setAllMovable() {
        for (int i = 0; i < size(); i++) setMovable(i, true);
        return this;
    }

    /**
     * Sets all unmovable.
     *
     * @return this gui
     */
    default @NotNull GUI setAllUnmovable() {
        for (int i = 0; i < size(); i++) setMovable(i, false);
        return this;
    }

    /**
     * Sets the given slot movable.
     *
     * @param slot    the slot
     * @param movable true if it should be movable
     * @return this gui
     */
    @NotNull GUI setMovable(int slot, boolean movable);

    /**
     * Gets the most matching content at the given slot.
     * The contents are filtered using {@link GUIContent#hasViewRequirements(Viewer)}
     * and for those remaining, the one with higher {@link GUIContent#getPriority()} is returned.
     *
     * @param viewer the viewer
     * @param slot   the slot
     * @return the content
     */
    default @Nullable GUIContent getContent(final @NotNull Viewer viewer, int slot) {
        return getContents(slot).stream()
                .filter(c -> c.hasViewRequirements(viewer))
                .min(Comparator.comparing(c -> -c.getPriority()))
                .orElse(null);
    }

    /**
     * Gets a copy of the contents at the given slot.
     *
     * @param slot the slot
     * @return this gui
     */
    @NotNull List<GUIContent> getContents(int slot);

    /**
     * Gets a copy of all the contents.
     *
     * @return this gui
     */
    @NotNull List<GUIContent> getContents();

    /**
     * Tries to add all the contents in the GUI.
     * If it fails (because of empty GUI), it throws an {@link IllegalArgumentException}.
     *
     * @param contents the contents
     * @return the gui
     */
    default @NotNull GUI addContent(final Item @NotNull ... contents) {
        return addContent(Arrays.stream(contents).map(ItemGUIContent::newInstance).toArray(GUIContent[]::new));
    }

    /**
     * Tries to add all the contents in the GUI.
     * If it fails (because of empty GUI), it throws an {@link IllegalArgumentException}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI addContent(final ItemGUIContent @NotNull ... contents) {
        return addContent((GUIContent[]) contents);
    }

    /**
     * Tries to add all the contents in the GUI.
     * If it fails (because of empty GUI), it throws an {@link IllegalArgumentException}.
     *
     * @param contents the contents
     * @return this gui
     */
    @NotNull GUI addContent(final GUIContent @NotNull ... contents);

    /**
     * Sets the given contents at the specified index.
     * These will be then filtered using {@link #getContent(Viewer, int)}
     *
     * @param slot     the slot
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setContents(int slot, final Item @NotNull ... contents) {
        return setContents(slot, Arrays.stream(contents).map(ItemGUIContent::newInstance).toArray(GUIContent[]::new));
    }

    /**
     * Sets the given contents at the specified index.
     * These will be then filtered using {@link #getContent(Viewer, int)}
     *
     * @param slot     the slot
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setContents(int slot, final ItemGUIContent @NotNull ... contents) {
        return setContents(slot, Arrays.stream(contents).toArray(GUIContent[]::new));
    }

    /**
     * Sets the given contents at the specified index.
     * These will be then filtered using {@link #getContent(Viewer, int)}
     *
     * @param slot     the slot
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setContents(int slot, final @NotNull Collection<GUIContent> contents) {
        return setContents(slot, contents.toArray(new GUIContent[0]));
    }

    /**
     * Sets the given contents at the specified index.
     * These will be then filtered using {@link #getContent(Viewer, int)}
     *
     * @param slot     the slot
     * @param contents the contents
     * @return this gui
     */
    @NotNull GUI setContents(int slot, final GUIContent @NotNull ... contents);

    /**
     * Removes the content from the given index.
     *
     * @param slot the slot
     * @return this gui
     */
    @NotNull GUI unsetContent(int slot);

    /**
     * Sets the given contents at the {@link #topSlots()}, the {@link #leftSlots()},
     * the {@link #bottomSlots()} and the {@link #rightSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setAllSides(final Item @NotNull ... contents) {
        return setTopAndBottomSides(contents).setLeftAndRightSides(contents);
    }

    /**
     * Sets the given contents at the {@link #topSlots()}, the {@link #leftSlots()},
     * the {@link #bottomSlots()} and the {@link #rightSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setAllSides(final ItemGUIContent @NotNull ... contents) {
        return setTopAndBottomSides(contents).setLeftAndRightSides(contents);
    }

    /**
     * Sets the given contents at the {@link #topSlots()}, the {@link #leftSlots()},
     * the {@link #bottomSlots()} and the {@link #rightSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setAllSides(final GUIContent @NotNull ... contents) {
        return setTopAndBottomSides(contents).setLeftAndRightSides(contents);
    }

    /**
     * Sets the given contents at the {@link #topSlots()}, the {@link #leftSlots()},
     * the {@link #bottomSlots()} and the {@link #rightSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setAllSides(final @NotNull Collection<GUIContent> contents) {
        return setTopAndBottomSides(contents).setLeftAndRightSides(contents);
    }

    /**
     * Removes the contents at the {@link #topSlots()}, the {@link #leftSlots()},
     * the {@link #bottomSlots()} and the {@link #rightSlots()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetAllSides() {
        return unsetTopAndBottomSides().unsetLeftAndRightSides();
    }

    /**
     * Sets the given contents at the {@link #topSlots()} and the {@link #bottomSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setTopAndBottomSides(final Item @NotNull ... contents) {
        return setTopSide(contents).setBottomSide(contents);
    }

    /**
     * Sets the given contents at the {@link #topSlots()} and the {@link #bottomSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setTopAndBottomSides(final ItemGUIContent @NotNull ... contents) {
        return setTopSide(contents).setBottomSide(contents);
    }

    /**
     * Sets the given contents at the {@link #topSlots()} and the {@link #bottomSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setTopAndBottomSides(final GUIContent @NotNull ... contents) {
        return setTopSide(contents).setBottomSide(contents);
    }

    /**
     * Sets the given contents at the {@link #topSlots()} and the {@link #bottomSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setTopAndBottomSides(final @NotNull Collection<GUIContent> contents) {
        return setTopSide(contents).setBottomSide(contents);
    }

    /**
     * Removes the contents at the {@link #topSlots()} and the {@link #bottomSlots()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetTopAndBottomSides() {
        return unsetTopSide().unsetBottomSide();
    }

    /**
     * Sets the given contents at the {@link #leftSlots()} and the {@link #rightSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setLeftAndRightSides(final Item @NotNull ... contents) {
        return setLeftSide(contents).setRightSide(contents);
    }

    /**
     * Sets the given contents at the {@link #leftSlots()} and the {@link #rightSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setLeftAndRightSides(final ItemGUIContent @NotNull ... contents) {
        return setLeftSide(contents).setRightSide(contents);
    }

    /**
     * Sets the given contents at the {@link #leftSlots()} and the {@link #rightSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setLeftAndRightSides(final GUIContent @NotNull ... contents) {
        return setLeftSide(contents).setRightSide(contents);
    }

    /**
     * Sets the given contents at the {@link #leftSlots()} and the {@link #rightSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setLeftAndRightSides(final @NotNull Collection<GUIContent> contents) {
        return setLeftSide(contents).setRightSide(contents);
    }

    /**
     * Removes the contents at the {@link #leftSlots()} and the {@link #rightSlots()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetLeftAndRightSides() {
        return unsetLeftSide().unsetRightSide();
    }

    /**
     * Sets the given contents at the {@link #topSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setTopSide(final Item @NotNull ... contents) {
        topSlots().forEach(s -> setContents(s, Arrays.stream(contents)
                .map(Item::copy).toArray(Item[]::new)));
        return this;
    }

    /**
     * Sets the given contents at the {@link #topSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setTopSide(final ItemGUIContent @NotNull ... contents) {
        topSlots().forEach(s -> setContents(s, Arrays.stream(contents)
                .map(ItemGUIContent::copy).toArray(ItemGUIContent[]::new)));
        return this;
    }

    /**
     * Sets the given contents at the {@link #topSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setTopSide(final GUIContent @NotNull ... contents) {
        topSlots().forEach(s -> setContents(s, Arrays.stream(contents)
                .map(GUIContent::copy).toArray(GUIContent[]::new)));
        return this;
    }

    /**
     * Sets the given contents at the {@link #topSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setTopSide(final @NotNull Collection<GUIContent> contents) {
        topSlots().forEach(s -> setContents(s, contents.stream()
                .map(GUIContent::copy).toArray(GUIContent[]::new)));
        return this;
    }

    /**
     * Removes the contents at the {@link #topSlots()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetTopSide() {
        topSlots().forEach(this::unsetContent);
        return this;
    }

    /**
     * Gets the slots on the top side.
     *
     * @return the slots
     */
    default @NotNull Set<Integer> topSlots() {
        Set<Integer> set = new TreeSet<>();
        for (int i = 0; i <= northEast(); i++) set.add(i);
        return set;
    }

    /**
     * Sets the given contents at the {@link #leftSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setLeftSide(final Item @NotNull ... contents) {
        leftSlots().forEach(s -> setContents(s, Arrays.stream(contents)
                .map(Item::copy).toArray(Item[]::new)));
        return this;
    }

    /**
     * Sets the given contents at the {@link #leftSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setLeftSide(final ItemGUIContent @NotNull ... contents) {
        leftSlots().forEach(s -> setContents(s, Arrays.stream(contents)
                .map(ItemGUIContent::copy).toArray(ItemGUIContent[]::new)));
        return this;
    }

    /**
     * Sets the given contents at the {@link #leftSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setLeftSide(final GUIContent @NotNull ... contents) {
        leftSlots().forEach(s -> setContents(s, Arrays.stream(contents)
                .map(GUIContent::copy).toArray(GUIContent[]::new)));
        return this;
    }

    /**
     * Sets the given contents at the {@link #leftSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setLeftSide(final @NotNull Collection<GUIContent> contents) {
        leftSlots().forEach(s -> setContents(s, contents.stream()
                .map(GUIContent::copy).toArray(GUIContent[]::new)));
        return this;
    }

    /**
     * Removes the contents at the {@link #leftSlots()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetLeftSide() {
        leftSlots().forEach(this::unsetContent);
        return this;
    }

    /**
     * Gets the slots on the left side.
     *
     * @return the slots
     */
    default @NotNull Set<Integer> leftSlots() {
        Set<Integer> set = new TreeSet<>();
        for (int i = northWest(); i <= southWest(); i += columns()) set.add(i);
        set.addAll(Arrays.asList(northWest(), middleWest(), southWest()));
        return set;
    }

    /**
     * Sets the given contents at the {@link #bottomSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setBottomSide(final Item @NotNull ... contents) {
        bottomSlots().forEach(s -> setContents(s, Arrays.stream(contents)
                .map(Item::copy).toArray(Item[]::new)));
        return this;
    }

    /**
     * Sets the given contents at the {@link #bottomSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setBottomSide(final ItemGUIContent @NotNull ... contents) {
        bottomSlots().forEach(s -> setContents(s, Arrays.stream(contents)
                .map(ItemGUIContent::copy).toArray(ItemGUIContent[]::new)));
        return this;
    }

    /**
     * Sets the given contents at the {@link #bottomSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setBottomSide(final GUIContent @NotNull ... contents) {
        bottomSlots().forEach(s -> setContents(s, Arrays.stream(contents)
                .map(GUIContent::copy).toArray(GUIContent[]::new)));
        return this;
    }

    /**
     * Sets the given contents at the {@link #bottomSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setBottomSide(final @NotNull Collection<GUIContent> contents) {
        bottomSlots().forEach(s -> setContents(s, contents.stream()
                .map(GUIContent::copy).toArray(GUIContent[]::new)));
        return this;
    }

    /**
     * Removes the contents at the {@link #bottomSlots()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetBottomSide() {
        bottomSlots().forEach(this::unsetContent);
        return this;
    }

    /**
     * Gets the slots on the bottom side.
     *
     * @return the slots
     */
    default @NotNull Set<Integer> bottomSlots() {
        Set<Integer> set = new TreeSet<>();
        for (int i = southWest(); i <= southEast(); i++) set.add(i);
        return set;
    }

    /**
     * Sets the given contents at the {@link #rightSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setRightSide(final Item @NotNull ... contents) {
        rightSlots().forEach(s -> setContents(s, Arrays.stream(contents)
                .map(Item::copy).toArray(Item[]::new)));
        return this;
    }

    /**
     * Sets the given contents at the {@link #rightSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setRightSide(final ItemGUIContent @NotNull ... contents) {
        rightSlots().forEach(s -> setContents(s, Arrays.stream(contents)
                .map(ItemGUIContent::copy).toArray(ItemGUIContent[]::new)));
        return this;
    }

    /**
     * Sets the given contents at the {@link #rightSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setRightSide(final GUIContent @NotNull ... contents) {
        rightSlots().forEach(s -> setContents(s, Arrays.stream(contents)
                .map(GUIContent::copy).toArray(GUIContent[]::new)));
        return this;
    }

    /**
     * Sets the given contents at the {@link #rightSlots()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setRightSide(final @NotNull Collection<GUIContent> contents) {
        rightSlots().forEach(s -> setContents(s, contents.stream()
                .map(GUIContent::copy).toArray(GUIContent[]::new)));
        return this;
    }

    /**
     * Removes all the contents at the {@link #rightSlots()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetRightSide() {
        rightSlots().forEach(this::unsetContent);
        return this;
    }

    /**
     * Gets the slots on the right side.
     *
     * @return the slots
     */
    default @NotNull Set<Integer> rightSlots() {
        Set<Integer> set = new TreeSet<>();
        for (int i = northEast(); i <= southEast(); i += columns()) set.add(i);
        set.addAll(Arrays.asList(northEast(), middleEast(), southEast()));
        return set;
    }

    /**
     * Sets the given contents at the index {@link #northWest()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setNorthWest(final Item @NotNull ... contents) {
        return setContents(northWest(), contents);
    }

    /**
     * Sets the given contents at the index {@link #northWest()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setNorthWest(final ItemGUIContent @NotNull ... contents) {
        return setContents(northWest(), contents);
    }

    /**
     * Sets the given contents at the index {@link #northWest()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setNorthWest(final GUIContent @NotNull ... contents) {
        return setContents(northWest(), contents);
    }

    /**
     * Removes all the contents at the index {@link #northWest()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetNorthWest() {
        return unsetContent(northWest());
    }

    /**
     * Sets the given contents at the index {@link #north()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setNorth(final Item @NotNull ... contents) {
        return setContents(north(), contents);
    }

    /**
     * Sets the given contents at the index {@link #north()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setNorth(final ItemGUIContent @NotNull ... contents) {
        return setContents(north(), contents);
    }

    /**
     * Sets the given contents at the index {@link #north()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setNorth(final GUIContent @NotNull ... contents) {
        return setContents(north(), contents);
    }

    /**
     * Removes all the contents at the index {@link #north()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetNorth() {
        return unsetContent(north());
    }

    /**
     * Sets the given contents at the index {@link #northEast()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setNorthEast(final Item @NotNull ... contents) {
        return setContents(northEast(), contents);
    }

    /**
     * Sets the given contents at the index {@link #northEast()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setNorthEast(final ItemGUIContent @NotNull ... contents) {
        return setContents(northEast(), contents);
    }

    /**
     * Sets the given contents at the index {@link #northEast()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setNorthEast(final GUIContent @NotNull ... contents) {
        return setContents(northEast(), contents);
    }

    /**
     * Removes all the contents at the index {@link #northEast()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetNorthEast() {
        return unsetContent(northEast());
    }

    /**
     * Sets the given contents at the index {@link #middleWest()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setMiddleWest(final Item @NotNull ... contents) {
        return setContents(middleWest(), contents);
    }

    /**
     * Sets the given contents at the index {@link #middleWest()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setMiddleWest(final ItemGUIContent @NotNull ... contents) {
        return setContents(middleWest(), contents);
    }

    /**
     * Sets the given contents at the index {@link #middleWest()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setMiddleWest(final GUIContent @NotNull ... contents) {
        return setContents(middleWest(), contents);
    }

    /**
     * Removes all the contents at the index {@link #middleWest()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetMiddleWest() {
        return unsetContent(middleWest());
    }

    /**
     * Sets the given contents at the index {@link #middle()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setMiddle(final Item @NotNull ... contents) {
        return setContents(middle(), contents);
    }

    /**
     * Sets the given contents at the index {@link #middle()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setMiddle(final ItemGUIContent @NotNull ... contents) {
        return setContents(middle(), contents);
    }

    /**
     * Sets the given contents at the index {@link #middle()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setMiddle(final GUIContent @NotNull ... contents) {
        return setContents(middle(), contents);
    }

    /**
     * Removes all the contents at the index {@link #middle()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetMiddle() {
        return unsetContent(middle());
    }

    /**
     * Sets the given contents at the index {@link #middleEast()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setMiddleEast(final Item @NotNull ... contents) {
        return setContents(middleEast(), contents);
    }

    /**
     * Sets the given contents at the index {@link #middleEast()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setMiddleEast(final ItemGUIContent @NotNull ... contents) {
        return setContents(middleEast(), contents);
    }

    /**
     * Sets the given contents at the index {@link #middleEast()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setMiddleEast(final GUIContent @NotNull ... contents) {
        return setContents(middleEast(), contents);
    }

    /**
     * Removes all the contents at the index {@link #middleEast()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetMiddleEast() {
        return unsetContent(middleEast());
    }

    /**
     * Sets the given contents at the index {@link #southWest()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setSouthWest(final Item @NotNull ... contents) {
        return setContents(southWest(), contents);
    }

    /**
     * Sets the given contents at the index {@link #southWest()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setSouthWest(final ItemGUIContent @NotNull ... contents) {
        return setContents(southWest(), contents);
    }

    /**
     * Sets the given contents at the index {@link #southWest()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setSouthWest(final GUIContent @NotNull ... contents) {
        return setContents(southWest(), contents);
    }

    /**
     * Removes all the contents at the index {@link #southWest()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetSouthWest() {
        return unsetContent(southWest());
    }

    /**
     * Sets the given contents at the index {@link #south()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setSouth(final Item @NotNull ... contents) {
        return setContents(south(), contents);
    }

    /**
     * Sets the given contents at the index {@link #south()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setSouth(final ItemGUIContent @NotNull ... contents) {
        return setContents(south(), contents);
    }

    /**
     * Sets the given contents at the index {@link #south()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setSouth(final GUIContent @NotNull ... contents) {
        return setContents(south(), contents);
    }

    /**
     * Removes all the contents at the index {@link #south()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetSouth() {
        return unsetContent(south());
    }

    /**
     * Sets the given contents at the index {@link #southEast()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setSouthEast(final Item @NotNull ... contents) {
        return setContents(southEast(), contents);
    }

    /**
     * Sets the given contents at the index {@link #southEast()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setSouthEast(final ItemGUIContent @NotNull ... contents) {
        return setContents(southEast(), contents);
    }

    /**
     * Sets the given contents at the index {@link #southEast()}.
     *
     * @param contents the contents
     * @return this gui
     */
    default @NotNull GUI setSouthEast(final GUIContent @NotNull ... contents) {
        return setContents(southEast(), contents);
    }

    /**
     * Removes all the contents at the index {@link #southEast()}.
     *
     * @return this gui
     */
    default @NotNull GUI unsetSouthEast() {
        return unsetContent(southEast());
    }

    /**
     * Gets the slot at the North-West position in this GUI.
     * For example, in the case of a <i>3x3</i> dimension:
     * <pre>
     *     | X |   |   |
     *     |   |   |   |
     *     |   |   |   |
     * </pre>
     *
     * @return the slots
     */
    default int northWest() {
        return 0;
    }

    /**
     * Gets the slot at the North position in this GUI.
     * For example, in the case of a <i>3x3</i> dimension:
     * <pre>
     *     |   | X |   |
     *     |   |   |   |
     *     |   |   |   |
     * </pre>
     *
     * @return the slots
     */
    default int north() {
        return columns() / 2;
    }

    /**
     * Gets the slot at the North-East position in this GUI.
     * For example, in the case of a <i>3x3</i> dimension:
     * <pre>
     *     |   |   | X |
     *     |   |   |   |
     *     |   |   |   |
     * </pre>
     *
     * @return the slots
     */
    default int northEast() {
        return Math.max(0, columns() - 1);
    }

    /**
     * Gets the slot at the start of the middle line.
     * For internal use only.
     *
     * @return the start of the line
     */
    default int middleLine() {
        int rows = (rows() - 1) / 2;
        double line = rows * columns();
        return (int) line;
    }

    /**
     * Gets the slot at the Middle-West position in this GUI.
     * For example, in the case of a <i>3x3</i> dimension:
     * <pre>
     *     |   |   |   |
     *     | X |   |   |
     *     |   |   |   |
     * </pre>
     *
     * @return the slots
     */
    default int middleWest() {
        return middleLine();
    }

    /**
     * Gets the slot at the Middle position in this GUI.
     * For example, in the case of a <i>3x3</i> dimension:
     * <pre>
     *     |   |   |   |
     *     |   | X |   |
     *     |   |   |   |
     * </pre>
     *
     * @return the slots
     */
    default int middle() {
        return columns() / 2 + middleLine();
    }

    /**
     * Gets the slot at the Middle-East position in this GUI.
     * For example, in the case of a <i>3x3</i> dimension:
     * <pre>
     *     |   |   |   |
     *     |   |   | X |
     *     |   |   |   |
     * </pre>
     *
     * @return the slots
     */
    default int middleEast() {
        return Math.max(0, columns() - 1) + middleLine();
    }

    /**
     * Gets the slot at the start of the south line.
     * For internal use only.
     *
     * @return the start of the line
     */
    default int southLine() {
        return Math.max(0, rows() - 1) * columns();
    }

    /**
     * Gets the slot at the South-West position in this GUI.
     * For example, in the case of a <i>3x3</i> dimension:
     * <pre>
     *     |   |   |   |
     *     |   |   |   |
     *     | X |   |   |
     * </pre>
     *
     * @return the slots
     */
    default int southWest() {
        return southLine();
    }

    /**
     * Gets the slot at the South position in this GUI.
     * For example, in the case of a <i>3x3</i> dimension:
     * <pre>
     *     |   |   |   |
     *     |   |   |   |
     *     |   | X |   |
     * </pre>
     *
     * @return the slots
     */
    default int south() {
        return columns() / 2 + southLine();
    }

    /**
     * Gets the slot at the South-East position in this GUI.
     * For example, in the case of a <i>3x3</i> dimension:
     * <pre>
     *     |   |   |   |
     *     |   |   |   |
     *     |   |   | X |
     * </pre>
     *
     * @return the slots
     */
    default int southEast() {
        return Math.max(0, columns() - 1) + southLine();
    }

    /**
     * Gets the rows of this GUI.
     *
     * @return the rows
     */
    int rows();

    /**
     * Gets the columns of this GUI.
     *
     * @return the columns
     */
    int columns();

    /**
     * Counts the empty slots of the current GUI.
     *
     * @return the slots
     */
    default @NotNull Set<Integer> emptySlots() {
        Set<Integer> slots = new HashSet<>();
        for (int i = 0; i < size(); i++)
            if (getContents(i).isEmpty()) slots.add(i);
        return slots;
    }

    /**
     * Sets the given content for the whole GUI.
     *
     * @param content the content
     * @return this gui
     */
    default @NotNull GUI fill(final @NotNull Item content) {
        for (int i = 0; i < size(); i++) setContents(i, content);
        return this;
    }

    /**
     * Sets the given content for the whole GUI.
     *
     * @param content the content
     * @return this gui
     */
    default @NotNull GUI fill(final @NotNull ItemGUIContent content) {
        for (int i = 0; i < size(); i++) setContents(i, content);
        return this;
    }

    /**
     * Sets the given content for the whole GUI.
     *
     * @param content the content
     * @return this gui
     */
    default @NotNull GUI fill(final @NotNull GUIContent content) {
        for (int i = 0; i < size(); i++) setContents(i, content);
        return this;
    }

    /**
     * Removes all the contents in this GUI.
     *
     * @return this gui
     */
    @NotNull GUI clear();

    /**
     * Forces the {@link Viewer} to execute the given command when clicking outside the GUI (will not include player's inventory slots).
     *
     * @param command the command
     * @return this gui
     */
    default @NotNull GUI onClickOutside(final @NotNull String command) {
        return onClickOutside(new GUICommand(command));
    }

    /**
     * Executes the given action when clicking outside the GUI (will not include player's inventory slots).
     *
     * @param action the action
     * @return this gui
     */
    @NotNull GUI onClickOutside(final @NotNull GUIAction action);

    /**
     * Click outside action.
     *
     * @return the action
     */
    @NotNull Optional<GUIAction> clickOutsideAction();

    /**
     * Forces the {@link Viewer} to execute the given command when opening this GUI.
     *
     * @param command the command
     * @return this gui
     */
    default @NotNull GUI onOpenGUI(final @NotNull String command) {
        return onOpenGUI(new GUICommand(command));
    }

    /**
     * Executes the given action when opening this GUI.
     *
     * @param action the action
     * @return this gui
     */
    @NotNull GUI onOpenGUI(final @NotNull GUIAction action);

    /**
     * Open gui action.
     *
     * @return the action
     */
    @NotNull Optional<GUIAction> openGUIAction();

    /**
     * Forces the {@link Viewer} to execute the given command when closing this GUI.
     * This will NOT be called when an action is passed to {@link #onChangeGUI(BiGUIAction)}
     * and another GUI is open.
     *
     * @param command the command
     * @return this gui
     */
    default @NotNull GUI onCloseGUI(final @NotNull String command) {
        return onCloseGUI(new GUICommand(command));
    }

    /**
     * Executes the given action when closing this GUI.
     * This will NOT be called when an action is passed to {@link #onChangeGUI(BiGUIAction)}
     * and another GUI is open.
     *
     * @param action the action
     * @return this gui
     */
    @NotNull GUI onCloseGUI(final @NotNull GUIAction action);

    /**
     * Close gui action.
     *
     * @return the action
     */
    @NotNull Optional<GUIAction> closeGUIAction();

    /**
     * Forces the {@link Viewer} to execute the given command when opening another GUI while having this one already open.
     * This will NOT call the action passed {@link #onCloseGUI(GUIAction)}.
     *
     * @param command the command
     * @return this gui
     */
    default @NotNull GUI onChangeGUI(final @NotNull String command) {
        return onChangeGUI(new BiGUICommand(command));
    }

    /**
     * Executes the given action when opening another GUI while having this one already open.
     * This will NOT call the action passed {@link #onCloseGUI(GUIAction)}.
     *
     * @param action the action
     * @return this gui
     */
    @NotNull GUI onChangeGUI(final @NotNull BiGUIAction action);

    /**
     * Change gui action.
     *
     * @return the action
     */
    @NotNull Optional<BiGUIAction> changeGUIAction();

    @Override
    default @NotNull GUI setVariable(final @NotNull String name, final @NotNull String value) {
        return (GUI) Metadatable.super.setVariable(name, value);
    }

    @Override
    default @NotNull GUI unsetVariable(final @NotNull String name) {
        return (GUI) Metadatable.super.unsetVariable(name);
    }

    /**
     * Copies all the contents, title and actions from this gui to the given one.
     *
     * @param other   the other gui
     * @param replace if false, if the other already has the content or title, it will not be replaced
     * @return this gui
     */
    default @NotNull GUI copyAll(final @NotNull GUI other, final boolean replace) {
        if (other.size() != size())
            throw new IllegalArgumentException(String.format("Cannot copy from GUI with different size %s != %s",
                    size(), other.size()));
        else {
            if (other.getTitle() == null || replace) other.setTitle(getTitle());

            copyAll((Metadatable) other, replace);

            for (int i = 0; i < size(); i++) {
                final @NotNull List<GUIContent> contents = getContents(i);
                if (!contents.isEmpty() && (other.getContents(i).isEmpty() || replace))
                    other.setContents(i, contents.toArray(new GUIContent[0]));
            }

            openGUIAction().ifPresent(a -> {
                @NotNull Optional<GUIAction> open = other.openGUIAction();
                if (!open.isPresent() || replace) other.onOpenGUI(a);
            });
            closeGUIAction().ifPresent(a -> {
                @NotNull Optional<GUIAction> close = other.closeGUIAction();
                if (!close.isPresent() || replace) other.onCloseGUI(a);
            });
            changeGUIAction().ifPresent(a -> {
                @NotNull Optional<BiGUIAction> change = other.changeGUIAction();
                if (!change.isPresent() || replace) other.onChangeGUI(a);
            });
            clickOutsideAction().ifPresent(a -> {
                @NotNull Optional<GUIAction> clickOutside = other.clickOutsideAction();
                if (!clickOutside.isPresent() || replace) other.onClickOutside(a);
            });

            return this;
        }
    }

    /**
     * Uses {@link #copyAll(GUI, boolean)} to copy from the given {@link GUI} to this one.
     *
     * @param other   the other gui
     * @param replace if false, if this already has the content or title, it will not be replaced
     * @return this gui
     */
    default @NotNull GUI copyFrom(final @NotNull GUI other, final boolean replace) {
        other.copyAll(this, replace);
        return this;
    }

    @Override
    default @NotNull GUI copyAll(final @NotNull Metadatable other, final boolean replace) {
        return (GUI) Metadatable.super.copyAll(other, replace);
    }

    @Override
    default @NotNull GUI copyFrom(final @NotNull Metadatable other, final boolean replace) {
        return (GUI) Metadatable.super.copyFrom(other, replace);
    }

    /**
     * Copies the current gui to a new one.
     *
     * @return the gui
     */
    default GUI copy() {
        return ObjectUtils.copy(this);
    }

    /**
     * Creates a new {@link GUI}.
     *
     * @param size the size
     * @return the gui
     */
    static @NotNull GUI newGUI(final int size) {
        return new DefaultGUI(size);
    }

    /**
     * Creates a new {@link GUI} capable of resizing.
     * Upon adding contents with {@link #addContent(Item...)}, the GUI will try to resize itself
     * until a threshold is met.
     * The user can also resize it using {@link ResizableGUI#resize(int)}.
     *
     * @param size the size
     * @return the resizable gui
     */
    static @NotNull ResizableGUI newResizableGUI(final int size) {
        return new ResizableGUI(size);
    }

    /**
     * Creates a new {@link TypeGUI}.
     *
     * @param type the type
     * @return the gui
     */
    static @NotNull GUI newGUI(final @NotNull GUIType type) {
        return new TypeGUI(type);
    }
}