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();
    }
} public class ItemListener implements Listener {
    @EventHandler(priority = EventPriority.LOW)
    public void onDamage(@NotNull EntityDamageByEntityEvent event) {
        Player damager;

        // Defensive stats
        try {
            if (event.getEntityType() == EntityType.PLAYER) {
                Modifier armorModifier = ItemManager.getModifier((Player) event.getEntity(), ItemStat.StatType.ARMOR);
                double armorReduction = event.getDamage(EntityDamageEvent.DamageModifier.ARMOR);
                double armor = (armorReduction - armorModifier.getBonus()) * armorModifier.getMultiplier();
                if (armor > 0.0D) {
                    armor = 0.0D;
                }

                event.setDamage(EntityDamageEvent.DamageModifier.ARMOR, armor);
            }
        } catch (UnsupportedOperationException ignored) {
        }

        // Attack stats
        Modifier damageModifier;
        ItemStack itemInHand;
        if (event.getDamager().getType() == EntityType.PLAYER) {
            damager = (Player) event.getDamager();
            itemInHand = damager.getEquipment().getItemInMainHand();
            damageModifier = ItemManager.getModifier(damager,
                    ItemUtils.isEmpty(itemInHand) ? ItemStat.StatType.HAND_DAMAGE : ItemStat.StatType.DAMAGE);
        } else if (event.getDamager() instanceof AbstractArrow &&
                ((AbstractArrow) event.getDamager()).getShooter() instanceof Player) {
            damager = (Player) ((AbstractArrow) event.getDamager()).getShooter();
            itemInHand = damager.getEquipment().getItemInMainHand();
            damageModifier = ItemManager.getModifier(damager, ItemStat.StatType.BOW_DAMAGE);
        } else {
            return;
        }

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

        if (!ItemManager.allowedForPlayer(damager, itemInHand, true)) {
            event.setCancelled(true);
            return;
        }

        // Checking battle system restrictions
        boolean forceWeapon = Config.getConfig().getBoolean("attack.force-weapon");
        boolean requireWeapon = Config.getConfig().getBoolean("attack.require-weapon");
        PlayerInventory inventory = damager.getInventory();
        if ((forceWeapon || requireWeapon)
                && SlotManager.instance().getSlot(inventory.getHeldItemSlot(), InventoryType.SlotType.QUICKBAR) == null) {
            List<Slot> activeSlots = SlotManager.instance().getActiveSlots();
            if (activeSlots.size() != 0) {
                if (forceWeapon) {
                    for (Slot activeSlot : activeSlots) {
                        if (!InventoryManager.isQuickEmptySlot(inventory.getItem(activeSlot.getQuickSlot()))) {
                            inventory.setHeldItemSlot(activeSlot.getQuickSlot());
                            break;
                        }
                    }
                }

                if (requireWeapon) {
                    event.setCancelled(true);
                    return;
                }
            }
        }

        double baseDamage = event.getDamage(EntityDamageEvent.DamageModifier.BASE);
        double damage = (CustomItem.isCustomItem(itemInHand) ? 1 : baseDamage)
                + damageModifier.getBonus() * damageModifier.getMultiplier();
        double critChance = ItemManager.getModifier(damager, ItemStat.StatType.CRIT_CHANCE).getMultiplier() - 1.0;
        if (Math.random() <= critChance) {
            damage *= ItemManager.getModifier(damager, ItemStat.StatType.CRIT_DAMAGE).getMultiplier();
            damager.getWorld().playSound(
                    event.getEntity().getLocation(),
                    SoundCompat.SUCCESSFUL_HIT.get(),
                    1,
                    (float) (0.5 + Math.random() * 0.4)
            );
            EffectUtils.playParticlesToAll(Particle.CRIT, 10, event.getEntity().getLocation());
        }

        event.setDamage(EntityDamageEvent.DamageModifier.BASE, damage);
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void onPlayerJump(@NotNull PlayerMoveEvent event) {
        Player player = event.getPlayer();

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

        Vector velocity = player.getVelocity();
        Modifier jumpModifier = ItemManager.getModifier(player, ItemStat.StatType.JUMP);

        if (jumpModifier.getBonus() == 0 && jumpModifier.getMultiplier() == 1) {
            return;
        }

        // === START: Magic constants ===
        if (event.getTo() != null && velocity.getY() == 0.41999998688697815D) {
            double jump = (1.5 + Math.sqrt(jumpModifier.getBonus())) * jumpModifier.getMultiplier();
            Vector moveDirection = event.getTo().toVector().subtract(event.getFrom().toVector());
            velocity.setX(moveDirection.getX() * jump * player.getWalkSpeed());
            velocity.setY(velocity.getY() * jump / 1.5);
            velocity.setZ(moveDirection.getZ() * jump * player.getWalkSpeed());
            player.setVelocity(velocity);
        }
        // === END: Magic constants ===
    }

    @EventHandler
    public void onPlayerFall(@NotNull EntityDamageEvent event) {
        if (event.getEntity().getType() != EntityType.PLAYER
                || event.getCause() != EntityDamageEvent.DamageCause.FALL) {
            return;
        }

        Player player = (Player) event.getEntity();

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

        Modifier jumpModifier = ItemManager.getModifier(player, ItemStat.StatType.JUMP);
        double height = (1.5D + jumpModifier.getBonus()) * jumpModifier.getMultiplier() * 1.5;
        event.setDamage(event.getDamage() - height);

        if (event.getDamage() <= 0.0D) {
            event.setCancelled(true);
        }
    }

    @EventHandler(priority = EventPriority.HIGHEST)
    public void onItemUse(@NotNull PlayerInteractEvent event) {
        Player player = event.getPlayer();

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

        if (!ItemManager.allowedForPlayer(player, event.getItem(), true)) {
            event.setCancelled(true);
            PlayerUtils.updateInventory(player);
            return;
        }

        if (CustomItem.isCustomItem(event.getItem())) {
            CustomItem customItem = ItemManager.getCustomItem(event.getItem());
            if (customItem == null) {
                return;
            }

            if (event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK) {
                customItem.onRightClick(player);
            } else {
                customItem.onLeftClick(player);
            }

            if (event.getItem().getType() != Material.BOW) {
                event.setCancelled(true);
            }
        }

        ItemManager.updateStats(player);
    }

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

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

        ItemManager.updateStats(event.getPlayer());
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void afterEquipChange(@NotNull InventoryClickEvent event) {
        Player player = (Player) event.getWhoClicked();

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

        if (CustomItem.isCustomItem(event.getCursor()) || CustomItem.isCustomItem(event.getCurrentItem())) {
            ItemManager.updateStats(player);
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void afterEquipChange(@NotNull final InventoryDragEvent event) {
        final Player player = (Player) event.getWhoClicked();

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

        new TrackedBukkitRunnable() {
            @Override
            public void run() {
                InventoryView inventoryView = event.getView();
                for (int slot : event.getRawSlots()) {
                    ItemStack item = inventoryView.getItem(slot);
                    if (CustomItem.isCustomItem(item)) {
                        ItemManager.updateStats((Player) event.getWhoClicked());
                    }
                }
            }
        }.runTaskLater(RPGInventory.getInstance(), 1);
    }

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

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

        final ItemStack newItem = player.getInventory().getItem(event.getNewSlot());
        final ItemStack oldItem = player.getInventory().getItem(event.getPreviousSlot());

        new TrackedBukkitRunnable() {
            @Override
            public void run() {
                if (CustomItem.isCustomItem(oldItem) || CustomItem.isCustomItem(newItem)) {
                    ItemManager.updateStats(event.getPlayer());
                }
            }
        }.runTaskLater(RPGInventory.getInstance(), 2);
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void afterPickupItem(@NotNull EntityPickupItemEvent event) {
        if (event.getEntityType() != EntityType.PLAYER) {
            return;
        }

        Player player = (Player) event.getEntity();
        if (!InventoryManager.playerIsLoaded(player)) {
            return;
        }

        ItemStack item = event.getItem().getItemStack();
        if (CustomItem.isCustomItem(item)) {
            ItemManager.updateStats(player);
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void afterDropItem(@NotNull PlayerDropItemEvent event) {
        Player player = event.getPlayer();

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

        if (CustomItem.isCustomItem(event.getItemDrop().getItemStack())) {
            ItemManager.updateStats(player);
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void afterBreakItem(@NotNull PlayerItemBreakEvent event) {
        Player player = event.getPlayer();

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

        if (CustomItem.isCustomItem(event.getBrokenItem())) {
            ItemManager.updateStats(player);
        }
    }
} public class InventoryManager {
    static final String TITLE = RPGInventory.getLanguage().getMessage("title");
    private static final Map<UUID, PlayerWrapper> INVENTORIES = new HashMap<>();

    private static ItemStack FILL_SLOT = null;
    private static Reporter reporter;

    private InventoryManager() {
    }

    public static boolean init(@NotNull RPGInventory instance) {
        reporter = instance.getReporter();

        try {
            Texture texture = Texture.parseTexture(Config.getConfig().getString("fill"));
            InventoryManager.FILL_SLOT = texture.getItemStack();
            ItemMeta meta = InventoryManager.FILL_SLOT.getItemMeta();
            if (meta != null) {
                meta.setDisplayName(" ");
                InventoryManager.FILL_SLOT.setItemMeta(meta);
            }
        } catch (Exception e) {
            reporter.report("Error on InventoryManager initialization", e);
            return false;
        }

        // Register events
        instance.getServer().getPluginManager().registerEvents(new InventoryListener(), instance);
        return true;
    }

    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    public static boolean validateUpdate(Player player, ActionType actionType, @NotNull Slot slot, ItemStack item) {
        return actionType == ActionType.GET || actionType == ActionType.DROP
                || actionType == ActionType.SET && slot.isValidItem(item)
                && ItemManager.allowedForPlayer(player, item, true);
    }

    @NotNull
    public static ItemStack getFillSlot() {
        return InventoryManager.FILL_SLOT;
    }

    public static boolean validatePet(Player player, InventoryAction action, @Nullable ItemStack currentItem, @NotNull ItemStack cursor) {
        ActionType actionType = ActionType.getTypeOfAction(action);

        if (ItemUtils.isNotEmpty(currentItem)
                && (actionType == ActionType.GET || action == InventoryAction.SWAP_WITH_CURSOR || actionType == ActionType.DROP)
                && PetManager.getCooldown(currentItem) > 0) {
            return false;
        }

        if (actionType == ActionType.SET) {
            if (PetType.isPetItem(cursor) && ItemManager.allowedForPlayer(player, cursor, true)) {
                PetEquipEvent event = new PetEquipEvent(player, cursor);
                RPGInventory.getInstance().getServer().getPluginManager().callEvent(event);

                if (event.isCancelled()) {
                    return false;
                }

                PetManager.respawnPet(event.getPlayer(), event.getPetItem());
                return true;
            }
        } else if (actionType == ActionType.GET || actionType == ActionType.DROP) {
            PetUnequipEvent event = new PetUnequipEvent(player);
            RPGInventory.getInstance().getServer().getPluginManager().callEvent(event);
            PetManager.despawnPet(event.getPlayer());
            return true;
        }

        return false;
    }

    public static boolean validateArmor(Player player, InventoryAction action, @NotNull Slot slot, ItemStack item) {
        ActionType actionType = ActionType.getTypeOfAction(action);
        return actionType != ActionType.OTHER && (actionType != ActionType.SET || slot.isValidItem(item))
                && ItemManager.allowedForPlayer(player, item, true);
    }

    public static void updateShieldSlot(@NotNull Player player, @NotNull Inventory inventory, @NotNull Slot slot, int slotId,
                                        InventoryType.SlotType slotType, InventoryAction action,
                                        ItemStack currentItem, @NotNull ItemStack cursor) {
        ActionType actionType = ActionType.getTypeOfAction(action);
        if (actionType == ActionType.GET) {
            if (slot.isCup(currentItem)) {
                return;
            }

            if (slotType == InventoryType.SlotType.QUICKBAR && InventoryAPI.isRPGInventory(inventory)) {
                inventory.setItem(slot.getSlotId(), slot.getCup());
            } else {
                player.getEquipment().setItemInOffHand(new ItemStack(Material.AIR));
            }
        } else if (actionType == ActionType.SET) {
            if (slot.isCup(currentItem)) {
                currentItem = null;
                action = InventoryAction.PLACE_ALL;
            }

            if (slotType == InventoryType.SlotType.QUICKBAR && InventoryAPI.isRPGInventory(inventory)) {
                inventory.setItem(slot.getSlotId(), cursor);
            } else {
                player.getEquipment().setItemInOffHand(cursor);
            }
        }

        InventoryManager.updateInventory(player, inventory, slotId, slotType, action, currentItem, cursor);
    }

    public static void updateQuickSlot(@NotNull Player player, @NotNull Inventory inventory, @NotNull Slot slot, int slotId,
                                       InventoryType.SlotType slotType, InventoryAction action,
                                       ItemStack currentItem, ItemStack cursor) {
        ActionType actionType = ActionType.getTypeOfAction(action);
        if (actionType == ActionType.GET) {
            if (slot.isCup(currentItem)) {
                return;
            }

            if (player.getInventory().getHeldItemSlot() == slot.getQuickSlot()) {
                InventoryUtils.heldFreeSlot(player, slot.getQuickSlot(), InventoryUtils.SearchType.NEXT);
            }

            if (slotType == InventoryType.SlotType.QUICKBAR && InventoryAPI.isRPGInventory(inventory)) {
                inventory.setItem(slot.getSlotId(), slot.getCup());
            } else {
                player.getInventory().setItem(slot.getQuickSlot(), slot.getCup());
            }

            action = InventoryAction.SWAP_WITH_CURSOR;
            cursor = slot.getCup();
        } else if (actionType == ActionType.SET) {
            if (slot.isCup(currentItem)) {
                currentItem = null;
                action = InventoryAction.PLACE_ALL;
            }

            if (slotType == InventoryType.SlotType.QUICKBAR && InventoryAPI.isRPGInventory(inventory)) {
                inventory.setItem(slot.getSlotId(), cursor);
            } else {
                player.getInventory().setItem(slot.getQuickSlot(), cursor);
            }
        }

        InventoryManager.updateInventory(player, inventory, slotId, slotType, action, currentItem, cursor);
    }

    public static void updateArmor(@NotNull Player player, @NotNull Inventory inventory, @NotNull Slot slot, int slotId, InventoryAction action, ItemStack currentItem, @NotNull ItemStack cursor) {
        ActionType actionType = ActionType.getTypeOfAction(action);

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

        // Equip armor
        if (actionType == ActionType.SET || action == InventoryAction.UNKNOWN) {
            switch (slot.getName()) {
                case "helmet":
                    InventoryManager.updateInventory(player, inventory, slotId, action, currentItem, cursor);
                    equipment.setHelmet(inventory.getItem(slotId));
                    break;
                case "chestplate":
                    InventoryManager.updateInventory(player, inventory, slotId, action, currentItem, cursor);
                    equipment.setChestplate(inventory.getItem(slotId));
                    break;
                case "leggings":
                    InventoryManager.updateInventory(player, inventory, slotId, action, currentItem, cursor);
                    equipment.setLeggings(inventory.getItem(slotId));
                    break;
                case "boots":
                    InventoryManager.updateInventory(player, inventory, slotId, action, currentItem, cursor);
                    equipment.setBoots(inventory.getItem(slotId));
                    break;
            }
        } else if (actionType == ActionType.GET || actionType == ActionType.DROP) { // Unequip armor
            InventoryManager.updateInventory(player, inventory, slotId, action, currentItem, cursor);

            switch (slot.getName()) {
                case "helmet":
                    equipment.setHelmet(null);
                    break;
                case "chestplate":
                    equipment.setChestplate(null);
                    break;
                case "leggings":
                    equipment.setLeggings(null);
                    break;
                case "boots":
                    equipment.setBoots(null);
                    break;
            }
        }
    }

    public static void syncArmor(PlayerWrapper playerWrapper) {
        Player player = (Player) playerWrapper.getPlayer();
        Inventory inventory = playerWrapper.getInventory();
        SlotManager sm = SlotManager.instance();
        EntityEquipment equipment = player.getEquipment();
        assert equipment != null;

        if (ArmorType.HELMET.getSlot() != -1) {
            ItemStack helmet = equipment.getHelmet();
            Slot helmetSlot = sm.getSlot(ArmorType.HELMET.getSlot(), InventoryType.SlotType.CONTAINER);
            inventory.setItem(ArmorType.HELMET.getSlot(), (ItemUtils.isEmpty(helmet))
                    && helmetSlot != null ? helmetSlot.getCup() : helmet);
        }

        if (ArmorType.CHESTPLATE.getSlot() != -1) {
            ItemStack savedChestplate = playerWrapper.getSavedChestplate();
            ItemStack chestplate = savedChestplate == null ? equipment.getChestplate() : savedChestplate;
            Slot chestplateSlot = sm.getSlot(ArmorType.CHESTPLATE.getSlot(), InventoryType.SlotType.CONTAINER);
            inventory.setItem(ArmorType.CHESTPLATE.getSlot(), (ItemUtils.isEmpty(chestplate))
                    && chestplateSlot != null ? chestplateSlot.getCup() : chestplate);
        }

        if (ArmorType.LEGGINGS.getSlot() != -1) {
            ItemStack leggings = equipment.getLeggings();
            Slot leggingsSlot = sm.getSlot(ArmorType.LEGGINGS.getSlot(), InventoryType.SlotType.CONTAINER);
            inventory.setItem(ArmorType.LEGGINGS.getSlot(), (ItemUtils.isEmpty(leggings))
                    && leggingsSlot != null ? leggingsSlot.getCup() : leggings);
        }

        if (ArmorType.BOOTS.getSlot() != -1) {
            ItemStack boots = equipment.getBoots();
            Slot bootsSlot = sm.getSlot(ArmorType.BOOTS.getSlot(), InventoryType.SlotType.CONTAINER);
            inventory.setItem(ArmorType.BOOTS.getSlot(), (ItemUtils.isEmpty(boots))
                    && bootsSlot != null ? bootsSlot.getCup() : boots);
        }
    }

    public static void syncQuickSlots(PlayerWrapper playerWrapper) {
        Player player = (Player) playerWrapper.getPlayer();
        for (Slot quickSlot : SlotManager.instance().getQuickSlots()) {
            playerWrapper.getInventory().setItem(quickSlot.getSlotId(), player.getInventory().getItem(quickSlot.getQuickSlot()));
        }
    }

    public static void syncInfoSlots(PlayerWrapper playerWrapper) {
        final Player player = (Player) playerWrapper.getPlayer();
        for (Slot infoSlot : SlotManager.instance().getInfoSlots()) {
            ItemStack cup = infoSlot.getCup();
            if (ItemUtils.isEmpty(cup)) {
                continue;
            }

            ItemMeta meta = cup.getItemMeta();
            if (meta != null) {
                List<String> lore = meta.getLore();
                if (lore == null) {
                    lore = new ArrayList<>();
                }

                for (int i = 0; i < lore.size(); i++) {
                    String line = lore.get(i);
                    lore.set(i, StringUtils.setPlaceholders(player, line));
                }

                meta.setLore(lore);
                cup.setItemMeta(meta);
            }
            playerWrapper.getInventory().setItem(infoSlot.getSlotId(), cup);
        }

        player.updateInventory();
    }

    public static void syncShieldSlot(@NotNull PlayerWrapper playerWrapper) {
        Slot slot = SlotManager.instance().getShieldSlot();
        if (slot == null) {
            return;
        }

        Player player = (Player) playerWrapper.getPlayer();
        ItemStack itemInHand = player.getEquipment().getItemInOffHand();
        playerWrapper.getInventory().setItem(slot.getSlotId(), ItemUtils.isEmpty(itemInHand) ? slot.getCup() : itemInHand);
    }

    private static void updateInventory(
            @NotNull Player player,
            @NotNull Inventory inventory,
            int slot,
            InventoryAction action,
            ItemStack currentItem,
            @NotNull ItemStack cursor
    ) {
        InventoryManager.updateInventory(player, inventory, slot, InventoryType.SlotType.CONTAINER, action, currentItem, cursor);
    }

    private static void updateInventory(
            @NotNull Player player,
            @NotNull Inventory inventory,
            int slot,
            InventoryType.SlotType slotType,
            InventoryAction action,
            ItemStack currentItem,
            @NotNull ItemStack cursorItem
    ) {
        if (ActionType.getTypeOfAction(action) == ActionType.DROP) {
            return;
        }

        if (action == InventoryAction.PLACE_ALL) {
            if (ItemUtils.isEmpty(currentItem)) {
                currentItem = cursorItem.clone();
            } else {
                currentItem.setAmount(currentItem.getAmount() + cursorItem.getAmount());
            }

            cursorItem = null;
        } else if (action == InventoryAction.PLACE_ONE) {
            if (ItemUtils.isEmpty(currentItem)) {
                currentItem = cursorItem.clone();
                currentItem.setAmount(1);
                cursorItem.setAmount(cursorItem.getAmount() - 1);
            } else if (currentItem.getMaxStackSize() < currentItem.getAmount() + 1) {
                currentItem.setAmount(currentItem.getAmount() + 1);
                cursorItem.setAmount(cursorItem.getAmount() - 1);
            }
        } else if (action == InventoryAction.PLACE_SOME) {
            cursorItem.setAmount(currentItem.getMaxStackSize() - currentItem.getAmount());
            currentItem.setAmount(currentItem.getMaxStackSize());
        } else if (action == InventoryAction.SWAP_WITH_CURSOR) {
            ItemStack tempItem = cursorItem.clone();
            cursorItem = currentItem.clone();
            currentItem = tempItem;
        } else if (action == InventoryAction.PICKUP_ALL) {
            cursorItem = currentItem.clone();
            currentItem = null;
        } else if (action == InventoryAction.PICKUP_HALF) {
            ItemStack item = currentItem.clone();
            if (currentItem.getAmount() % 2 == 0) {
                item.setAmount(item.getAmount() / 2);
                currentItem = item.clone();
                cursorItem = item.clone();
            } else {
                currentItem = item.clone();
                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);
            RPGInventory.getInstance().getServer().getPluginManager().callEvent(event);

            if (event.isCancelled()) {
                return;
            }

            InventoryManager.INVENTORIES.put(player.getUniqueId(), playerWrapper);

            InventoryLocker.lockSlots(player);
            PetManager.initPlayer(player);

            RPGInventory.getInstance().getServer().getPluginManager().callEvent(new PlayerInventoryLoadEvent.Post(player));
        } catch (IOException e) {
            reporter.report("Error on inventory load", e);
        }
    }

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

        InventoryManager.INVENTORIES.get(player.getUniqueId()).onUnload();
        savePlayerInventory(player);
        InventoryLocker.unlockSlots(player);

        InventoryManager.INVENTORIES.remove(player.getUniqueId());

        RPGInventory.getInstance().getServer().getPluginManager().callEvent(new PlayerInventoryUnloadEvent.Post(player));
    }

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

        PlayerWrapper playerWrapper = InventoryManager.INVENTORIES.get(player.getUniqueId());
        try {
            Path dataFolder = RPGInventory.getInstance().getDataPath();
            Path folder = dataFolder.resolve("inventories");
            Files.createDirectories(folder);

            Path file = folder.resolve(player.getUniqueId() + ".inv");
            Files.deleteIfExists(file);

            Serialization.save(playerWrapper.createSnapshot(), file);
        } catch (IOException | NullPointerException e) {
            Log.w(e, "Error on inventory save");
        }
    }

    @NotNull
    public static PlayerWrapper get(@NotNull OfflinePlayer player) {
        PlayerWrapper playerWrapper = InventoryManager.INVENTORIES.get(player.getUniqueId());
        if (playerWrapper == null) {
            throw new IllegalStateException(player.getName() + "'s inventory should be initialized!");
        }

        return playerWrapper;
    }

    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    public static boolean isQuickEmptySlot(@Nullable ItemStack item) {
        for (Slot quickSlot : SlotManager.instance().getQuickSlots()) {
            if (quickSlot.isCup(item)) {
                return true;
            }
        }

        return false;
    }

    public static boolean isFilledSlot(@Nullable ItemStack item) {
        return InventoryManager.FILL_SLOT.equals(item);
    }

    public static boolean isEmptySlot(@Nullable ItemStack item) {
        for (Slot slot : SlotManager.instance().getSlots()) {
            if (slot.isCup(item)) {
                return true;
            }
        }

        return false;
    }

    @Contract("null -> false")
    public static boolean playerIsLoaded(@Nullable AnimalTamer player) {
        return player != null && InventoryManager.INVENTORIES.containsKey(player.getUniqueId());
    }

    public static boolean isAllowedWorld(@NotNull World world) {
        List<String> list = Config.getConfig().getStringList("worlds.list");

        ListType listType = SafeEnums.valueOf(ListType.class, Config.getConfig().getString("worlds.mode"), "list type");
        if (listType != null) {
            switch (listType) {
                case BLACKLIST:
                    return !list.contains(world.getName());
                case WHITELIST:
                    return list.contains(world.getName());
            }
        }

        return true;
    }

    public static boolean buySlot(@NotNull Player player, PlayerWrapper playerWrapper, Slot slot) {
        double cost = slot.getCost();

        if (!playerWrapper.isPreparedToBuy(slot)) {
            PlayerUtils.sendMessage(player, RPGInventory.getLanguage().getMessage("error.buyable", slot.getCost()));
            playerWrapper.prepareToBuy(slot);
            return false;
        }

        if (!PlayerUtils.checkMoney(player, cost) || !RPGInventory.getEconomy().withdrawPlayer(player, cost).transactionSuccess()) {
            return false;
        }

        playerWrapper.setBuyedSlots(slot.getName());
        PlayerUtils.sendMessage(player, RPGInventory.getLanguage().getMessage("message.buyed"));

        return true;
    }

    public static void initPlayer(@NotNull final Player player, boolean skipJoinMessage) {
        ResourcePackModule rpModule = RPGInventory.getResourcePackModule();
        if (rpModule != null) {
            rpModule.loadResourcePack(player, skipJoinMessage);
        } else {
            if (!skipJoinMessage) {
                EffectUtils.showDefaultJoinMessage(player);
            }
            InventoryManager.loadPlayerInventory(player);
        }

        if (RPGInventory.getPermissions().has(player, "rpginventory.admin")) {
            RPGInventory.getInstance().checkUpdates(player);
        }
    }

    private enum ListType {
        BLACKLIST, WHITELIST
    }
} public class FileLanguage {
    private final RPGInventory plugin;
    private final HashMap<String, MessageFormat> messageCache = new HashMap<>();
    private final Properties language = new Properties();
    @NotNull
    private final Path langFile;

    public FileLanguage(RPGInventory plugin) {
        this.plugin = plugin;
        String locale = Config.getConfig().getString("language");
        this.langFile = this.plugin.getDataPath().resolve(String.format("lang/%s.lang", locale));
        this.saveDefault();
        this.checkAndUpdateOutdatedLocaleFile();
        this.load();
        this.validateLocaleFile();
    }

    private void saveDefault() {
        if (Files.exists(this.langFile)) {
            return;
        }

        String path = "lang/" + this.langFile.getFileName();
        try {
            this.plugin.saveResource(path, true);
        } catch (Exception ex) {
            Log.w("Failed to load {0}: {1}; using en.lang", this.langFile.getFileName(), ex.toString());

            try (InputStream is = this.plugin.getResource("lang/en.lang")) {
                Objects.requireNonNull(is);
                Files.copy(is, Paths.get(this.langFile.toUri()), StandardCopyOption.REPLACE_EXISTING);
            } catch (IOException | NullPointerException e) {
                Log.s("Failed to write default locale to {0}: {1}; continue without localization.",
                        this.langFile.getFileName(), e.toString());
            }
        }
    }

    private void load() {
        try (InputStream is = Files.newInputStream(this.langFile);
             InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8)) {
            this.language.load(isr);
        } catch (IOException e) {
            Log.s("Failed to load locale file: {0}; continue without localization.", e.toString());
        }
    }

    //Oh crap.
    private void checkAndUpdateOutdatedLocaleFile() {
        final Path path = this.langFile;
        List<String> lines;
        try {
            lines = Files.readAllLines(path, StandardCharsets.UTF_8);
        } catch (IOException e) {
            Log.w(e, "Failed to read locale file");
            return;
        }

        if (lines.get(0).startsWith("#version")) {
            return;
        }

        final Pattern pattern = Pattern.compile("%(s|d|.2f)");
        final LinkedList<String> newLines = new LinkedList<>();
        newLines.add("#version: 2.0 | Do not remove this line!");

        for (int i1 = 0; i1 < lines.size(); i1++) {
            String line = lines.get(i1);
            String newLine = line.replace("\"", "");
            Matcher m;
            for (int i = 0; (m = pattern.matcher(newLine)).find(); i++) {
                newLine = m.replaceFirst("{" + i + "}");
            }

            if (lines.size() > i1 + 1) {
                String nextLine = lines.get(i1 + 1);
                if ("\n ".length() < nextLine.length()
                        && (!nextLine.contains(":") || nextLine.indexOf(':') > nextLine.indexOf(' '))) {
                    newLine = newLine + "\\";
                }
            }

            newLines.add(newLine);
        }

        try {
            Files.write(
                    path, newLines, StandardCharsets.UTF_8,
                    StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING
            );
        } catch (IOException e) {
            Log.w("Failed to save locale file: {0}", e.toString());
        }
    }

    private void validateLocaleFile() {
        Properties properties = new Properties();
        InputStream defaultLocale = this.plugin.getResource("lang/en.lang");
        try (InputStreamReader isr = new InputStreamReader(Objects.requireNonNull(defaultLocale), StandardCharsets.UTF_8)) {
            properties.load(isr);
        } catch (IOException | NullPointerException e) {
            Log.w(e, "Failed to read inbuilt locale file");
            //Just ignore. We can't help with that shit.
            return;
        }

        if (this.language.keySet().containsAll(properties.keySet())) {
            return;
        }

        for (Object key : properties.keySet()) {
            if (!this.language.containsKey(key)) {
                this.language.setProperty((String) key, properties.getProperty((String) key));
            }
        }
    }

    @NotNull
    @Deprecated
    public String getCaption(String name, Object... args) {
        return this.getMessage(name, args);
    }

    @NotNull
    public String getMessage(String key) {
        return this.getMessage(key, false);
    }

    @NotNull
    public String getMessage(String key, boolean stripColor) {
        return this.getMessage(key, stripColor, (Object[]) null);
    }

    @NotNull
    public String getMessage(String key, Object... args) {
        return this.getMessage(key, false, args);
    }

    @NotNull
    public String getMessage(String key, boolean stripColor, Object... args) {
        if (!this.messageCache.containsKey(key)) {
            this.messageCache.put(key, new MessageFormat(
                    ChatColor.translateAlternateColorCodes(
                            '&', this.language.getProperty(key, "Unknown localization key: \"" + key + "\"")
                    )
            ));
        }
        String out = this.messageCache.get(key).format(args);
        return stripColor ? ChatColor.stripColor(out) : out;
    }
} 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 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 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 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.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.endlesscode.rpginventory.RPGInventory;
import ru.endlesscode.rpginventory.event.listener.ItemListener;
import ru.endlesscode.rpginventory.inventory.InventoryManager;
import ru.endlesscode.rpginventory.misc.FileLanguage;
import ru.endlesscode.rpginventory.misc.config.Config;
import ru.endlesscode.rpginventory.pet.PetManager;
import ru.endlesscode.rpginventory.pet.PetType;
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.StringUtils;
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;
/*
 * This file is part of RPGInventory.
 * Copyright (C) 2015-2017 osipf
 *
 * RPGInventory is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * RPGInventory is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with RPGInventory.  If not, see <http://www.gnu.org/licenses/>.
 */

package ru.endlesscode.rpginventory.item;



/**
 * Created by OsipXD on 18.09.2015
 * It is part of the RpgInventory.
 * All rights reserved 2014 - 2016 © «EndlessCode Group»
 */
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);