public class RPGInventory extends PluginLifecycle {
    private static RPGInventory instance;

    private Permission perms;
    private Economy economy;

    private BukkitLevelSystem.Provider levelSystemProvider;
    private BukkitClassSystem.Provider classSystemProvider;

    private FileLanguage language;
    private boolean placeholderApiHooked = false;
    private boolean myPetHooked = false;
    private ResourcePackModule resourcePackModule = null;

    public static RPGInventory getInstance() {
        return instance;
    }

    public static FileLanguage getLanguage() {
        return instance.language;
    }

    public static Permission getPermissions() {
        return instance.perms;
    }

    public static Economy getEconomy() {
        return instance.economy;
    }

    @Contract(pure = true)
    public static boolean economyConnected() {
        return instance.economy != null;
    }

    @Contract(pure = true)
    public static boolean isPlaceholderApiHooked() {
        return instance.placeholderApiHooked;
    }

    @Contract(pure = true)
    public static boolean isMyPetHooked() {
        return instance.myPetHooked;
    }

    public static BukkitLevelSystem getLevelSystem(@NotNull Player player) {
        return instance.levelSystemProvider.getSystem(player);
    }

    public static BukkitClassSystem getClassSystem(@NotNull Player player) {
        return instance.classSystemProvider.getSystem(player);
    }

    @Nullable
    public static ResourcePackModule getResourcePackModule() {
        return instance.resourcePackModule;
    }

    @Override
    public void init() {
        instance = this;
        Log.init(this.getLogger());
        Config.init(this);
    }

    @Override
    public void onLoad() {
        if (checkMimic()) {
            getServer()
                    .getServicesManager()
                    .register(BukkitItemsRegistry.class, new RPGInventoryItemsRegistry(), this, ServicePriority.High);
        }
    }

    @Override
    public void onEnable() {
        if (!initMimicSystems()) {
            return;
        }

        loadConfigs();
        Serialization.registerTypes();

        if (!this.checkRequirements()) {
            this.getPluginLoader().disablePlugin(this);
            return;
        }

        PluginManager pm = this.getServer().getPluginManager();

        // Hook Placeholder API
        if (pm.isPluginEnabled("PlaceholderAPI")) {
            new StringUtils.Placeholders().register();
            placeholderApiHooked = true;
            Log.i("Placeholder API hooked!");
        } else {
            placeholderApiHooked = false;
        }

        loadModules();

        this.loadPlayers();
        this.startMetrics();

        // Enable commands
        this.getCommand("rpginventory")
                .setExecutor(new TrackedCommandExecutor(new RPGInventoryCommandExecutor(), getReporter()));

        this.checkUpdates(null);
    }

    private boolean initMimicSystems() {
        boolean isMimicFound = checkMimic();
        if (isMimicFound) {
            ServicesManager servicesManager = getServer().getServicesManager();
            this.levelSystemProvider = servicesManager.load(BukkitLevelSystem.Provider.class);
            Log.i("Level system ''{0}'' found.", this.levelSystemProvider.getId());
            this.classSystemProvider = servicesManager.load(BukkitClassSystem.Provider.class);
            Log.i("Class system ''{0}'' found.", this.classSystemProvider.getId());
        } else {
            Log.s("Mimic is required for RPGInventory to use levels and classes from other RPG plugins!");
            Log.s("Download it from SpigotMC: https://www.spigotmc.org/resources/82515/");
            getServer().getPluginManager().disablePlugin(this);
        }
        return isMimicFound;
    }

    void reload() {
        // Unload
        saveData();
        removeListeners();

        // Load
        loadConfigs();
        loadModules();
        loadPlayers();
    }

    private void loadConfigs() {
        this.updateConfig();
        Config.reload();
        language = new FileLanguage(this);
    }

    private void loadModules() {
        PluginManager pm = getServer().getPluginManager();

        // Hook MyPet
        if (pm.isPluginEnabled("MyPet") && MyPetManager.init(this)) {
            myPetHooked = true;
            Log.i("MyPet used as pet system");
        } else {
            myPetHooked = false;
            Log.i(PetManager.init(this) ? "Pet system is enabled" : "Pet system isn''t loaded");
        }

        // Load modules
        Log.i(CraftManager.init(this) ? "Craft extensions is enabled" : "Craft extensions isn''t loaded");
        Log.i(InventoryLocker.init(this) ? "Inventory lock system is enabled" : "Inventory lock system isn''t loaded");
        Log.i(ItemManager.init(this) ? "Item system is enabled" : "Item system isn''t loaded");
        Log.i(BackpackManager.init(this) ? "Backpack system is enabled" : "Backpack system isn''t loaded");

        // Registering other listeners
        pm.registerEvents(new ArmorEquipListener(), this);
        pm.registerEvents(new HandSwapListener(), this);
        pm.registerEvents(new PlayerListener(), this);
        pm.registerEvents(new WorldListener(), this);

        if (SlotManager.instance().getElytraSlot() != null) {
            pm.registerEvents(new ElytraListener(), this);
        }
        this.resourcePackModule = ResourcePackModule.init(this);
    }

    private void removeListeners() {
        ProtocolLibrary.getProtocolManager().removePacketListeners(this);
        HandlerList.unregisterAll(this);
    }

    private boolean checkMimic() {
        if (getServer().getPluginManager().getPlugin("Mimic") == null) {
            return false;
        } else if (MimicApiLevel.checkApiLevel(MimicApiLevel.VERSION_0_6)) {
            return true;
        } else {
            Log.w("At least Mimic 0.6.1 required for RPGInventory.");
            return false;
        }
    }

    private boolean checkRequirements() {
        // Check if plugin is enabled
        if (!Config.getConfig().getBoolean("enabled")) {
            Log.w("RPGInventory is disabled in the config!");
            return false;
        }

        // Check version compatibility
        if (VersionHandler.isNotSupportedVersion()) {
            Log.w("This version of RPG Inventory is not tested with \"{0}\"!", Bukkit.getBukkitVersion());
        } else if (VersionHandler.isExperimentalSupport()) {
            Log.w("Support of {0} is experimental! Use RPGInventory with caution.", Bukkit.getBukkitVersion());
        }

        // Check dependencies
        if (this.setupPermissions()) {
            Log.i("Permissions hooked: {0}", perms.getName());
        } else {
            Log.s("Permissions not found!");
            return false;
        }

        if (this.setupEconomy()) {
            Log.i("Economy hooked: {0}", economy.getName());
        } else {
            Log.w("Economy not found!");
        }

        return InventoryManager.init(this) && SlotManager.init();
    }

    @Override
    public void onDisable() {
        saveData();
    }

    private void saveData() {
        BackpackManager.saveBackpacks();
        this.savePlayers();
    }

    private void startMetrics() {
        new Metrics(holder, 4210);
    }

    private void savePlayers() {
        if (this.getServer().getOnlinePlayers().size() == 0) {
            return;
        }

        Log.i("Saving players inventories...");
        for (Player player : this.getServer().getOnlinePlayers()) {
            InventoryManager.unloadPlayerInventory(player);
        }
    }

    private void loadPlayers() {
        if (this.getServer().getOnlinePlayers().size() == 0) {
            return;
        }

        Log.i("Loading players inventories...");
        for (Player player : this.getServer().getOnlinePlayers()) {
            InventoryManager.loadPlayerInventory(player);
        }
    }

    private boolean setupPermissions() {
        RegisteredServiceProvider<Permission> permissionProvider = getServer().getServicesManager().getRegistration(Permission.class);
        if (permissionProvider != null) {
            perms = permissionProvider.getProvider();
        }

        return perms != null;
    }

    private boolean setupEconomy() {
        RegisteredServiceProvider<Economy> rsp = this.getServer().getServicesManager().getRegistration(Economy.class);
        if (rsp != null) {
            economy = rsp.getProvider();
        }

        return economy != null;
    }

    public void checkUpdates(@Nullable final Player player) {
        if (!Config.getConfig().getBoolean("check-update")) {
            return;
        }

        new TrackedBukkitRunnable() {
            @Override
            public void run() {
                Updater updater = new Updater(RPGInventory.instance, Updater.UpdateType.NO_DOWNLOAD);
                if (updater.getResult() == Updater.UpdateResult.UPDATE_AVAILABLE) {
                    String[] lines = {
                            StringUtils.coloredLine("&3=================&b[&eRPGInventory&b]&3==================="),
                            StringUtils.coloredLine("&6New version available: &a" + updater.getLatestName() + "&6!"),
                            StringUtils.coloredLine(updater.getDescription()),
                            StringUtils.coloredLine("&6Changelog: &e" + updater.getInfoLink()),
                            StringUtils.coloredLine("&6Download it on &eSpigotMC&6!"),
                            StringUtils.coloredLine("&3==================================================")
                    };

                    for (String line : lines) {
                        if (player == null) {
                            StringUtils.coloredConsole(line);
                        } else {
                            PlayerUtils.sendMessage(player, line);
                        }
                    }
                }
            }
        }.runTaskAsynchronously(RPGInventory.getInstance());
    }

    private void updateConfig() {
        final Version version = Version.parseVersion(this.getDescription().getVersion());

        if (!Config.getConfig().contains("version")) {
            Config.getConfig().set("version", version.toString());
            Config.save();
            return;
        }

        final Version configVersion = Version.parseVersion(Config.getConfig().getString("version"));
        if (version.compareTo(configVersion) > 0) {
            ConfigUpdater.update(configVersion);
            Config.getConfig().set("version", null);
            Config.getConfig().set("version", version.toString());
            Config.save();
        }
    }

    @NotNull
    public Path getDataPath() {
        return getDataFolder().toPath();
    }
} @SuppressWarnings({"unused"})
public class InventoryAPI {
    /**
     * Checks if opened inventory is RPGInventory.
     *
     * @param inventory - opened inventory
     * @return true - if opened RPGInventory, false - otherwise
     */
    public static boolean isRPGInventory(@NotNull Inventory inventory) {
        InventoryHolder holder = inventory.getHolder();
        return holder instanceof PlayerWrapper;
    }

    /**
     * Get all passive item from RPGInventory of specific player.
     *
     * @param player - the player
     * @return List of not null passive item
     */
    @NotNull
    public static List<ItemStack> getPassiveItems(@Nullable Player player) {
        List<ItemStack> passiveItems = new ArrayList<>();

        if (!InventoryManager.playerIsLoaded(player)) {
            return passiveItems;
        }

        Inventory inventory = InventoryManager.get(player).getInventory();
        for (Slot slot : SlotManager.instance().getPassiveSlots()) {
            for (int slotId : slot.getSlotIds()) {
                ItemStack item = inventory.getItem(slotId);
                if (ItemUtils.isNotEmpty(item) && !InventoryManager.isEmptySlot(item)) {
                    passiveItems.add(item);
                }
            }
        }

        return passiveItems;
    }

    /**
     * Get all active item from RPGInventory of specific player.
     *
     * @param player - the player
     * @return List of not null active item
     */
    @NotNull
    public static List<ItemStack> getActiveItems(@Nullable Player player) {
        List<ItemStack> activeItems = new ArrayList<>();

        if (!InventoryManager.playerIsLoaded(player)) {
            return activeItems;
        }

        Inventory inventory = InventoryManager.get(player).getInventory();
        for (Slot slot : SlotManager.instance().getActiveSlots()) {
            ItemStack item = inventory.getItem(slot.getSlotId());

            if (ItemUtils.isNotEmpty(item) && !InventoryManager.isQuickEmptySlot(item)) {
                activeItems.add(item);
            }
        }

        return activeItems;
    }
} public class PetEquipEvent extends PlayerEvent implements Cancellable {
    private static final HandlerList handlers = new HandlerList();
    private final ItemStack petItem;

    private boolean cancelled;

    public PetEquipEvent(Player player, ItemStack petItem) {
        super(player);
        this.petItem = petItem;
    }

    @NotNull
    @SuppressWarnings("unused")
    public static HandlerList getHandlerList() {
        return handlers;
    }

    public ItemStack getPetItem() {
        return petItem;
    }

    @NotNull
    @Override
    public HandlerList getHandlers() {
        return handlers;
    }

    @Override
    public boolean isCancelled() {
        return this.cancelled;
    }

    @Override
    public void setCancelled(boolean cancelled) {
        this.cancelled = cancelled;
    }
} public class PetUnequipEvent extends PlayerEvent {
    private static final HandlerList handlers = new HandlerList();

    public PetUnequipEvent(Player player) {
        super(player);
    }

    @NotNull
    @SuppressWarnings("unused")
    public static HandlerList getHandlerList() {
        return handlers;
    }

    @NotNull
    public HandlerList getHandlers() {
        return handlers;
    }
} public class PlayerInventoryLoadEvent extends PlayerEvent {
    private static final HandlerList handlers = new HandlerList();

    private PlayerInventoryLoadEvent(Player who) {
        super(who);
    }

    @NotNull
    @SuppressWarnings("unused")
    public static HandlerList getHandlerList() {
        return handlers;
    }

    @NotNull
    @Override
    public HandlerList getHandlers() {
        return handlers;
    }

    public static class Pre extends PlayerInventoryLoadEvent implements Cancellable {
        private boolean cancelled = false;

        public Pre(Player who) {
            super(who);
        }

        @Override
        public boolean isCancelled() {
            return this.cancelled;
        }

        @Override
        public void setCancelled(boolean cancelled) {
            this.cancelled = cancelled;
        }
    }

    public static class Post extends PlayerInventoryLoadEvent {
        public Post(Player who) {
            super(who);
        }
    }
} public class PlayerInventoryUnloadEvent extends PlayerEvent {
    private static final HandlerList handlers = new HandlerList();

    private PlayerInventoryUnloadEvent(Player who) {
        super(who);
    }

    @NotNull
    @SuppressWarnings("unused")
    public static HandlerList getHandlerList() {
        return handlers;
    }

    @NotNull
    @Override
    public HandlerList getHandlers() {
        return handlers;
    }

    @SuppressWarnings("unused")
    private static class Pre extends PlayerInventoryUnloadEvent {
        public Pre(Player who) {
            super(who);
        }
    }

    public static class Post extends PlayerInventoryUnloadEvent {
        public Post(Player who) {
            super(who);
        }
    }
} public class InventoryListener implements Listener {

    private final static int QUICKBAR_SIZE = 9;

    @EventHandler(priority = EventPriority.MONITOR)
    public void onPlayerJoin(@NotNull final PlayerJoinEvent event) {
        final Player player = event.getPlayer();

        if (InventoryManager.isAllowedWorld(player.getWorld())) {
            InventoryManager.initPlayer(player, false);
        }
    }

    @EventHandler
    public void onPlayerQuit(@NotNull PlayerQuitEvent event) {
        InventoryManager.unloadPlayerInventory(event.getPlayer());
    }

    @EventHandler(priority = EventPriority.HIGHEST)
    public void onLoadInventory(@NotNull PlayerInventoryLoadEvent.Post event) {
        Player player = event.getPlayer();
        ItemManager.updateStats(player);
        PlayerInventory playerInventory = player.getInventory();

        // Sync armor
        playerInventory.setArmorContents(ItemUtils.syncItems(playerInventory.getArmorContents()));

        // Sync inventory
        playerInventory.setContents(ItemUtils.syncItems(playerInventory.getContents()));

        // Sync RPG Inventory
        Inventory inventory = InventoryManager.get(player).getInventory();
        inventory.setContents(ItemUtils.syncItems(inventory.getContents()));
    }

    @EventHandler(priority = EventPriority.LOWEST)
    public void prePlayerRespawn(@NotNull PlayerRespawnEvent event) {
        Player player = event.getPlayer();

        if (InventoryManager.playerIsLoaded(player)) {
            InventoryLocker.lockSlots(player);
        }
    }

    @EventHandler
    public void onQuickSlotHeld(@NotNull PlayerItemHeldEvent event) {
        Player player = event.getPlayer();

        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        int slotId = event.getNewSlot();
        Slot slot = InventoryManager.getQuickSlot(slotId);
        if (slot != null && slot.isCup(player.getInventory().getItem(slotId))) {
            event.setCancelled(true);
            InventoryUtils.heldFreeSlot(player, slotId,
                    (event.getPreviousSlot() + 1) % QUICKBAR_SIZE == slotId ? InventoryUtils.SearchType.NEXT : InventoryUtils.SearchType.PREV);
        }
    }

    @EventHandler
    public void onBreakItem(@NotNull PlayerItemBreakEvent event) {
        this.onItemDisappeared(event, event.getBrokenItem());
    }

    @EventHandler
    public void onDropQuickSlot(@NotNull PlayerDropItemEvent event) {
        this.onItemDisappeared(event, event.getItemDrop().getItemStack());
    }

    private void onItemDisappeared(PlayerEvent event, @NotNull ItemStack item) {
        final Player player = event.getPlayer();
        final PlayerInventory inventory = player.getInventory();
        final int slotId = inventory.getHeldItemSlot();

        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        if (ItemUtils.isEmpty(inventory.getItemInMainHand()) || item.equals(inventory.getItemInMainHand())) {
            final Slot slot = InventoryManager.getQuickSlot(slotId);
            if (slot != null) {
                new TrackedBukkitRunnable() {
                    @Override
                    public void run() {
                        InventoryUtils.heldFreeSlot(player, slotId, InventoryUtils.SearchType.NEXT);
                        inventory.setItem(slotId, slot.getCup());
                    }
                }.runTaskLater(RPGInventory.getInstance(), 1);
            }
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onPickupToQuickSlot(@NotNull EntityPickupItemEvent event) {
        if (event.getEntityType() != EntityType.PLAYER) {
            return;
        }

        Player player = (Player) event.getEntity();
        if (!InventoryManager.playerIsLoaded(player) || !ItemManager.allowedForPlayer(player, event.getItem().getItemStack(), false)) {
            return;
        }

        for (Slot quickSlot : SlotManager.instance().getQuickSlots()) {
            int slotId = quickSlot.getQuickSlot();
            if (quickSlot.isCup(player.getInventory().getItem(slotId)) && quickSlot.isValidItem(event.getItem().getItemStack())) {
                player.getInventory().setItem(slotId, event.getItem().getItemStack());
                event.getItem().remove();

                player.playSound(player.getLocation(), SoundCompat.ITEM_PICKUP.get(), .3f, 1.7f);
                if (Config.getConfig().getBoolean("attack.auto-held")) {
                    player.getInventory().setHeldItemSlot(quickSlot.getQuickSlot());
                }

                event.setCancelled(true);
            }
        }
    }

    @EventHandler(ignoreCancelled = true)
    public void onInventoryDrag(@NotNull InventoryDragEvent event) {
        Player player = (Player) event.getWhoClicked();

        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        for (Integer rawSlotId : event.getRawSlots()) {
            ItemStack cursor = event.getOldCursor();
            Inventory inventory = event.getInventory();

            if (PetManager.isPetItem(cursor)) {
                event.setCancelled(true);
                return;
            }

            if (inventory.getType() == InventoryType.CRAFTING) {
                if (InventoryManager.get(player).isOpened()) {
                    return;
                }

                final boolean isCraftSlot = rawSlotId >= 1 && rawSlotId <= 4;
                if (rawSlotId == Slot.SHIELD_RAW_SLOT_ID
                        || isCraftSlot && Config.craftSlotsAction == VanillaSlotAction.RPGINV) {
                    event.setCancelled(true);
                    return;
                }
            } else if (InventoryAPI.isRPGInventory(inventory)) {
                if (rawSlotId < 54) {
                    event.setCancelled(true);
                    return;
                }
            }
        }
    }

    @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
    public void onInventoryClick(@NotNull final InventoryClickEvent event) {
        final Player player = (Player) event.getWhoClicked();

        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        final InventoryType.SlotType slotType = event.getSlotType();
        if (slotType == InventoryType.SlotType.OUTSIDE) {
            return;
        }

        final InventoryAction action = event.getAction();
        if ((action == InventoryAction.HOTBAR_SWAP || action == InventoryAction.HOTBAR_MOVE_AND_READD)
                && SlotManager.instance().getSlot(event.getHotbarButton(), InventoryType.SlotType.QUICKBAR) != null) {
            event.setCancelled(true);
            return;
        }

        // Crafting inventory is Player's vanilla inventory
        final Inventory inventory = event.getInventory();
        final int rawSlot = event.getRawSlot();
        if (inventory.getType() == InventoryType.CRAFTING) {
            PlayerWrapper playerWrapper = InventoryManager.get(player);

            if (playerWrapper.isOpened()) {
                return;
            }

            boolean openRpgInventory = false;
            switch (slotType) {
                case CRAFTING:
                case RESULT:
                    openRpgInventory = Config.craftSlotsAction == VanillaSlotAction.RPGINV;
                    break;
                case QUICKBAR:
                    openRpgInventory = rawSlot == Slot.SHIELD_RAW_SLOT_ID && Config.armorSlotsAction == VanillaSlotAction.RPGINV;
                    break;
                case ARMOR:
                    openRpgInventory = Config.armorSlotsAction == VanillaSlotAction.RPGINV;
            }

            if (openRpgInventory) {
                playerWrapper.openInventoryDeferred(true);
                event.setCancelled(true);
                return;
            }
        }

        // In RPG Inventory or quick slot
        final Slot slot = SlotManager.instance().getSlot(event.getSlot(), slotType);
        final boolean isRpgInventory = InventoryAPI.isRPGInventory(inventory);
        final boolean isQuickSlot = slotType == InventoryType.SlotType.QUICKBAR && slot != null
                && (slot.isQuick() || slot.getSlotType() == Slot.SlotType.SHIELD);
        if ((isRpgInventory || isQuickSlot) && player.getGameMode() != GameMode.CREATIVE) {
            final boolean isInInventory = rawSlot < inventory.getSize();
            if (isInInventory && slot == null || action == InventoryAction.MOVE_TO_OTHER_INVENTORY) {
                event.setCancelled(true);
                return;
            }

            if (slot == null || !isQuickSlot && !isInInventory) {
                return;
            }

            PlayerWrapper playerWrapper = null;
            if (isRpgInventory) {
                playerWrapper = (PlayerWrapper) inventory.getHolder();

                // Check flying
                if (playerWrapper != null && playerWrapper.isFlying()) {
                    PlayerUtils.sendMessage(player, RPGInventory.getLanguage().getMessage("error.fall"));
                    event.setCancelled(true);
                    return;
                }
            }

            final ItemStack currentItem = event.getCurrentItem();
            final ActionType actionType = ActionType.getTypeOfAction(action);

            if (!validateClick(player, playerWrapper, slot, actionType, currentItem, slotType) || slot.getSlotType() == Slot.SlotType.INFO) {
                event.setCancelled(true);
                return;
            }

            if (slot.getSlotType() == Slot.SlotType.ACTION) {
                ((ActionSlot) slot).preformAction(player);
                event.setCancelled(true);
                return;
            }

            final ItemStack cursor = event.getCursor();
            if (playerWrapper != null && slot.getSlotType() == Slot.SlotType.ARMOR) {
                onArmorSlotClick(event, playerWrapper, slot, cursor, currentItem);
                return;
            }

            if (slot.getSlotType() == Slot.SlotType.ACTIVE || slot.getSlotType() == Slot.SlotType.PASSIVE
                    || slot.getSlotType() == Slot.SlotType.SHIELD || slot.getSlotType() == Slot.SlotType.ELYTRA) {
                event.setCancelled(!InventoryManager.validateUpdate(player, actionType, slot, cursor));

                if ((slot.getSlotType() == Slot.SlotType.SHIELD || slot.getSlotType() == Slot.SlotType.ELYTRA) &&
                        actionType == ActionType.DROP) {
                    event.setCancelled(true);
                }
            } else if (slot.getSlotType() == Slot.SlotType.PET) {
                if (RPGInventory.isMyPetHooked()) {
                    event.setCancelled(!MyPetManager.validatePet(player, action, currentItem, cursor));
                } else {
                    event.setCancelled(!InventoryManager.validatePet(player, action, currentItem, cursor));
                }
            } else if (slot.getSlotType() == Slot.SlotType.BACKPACK) {
                if (event.getClick() == ClickType.RIGHT && BackpackManager.open(player, currentItem)) {
                    event.setCancelled(true);
                } else if (actionType != ActionType.GET) {
                    event.setCancelled(!BackpackManager.isBackpack(cursor));
                }
            }

            if (!event.isCancelled()) {
                BukkitRunnable cupPlacer = new TrackedBukkitRunnable() {
                    @Override
                    public void run() {
                        ItemStack currentItem = inventory.getItem(rawSlot);
                        if (currentItem == null || currentItem.getType() == Material.AIR) {
                            inventory.setItem(rawSlot, slot.getCup());
                            player.updateInventory();
                        }
                    }
                };

                if (slot.isQuick()) {
                    InventoryManager.updateQuickSlot(player, inventory, slot, event.getSlot(), slotType,
                            action, currentItem, cursor);
                    event.setCancelled(true);
                } else if (slot.getSlotType() == Slot.SlotType.SHIELD) {
                    InventoryManager.updateShieldSlot(player, inventory, slot, event.getSlot(), slotType,
                            action, currentItem, cursor);
                    event.setCancelled(true);

                    if (isRpgInventory && (actionType == ActionType.GET || actionType == ActionType.DROP)) {
                        cupPlacer.runTaskLater(RPGInventory.getInstance(), 1);
                    }
                } else if (actionType == ActionType.GET || actionType == ActionType.DROP) {
                    cupPlacer.runTaskLater(RPGInventory.getInstance(), 1);
                } else if (slot.isCup(currentItem)) {
                    event.setCurrentItem(null);
                }
            }
        }
    }

    /**
     * Check
     *
     * @return Click is valid
     */
    private boolean validateClick(Player player, @Nullable PlayerWrapper playerWrapper, @NotNull Slot slot,
                                  ActionType actionType, ItemStack currentItem, InventoryType.SlotType slotType) {
        if (playerWrapper != null) {
            if (player != playerWrapper.getPlayer()) {
                return false;
            }

            if (!PlayerUtils.checkLevel(player, slot.getRequiredLevel())) {
                PlayerUtils.sendMessage(player, RPGInventory.getLanguage().getMessage("error.level", slot.getRequiredLevel()));
                return false;
            }

            if (!slot.isFree() && !playerWrapper.isBuyedSlot(slot.getName()) && !InventoryManager.buySlot(player, playerWrapper, slot)) {
                return false;
            }
        }

        return !((actionType == ActionType.GET && slot.getSlotType() != Slot.SlotType.ACTION
                || actionType == ActionType.DROP) && slot.isCup(currentItem) && slotType != InventoryType.SlotType.QUICKBAR);
    }

    private void onArmorSlotClick(InventoryClickEvent event, PlayerWrapper playerWrapper, @NotNull final Slot slot,
                                  @NotNull ItemStack cursor, ItemStack currentItem) {
        final Player player = playerWrapper.getPlayer().getPlayer();

        final Inventory inventory = event.getInventory();
        final int rawSlot = event.getRawSlot();
        InventoryAction action = event.getAction();
        ActionType actionType = ActionType.getTypeOfAction(action);

        if (InventoryManager.validateArmor(player, action, slot, cursor) && playerWrapper.getInventoryView() != null) {
            // Event of equip armor
            InventoryClickEvent fakeEvent = new InventoryClickEvent(playerWrapper.getInventoryView(),
                    InventoryType.SlotType.ARMOR, InventoryUtils.getArmorSlotId(slot), event.getClick(), action);
            Bukkit.getPluginManager().callEvent(fakeEvent);

            if (fakeEvent.isCancelled()) {
                event.setCancelled(true);
                return;
            }

            InventoryManager.updateArmor(player, inventory, slot, rawSlot, action, currentItem, cursor);

            if (actionType == ActionType.GET) {
                inventory.setItem(rawSlot, slot.getCup());
            } else if (slot.isCup(currentItem)) {
                player.setItemOnCursor(new ItemStack(Material.AIR));
            }

            player.updateInventory();
        }

        if (actionType == ActionType.DROP) {
            new TrackedBukkitRunnable() {
                @Override
                public void run() {
                    inventory.setItem(rawSlot, slot.getCup());
                    player.updateInventory();
                }
            }.runTaskLater(RPGInventory.getInstance(), 1);
        } else {
            event.setCancelled(true);
        }
    }

    @EventHandler(priority = EventPriority.LOW)
    public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
        Inventory inventory = event.getInventory();
        HumanEntity player = event.getPlayer();

        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        if (InventoryAPI.isRPGInventory(inventory)) {
            PlayerWrapper playerWrapper = (PlayerWrapper) inventory.getHolder();
            InventoryManager.syncQuickSlots(playerWrapper);
            InventoryManager.syncInfoSlots(playerWrapper);
            InventoryManager.syncShieldSlot(playerWrapper);
            InventoryManager.syncArmor(playerWrapper);
        }
    }

    @EventHandler
    public void onInventoryClose(@NotNull InventoryCloseEvent event) {
        if (InventoryAPI.isRPGInventory(event.getInventory())) {
            PlayerWrapper playerWrapper = (PlayerWrapper) event.getInventory().getHolder();
            if (playerWrapper == null || event.getPlayer() != playerWrapper.getPlayer()) {
                return;
            }

            playerWrapper.onClose();
        }
    }

    @EventHandler(priority = EventPriority.LOWEST)
    public void onWorldChanged(@NotNull PlayerChangedWorldEvent event) {
        Player player = event.getPlayer();
        if (!InventoryManager.playerIsLoaded(player)) {
            if (InventoryManager.isAllowedWorld(player.getWorld())) {
                InventoryManager.initPlayer(player, InventoryManager.isAllowedWorld(event.getFrom()));
            }

            return;
        }

        if (!InventoryManager.isAllowedWorld(player.getWorld())) {
            InventoryManager.unloadPlayerInventory(player);
        } else if (InventoryManager.get(player).hasPet()) {
            PetManager.respawnPet(player);
        }
    }
} public class Slot {

    public static final int SHIELD_RAW_SLOT_ID = 45;
    private static final int SHIELD_SLOT_ID = 40;

    private final String name;
    @NotNull
    private final SlotType slotType;

    private final List<String> allowed = new ArrayList<>();
    private final List<String> denied = new ArrayList<>();

    @NotNull
    private final List<Integer> slotIds;
    @NotNull
    private final ItemStack cup;
    private final int requiredLevel;
    private final int cost;
    private final int quickSlot;
    private final boolean drop;

    public Slot(String name, @NotNull SlotType slotType, @NotNull ConfigurationSection config) {
        this.name = name;

        this.slotType = slotType;
        final List<Integer> slotList = config.getIntegerList("slot");
        this.slotIds = slotList.isEmpty() ? Collections.singletonList(config.getInt("slot")) : slotList;
        this.requiredLevel = config.getInt("cost.required-level", 0);
        this.cost = config.getInt("cost.money", 0);
        this.drop = config.getBoolean("drop", true);

        int quickSlot = config.contains("quickbar") ? InventoryUtils.getQuickSlot(config.getInt("quickbar")) : -1;
        if (slotType == SlotType.SHIELD) {
            quickSlot = SHIELD_SLOT_ID;
        } else if (slotType != SlotType.ACTIVE && quickSlot == -1 || !slotType.isAllowQuick() || slotIds.size() > 1) {
            if (config.contains("quickbar")) {
                Log.w("Option \"quickbar\" is ignored for slot \"{0}\"!", name);
            }
            quickSlot = -1;
        }
        this.quickSlot = quickSlot;

        if (slotType.isReadItemList()) {
            for (String item : config.getStringList("items")) {
                if (item.startsWith("-")) {
                    this.denied.add(item.substring(1));
                } else {
                    this.allowed.add(item);
                }
            }
        } else {
            if (config.contains("items")) {
                Log.w("Option \"items\" is ignored for slot \"{0}\"!", name);
            }

            if (slotType == SlotType.ELYTRA) {
                this.allowed.add("ELYTRA");
            }
        }

        // Setup cup slot
        Texture texture = Texture.parseTexture(config.getString("holder.item"));
        ItemStack cup = texture.getItemStack();
        ItemMeta meta = cup.getItemMeta();
        if (meta != null) {
            meta.setDisplayName(config.contains("holder.name") ? StringUtils.coloredLine(config.getString("holder.name")) : "[Holder name missing]");
            meta.setLore(config.contains("holder.lore") ? StringUtils.coloredLines(config.getStringList("holder.lore")) : Collections.singletonList("[Holder lore missing]"));
            cup.setItemMeta(meta);
        }
        this.cup = cup;
    }

    private static boolean searchItem(List<String> materialList, @NotNull ItemStack itemStack) {
        for (String material : materialList) {
            String[] data = material.split(":");

            if (material.equals("ALL")) {
                return true;
            }

            if (!itemStack.getType().name().equals(data[0])) {
                continue;
            }

            if (data.length > 1) {
                String[] borders = data[1].split("-");
                int itemTextureData = ItemUtils.getTextureData(itemStack);

                if (borders.length == 1 && itemTextureData != Integer.parseInt(data[1])) {
                    continue;
                } else if (borders.length == 2) {
                    int min = Integer.parseInt(borders[0]);
                    int max = Integer.parseInt(borders[1]);

                    if (min > max) {
                        int temp = max;
                        max = min;
                        min = temp;
                    }

                    if (itemTextureData < min || itemTextureData > max) {
                        continue;
                    }
                }
            }

            return true;
        }

        return false;
    }

    @NotNull
    public ItemStack getCup() {
        return this.cup.clone();
    }

    public boolean isCup(@Nullable ItemStack itemStack) {
        return this.cup.equals(itemStack);
    }

    boolean containsSlot(int slot) {
        return this.slotIds.contains(slot);
    }

    public boolean isValidItem(@Nullable ItemStack itemStack) {
        return ItemUtils.isNotEmpty(itemStack) && !this.isDenied(itemStack) && this.isAllowed(itemStack);
    }

    public boolean isFree() {
        return this.cost == 0;
    }

    public List<String> getAllowedItems() {
        return allowed;
    }

    public List<String> getDeniedItems() {
        return denied;
    }

    boolean itemListIsEmpty() {
        return this.allowed.isEmpty() && this.denied.isEmpty();
    }

    private boolean isDenied(@NotNull ItemStack item) {
        return searchItem(this.denied, item);
    }

    private boolean isAllowed(@NotNull ItemStack item) {
        return searchItem(this.allowed, item);
    }

    @NotNull
    public SlotType getSlotType() {
        return slotType;
    }

    public String getName() {
        return name;
    }

    public int getRequiredLevel() {
        return requiredLevel;
    }

    public int getSlotId() {
        return slotIds.get(0);
    }

    public int getQuickSlot() {
        return this.quickSlot;
    }

    public boolean isQuick() {
        return this.quickSlot != -1 && this.slotType != SlotType.SHIELD;
    }

    @NotNull
    public List<Integer> getSlotIds() {
        return slotIds;
    }

    public int getCost() {
        return cost;
    }

    public boolean isDrop() {
        return drop;
    }

    @SuppressWarnings("unused")
    public enum SlotType {
        GENERIC(true, true, false, true),
        ACTION(false, true, false, false),
        PET(false, false, true, false),
        ARMOR(false, false, false, true),
        ACTIVE(true, false, false, true),
        BACKPACK(true, false, false, false),
        PASSIVE(true, true, false, true),
        SHIELD(false, false, true, true),
        ELYTRA(false, false, true, false),
        INFO(false, false, false, false);

        private final boolean allowQuick;
        private final boolean allowMultiSlots;
        private final boolean unique;
        private final boolean readItemList;

        SlotType(boolean allowQuick, boolean allowMultiSlots, boolean unique, boolean readItemList) {
            this.allowQuick = allowQuick;
            this.allowMultiSlots = allowMultiSlots;
            this.unique = unique;
            this.readItemList = readItemList;
        }

        public boolean isAllowQuick() {
            return allowQuick;
        }

        public boolean isAllowMultiSlots() {
            return allowMultiSlots;
        }

        public boolean isUnique() {
            return unique;
        }

        public boolean isReadItemList() {
            return readItemList;
        }
    }
} public class SlotManager {

    private static final String CONFIG_NAME = "slots.yml";

    @Nullable
    private static SlotManager slotManager = null;

    private final List<Slot> slots = new ArrayList<>();

    @NotNull
    private final Path slotsFile;
    @NotNull
    private final FileConfiguration slotsConfig;

    private SlotManager() {
        this.slotsFile = RPGInventory.getInstance().getDataPath().resolve(CONFIG_NAME);
        if (Files.notExists(slotsFile)) {
            RPGInventory.getInstance().saveResource(CONFIG_NAME, false);
        }

        this.slotsConfig = YamlConfiguration.loadConfiguration(slotsFile.toFile());

        @Nullable final ConfigurationSection slots = this.slotsConfig.getConfigurationSection("slots");
        if (slots == null) {
            Log.s("Section ''slots'' not found in {0}", CONFIG_NAME);
            return;
        }

        for (String slotName : slots.getKeys(false)) {
            final ConfigurationSection slotConfiguration = slots.getConfigurationSection(slotName);
            assert slotConfiguration != null;

            Slot.SlotType slotType = SafeEnums.valueOfOrDefault(Slot.SlotType.class, slotConfiguration.getString("type"), Slot.SlotType.GENERIC, "slot type");
            Slot slot;
            if (slotType == Slot.SlotType.ACTION) {
                slot = new ActionSlot(slotName, slotConfiguration);
            } else {
                slot = new Slot(slotName, slotType, slotConfiguration);
            }

            if (this.validateSlot(slot)) {
                this.slots.add(slot);
            } else {
                Log.w("Slot \"{0}\" was not been added.", slot.getName());
            }
        }
    }

    public static boolean init() {
        try {
            SlotManager.slotManager = new SlotManager();
        } catch (Exception e) {
            Log.w(e, "Failed to initialize SlotManager");
            return false;
        }

        return true;
    }

    @NotNull
    public static SlotManager instance() {
        return Objects.requireNonNull(SlotManager.slotManager, "Plugin is not initialized yet");
    }

    private boolean validateSlot(Slot slot) {
        if (slot.getSlotType().isReadItemList() && slot.itemListIsEmpty()) {
            Log.w("Slot with type {0} must contains list of allowed items", slot.getSlotType());
        }

        if (!validateItems(slot.getAllowedItems()) || !validateItems(slot.getDeniedItems())) {
            return false;
        }

        if (!slot.getSlotType().isAllowMultiSlots() && slot.getSlotIds().size() > 1) {
            Log.w("Slot with type {0} can not contain more than one slotId", slot.getSlotType());
            return false;
        }

        for (int slotId : slot.getSlotIds()) {
            if (slotId < 0 || slotId > 53) {
                Log.w("Slot IDs should be in range 0..53, but it was {0}", slotId);
                return false;
            }
        }

        for (Slot existingSlot : this.slots) {
            for (int slotId : slot.getSlotIds()) {
                if (existingSlot.getSlotIds().contains(slotId)) {
                    Log.w("Slot {0} is occupied by {1}", slotId, existingSlot.getName());
                    return false;
                }
            }

            if (slot.isQuick() && existingSlot.isQuick()) {
                int slotId = slot.getQuickSlot();
                int existingSlotId = existingSlot.getQuickSlot();
                if (slotId == existingSlotId) {
                    Log.w("Quickbar slot {0} is occupied by {1}", slotId, existingSlot.getName());
                    return false;
                }
            }

            if (slot.getSlotType().isUnique() && slot.getSlotType() == existingSlot.getSlotType()) {
                Log.w("You can not create more then one slot with type {0}!", slot.getSlotType());
                return false;
            }
        }

        return true;
    }

    private boolean validateItems(List<String> itemsPatterns) {
        for (String itemPattern : itemsPatterns) {
            if (!itemPattern.matches("^[\\w_]+(:\\d+(-\\d)?)?$")) {
                Log.w("Allowed and denied items should fit to pattern ''[string]:[number]-[number]''");
                Log.w("But it was: {0}", itemPattern);
                return false;
            }
        }
        return true;
    }

    @Nullable
    public Slot getSlot(String name) {
        for (Slot slot : this.slots) {
            if (slot.getName().equalsIgnoreCase(name)) {
                return slot;
            }
        }

        return null;
    }

    @Nullable
    public Slot getSlot(int slotId, InventoryType.SlotType slotType) {
        for (Slot slot : this.slots) {
            if (slotType == InventoryType.SlotType.QUICKBAR) {
                if ((slot.isQuick() || slot.getSlotType() == Slot.SlotType.SHIELD) && slot.getQuickSlot() == slotId) {
                    return slot;
                }
            } else if (slot.containsSlot(slotId)) {
                return slot;
            }
        }

        return null;
    }

    @NotNull
    public List<Slot> getQuickSlots() {
        List<Slot> quickSlots = new ArrayList<>();
        for (Slot slot : this.slots) {
            if (slot.isQuick()) {
                quickSlots.add(slot);
            }
        }

        return quickSlots;
    }

    @NotNull
    public List<Slot> getPassiveSlots() {
        List<Slot> passiveSlots = new ArrayList<>();
        for (Slot slot : this.slots) {
            final Slot.SlotType type = slot.getSlotType();
            if (type == Slot.SlotType.PASSIVE || type == Slot.SlotType.BACKPACK || type == Slot.SlotType.ELYTRA) {
                passiveSlots.add(slot);
            }
        }

        return passiveSlots;
    }

    @NotNull
    public List<Slot> getActiveSlots() {
        List<Slot> activeSlots = new ArrayList<>();
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.ACTIVE) {
                activeSlots.add(slot);
            }
        }

        return activeSlots;
    }

    @NotNull
    public List<Slot> getArmorSlots() {
        List<Slot> armorSlots = new ArrayList<>(4);
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.ARMOR) {
                armorSlots.add(slot);
            }
        }

        return armorSlots;
    }

    @NotNull
    public List<Slot> getInfoSlots() {
        List<Slot> infoSlots = new ArrayList<>();
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.INFO) {
                infoSlots.add(slot);
            }
        }

        return infoSlots;
    }

    @NotNull
    public List<Slot> getSlots() {
        return this.slots;
    }

    @Nullable
    public Slot getPetSlot() {
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.PET) {
                return slot;
            }
        }

        return null;
    }

    @Nullable
    public Slot getShieldSlot() {
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.SHIELD) {
                return slot;
            }
        }

        return null;
    }

    @Nullable
    public Slot getBackpackSlot() {
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.BACKPACK) {
                return slot;
            }
        }

        return null;
    }

    @Nullable
    public Slot getElytraSlot() {
        for (Slot slot : this.slots) {
            if (slot.getSlotType() == Slot.SlotType.ELYTRA) {
                return slot;
            }
        }

        return null;
    }

    public void saveDefaults() {
        try {
            this.slotsConfig.save(this.slotsFile.toFile());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
} public class ItemManager {

    private static final String CONFIG_NAME = "items.yml";

    private static final Map<String, CustomItem> CUSTOM_ITEMS = new HashMap<>();
    private static final List<String> LORE_PATTERN = Config.getConfig().getStringList("items.lore-pattern");
    private static final String LORE_SEPARATOR = Config.getConfig().getString("items.separator");

    private ItemManager() {
    }

    public static boolean init(@NotNull RPGInventory instance) {
        try {
            Path itemsFile = RPGInventory.getInstance().getDataPath().resolve(CONFIG_NAME);
            if (Files.notExists(itemsFile)) {
                RPGInventory.getInstance().saveResource(CONFIG_NAME, false);
            }

            FileConfiguration itemsConfig = YamlConfiguration.loadConfiguration(itemsFile.toFile());

            @Nullable final ConfigurationSection items = itemsConfig.getConfigurationSection("items");
            if (items == null) {
                Log.s("Section ''items'' not found in {0}", CONFIG_NAME);
                return false;
            }

            CUSTOM_ITEMS.clear();
            for (String key : items.getKeys(false)) {
                ConfigurationSection section = items.getConfigurationSection(key);
                if (section != null) {
                    tryToAddItem(key, section);
                }
            }

            if (CUSTOM_ITEMS.isEmpty()) {
                Log.i("No one configured item found");
                return false;
            }

            Log.i("{0} item(s) has been loaded", CUSTOM_ITEMS.size());

            instance.getServer().getPluginManager().registerEvents(new ItemListener(), instance);
            return true;
        } catch (Exception e) {
            instance.getReporter().report("Error on InventoryManager initialization", e);
            return false;
        }
    }

    private static void tryToAddItem(String name, @NotNull ConfigurationSection config) {
        try {
            Texture texture = Texture.parseTexture(config.getString("texture"));
            if (texture.isEmpty()) {
                Log.s("Item ''{0}'' has not been added because its texture is not valid.", name);
                return;
            }
            CustomItem customItem = new CustomItem(name, texture, config);
            CUSTOM_ITEMS.put(name, customItem);
        } catch (Exception e) {
            Log.s("Item ''{0}'' can''t be added: {1}", name, e.toString());
            Log.d(e);
        }
    }

    public static Modifier getModifier(Player player, ItemStat.StatType statType) {
        return getModifier(player, statType, false);
    }

    @SuppressWarnings("SameParameterValue")
    private static Modifier getModifier(Player player, ItemStat.StatType statType, boolean notifyPlayer) {
        List<ItemStack> effectiveItems = InventoryUtils.collectEffectiveItems(player, notifyPlayer);
        double minBonus = 0;
        double maxBonus = 0;
        float minMultiplier = 1;
        float maxMultiplier = 1;
        for (ItemStack item : effectiveItems) {
            CustomItem customItem;
            ItemStat stat;
            if (!CustomItem.isCustomItem(item) || (customItem = ItemManager.getCustomItem(item)) == null
                    || (stat = customItem.getStat(statType)) == null) {
                continue;
            }

            int sign = stat.getOperationType() == ItemStat.OperationType.MINUS ? -1 : 1;
            if (stat.isPercentage()) {
                minMultiplier += sign * stat.getMinValue() / 100;
                maxMultiplier += sign * stat.getMaxValue() / 100;
            } else {
                minBonus += sign * stat.getMinValue();
                maxBonus += sign * stat.getMaxValue();
            }
        }

        return new Modifier(minBonus, maxBonus, minMultiplier, maxMultiplier);
    }

    @NotNull
    public static List<String> getItemList() {
        return new ArrayList<>(CUSTOM_ITEMS.keySet());
    }

    public static boolean hasItem(String itemId) {
        return CUSTOM_ITEMS.containsKey(itemId);
    }

    @NotNull
    public static ItemStack getItem(String itemId) {
        CustomItem customItem = CUSTOM_ITEMS.get(itemId);
        return customItem == null ? new ItemStack(Material.AIR) : customItem.getItemStack();
    }

    @Nullable
    public static CustomItem getCustomItem(@Nullable ItemStack item) {
        if (ItemUtils.isEmpty(item)) {
            return null;
        }

        String tag = ItemUtils.getTag(item, ItemUtils.ITEM_TAG);
        if (tag.isEmpty()) {
            return null;
        }

        return CUSTOM_ITEMS.get(tag);
    }

    public static boolean allowedForPlayer(Player player, ItemStack item, boolean notifyPlayer) {
        ClassedItem classedItem;
        if (CustomItem.isCustomItem(item)) {
            classedItem = ItemManager.getCustomItem(item);
        } else if (PetType.isPetItem(item)) {
            classedItem = PetManager.getPetFromItem(item);
        } else {
            return true;
        }

        if (classedItem == null) {
            return true;
        }

        if (!PlayerUtils.checkLevel(player, classedItem.getLevel())) {
            if (notifyPlayer) {
                PlayerUtils.sendMessage(
                        player, RPGInventory.getLanguage().getMessage("error.item.level", classedItem.getLevel())
                );
            }

            return false;
        }

        if (classedItem.getClasses() == null || PlayerUtils.checkClass(player, classedItem.getClasses())) {
            return true;
        }

        if (notifyPlayer) {
            PlayerUtils.sendMessage(
                    player, RPGInventory.getLanguage().getMessage("error.item.class", classedItem.getClassesString())
            );
        }

        return false;
    }

    public static void updateStats(final Player player) {
        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        InventoryManager.get(player).updateStatsLater();
    }

    @NotNull
    static List<String> buildLore(@NotNull CustomItem item) {
        FileLanguage lang = RPGInventory.getLanguage();
        List<String> lore = new ArrayList<>();
        boolean lastIsSeparator = false;
        for (String loreElement : LORE_PATTERN) {
            switch (loreElement) {
                case "_UNBREAKABLE_":
                    if (item.isUnbreakable()) {
                        lore.add(lang.getMessage("item.unbreakable"));
                        lastIsSeparator = false;
                    }
                    break;
                case "_DROP_":
                    if (!item.isDrop()) {
                        lore.add(lang.getMessage("item.nodrop"));
                        lastIsSeparator = false;
                    }
                    break;
                case "_SEPARATOR_":
                    if (!lastIsSeparator) {
                        lore.add(LORE_SEPARATOR);
                        lastIsSeparator = true;
                    }
                    break;
                case "_LEVEL_":
                    if (item.getLevel() != -1) {
                        lore.add(lang.getMessage("item.level", item.getLevel()));
                        lastIsSeparator = false;
                    }
                    break;
                case "_CLASS_":
                    if (item.getClasses() != null) {
                        lore.add(lang.getMessage("item.class", item.getClassesString()));
                        lastIsSeparator = false;
                    }
                    break;
                case "_LORE_":
                    if (item.getLore() != null) {
                        lore.addAll(item.getLore());
                        lastIsSeparator = false;
                    }
                    break;
                case "_SKILLS_":
                    if (item.hasLeftClickCaption()) {
                        lore.add(lang.getMessage("item.left-click", item.getLeftClickCaption()));
                        lastIsSeparator = false;
                    }
                    if (item.hasRightClickCaption()) {
                        lore.add(lang.getMessage("item.right-click", item.getRightClickCaption()));
                        lastIsSeparator = false;
                    }
                    break;
                case "_STATS_":
                    if (item.isStatsHidden()) {
                        lore.add(lang.getMessage("item.hide"));
                        lastIsSeparator = false;
                    } else {
                        for (ItemStat stat : item.getStats()) {
                            lore.add(lang.getMessage("stat." + stat.getType().name().toLowerCase(), stat.getStringValue()));
                            lastIsSeparator = false;
                        }
                    }
                    break;
                default:
                    lore.add(StringUtils.coloredLine(loreElement));
            }
        }

        if (lastIsSeparator) {
            lore.remove(lore.size() - 1);
        }

        if (lore.get(0).equals(LORE_SEPARATOR)) {
            lore.remove(0);
        }

        return lore;
    }
} public class Texture {

    private static final Texture EMPTY_TEXTURE = new Texture(new ItemStack(Material.AIR));

    @NotNull
    private final ItemStack prototype;
    private final int data;

    private Texture(@NotNull ItemStack prototype) {
        this(prototype, (short) -1);
    }

    private Texture(@NotNull ItemStack prototype, int data) {
        this.prototype = prototype;
        this.data = data;
    }

    public boolean isEmpty() {
        return this.equals(EMPTY_TEXTURE);
    }

    @NotNull
    public ItemStack getItemStack() {
        return prototype.clone();
    }

    public int getData() {
        return data;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (!(o instanceof Texture)) {
            return false;
        }

        Texture texture = (Texture) o;
        return prototype.equals(texture.prototype);
    }

    @Override
    public int hashCode() {
        return Objects.hash(prototype);
    }

    public static Texture parseTexture(String texture) {
        if (texture == null) {
            return EMPTY_TEXTURE;
        }

        String[] textureParts = texture.split(":");

        Material material = MaterialCompat.getMaterialOrNull(textureParts[0]);
        if (material == null) {
            Log.w("Unknown material: {0}", textureParts[0]);
            return EMPTY_TEXTURE;
        }

        ItemStack item = ItemUtils.toBukkitItemStack(new ItemStack(material));
        if (ItemUtils.isEmpty(item)) {
            return EMPTY_TEXTURE;
        }

        if (textureParts.length > 1) {
            // MONSTER_EGG before 1.13
            if (material.name().equals("MONSTER_EGG")) {
                return parseLegacyMonsterEgg(item, textureParts[1]);
            } else if (material.name().startsWith("LEATHER_")) {
                return parseLeatherArmor(item, textureParts[1]);
            } else {
                return parseItemWithData(item, textureParts[1]);
            }
        }

        return new Texture(item);
    }

    private static Texture parseLegacyMonsterEgg(ItemStack item, String entityType) {
        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(item);
        nbt.put(ItemUtils.ENTITY_TAG, NbtFactory.ofCompound("temp").put("id", entityType));

        return new Texture(item);
    }

    private static Texture parseLeatherArmor(ItemStack item, String hexColor) {
        try {
            LeatherArmorMeta meta = (LeatherArmorMeta) item.getItemMeta();
            assert meta != null;
            meta.setColor(Color.fromRGB(Integer.parseInt(hexColor, 16)));
            item.setItemMeta(meta);
        } catch (ClassCastException | IllegalArgumentException | NullPointerException e) {
            Log.w("Can''t parse leather color: {0}", e.toString());
        }

        return new Texture(item);
    }

    private static Texture parseItemWithData(ItemStack item, String textureDataValue) {
        if (item.getItemMeta() == null) {
            return new Texture(item);
        }

        int textureData = -1;
        try {
            textureData = Integer.parseInt(textureDataValue);
        } catch (NumberFormatException e) {
            Log.w("Can''t parse texture modifier. Specify a number instead of \"{0}\"", textureData);
        }

        if (Config.texturesType == TexturesType.DAMAGE) {
            return parseItemWithDurability(item, textureData);
        }
        return parseItemWithCustomModelData(item, textureData);
    }

    private static Texture parseItemWithDurability(ItemStack item, int damage) {
        ItemMeta meta = item.getItemMeta();
        assert meta != null;

        meta.addItemFlags(ItemFlag.values());
        if (damage != -1) {
            ((Damageable) meta).setDamage(damage);
            if (ItemUtils.isItemHasDurability(item)) {
                meta.setUnbreakable(true);
            }
        }
        item.setItemMeta(meta);

        return new Texture(item, damage);
    }

    private static Texture parseItemWithCustomModelData(ItemStack item, int customModelData) {
        ItemMeta meta = item.getItemMeta();
        assert meta != null;

        meta.addItemFlags(ItemFlag.values());
        if (customModelData != -1) {
            meta.setCustomModelData(customModelData);
        }
        item.setItemMeta(meta);

        return new Texture(item, customModelData);
    }
} public class Config {

    /* Config options */
    public static VanillaSlotAction craftSlotsAction = VanillaSlotAction.RPGINV;
    public static VanillaSlotAction armorSlotsAction = VanillaSlotAction.DEFAULT;

    public static TexturesType texturesType = TexturesType.DAMAGE;

    private static final FileConfiguration config = new YamlConfiguration();
    private static Path configFile;

    public static void init(RPGInventory plugin) {
        configFile = plugin.getDataPath().resolve("config.yml");
        plugin.saveDefaultConfig();

        final InputStream defaultConfigStream = plugin.getResource("config.yml");
        if (defaultConfigStream != null) {
            loadDefaultConfig(defaultConfigStream);
        }

        reload();
    }

    public static FileConfiguration getConfig() {
        return config;
    }

    public static void reload() {
        try {
            config.load(configFile.toFile());
        } catch (IOException | InvalidConfigurationException e) {
            Log.w(e, "Error on load config.yml");
        }

        copyOptionsFromConfig();
        save();
    }

    public static void save() {
        try {
            config.save(configFile.toFile());
        } catch (IOException e) {
            Log.w(e, "Error on save config.yml");
        }
    }

    private static void loadDefaultConfig(@NotNull InputStream defaultConfigStream) {
        final Path exampleConfigPath = configFile.getParent().resolve("config-example.yml");

        // Update example config
        try (final InputStream stream = defaultConfigStream) {
            Files.copy(stream, exampleConfigPath, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            Log.w(e, "Error on copying config-example.yml");
        }

        config.setDefaults(YamlConfiguration.loadConfiguration(exampleConfigPath.toFile()));
        config.options().copyDefaults(true);
    }

    private static void copyOptionsFromConfig() {
        craftSlotsAction = VanillaSlotAction.parseString(config.getString("craft-slots-action"));
        armorSlotsAction = VanillaSlotAction.parseString(config.getString("armor-slots-action"));

        texturesType = TexturesType.parseString(config.getString("textures-type"));
    }
} public class Serialization {

    private static final String ROOT_TAG = "data";

    public static void registerTypes() {
        ConfigurationSerialization.registerClass(InventorySnapshot.class);
        ConfigurationSerialization.registerClass(SlotSnapshot.class);
        ConfigurationSerialization.registerClass(Backpack.class);
    }

    @Nullable
    public static PlayerWrapper loadPlayerOrNull(Player player, @NotNull Path file) {
        PlayerWrapper playerWrapper;
        try {
            playerWrapper = loadPlayer(player, file);
        } catch (IOException | InvalidConfigurationException e) {
            Log.w(e);
            FileUtils.resolveException(file);
            playerWrapper = null;
        }
        return playerWrapper;
    }

    @NotNull
    private static PlayerWrapper loadPlayer(Player player, @NotNull Path file)
            throws IOException, InvalidConfigurationException {
        PlayerWrapper playerWrapper;
        try {
            InventorySnapshot inventorySnapshot = (InventorySnapshot) load(file);
            playerWrapper = inventorySnapshot.restore(player);
        } catch (InvalidConfigurationException e) {
            if (e.getCause() instanceof ReaderException) {
                Log.w("Can''t load {0}''s inventory. Trying to use legacy loader...", player.getName());
                playerWrapper = LegacySerialization.loadPlayer(player, file);
            } else {
                throw e;
            }
        }
        return playerWrapper;
    }

    public static Backpack loadBackpack(@NotNull Path file) throws IOException, InvalidConfigurationException {
        Backpack backpack;
        try {
            backpack = (Backpack) load(file);
        } catch (InvalidConfigurationException e) {
            if (e.getCause() instanceof ReaderException) {
                Log.w("Can''t load backpack {0}. Trying to use legacy loader...", file.getFileName().toString());
                backpack = LegacySerialization.loadBackpack(file);
            } else {
                throw e;
            }
        }

        return backpack;
    }

    public static void save(@NotNull Object data, @NotNull Path file) throws IOException {
        final FileConfiguration serializedData = new YamlConfiguration();
        serializedData.set(ROOT_TAG, data);

        Path tempFile = Files.createTempFile(file.getParent(), file.getFileName().toString(), null);
        try (OutputStreamWriter stream = new OutputStreamWriter(new GZIPOutputStream(Files.newOutputStream(tempFile)), StandardCharsets.UTF_8)) {
            stream.write(serializedData.saveToString());
        }
        Files.move(tempFile, file, StandardCopyOption.REPLACE_EXISTING);
    }

    @NotNull
    private static Object load(@NotNull Path file)
            throws IOException, InvalidConfigurationException {
        final FileConfiguration serializedData = new YamlConfiguration();
        try (InputStreamReader reader = new InputStreamReader(new GZIPInputStream(Files.newInputStream(file)), StandardCharsets.UTF_8)) {
            serializedData.load(reader);
        }

        return Objects.requireNonNull(serializedData.get(ROOT_TAG), "Serialized data not found");
    }
} public class PetManager {

    private static final String CONFIG_NAME = "pets.yml";

    private static final String METADATA_KEY_PET_OWNER = "rpginventory:petowner";
    private static final Map<String, PetType> PETS = new HashMap<>();
    private static final Map<String, PetFood> PET_FOOD = new HashMap<>();
    private static final String DEATH_TIME_TAG = "pet.deathTime";
    private static CooldownsTimer COOLDOWNS_TIMER;
    private static int SLOT_PET;

    private PetManager() {
    }

    public static boolean init(@NotNull RPGInventory instance) {
        SLOT_PET = SlotManager.instance().getPetSlot() != null ? SlotManager.instance().getPetSlot().getSlotId() : -1;

        if (!PetManager.isEnabled()) {
            Log.i("Slot for pets not found");
            return false;
        }

        try {
            Path dataPath = RPGInventory.getInstance().getDataPath();
            Path petsFile = dataPath.resolve(CONFIG_NAME);
            if (Files.notExists(petsFile)) {
                String suffix = VersionHandler.isLegacy() ? ".legacy" : "";
                String defaultConfigName = CONFIG_NAME + suffix;
                RPGInventory.getInstance().saveResource(defaultConfigName, false);
                if (!suffix.isEmpty()) {
                    Files.move(dataPath.resolve(defaultConfigName), petsFile);
                }
            }

            FileConfiguration petsConfig = YamlConfiguration.loadConfiguration(petsFile.toFile());

            @Nullable final ConfigurationSection pets = petsConfig.getConfigurationSection("pets");
            if (pets == null) {
                Log.s("Section ''pets'' not found in {0}", CONFIG_NAME);
                return false;
            }

            PETS.clear();
            for (String key : pets.getKeys(false)) {
                ConfigurationSection section = pets.getConfigurationSection(key);
                if (section != null) {
                    tryToAddPet(key, section);
                }
            }

            @Nullable final ConfigurationSection food = petsConfig.getConfigurationSection("food");
            if (food == null) {
                Log.s("Section ''food'' not found in {0}", CONFIG_NAME);
            } else {
                PET_FOOD.clear();
                for (String key : food.getKeys(false)) {
                    ConfigurationSection section = food.getConfigurationSection(key);
                    if (section != null) {
                        tryToAddPetFood(key, section);
                    }
                }
            }
        } catch (Exception e) {
            instance.getReporter().report("Error on PetManager initialization", e);
            return false;
        }

        if (PETS.isEmpty()) {
            Log.i("No one configured pet found");
            return false;
        }

        Log.i("{0} pet(s) has been loaded", PetManager.PETS.size());
        Log.i("{0} food(s) has been loaded", PetManager.PET_FOOD.size());

        // Register events
        instance.getServer().getPluginManager().registerEvents(new PetListener(), instance);
        PetManager.COOLDOWNS_TIMER = new CooldownsTimer(instance);
        PetManager.COOLDOWNS_TIMER.runTaskTimer(instance, 20, CooldownsTimer.TICK_PERIOD);
        return true;
    }

    private static void tryToAddPet(String name, @NotNull ConfigurationSection config) {
        try {
            Texture texture = Texture.parseTexture(config.getString("item"));
            if (texture.isEmpty()) {
                Log.s("Pet ''{0}'' has not been added because its item is not valid.", name);
                return;
            }
            PetType petType = new PetType(texture, config);
            PetManager.PETS.put(name, petType);
        } catch (Exception e) {
            Log.s("Pet ''{0}'' can''t be added: {1}", name, e.toString());
            Log.d(e);
        }
    }

    private static void tryToAddPetFood(String name, @NotNull ConfigurationSection config) {
        try {
            Texture texture = Texture.parseTexture(config.getString("item"));
            if (texture.isEmpty()) {
                Log.s("Pet food ''{0}'' has not been added because its item is not valid.", name);
                return;
            }
            PetFood pet = new PetFood(texture, config);
            PetManager.PET_FOOD.put(name, pet);
        } catch (Exception e) {
            Log.s("Pet food ''{0}'' can''t be added: {1}", name, e.toString());
            Log.d(e);
        }
    }

    public static void initPlayer(@NotNull Player player) {
        if (!isEnabled() || !InventoryManager.playerIsLoaded(player)) {
            return;
        }

        respawnPet(player);
    }

    @Contract(pure = true)
    public static boolean isEnabled() {
        return SLOT_PET != -1;
    }

    public static int getPetSlotId() {
        return SLOT_PET;
    }

    @NotNull
    public static List<String> getPetList() {
        return new ArrayList<>(PETS.keySet());
    }

    @NotNull
    public static List<String> getFoodList() {
        return new ArrayList<>(PET_FOOD.keySet());
    }

    public static void startCooldownTimer(Player player, ItemStack petItem) {
        PetManager.COOLDOWNS_TIMER.addPetCooldown(player, petItem);
    }

    public static void teleportPet(@NotNull final Player player, @Nullable final Location to) {
        if (!InventoryManager.playerIsLoaded(player) || !PetManager.isEnabled()) {
            return;
        }

        final LivingEntity petEntity = InventoryManager.get(player).getPet();
        if (petEntity == null) {
            return;
        }

        Location baseLocation = to != null ? to : player.getLocation();
        Location newPetLoc = LocationUtils.getLocationNearPoint(baseLocation, 3);
        petEntity.teleport(newPetLoc);
    }

    /** @see #respawnPet(Player, ItemStack) */
    @Deprecated
    public static void spawnPet(@NotNull final Player player, @NotNull ItemStack petItem) {
        respawnPet(player, petItem);
    }

    public static void respawnPet(@Nullable OfflinePlayer player) {
        if (!InventoryManager.playerIsLoaded(player) || !PetManager.isEnabled()) {
            return;
        }

        Inventory inventory = InventoryManager.get(player).getInventory();
        ItemStack petItem = inventory.getItem(SLOT_PET);
        if (petItem != null) {
            respawnPet((Player) player, petItem);
        }
    }

    public static void respawnPet(@NotNull final Player player, @NotNull ItemStack petItem) {
        if (!InventoryManager.playerIsLoaded(player) || !PetManager.isEnabled()) {
            return;
        }

        PetManager.despawnPet(player);

        final PetType petType = PetManager.getPetFromItem(petItem);
        if (petType == null) {
            return;
        }

        if (PetManager.getCooldown(petItem) > 0) {
            PetManager.startCooldownTimer(player, petItem);
            return;
        }

        Location petLoc = LocationUtils.getLocationNearPoint(player.getLocation(), 3);
        Animals pet = (Animals) player.getWorld().spawnEntity(petLoc, petType.getSkin());
        pet.teleport(petLoc);
        EffectUtils.playSpawnEffect(pet);
        Map<String, String> features = petType.getFeatures();

        boolean hasInitializationErrors = false;
        switch (petType.getRole()) {
            case MOUNT:
                switch (pet.getType()) {
                    case HORSE:
                        Horse horsePet = (Horse) pet;
                        HorseInventory horseInv = horsePet.getInventory();
                        horseInv.setSaddle(new ItemStack(Material.SADDLE));

                        if (features.containsKey("CHEST") && features.get("CHEST").equals("TRUE")) {
                            try {
                                //noinspection deprecation
                                horsePet.setCarryingChest(true);
                            } catch (UnsupportedOperationException ignored) {
                                //org.bukkit.craftbukkit.entity.CraftHorse.setCarryingChest (CraftHorse.class:56)
                                Log.w("Failed to add a chest to the horse");
                            }
                        }

                        if (features.containsKey("ARMOR")) {
                            String materialName = features.get("ARMOR");
                            Material armorMaterial = MaterialCompat.getMaterialOrNull(materialName);
                            if (armorMaterial != null) {
                                horseInv.setArmor(new ItemStack(armorMaterial));
                            } else {
                                Log.w("Failed to add an armor to the horse. Unknown material: {0}", materialName);
                            }
                        }

                        Horse.Color color = SafeEnums.getHorseColor(features.getOrDefault("COLOR", "BROWN"));
                        if (color != null) {
                            horsePet.setColor(color);
                        } else {
                            hasInitializationErrors = true;
                        }

                        Horse.Style style = SafeEnums.getHorseStyle(features.getOrDefault("STYLE", "NONE"));
                        if (style != null) {
                            horsePet.setStyle(style);
                        } else {
                            hasInitializationErrors = true;
                        }

                        break;
                    case PIG:
                        Pig pigPet = (Pig) pet;
                        pigPet.setSaddle(true);
                }

                break;
            case COMPANION:
                switch (pet.getType()) {
                    case WOLF:
                        Wolf wolfPet = (Wolf) pet;
                        DyeColor wolfCollarColor = SafeEnums.getDyeColor(features.getOrDefault("COLLAR", "RED"));
                        if (wolfCollarColor != null) {
                            wolfPet.setCollarColor(wolfCollarColor);
                        } else {
                            hasInitializationErrors = true;
                        }
                        break;
                    case CAT:
                        Cat catPet = (Cat) pet;
                        Cat.Type catType = SafeEnums.getCatType(features.getOrDefault("TYPE", "TABBY"));
                        if (catType != null) {
                            catPet.setCatType(catType);
                        } else {
                            hasInitializationErrors = true;
                        }

                        DyeColor catCollarColor = SafeEnums.getDyeColor(features.getOrDefault("COLLAR", "RED"));
                        if (catCollarColor != null) {
                            catPet.setCollarColor(catCollarColor);
                        } else {
                            hasInitializationErrors = true;
                        }
                }
        }

        if (pet instanceof Tameable) {
            ((Tameable) pet).setTamed(true);
            ((Tameable) pet).setOwner(player);
        }

        pet.setBreed(false);
        if (petType.isAdult()) {
            pet.setAdult();
        } else {
            pet.setBaby();
        }
        pet.setAgeLock(true);

        pet.setCustomName(RPGInventory.getLanguage().getMessage("pet.name", petType.getName(), player.getName()));
        pet.setCustomNameVisible(true);
        pet.setCanPickupItems(false);
        pet.setRemoveWhenFarAway(false);

        AttributeInstance maxHealth = pet.getAttribute(Attribute.GENERIC_MAX_HEALTH);
        assert maxHealth != null;
        maxHealth.setBaseValue(petType.getHealth());
        pet.setHealth(PetManager.getHealth(petItem, maxHealth.getBaseValue()));

        AttributeInstance speedAttribute = pet.getAttribute(Attribute.GENERIC_MOVEMENT_SPEED);
        assert speedAttribute != null;
        speedAttribute.setBaseValue(petType.getSpeed());

        pet.setMetadata(PetManager.METADATA_KEY_PET_OWNER, new FixedMetadataValue(RPGInventory.getInstance(), player.getUniqueId()));

        if (hasInitializationErrors) {
            Log.w("The pet ''{0}'' has errors on initialization and can be differ from expected result.", pet.getCustomName());
        }

        InventoryManager.get(player).setPet(pet);
    }

    public static void despawnPet(OfflinePlayer player) {
        if (!InventoryManager.playerIsLoaded(player) || !PetManager.isEnabled()) {
            return;
        }

        PlayerWrapper playerWrapper = InventoryManager.get(player);
        Inventory inventory = playerWrapper.getInventory();

        LivingEntity pet = playerWrapper.getPet();
        ItemStack petItem = inventory.getItem(SLOT_PET);

        if (pet == null) {
            return;
        }

        if (petItem != null) {
            PetManager.saveHealth(petItem, pet.getHealth());
            inventory.setItem(SLOT_PET, petItem);
        }

        EffectUtils.playDespawnEffect(pet);
        pet.remove();
        playerWrapper.setPet(null);
    }

    public static void despawnPet(Tameable petEntity) {
        if (!PetManager.isEnabled()) {
            return;
        }

        EffectUtils.playDespawnEffect(petEntity);
        petEntity.remove();
    }

    /**
     * Gets the owner of present LivingEntity, if he is exists.
     *
     * @param entity LivingEntity
     * @return if exists, the UUID of the Player that owning that entity, otherwise null
     * @since 2.1.7
     */
    @Nullable
    public static UUID getPetOwner(@NotNull LivingEntity entity) {
        if (!entity.hasMetadata(PetManager.METADATA_KEY_PET_OWNER)) {
            return null;
        }

        final List<MetadataValue> metadata = entity.getMetadata(PetManager.METADATA_KEY_PET_OWNER);
        return metadata.stream()
                .filter(value -> RPGInventory.getInstance().equals(value.getOwningPlugin()))
                .findFirst()
                .map(it -> (UUID) it.value())
                .orElse(null);
    }

    @Nullable
    @Contract("null -> null")
    public static PetFood getFoodFromItem(@Nullable ItemStack item) {
        String id;

        if (ItemUtils.isEmpty(item) || (id = ItemUtils.getTag(item, ItemUtils.FOOD_TAG)).isEmpty()) {
            return null;
        }

        return PET_FOOD.get(id);
    }

    @Nullable
    @Contract("null -> null")
    public static PetType getPetFromItem(@Nullable ItemStack item) {
        String id;

        if (ItemUtils.isEmpty(item) || (id = ItemUtils.getTag(item, ItemUtils.PET_TAG)).isEmpty()) {
            return null;
        }

        return PETS.get(id);
    }

    @Nullable
    public static PetType getPetFromEntity(@Nullable LivingEntity entity, OfflinePlayer player) {
        if (entity == null || !InventoryManager.playerIsLoaded(player) || !PetManager.isEnabled()) {
            return null;
        }

        PlayerWrapper playerWrapper = InventoryManager.get(player);
        return (entity == playerWrapper.getPet())
                ? PetManager.getPetFromItem(playerWrapper.getInventory().getItem(SLOT_PET))
                : null;
    }

    static void addGlow(@NotNull ItemMeta meta) {
        meta.addEnchant(Enchantment.DURABILITY, 88, true);
        meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
    }

    public static void saveDeathTime(@NotNull ItemStack item) {
        saveDeathTime(item, System.currentTimeMillis());
    }

    public static void saveDeathTime(@NotNull ItemStack item, long deathTime) {
        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(item);
        if (deathTime == 0) {
            nbt.remove(DEATH_TIME_TAG);
        } else {
            nbt.put(DEATH_TIME_TAG, deathTime);
        }

        NbtFactoryMirror.setItemTag(item, nbt);
    }

    public static long getDeathTime(@NotNull ItemStack item) {
        if (ItemUtils.isEmpty(item)) {
            return 0L;
        }

        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(item.clone());
        return nbt.containsKey(DEATH_TIME_TAG) ? nbt.getLong(DEATH_TIME_TAG) : 0L;
    }

    public static int getCooldown(@NotNull ItemStack item) {
        long deathTime = getDeathTime(item);
        if (deathTime == 0) {
            return 0;
        }

        PetType petFromItem = getPetFromItem(item);
        if (petFromItem == null) {
            return 0;
        }

        int secondsSinceDeath = (int) ((System.currentTimeMillis() - deathTime) / 1000);
        int petCooldown = petFromItem.getCooldown();
        int itemCooldown = petCooldown - secondsSinceDeath;
        if (itemCooldown < 0 || itemCooldown > petCooldown) {
            itemCooldown = 0;
        }

        if (itemCooldown == 0) {
            PetManager.saveDeathTime(item, 0);
        }

        return itemCooldown;
    }

    public static void saveHealth(@NotNull ItemStack item, double health) {
        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(item);

        if (health == 0) {
            nbt.remove("pet.health");
        } else {
            nbt.put("pet.health", health);
        }

        NbtFactoryMirror.setItemTag(item, nbt);
    }

    public static double getHealth(ItemStack item, double maxHealth) {
        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(item.clone());

        if (!nbt.containsKey("pet.health")) {
            return maxHealth;
        }

        double health = nbt.getDouble("pet.health");
        return Math.min(health, maxHealth);
    }

    @Nullable
    public static ItemStack getPetItem(String petId) {
        PetType petType = PETS.get(petId);
        return petType == null ? null : petType.getSpawnItem();
    }

    @Nullable
    public static ItemStack getFoodItem(String petId) {
        PetFood food = PET_FOOD.get(petId);
        return food == null ? null : food.getFoodItem();
    }

    public static ItemStack toPetItem(ItemStack item) {
        if (ItemUtils.isEmpty(item)) {
            return item;
        }

        List<String> itemLore = item.getItemMeta().getLore();
        for (PetType petType : PETS.values()) {
            List<String> petItemLore = petType.getSpawnItem().getItemMeta().getLore();
            if (itemLore.equals(petItemLore)) {
                return petType.getSpawnItem();
            }
        }

        return item;
    }

    public static boolean isPetItem(@Nullable ItemStack item) {
        if (ItemUtils.isEmpty(item) || !item.hasItemMeta() || !item.getItemMeta().hasLore()) {
            return false;
        }

        List<String> itemLore = item.getItemMeta().getLore();
        for (PetType petType : PETS.values()) {
            List<String> petItemLore = petType.getSpawnItem().getItemMeta().getLore();
            if (itemLore.equals(petItemLore)) {
                return true;
            }
        }

        return false;
    }
} public class PetType extends ClassedItem {
    // Design
    @NotNull
    private final String name;
    @NotNull
    private final String itemName;
    @NotNull
    private final List<String> lore;

    // Stats
    private final Role role;
    private final EntityType skin;
    private final double health;
    private final double damage;
    private final double speed;

    // Options
    private final boolean attackMobs;
    private final boolean attackPlayers;
    private final boolean revival;
    private final int cooldown;
    private ItemStack spawnItem;
    private Map<String, String> features;

    PetType(Texture texture, @NotNull ConfigurationSection config) {
        super(texture, config);

        this.name = StringUtils.coloredLine(config.getString("name"));
        this.itemName = StringUtils.coloredLine(config.getString("item-name"));
        this.lore = StringUtils.coloredLines(config.getStringList("lore"));

        this.role = SafeEnums.valueOfOrDefault(Role.class, config.getString("type", "COMPANION"), Role.COMPANION);
        this.skin = role.getPossibleSkin(config.getString("skin"));

        this.health = config.getDouble("health");
        this.damage = config.getDouble("damage", 0);
        this.speed = config.getDouble("speed");

        this.attackMobs = config.getBoolean("attack-mobs", false);
        this.attackPlayers = config.getBoolean("attack-players", false);
        this.revival = config.getBoolean("revival", true);
        this.cooldown = this.revival ? config.getInt("cooldown") : 0;

        this.createSpawnItem(config.getName());
        this.storeFeatures(config.getStringList("features"));
    }

    @Contract("null -> false")
    public static boolean isPetItem(ItemStack item) {
        return ItemUtils.isNotEmpty(item) && ItemUtils.hasTag(item, ItemUtils.PET_TAG);
    }

    @Nullable
    public static ItemStack clone(@NotNull ItemStack oldItem) {
        PetType petType = PetManager.getPetFromItem(oldItem);

        if (petType == null) {
            return null;
        }

        ItemStack newItem = petType.getSpawnItem();
        PetManager.saveDeathTime(newItem, PetManager.getDeathTime(oldItem));
        PetManager.saveHealth(newItem, PetManager.getHealth(oldItem, petType.getHealth()));

        return newItem;
    }

    private void storeFeatures(@Nullable List<String> featureList) {
        // Load features
        Map<String, String> features;
        if (featureList != null && featureList.size() > 0) {
            features = new HashMap<>(featureList.size());
            for (String feature : featureList) {
                String[] data = feature.replaceAll(" ", "").split(":");
                features.put(data[0], data[1]);
            }
        } else {
            features = Collections.emptyMap();
        }

        this.features = features;
    }

    private void createSpawnItem(String id) {
        ItemStack spawnItem = this.texture.getItemStack();

        // Set lore and display itemName
        ItemMeta meta = spawnItem.getItemMeta();
        meta.setDisplayName(this.itemName);

        FileLanguage lang = RPGInventory.getLanguage();
        List<String> lore = new ArrayList<>();
        lore.add(StringUtils.coloredLine("&8" + lang.getMessage("pet.role." + this.role.name().toLowerCase())));

        // Add class and level requirements
        if (this.getLevel() != -1) {
            lore.add(lang.getMessage("item.level", this.getLevel()));
        }

        if (this.getClasses() != null) {
            lore.add(lang.getMessage("item.class", this.getClassesString()));
        }

        lore.addAll(this.lore);

        lore.add(lang.getMessage("pet.health", (int) (this.health)));
        if (this.role == Role.COMPANION && (this.attackMobs || this.attackPlayers)) {
            lore.add(lang.getMessage("pet.damage", (int) (this.damage)));
        }
        lore.add(lang.getMessage("pet.speed", Utils.round(this.speed, 2)));
        lore.add(lang.getMessage("pet.revival." + (this.revival ? "yes" : "no"), this.cooldown));

        meta.setLore(lore);
        spawnItem.setItemMeta(meta);

        this.spawnItem = ItemUtils.setTag(spawnItem, ItemUtils.PET_TAG, id);
    }

    public ItemStack getSpawnItem() {
        return this.spawnItem;
    }

    @NotNull
    public String getName() {
        return name;
    }

    public double getHealth() {
        return health;
    }

    public boolean isRevival() {
        return revival;
    }

    public int getCooldown() {
        return cooldown;
    }

    public double getSpeed() {
        double speed = this.speed * Attributes.ONE_BPS;

        if (this.role == Role.MOUNT) {
            speed /= Attributes.GALLOP_MULTIPLIER;
        }

        if (!isAdult()) {
            speed /= 1.5;
        }

        return speed;
    }

    public double getDamage() {
        return damage;
    }

    public boolean isAttackPlayers() {
        return attackPlayers;
    }

    public boolean isAttackMobs() {
        return attackMobs;
    }

    boolean isAdult() {
        return !features.containsKey("BABY") || features.get("BABY").equals("FALSE");
    }

    public Role getRole() {
        return role;
    }

    public Map<String, String> getFeatures() {
        return features;
    }

    public EntityType getSkin() {
        return skin;
    }

    public enum Role {
        COMPANION("WOLF", "CAT"),
        MOUNT("HORSE", "PIG");

        private final String defaultSkin;
        @NotNull
        private final List<String> possibleSkins;

        Role(String... skins) {
            this.defaultSkin = skins[0];
            this.possibleSkins = Arrays.asList(skins);
        }

        public EntityType getPossibleSkin(String skin) {
            String possibleSkin = skin;
            if (possibleSkin == null) {
                possibleSkin = defaultSkin;
            } else if (!possibleSkins.contains(skin)) {
                possibleSkin = defaultSkin;
            }

            return EntityType.valueOf(possibleSkin);
        }
    }
} public class ResourcePackModule implements Listener {

    private static final int TICKS_IN_SECOND = 20;

    private final Plugin plugin;
    private final String resourcePackUrl;
    private final String resourcePackHash;

    private final List<UUID> loadList = new ArrayList<>();

    private ResourcePackModule(Plugin plugin, @NotNull String resourcePackUrl, String resourcePackHash) {
        this.plugin = plugin;
        this.resourcePackUrl = resourcePackUrl;
        this.resourcePackHash = resourcePackHash;
    }

    @Nullable
    public static ResourcePackModule init(Plugin plugin) {
        final FileConfiguration config = Config.getConfig();
        if (!config.getBoolean("resource-pack.enabled", false)) {
            Log.i("Resource-pack is disabled in config");
            return null;
        }

        final String rpUrl = Config.getConfig().getString("resource-pack.url");
        final String rpHash = Config.getConfig().getString("resource-pack.hash");
        ResourcePackValidator validator = new ResourcePackValidator();
        boolean isLegalUrlAndHash = validator.validateUrlAndHash(rpUrl, rpHash);
        printErrorsIfNotEmpty(validator.getErrors());
        if (!isLegalUrlAndHash) {
            Log.s("Resource-pack can not be enabled");
            return null;
        }

        ResourcePackModule resourcePackModule = new ResourcePackModule(plugin, rpUrl, rpHash);
        plugin.getServer().getPluginManager().registerEvents(resourcePackModule, plugin);
        ProtocolLibrary.getProtocolManager().addPacketListener(resourcePackModule.new ResourcePackPacketAdapter(plugin));
        return resourcePackModule;
    }

    private static void printErrorsIfNotEmpty(@NotNull List<String> messages) {
        if (messages.isEmpty()) {
            return;
        }

        Log.w("");
        Log.w("######### Something wrong with RP settings! ##########");
        for (String message : messages) {
            Log.w("# {0}", message);
        }
        Log.w("######################################################");
        Log.w("");
    }

    public void loadResourcePack(@NotNull Player player, boolean skipJoinMessage) {
        player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, 60, 1));

        if (skipJoinMessage) {
            this.sendResourcePack(player);
        } else if (InventoryManager.isNewPlayer(player)) {
            if (!EffectUtils.showJoinMessage(player, "rp-info", () -> this.sendResourcePack(player))) {
                this.sendResourcePack(player);
            }
        } else {
            EffectUtils.showDefaultJoinMessage(player);
            this.sendResourcePack(player);
        }
    }

    private void sendResourcePack(@NotNull final Player player) {
        new TrackedBukkitRunnable() {
            @Override
            public void run() {
                player.setResourcePack(resourcePackUrl);
            }
        }.runTaskLater(this.plugin, TICKS_IN_SECOND);
    }

    @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
    public void onTargetWhenPlayerNotLoaded(@NotNull EntityTargetLivingEntityEvent event) {
        if (event.getTarget() == null || event.getTarget().getType() != EntityType.PLAYER) {
            return;
        }

        if (!InventoryManager.playerIsLoaded((Player) event.getTarget())) {
            event.setCancelled(true);
        }
    }

    @EventHandler(priority = EventPriority.LOW)
    public void onPlayerInteractWhenNotLoaded(@NotNull PlayerInteractEvent event) {
        Player player = event.getPlayer();
        if (InventoryManager.isAllowedWorld(player.getWorld()) && !InventoryManager.playerIsLoaded(player)) {
            PlayerUtils.sendMessage(player, RPGInventory.getLanguage().getMessage("error.rp.denied"));
            event.setCancelled(true);
        }
    }

    @EventHandler
    public void onPlayerQuit(@NotNull PlayerQuitEvent event) {
        Player player = event.getPlayer();
        if (InventoryManager.isAllowedWorld(player.getWorld()) && !InventoryManager.playerIsLoaded(player)) {
            player.removePotionEffect(PotionEffectType.BLINDNESS);
            this.removePlayerFromLoadList(event.getPlayer());
        }
    }

    private void removePlayerFromLoadList(@NotNull Player player) {
        loadList.remove(player.getUniqueId());
    }


    class ResourcePackPacketAdapter extends PacketAdapter {

        ResourcePackPacketAdapter(Plugin plugin) {
            super(plugin, WrapperPlayClientResourcePackStatus.TYPE, WrapperPlayServerResourcePackSend.TYPE);
        }

        @Override
        public void onPacketSending(@NotNull PacketEvent event) {
            WrapperPlayServerResourcePackSend packet = new WrapperPlayServerResourcePackSend(event.getPacket());
            if (!packet.getUrl().equals(resourcePackUrl)) {
                return;
            }

            packet.setHash(resourcePackHash);
            final Player player = event.getPlayer();
            loadList.add(player.getUniqueId());
        }

        @Override
        public void onPacketReceiving(@NotNull PacketEvent event) {
            WrapperPlayClientResourcePackStatus packet = new WrapperPlayClientResourcePackStatus(event.getPacket());
            final Player player = event.getPlayer();
            if (loadList.contains(player.getUniqueId())) {
                switch (packet.getResult()) {
                    case ACCEPTED:
                        break;
                    case SUCCESSFULLY_LOADED:
                        onSuccessfullyLoaded(player);
                        break;
                    case DECLINED:
                    case FAILED_DOWNLOAD:
                        onFailedOrDeclined(player);
                }
            }
        }

        private void onSuccessfullyLoaded(@NotNull Player player) {
            final UUID playerId = player.getUniqueId();
            new TrackedBukkitRunnable() {
                @Override
                public void run() {
                    InventoryManager.loadPlayerInventory(player);
                    player.removePotionEffect(PotionEffectType.BLINDNESS);
                }
            }.runTaskLater(this.plugin, 1);
            loadList.remove(playerId);
        }

        private void onFailedOrDeclined(@NotNull Player player) {
            new TrackedBukkitRunnable() {
                @Override
                public void run() {
                    player.kickPlayer(RPGInventory.getLanguage().getMessage("error.rp.denied"));
                }
            }.runTaskLater(this.plugin, TICKS_IN_SECOND);
            loadList.remove(player.getUniqueId());
        }
    }
} public class EffectUtils {

    public static void playParticlesToAll(Particle particle, int particleNum, @NotNull Location location) {
        playParticlesToAll(particle, particleNum, location, 30.0D);
    }

    private static void playParticlesToAll(Particle particle, int particleNum, @NotNull Location location, double distance) {
        playParticlesToAll(particle, particleNum, location, LocationUtils.getRandomVector(), distance);
    }

    private static void playParticlesToAll(Particle particle, int particleNum, @NotNull Location location, @NotNull Vector direction, double distance) {
        for (Player player : LocationUtils.getNearbyPlayers(location, distance)) {
            playParticles(player, particle, particleNum, location, direction);
        }
    }

    private static void playParticles(Player player, Particle particle, int particleNum, Location location, Vector direction) {
        player.spawnParticle(particle, location, particleNum, direction.getX(), direction.getY(), direction.getZ());
    }

    public static void playSpawnEffect(Entity entity) {
        Location loc = entity.getLocation();

        entity.getWorld().playSound(loc, SoundCompat.ENDERMAN_TELEPORT.get(), 1, (float) (1.2 + Math.random() * 0.4));
        playParticlesToAll(Particle.EXPLOSION_LARGE, 3, loc);
    }

    public static void playDespawnEffect(Entity entity) {
        Location loc = entity.getLocation();

        entity.getWorld().playSound(loc, SoundCompat.ENDERMAN_TELEPORT.get(), 1, (float) (0.6 + Math.random() * 0.4));
        playParticlesToAll(Particle.SMOKE_NORMAL, 3, loc);
    }


    public static void showDefaultJoinMessage(Player player) {
        showJoinMessage(player, "default", null);
    }

    public static boolean showJoinMessage(Player player, String messageId, @Nullable Runnable callback) {
        String configPrefix = "join-messages." + messageId;
        if (Config.getConfig().getBoolean(configPrefix + ".enabled", false)) {
            EffectUtils.sendTitle(player,
                    Config.getConfig().getInt("join-messages.delay", 2),
                    Config.getConfig().getString(configPrefix + ".title"),
                    Config.getConfig().getStringList(configPrefix + ".text"),
                    callback);
            return true;
        } else {
            return false;
        }
    }

    private static void sendTitle(final Player player, int delay, String title, @NotNull final List<String> subtitles, @Nullable final Runnable callback) {
        if (delay < 2) {
            delay = 2;
        }

        final WrapperPlayServerTitle titlePacket = new WrapperPlayServerTitle();
        int time = (subtitles.size() == 0 ? delay : delay * subtitles.size()) - 1;
        try {
            WrapperPlayServerTitle resetPacket = new WrapperPlayServerTitle();
            resetPacket.setAction(EnumWrappers.TitleAction.RESET);
            ProtocolLibrary.getProtocolManager().sendServerPacket(player, resetPacket.getHandle());

            WrapperPlayServerTitle timesPacket = new WrapperPlayServerTitle();
            timesPacket.setAction(EnumWrappers.TitleAction.TIMES);
            timesPacket.setFadeIn(10);
            timesPacket.setFadeOut(10);
            timesPacket.setStay(20 * time);
            ProtocolLibrary.getProtocolManager().sendServerPacket(player, timesPacket.getHandle());

            title = StringUtils.coloredLine(StringUtils.setPlaceholders(player, title));
            titlePacket.setAction(EnumWrappers.TitleAction.TITLE);
            titlePacket.setTitle(WrappedChatComponent.fromChatMessage(StringUtils.coloredLine(title))[0]);
            ProtocolLibrary.getProtocolManager().sendServerPacket(player, titlePacket.getHandle());
        } catch (InvocationTargetException e) {
            throw new IllegalStateException("Unable to send packet", e);
        }

        new TrackedBukkitRunnable() {
            int line = 0;

            @Override
            public void run() {
                try {
                    if (line == subtitles.size()) {
                        this.cancel();
                        if (callback != null) {
                            callback.run();
                        }
                        return;
                    }

                    String subtitle = StringUtils.coloredLine(StringUtils.setPlaceholders(player, subtitles.get(line)));
                    titlePacket.setAction(EnumWrappers.TitleAction.SUBTITLE);
                    titlePacket.setTitle(WrappedChatComponent.fromChatMessage(subtitle)[0]);
                    ProtocolLibrary.getProtocolManager().sendServerPacket(player, titlePacket.getHandle());
                } catch (InvocationTargetException e) {
                    throw new IllegalStateException("Unable to send packet", e);
                }

                line++;
            }
        }.runTaskTimer(RPGInventory.getInstance(), 0, 20 * delay);
    }
} public class InventoryUtils {
    public static void heldFreeSlot(@NotNull Player player, int start, SearchType type) {
        if (type == SearchType.NEXT) {
            for (int i = start + 1; i < start + 9; i++) {
                int index = i % 9;
                if (!InventoryManager.isQuickEmptySlot(player.getInventory().getItem(index))) {
                    player.getInventory().setHeldItemSlot(index);
                    return;
                }
            }
        } else {
            for (int i = start - 1; i > start - 9; i--) {
                int index = (i + 9) % 9;
                if (!InventoryManager.isQuickEmptySlot(player.getInventory().getItem(index))) {
                    player.getInventory().setHeldItemSlot(index);
                    return;
                }
            }
        }
    }

    @Contract(pure = true)
    public static int getQuickSlot(int slotId) {
        return slotId % 9;
    }

    public static boolean playerNeedArmor(Player player, ArmorType armorType) {
        ItemStack armorItem = armorType.getItem(player);
        return ItemUtils.isEmpty(armorItem) && armorType != ArmorType.UNKNOWN;
    }

    public static int getArmorSlotId(Slot slot) {
        switch (slot.getName()) {
            case "helmet":
                return 5;
            case "chestplate":
                return 6;
            case "leggings":
                return 7;
            default:
                return 8;
        }
    }

    @NotNull
    public static List<ItemStack> collectEffectiveItems(@NotNull Player player, boolean notifyPlayer) {
        List<ItemStack> items = new ArrayList<>(InventoryAPI.getPassiveItems(player));
        Collections.addAll(items, player.getInventory().getArmorContents());

        EntityEquipment equipment = player.getEquipment();
        assert equipment != null;

        ItemStack itemInHand = equipment.getItemInMainHand();
        if (CustomItem.isCustomItem(itemInHand) && ItemManager.allowedForPlayer(player, itemInHand, notifyPlayer)) {
            items.add(itemInHand);
        }

        itemInHand = equipment.getItemInOffHand();
        if (CustomItem.isCustomItem(itemInHand) && ItemManager.allowedForPlayer(player, itemInHand, notifyPlayer)) {
            items.add(itemInHand);
        }

        return items;
    }

    public enum SearchType {
        NEXT,
        PREV
    }
} public class ItemUtils {
    public static final String ENTITY_TAG = "EntityTag";

    public static final String BACKPACK_UID_TAG = "backpack.uid";
    public static final String BACKPACK_TAG = "backpack.id";
    public static final String ITEM_TAG = "rpginv.id";
    public static final String FOOD_TAG = "food.id";
    public static final String PET_TAG = "pet.id";

    @NotNull
    public static ItemStack setTag(ItemStack item, @NotNull String tag, @NotNull String value) {
        ItemStack bukkitItem = toBukkitItemStack(item);
        if (isEmpty(bukkitItem)) {
            return bukkitItem;
        }

        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(bukkitItem);
        if (!nbt.containsKey(tag)) {
            nbt.put(tag, value);
        }
        NbtFactoryMirror.setItemTag(bukkitItem, nbt);

        return bukkitItem;
    }

    @NotNull
    public static String getTag(@NotNull ItemStack item, @NotNull String tag) {
        return getTag(item, tag, "");
    }

    @NotNull
    @SuppressWarnings("WeakerAccess")
    public static String getTag(@NotNull ItemStack item, @NotNull String tag, @NotNull String defaultValue) {
        final ItemStack bukkitItem = toBukkitItemStack(item);
        if (isEmpty(bukkitItem)) {
            return "";
        }

        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(bukkitItem);
        return nbt.containsKey(tag) ? nbt.getString(tag) : defaultValue;
    }

    @Contract("null, _ -> false")
    public static boolean hasTag(@Nullable ItemStack originalItem, String tag) {
        if (isEmpty(originalItem) || !originalItem.hasItemMeta()) {
            return false;
        }

        ItemStack item = toBukkitItemStack(originalItem.clone());
        if (isEmpty(item)) {
            return false;
        }

        NbtCompound nbt = NbtFactoryMirror.fromItemCompound(item);
        return nbt.containsKey(tag);
    }

    public static boolean isItemHasDurability(ItemStack item) {
        return item.getType().getMaxDurability() > 0;
    }

    @NotNull
    public static ItemStack[] syncItems(ItemStack[] items) {
        for (int i = 0; i < items.length; i++) {
            items[i] = ItemUtils.syncItem(items[i]);
        }

        return items;
    }

    @NotNull
    @Contract("null -> !null")
    private static ItemStack syncItem(@Nullable ItemStack item) {
        if (ItemUtils.isEmpty(item)) {
            return new ItemStack(Material.AIR);
        }

        int itemTextureData = getTextureData(item);
        int amount = item.getAmount();
        int foundTextureData;
        if (CustomItem.isCustomItem(item)) {
            CustomItem custom = ItemManager.getCustomItem(item);

            if (custom == null) {
                return new ItemStack(Material.AIR);
            }

            foundTextureData = custom.getTextureData();
            item = ItemManager.getItem(ItemUtils.getTag(item, ItemUtils.ITEM_TAG));
        } else if (BackpackManager.isBackpack(item)) {
            BackpackType type = BackpackManager.getBackpackType(ItemUtils.getTag(item, ItemUtils.BACKPACK_TAG));

            if (type == null) {
                return new ItemStack(Material.AIR);
            }

            foundTextureData = type.getTextureData();

            String bpUID = ItemUtils.getTag(item, ItemUtils.BACKPACK_UID_TAG);
            if (!bpUID.isEmpty()) {
                ItemUtils.setTag(item, ItemUtils.BACKPACK_UID_TAG, bpUID);
            }
        } else if (PetType.isPetItem(item)) {
            PetType petType = PetManager.getPetFromItem(item);
            if (petType == null) {
                return new ItemStack(Material.AIR);
            }
            foundTextureData = petType.getTextureData();

            long deathTime = PetManager.getDeathTime(item);
            double health = PetManager.getHealth(item, petType.getHealth());

            item = petType.getSpawnItem();
            PetManager.saveDeathTime(item, deathTime);
            PetManager.saveHealth(item, health);
        } else if (PetFood.isFoodItem(item)) {
            PetFood food = PetManager.getFoodFromItem(item);
            if (food == null) {
                return new ItemStack(Material.AIR);
            }
            foundTextureData = food.getTextureData();

            item = food.getFoodItem();
        } else {
            return item;
        }

        setTextureData(item, foundTextureData == -1 ? itemTextureData : foundTextureData);
        item.setAmount(amount);
        return item;
    }

    public static int getTextureData(@NotNull ItemStack itemStack) {
        ItemMeta meta = itemStack.getItemMeta();
        if (meta == null) {
            return 0;
        }

        int data;
        if (Config.texturesType == TexturesType.DAMAGE) {
            data = ((Damageable) meta).getDamage();
        } else if (meta.hasCustomModelData()) {
            data = meta.getCustomModelData();
        } else {
            data = 0;
        }
        return data;
    }

    private static void setTextureData(@NotNull ItemStack itemStack, int data) {
        ItemMeta meta = itemStack.getItemMeta();
        if (meta != null) {
            if (Config.texturesType == TexturesType.DAMAGE) {
                ((Damageable) meta).setDamage(data);
            } else {
                meta.setCustomModelData(data);
            }
            itemStack.setItemMeta(meta);
        }
    }

    @Contract("null -> true")
    public static boolean isEmpty(@Nullable ItemStack item) {
        return item == null || item.getType() == Material.AIR;
    }

    @Contract("null -> false")
    public static boolean isNotEmpty(@Nullable ItemStack item) {
        return item != null && item.getType() != Material.AIR;
    }

    @NotNull
    public static ItemStack toBukkitItemStack(ItemStack item) {
        return MinecraftReflection.getBukkitItemStack(item);
    }
} @SuppressWarnings("CheckStyle")
public final class Log {

    private static Logger logger;

    private Log() {
        // static class
    }

    public static void init(@NotNull Logger logger) {
        Log.logger = logger;
    }

    public static void i(@NotNull String message, Object... args) {
        logger.info(prepareMessage(message, args));
    }

    public static void w(Throwable t) {
        logger.log(Level.WARNING, t.getMessage(), t);
    }

    public static void w(@NotNull String message, Object... args) {
        logger.warning(prepareMessage(message, args));
    }

    public static void w(Throwable t, @NotNull String message, Object... args) {
        logger.log(Level.WARNING, prepareMessage(message, args), t);
    }

    public static void d(Throwable t) {
        logger.log(Level.FINE, "", t);
    }

    public static void s(@NotNull String message, Object... args) {
        logger.severe(prepareMessage(message, args));
    }

    private static String prepareMessage(String message, Object... args) {
        return MessageFormat.format(message, args);
    }
} public class PlayerUtils {
    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    public static boolean checkLevel(@NotNull Player player, int required) {
        BukkitLevelSystem levelSystem = RPGInventory.getLevelSystem(player);
        return levelSystem.didReachLevel(required);
    }

    public static boolean checkClass(@NotNull Player player, @NotNull List<String> classes) {
        BukkitClassSystem classSystem = RPGInventory.getClassSystem(player);
        return classSystem.hasAnyOfClasses(classes);
    }

    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    public static boolean checkMoney(@NotNull Player player, double cost) {
        double balance = (RPGInventory.economyConnected() ? RPGInventory.getEconomy().getBalance(player) : 0);
        if (balance < cost) {
            PlayerUtils.sendMessage(player, RPGInventory.getLanguage().getMessage("error.money", StringUtils.doubleToString(cost - balance)));
            return false;
        }

        return true;
    }

    public static void updateInventory(@NotNull final Player player) {
        new TrackedBukkitRunnable() {
            @Override
            public void run() {
                player.updateInventory();
            }
        }.runTaskLater(RPGInventory.getInstance(), 1);
    }

    public static void sendMessage(@NotNull Player player, String message) {
        if (InventoryManager.playerIsLoaded(player)) {
            PlayerWrapper wrapper = InventoryManager.get(player);

            if (wrapper.getLastMessage().equals(message)) {
                return;
            }

            wrapper.setLastMessage(message);
        }

        player.sendMessage(message);
    }
} public class SafeEnums {

    private SafeEnums() {
        // Shouldn't be instantiated
    }

    @Nullable
    public static DyeColor getDyeColor(String name) {
        return valueOf(DyeColor.class, name, "color");
    }

    @Nullable
    public static Horse.Color getHorseColor(String name) {
        return valueOf(Horse.Color.class, name, "horse color");
    }

    @Nullable
    public static Horse.Style getHorseStyle(String name) {
        return valueOf(Horse.Style.class, name, "horse style");
    }

    @Nullable
    public static Cat.Type getCatType(String name) {
        return valueOf(Cat.Type.class, name, "cat type");
    }

    @NotNull
    public static <T extends Enum<T>> T valueOfOrDefault(Class<T> enumClass, String name, T defaultValue) {
        return valueOfOrDefault(enumClass, name, defaultValue, enumClass.getSimpleName());
    }

    @NotNull
    public static <T extends Enum<T>> T valueOfOrDefault(Class<T> enumClass, String name, T defaultValue, String alias) {
        T value = valueOf(enumClass, name, alias);
        if (value != null) {
            return value;
        } else {
            Log.w("Used {0} {1} by default.", defaultValue.name(), alias);
            return defaultValue;
        }
    }

    @Nullable
    public static <T extends Enum<T>> T valueOf(Class<T> enumClass, String name, String alias) {
        if (name == null) {
            return null;
        }

        try {
            return Enum.valueOf(enumClass, name.toUpperCase());
        } catch (IllegalArgumentException e) {
            Log.w("Unknown {0}: {1}. Available values: {2}", alias, name, Arrays.toString(enumClass.getEnumConstants()));
            return null;
        }
    }
} public class StringUtils {
    @NotNull
    public static String coloredLine(@NotNull String line) {
        return ChatColor.translateAlternateColorCodes('&', line);
    }

    @NotNull
    public static List<String> coloredLines(@Nullable List<String> lines) {
        if (lines == null) {
            return Collections.emptyList();
        }

        List<String> coloredLines = new ArrayList<>(lines.size());
        for (String line : lines) {
            coloredLines.add(StringUtils.coloredLine(line));
        }

        return coloredLines;
    }

    public static String doubleToString(double value) {
        return value == (long) value ? String.format("%d", (long) value) : String.format("%s", value);
    }

    public static void coloredConsole(String message) {
        Bukkit.getServer().getConsoleSender().sendMessage(message);
    }

    @NotNull
    public static String setPlaceholders(@NotNull Player player, @NotNull String line) {
        if (RPGInventory.isPlaceholderApiHooked()) {
            try {
                return PlaceholderAPI.setPlaceholders(player, line);
            } catch (Throwable e) {
                Log.w("Can''t set placeholders for line \"{0}\"", line);
                Log.w("Make sure that you use the latest version of PlaceholderAPI.");
                Log.w(e, "If it is, please report about the error to PlaceholderAPI author:");
                return line;
            }
        }

        return line;
    }

    public static class Placeholders extends PlaceholderExpansion {
        @Override
        public String getIdentifier() {
            return "rpginv";
        }

        @Override
        public String getAuthor() {
            return "osipxd";
        }

        @Override
        public String getVersion() {
            return "1.0";
        }

        @Override
        public boolean canRegister() {
            return true;
        }

        @Override
        public String onPlaceholderRequest(@Nullable Player player, @NotNull String identifier) {
            if (!InventoryManager.playerIsLoaded(player)) {
                return null;
            }

            switch (identifier) {
                case "damage_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.DAMAGE).toString();
                case "bow_damage_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.BOW_DAMAGE).toString();
                case "hand_damage_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.HAND_DAMAGE).toString();
                case "crit_damage_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.CRIT_DAMAGE).toString();
                case "crit_chance":
                    return ItemManager.getModifier(player, ItemStat.StatType.CRIT_CHANCE).toString();
                case "armor_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.ARMOR).toString();
                case "speed_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.SPEED).toString();
                case "jump_bonus":
                    return ItemManager.getModifier(player, ItemStat.StatType.JUMP).toString();
            }

            return null;
        }
    }
}
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.World;
import org.bukkit.entity.AnimalTamer;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.endlesscode.inspector.report.Reporter;
import ru.endlesscode.rpginventory.RPGInventory;
import ru.endlesscode.rpginventory.api.InventoryAPI;
import ru.endlesscode.rpginventory.event.PetEquipEvent;
import ru.endlesscode.rpginventory.event.PetUnequipEvent;
import ru.endlesscode.rpginventory.event.PlayerInventoryLoadEvent;
import ru.endlesscode.rpginventory.event.PlayerInventoryUnloadEvent;
import ru.endlesscode.rpginventory.event.listener.InventoryListener;
import ru.endlesscode.rpginventory.inventory.slot.Slot;
import ru.endlesscode.rpginventory.inventory.slot.SlotManager;
import ru.endlesscode.rpginventory.item.ItemManager;
import ru.endlesscode.rpginventory.item.Texture;
import ru.endlesscode.rpginventory.misc.config.Config;
import ru.endlesscode.rpginventory.misc.serialization.Serialization;
import ru.endlesscode.rpginventory.pet.PetManager;
import ru.endlesscode.rpginventory.pet.PetType;
import ru.endlesscode.rpginventory.resourcepack.ResourcePackModule;
import ru.endlesscode.rpginventory.utils.EffectUtils;
import ru.endlesscode.rpginventory.utils.InventoryUtils;
import ru.endlesscode.rpginventory.utils.ItemUtils;
import ru.endlesscode.rpginventory.utils.Log;
import ru.endlesscode.rpginventory.utils.PlayerUtils;
import ru.endlesscode.rpginventory.utils.SafeEnums;
import ru.endlesscode.rpginventory.utils.StringUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
                currentItem.setAmount(item.getAmount() / 2);
                cursorItem = item.clone();
                cursorItem.setAmount(item.getAmount() / 2 + 1);
            }
        } else if (action == InventoryAction.MOVE_TO_OTHER_INVENTORY) {
            player.getInventory().addItem(currentItem);
            currentItem = null;
        }

        if (slotType == InventoryType.SlotType.QUICKBAR) {
            if (slot < 9) { // Exclude shield
                player.getInventory().setItem(slot, currentItem);
            }
        } else {
            inventory.setItem(slot, currentItem);
        }

        player.setItemOnCursor(cursorItem);
        player.updateInventory();
    }

    static void lockEmptySlots(Player player) {
        lockEmptySlots(INVENTORIES.get(player.getUniqueId()).getInventory());
    }

    public static void lockEmptySlots(Inventory inventory) {
        for (int i = 0; i < inventory.getSize(); i++) {
            Slot slot = SlotManager.instance().getSlot(i, InventoryType.SlotType.CONTAINER);
            if (slot == null) {
                inventory.setItem(i, FILL_SLOT);
            } else if (ItemUtils.isEmpty(inventory.getItem(i))) {
                inventory.setItem(i, slot.getCup());
            }
        }
    }

    static void unlockEmptySlots(Player player) {
        Inventory inventory = INVENTORIES.get(player.getUniqueId()).getInventory();

        for (int i = 0; i < inventory.getSize(); i++) {
            Slot slot = SlotManager.instance().getSlot(i, InventoryType.SlotType.CONTAINER);
            if (slot == null || slot.isCup(inventory.getItem(i))) {
                inventory.setItem(i, null);
            }
        }
    }

    public static boolean isQuickSlot(int slot) {
        return getQuickSlot(slot) != null;
    }

    @Nullable
    public static Slot getQuickSlot(int slot) {
        for (Slot quickSlot : SlotManager.instance().getQuickSlots()) {
            if (slot == quickSlot.getQuickSlot()) {
                return quickSlot;
            }
        }

        return null;
    }

    static void lockQuickSlots(@NotNull Player player) {
        for (Slot quickSlot : SlotManager.instance().getQuickSlots()) {
            int slotId = quickSlot.getQuickSlot();

            if (ItemUtils.isEmpty(player.getInventory().getItem(slotId))) {
                player.getInventory().setItem(slotId, quickSlot.getCup());
            }

            if (player.getInventory().getHeldItemSlot() == slotId) {
                if (quickSlot.isCup(player.getInventory().getItem(slotId))) {
                    InventoryUtils.heldFreeSlot(player, slotId, InventoryUtils.SearchType.NEXT);
                }
            }
        }
    }

    static void unlockQuickSlots(@NotNull Player player) {
        for (Slot quickSlot : SlotManager.instance().getQuickSlots()) {
            int slotId = quickSlot.getQuickSlot();
            if (quickSlot.isCup(player.getInventory().getItem(slotId))) {
                player.getInventory().setItem(slotId, null);
            }
        }
    }

    public static boolean isNewPlayer(@NotNull Player player) {
        Path dataFolder = RPGInventory.getInstance().getDataFolder().toPath();
        return Files.notExists(dataFolder.resolve("inventories/" + player.getUniqueId() + ".inv"));
    }

    public static void loadPlayerInventory(Player player) {
        if (!InventoryManager.isAllowedWorld(player.getWorld())) {
            InventoryManager.INVENTORIES.remove(player.getUniqueId());
            return;
        }

        try {
            Path dataFolder = RPGInventory.getInstance().getDataPath();
            Path folder = dataFolder.resolve("inventories");
            Files.createDirectories(folder);

            // Load inventory from file
            Path file = folder.resolve(player.getUniqueId() + ".inv");

            PlayerWrapper playerWrapper = null;
            if (Files.exists(file)) {
                playerWrapper = Serialization.loadPlayerOrNull(player, file);
                if (playerWrapper == null) {
                    Log.s("Error on loading {0}''s inventory.", player.getName());
                    Log.s("Will be created new inventory. Old file was renamed.");
                }
            }

            if (playerWrapper == null) {
                playerWrapper = new PlayerWrapper(player);
                playerWrapper.setBuyedSlots(0);
            }

            PlayerInventoryLoadEvent.Pre event = new PlayerInventoryLoadEvent.Pre(player);