WrappersAdapter.java

package it.fulminazzo.yagl;

import it.fulminazzo.yagl.items.AbstractItem;
import it.fulminazzo.yagl.particles.BlockDataOption;
import it.fulminazzo.yagl.particles.Particle;
import it.fulminazzo.yagl.utils.EnumUtils;
import it.fulminazzo.yagl.wrappers.Enchantment;
import it.fulminazzo.yagl.wrappers.Potion;
import it.fulminazzo.yagl.wrappers.PotionEffect;
import it.fulminazzo.yagl.wrappers.Sound;
import it.fulminazzo.fulmicollection.objects.Refl;
import it.fulminazzo.fulmicollection.structures.CacheMap;
import it.fulminazzo.fulmicollection.structures.tuples.Triple;
import it.fulminazzo.fulmicollection.structures.tuples.Tuple;
import it.fulminazzo.fulmicollection.utils.ReflectionUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.bukkit.Effect;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.material.MaterialData;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.potion.PotionType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.reflect.Constructor;
import java.util.Map;
import java.util.function.Function;

/**
 * A utility class to convert objects from this library to Minecraft Bukkit and vice versa.
 */
@SuppressWarnings("deprecation")
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class WrappersAdapter {
    private static final Map<Particle, Tuple<org.bukkit.Particle, ?>> PARTICLE_CACHE = new CacheMap<>();

    /**
     * Spawn particle.
     *
     * @param world    the world
     * @param particle the particle
     * @param x        the x
     * @param y        the y
     * @param z        the z
     * @param count    the count
     */
    public static void spawnParticle(final @NotNull World world, final @NotNull Particle particle,
                                     double x, double y, double z, int count) {
        spawnParticle(world, particle, x, y, z, count, 0.0, 0.0, 0.0);
    }

    /**
     * Spawn particle.
     *
     * @param world    the world
     * @param particle the particle
     * @param location the location
     * @param count    the count
     */
    public static void spawnParticle(final @NotNull World world, final @NotNull Particle particle,
                                     final @NotNull Location location, int count) {
        spawnParticle(world, particle, location, count, 0.0, 0.0, 0.0);
    }

    /**
     * Spawn particle.
     *
     * @param world    the world
     * @param particle the particle
     * @param x        the x
     * @param y        the y
     * @param z        the z
     * @param count    the count
     * @param offsetX  the offset x
     * @param offsetY  the offset y
     * @param offsetZ  the offset z
     */
    public static void spawnParticle(final @NotNull World world, final @NotNull Particle particle,
                                     double x, double y, double z, int count,
                                     double offsetX, double offsetY, double offsetZ) {
        spawnParticle(world, particle, x, y, z, count, offsetX, offsetY, offsetZ, 0.0);
    }

    /**
     * Spawn particle.
     *
     * @param world    the world
     * @param particle the particle
     * @param location the location
     * @param count    the count
     * @param offsetX  the offset x
     * @param offsetY  the offset y
     * @param offsetZ  the offset z
     */
    public static void spawnParticle(final @NotNull World world, final @NotNull Particle particle,
                                     final @NotNull Location location, int count,
                                     double offsetX, double offsetY, double offsetZ) {
        spawnParticle(world, particle, location, count, offsetX, offsetY, offsetZ, 0.0);
    }

    /**
     * Spawn particle.
     *
     * @param world    the world
     * @param particle the particle
     * @param x        the x
     * @param y        the y
     * @param z        the z
     * @param count    the count
     * @param offsetX  the offset x
     * @param offsetY  the offset y
     * @param offsetZ  the offset z
     * @param speed    the speed
     */
    public static void spawnParticle(final @NotNull World world, final @NotNull Particle particle,
                                     double x, double y, double z, int count,
                                     double offsetX, double offsetY, double offsetZ, double speed) {
        spawnParticle(world, particle, new Location(world, x, y, z), count, offsetX, offsetY, offsetZ, speed);
    }

    /**
     * Spawn particle.
     *
     * @param world    the world
     * @param particle the particle
     * @param location the location
     * @param count    the count
     * @param offsetX  the offset x
     * @param offsetY  the offset y
     * @param offsetZ  the offset z
     * @param speed    the speed
     */
    public static void spawnParticle(final @NotNull World world, final @NotNull Particle particle,
                                     final @NotNull Location location, int count,
                                     double offsetX, double offsetY, double offsetZ, double speed) {
        spawnParticleCommon(world, particle, location, count, offsetX, offsetY, offsetZ, speed);
    }

    /**
     * Spawn particle.
     *
     * @param player   the player
     * @param particle the particle
     * @param x        the x
     * @param y        the y
     * @param z        the z
     * @param count    the count
     */
    public static void spawnParticle(final @NotNull Player player, final @NotNull Particle particle,
                                     double x, double y, double z, int count) {
        spawnParticle(player, particle, x, y, z, count, 0.0, 0.0, 0.0);
    }

    /**
     * Spawn particle.
     *
     * @param player   the player
     * @param particle the particle
     * @param location the location
     * @param count    the count
     */
    public static void spawnParticle(final @NotNull Player player, final @NotNull Particle particle,
                                     final @NotNull Location location, int count) {
        spawnParticle(player, particle, location, count, 0.0, 0.0, 0.0);
    }

    /**
     * Spawn particle.
     *
     * @param player   the player
     * @param particle the particle
     * @param x        the x
     * @param y        the y
     * @param z        the z
     * @param count    the count
     * @param offsetX  the offset x
     * @param offsetY  the offset y
     * @param offsetZ  the offset z
     */
    public static void spawnParticle(final @NotNull Player player, final @NotNull Particle particle,
                                     double x, double y, double z, int count,
                                     double offsetX, double offsetY, double offsetZ) {
        spawnParticle(player, particle, x, y, z, count, offsetX, offsetY, offsetZ, 0.0);
    }

    /**
     * Spawn particle.
     *
     * @param player   the player
     * @param particle the particle
     * @param location the location
     * @param count    the count
     * @param offsetX  the offset x
     * @param offsetY  the offset y
     * @param offsetZ  the offset z
     */
    public static void spawnParticle(final @NotNull Player player, final @NotNull Particle particle,
                                     final @NotNull Location location, int count,
                                     double offsetX, double offsetY, double offsetZ) {
        spawnParticle(player, particle, location, count, offsetX, offsetY, offsetZ, 0.0);
    }

    /**
     * Spawn particle.
     *
     * @param player   the player
     * @param particle the particle
     * @param x        the x
     * @param y        the y
     * @param z        the z
     * @param count    the count
     * @param offsetX  the offset x
     * @param offsetY  the offset y
     * @param offsetZ  the offset z
     * @param speed    the speed
     */
    public static void spawnParticle(final @NotNull Player player, final @NotNull Particle particle,
                                     double x, double y, double z, int count,
                                     double offsetX, double offsetY, double offsetZ, double speed) {
        spawnParticle(player, particle, new Location(player.getWorld(), x, y, z), count, offsetX, offsetY, offsetZ, speed);
    }

    /**
     * Spawn particle.
     *
     * @param player   the player
     * @param particle the particle
     * @param location the location
     * @param count    the count
     * @param offsetX  the offset x
     * @param offsetY  the offset y
     * @param offsetZ  the offset z
     * @param speed    the speed
     */
    public static void spawnParticle(final @NotNull Player player, final @NotNull Particle particle,
                                     final @NotNull Location location, int count,
                                     double offsetX, double offsetY, double offsetZ, double speed) {
        spawnParticleCommon(player, particle, location, count, offsetX, offsetY, offsetZ, speed);
    }

    private static <T> void spawnParticleCommon(final @NotNull T target, final @NotNull Particle particle,
                                                final @NotNull Location location, int count,
                                                double offsetX, double offsetY, double offsetZ, double speed) {
        Tuple<org.bukkit.Particle, ?> tuple = PARTICLE_CACHE.computeIfAbsent(particle, p -> wParticleToParticle(particle));
        final org.bukkit.Particle actual = tuple.getKey();
        final Object option = tuple.getValue();

        if (target instanceof Player) {
          Player player = (Player) target;
          if (option == null) player.spawnParticle(actual, location, count, offsetX, offsetY, offsetZ, speed);
          else player.spawnParticle(actual, location, count, offsetX, offsetY, offsetZ, speed, option);
        } else if (target instanceof World) {
            World world = (World) target;
            if (option == null) world.spawnParticle(actual, location, count, offsetX, offsetY, offsetZ, speed);
            else world.spawnParticle(actual, location, count, offsetX, offsetY, offsetZ, speed, option);
        } else throw new IllegalArgumentException(String.format("Do not know how to spawn particles for '%s'", target));
    }

    /**
     * Converts the given {@link Particle} to a tuple containing the corresponding {@link org.bukkit.Particle} and
     * the parsed particle option (if present).
     *
     * @param particle the particle
     * @return the tuple
     */
    public static @NotNull Tuple<org.bukkit.Particle, ?> wParticleToParticle(final @NotNull Particle particle) {
        return wParticleToGeneral(particle, org.bukkit.Particle.class, org.bukkit.Particle::getDataType);
    }

    /**
     * Spawn effect.
     *
     * @param world    the world
     * @param particle the particle
     * @param x        the x
     * @param y        the y
     * @param z        the z
     */
    public static void spawnEffect(final @NotNull World world, final @NotNull Particle particle,
                                   double x, double y, double z) {
        spawnEffect(world, particle, new Location(world, x, y, z));
    }

    /**
     * Spawn effect.
     *
     * @param world    the world
     * @param particle the particle
     * @param location the location
     */
    public static void spawnEffect(final @NotNull World world, final @NotNull Particle particle,
                                   final @NotNull Location location) {
        world.getPlayers().forEach(p -> spawnEffect(p, particle, location));
    }

    /**
     * Spawn effect.
     *
     * @param player   the player
     * @param particle the particle
     * @param x        the x
     * @param y        the y
     * @param z        the z
     */
    public static void spawnEffect(final @NotNull Player player, final @NotNull Particle particle,
                                     double x, double y, double z) {
        spawnEffect(player, particle, new Location(player.getWorld(), x, y, z));
    }

    /**
     * Spawn effect.
     *
     * @param player   the player
     * @param particle the particle
     * @param location the location
     */
    public static void spawnEffect(final @NotNull Player player, final @NotNull Particle particle,
                                   final @NotNull Location location) {
        Tuple<Effect, ?> tuple = wParticleToEffect(particle);
        Effect actual = tuple.getKey();
        Object option = tuple.getValue();
        player.playEffect(location, actual, option);
    }

    /**
     * Converts the given {@link Particle} to a tuple containing the corresponding {@link Effect} and
     * the parsed particle option (if present).
     *
     * @param particle the particle
     * @return the tuple
     */
    public static @NotNull Tuple<Effect, ?> wParticleToEffect(final @NotNull Particle particle) {
        return wParticleToGeneral(particle, Effect.class, Effect::getData);
    }

    @SuppressWarnings("unchecked")
    private static <T extends Enum<?>> @NotNull Tuple<T, ?> wParticleToGeneral(final @NotNull Particle particle,
                                                                               final @NotNull Class<T> tClass,
                                                                               final @NotNull Function<T, Class<?>> dataTypeGetter) {
        T actual;
        if (tClass.getCanonicalName().equals("org.bukkit.Particle")) actual = (T) ParticleConverter.convertToBukkit(particle);
        else actual = EnumUtils.valueOf(tClass, particle.getType());
        Object option = particle.getOption();
        Class<?> dataType = dataTypeGetter.apply(actual);
        if (option == null || dataType == null) return new Tuple<>(actual, null);
        else {
            if (ReflectionUtils.isPrimitiveOrWrapper(dataType)) return new Tuple<>(actual, option);
            else try {
                final Object finalOption = convertOption(dataType, option);
                return new Tuple<>(actual, finalOption);
            } catch (ArrayIndexOutOfBoundsException e) {
                throw new IllegalArgumentException(String.format("Could not find constructor for data type '%s'",
                        dataType.getSimpleName()));
            } catch (ClassCastException e) {
                throw new IllegalArgumentException(String.format("Invalid option '%s' for '%s'",
                        option.getClass().getSimpleName(), actual.name()));
            }
        }
    }

    /**
     * Converts the given raw option in the specified data type.
     *
     * @param dataType the data type
     * @param option   the option
     * @return the object
     */
    @SuppressWarnings("unchecked")
    static @Nullable Object convertOption(@NotNull Class<?> dataType, @NotNull Object option) {
        // Check options
        if (option instanceof AbstractItem) return itemToItemStack((AbstractItem) option);
        else if (option instanceof Potion) return wPotionToPotion((Potion) option);
        else if (option instanceof Color) return wColorToColor((Color) option);
        // Check data types
        else if (dataType.isEnum()) return EnumUtils.valueOf(dataType, option.toString());
        else if (dataType.equals(MaterialData.class)) {
            if (!(option instanceof Tuple))
                throw new IllegalArgumentException(String.format("Expected %s but got %s",
                        Tuple.class.getSimpleName(), option.getClass().getSimpleName()));
            Tuple<String, Integer> tuple = (Tuple<String, Integer>) option;
            Material material = EnumUtils.valueOf(Material.class, tuple.getKey());
            Integer data = tuple.getValue();
            return material.getNewData((byte) (data == null ? 0 : data));
        } else if (dataType.getCanonicalName().equals("org.bukkit.Vibration")) return option;
        else if (dataType.getCanonicalName().equals("org.bukkit.block.data.BlockData")) {
            String raw = option.toString();
            BlockDataOption blockDataOption = new BlockDataOption(raw);
            Material material = EnumUtils.valueOf(Material.class, blockDataOption.getMaterial());
            if (!material.isBlock())
                throw new IllegalArgumentException(String.format("Cannot use non-block material '%s' as block data", material));
            String nbt = blockDataOption.getNBT().trim();
            return nbt.isEmpty() ? material.createBlockData() : material.createBlockData(String.format("[%s]", nbt));
        } else {
            // Try creation from the data type
            final Object finalOption;
            Constructor<?> constructor = dataType.getDeclaredConstructors()[0];
            int size = constructor.getParameterCount();
            if (size == 2) {
                Tuple<?, ?> t = (Tuple<?, ?>) option;
                finalOption = new Refl<>(dataType, prepareParameters(t.getKey(), t.getValue())).getObject();
            } else if (size == 3) {
                Triple<?, ?, ?> t = (Triple<?, ?, ?>) option;
                finalOption = new Refl<>(dataType, prepareParameters(t.getFirst(), t.getSecond(), t.getThird())).getObject();
            } else throw new IllegalArgumentException("Cannot create option from constructor: " + constructor);
            return finalOption;
        }
    }

    private static Object @NotNull [] prepareParameters(final Object @NotNull ... parameters) {
        for (int i = 0; i < parameters.length; i++) {
            Object o = parameters[i];
            if (o instanceof Color) parameters[i] = wColorToColor((Color) o);
        }
        return parameters;
    }

    /**
     * Converts the given {@link AbstractItem} to an {@link ItemStack}.
     *
     * @param item the item
     * @return the item stack
     */
    public static @Nullable ItemStack itemToItemStack(final @Nullable AbstractItem item) {
        final Class<?> itemUtils;
        try {
            itemUtils = ReflectionUtils.getClass("it.fulminazzo.yagl.ItemAdapter");
        } catch (IllegalArgumentException e) {
            throw new IllegalStateException("Could not find ItemAdapter class. This function requires the 'item:bukkit' module to be added");
        }
        return new Refl<>(itemUtils).invokeMethod("itemToItemStack", item);
    }

    /**
     * Converts the given wrapper {@link Color} to a {@link org.bukkit.Color}.
     *
     * @param color the color
     * @return the color
     */
    @SuppressWarnings("DataFlowIssue")
    public static @NotNull org.bukkit.Color wColorToColor(final @NotNull Color color) {
        org.bukkit.Color actualColor;
        try {
            actualColor = new Refl<>(org.bukkit.Color.class).invokeMethod("fromARGB",
                    color.getAlpha(), color.getRed(), color.getGreen(), color.getBlue());
        } catch (Exception e) {
            actualColor = org.bukkit.Color.fromRGB(color.getRed(), color.getGreen(), color.getBlue());
        }
        return actualColor;
    }

    /**
     * Converts the given {@link org.bukkit.Color} to a wrapper {@link Color}.
     *
     * @param color the color
     * @return the color
     */
    @SuppressWarnings("DataFlowIssue")
    public static @NotNull Color colorToWColor(final @NotNull org.bukkit.Color color) {
        try {
            return new Color(new Refl<>(color).invokeMethod("getAlpha"), color.getRed(), color.getGreen(), color.getBlue());
        } catch (Exception e) {
            return new Color(color.getRed(), color.getGreen(), color.getBlue());
        }
    }

    /**
     * Plays the given {@link Sound} using Bukkit methods at the player's {@link Location}.
     * In older versions, {@link Sound#getCategory()} is ignored.
     *
     * @param player the player
     * @param sound  the sound
     */
    public static void playSound(final @NotNull Player player, final @NotNull Sound sound) {
        playSound(player, player.getLocation(), sound);
    }

    /**
     * Plays the given {@link Sound} using Bukkit methods.
     * In older versions, {@link Sound#getCategory()} is ignored.
     *
     * @param player   the player
     * @param location the location
     * @param sound    the sound
     */
    public static void playSound(final @NotNull Player player, final @NotNull Location location, final @NotNull Sound sound) {
        playInternalSound(player, location, sound, s -> EnumUtils.valueOf(org.bukkit.Sound.class, s.getName()));
    }

    /**
     * Plays the given {@link Sound} using Bukkit methods at the player's {@link Location}.
     * In older versions, {@link Sound#getCategory()} is ignored.
     * The {@link Player#playSound(Location, org.bukkit.Sound, float, float)} method is used
     * to skip sound verification.
     *
     * @param player the player
     * @param sound  the sound
     */
    public static void playCustomSound(final @NotNull Player player, final @NotNull Sound sound) {
        playCustomSound(player, player.getLocation(), sound);
    }

    /**
     * Plays the given {@link Sound} using Bukkit methods.
     * In older versions, {@link Sound#getCategory()} is ignored.
     * The {@link Player#playSound(Location, org.bukkit.Sound, float, float)} method is used
     * to skip sound verification.
     *
     * @param player   the player
     * @param location the location
     * @param sound    the sound
     */
    public static void playCustomSound(final @NotNull Player player, final @NotNull Location location, final @NotNull Sound sound) {
        playInternalSound(player, location, sound, Sound::getName);
    }

    private static <T> void playInternalSound(final @NotNull Player player, final @NotNull Location location,
                                              final @NotNull Sound sound, final @NotNull Function<Sound, T> actualSound) {
        final Refl<Player> playerRefl = new Refl<>(player);
        final T actual = actualSound.apply(sound);
        try {
            String category = sound.getCategory();
            if (category != null) {
                final Object actualCategory = EnumUtils.valueOf(Class.forName("org.bukkit.SoundCategory"), category);
                playerRefl.callMethod("playSound", location, actual, actualCategory, sound.getVolume(), sound.getPitch());
                return;
            }
        } catch (ClassNotFoundException e) {
            // Prevent other versions from complaining about method not found.
        }
        playerRefl.callMethod("playSound", location, actual, sound.getVolume(), sound.getPitch());
    }

    /**
     * Converts the given wrapper {@link PotionEffect} to a {@link org.bukkit.potion.PotionEffect}.
     *
     * @param potionEffect the potion effect
     * @return the potion effect
     */
    public static @NotNull org.bukkit.potion.PotionEffect wPotionEffectToPotionEffect(final @NotNull PotionEffect potionEffect) {
        String effect = potionEffect.getName();
        PotionEffectType type;
        try {
            type = EnumUtils.valueOf(PotionEffectType.class, effect, "getByName");
        } catch (IllegalArgumentException e) {
            // For Minecraft 1.20.6 and above
            effect = effect.replace(" ", "_").toLowerCase();
            if (effect.equalsIgnoreCase("bad_luck")) effect = "unluck";
            type = EnumUtils.valueOf(PotionEffectType.class, effect, "getByName");
        }
        try {
            return new org.bukkit.potion.PotionEffect(type, potionEffect.getDurationInTicks(), potionEffect.getAmplifier(),
                    potionEffect.isShowingParticles(), potionEffect.isShowingParticles(),
                    potionEffect.isShowingIcon());
        } catch (NoSuchMethodError e) {
            return new org.bukkit.potion.PotionEffect(type, potionEffect.getDurationInTicks(), potionEffect.getAmplifier(),
                    potionEffect.isShowingParticles(), potionEffect.isShowingParticles());
        }
    }

    /**
     * Converts the given potion effect to a {@link PotionEffect}.
     * On older versions, {@link PotionEffect#isShowingIcon()} will be ignored.
     *
     * @param potionEffect the potion effect
     * @return the potion effect
     */
    public static @NotNull PotionEffect potionEffectToWPotionEffect(final @NotNull org.bukkit.potion.PotionEffect potionEffect) {
        try {
            return new PotionEffect(potionEffect.getType().getName(), (double) potionEffect.getDuration() / Constants.TICKS_IN_SECOND,
                    potionEffect.getAmplifier() + 1, potionEffect.hasParticles(), potionEffect.hasIcon());
        } catch (NoSuchMethodError e) {
            return new PotionEffect(potionEffect.getType().getName(), (double) potionEffect.getDuration() / Constants.TICKS_IN_SECOND,
                    potionEffect.getAmplifier() + 1, potionEffect.hasParticles());
        }
    }

    /**
     * Converts the given wrapper {@link Potion} to a Bukkit potion.
     * Because of Bukkit recent changes (1.20.6+), <code>org.bukkit.potion.Potion</code> does not exist anymore.
     * For retro-compatibility reasons, this class will return a {@link PotionWrapper} instead,
     * from which the actual <code>org.bukkit.potion.Potion</code> object can be retrieved.
     *
     * @param potion the potion
     * @return the wrapped potion
     */
    public static @NotNull PotionWrapper wPotionToPotion(final @NotNull Potion potion) {
        return new PotionWrapper(EnumUtils.valueOf(PotionType.class, potion.getName()),
                potion.getLevel(), potion.isSplash(), potion.isExtended());
    }

    /**
     * Converts the given Bukkit potion to a wrapper {@link Potion}.
     * Because of Bukkit recent changes (1.20.6+), <code>org.bukkit.potion.Potion</code> does not exist anymore.
     * For retro-compatibility reasons, this class allows a generic parameter to be passed,
     * but it will require an object of type org.bukkit.potion.Potion.
     *
     * @param <P>    the type of the potion (org.bukkit.potion.Potion).
     * @param potion the bukkit potion
     * @return the potion
     */
    public static <P> @NotNull Potion potionToWPotion(final @NotNull P potion) {
        return potionToWPotion(new PotionWrapper(potion));
    }

    /**
     * Converts the given potion wrapper to a wrapper {@link Potion}.
     *
     * @param potion the potion wrapper
     * @return the potion
     */
    public static @NotNull Potion potionToWPotion(final @NotNull PotionWrapper potion) {
        return new Potion(potion.getType().name(), potion.getLevel(), potion.isSplash(), potion.hasExtendedDuration());
    }

    /**
     * Converts the given wrapper {@link Enchantment} to a {@link Tuple} containing its corresponding one and its level.
     *
     * @param enchantment the enchantment
     * @return the tuple
     */
    @SuppressWarnings("DataFlowIssue")
    public static @NotNull Tuple<org.bukkit.enchantments.Enchantment, Integer> wEnchantToEnchant(final @NotNull Enchantment enchantment) {
        String raw = enchantment.getName();
        org.bukkit.enchantments.Enchantment actual = null;
        try {
            Object key = getNamespacedKey(raw);
            actual = new Refl<>(org.bukkit.enchantments.Enchantment.class).invokeMethod("getByKey", key);
            if (actual == null) throw new IllegalArgumentException("Cannot find from getKey");
        } catch (Exception e) {
            // Prevent other versions from complaining about method not found.
            Map<String, org.bukkit.enchantments.Enchantment> byName = new Refl<>(org.bukkit.enchantments.Enchantment.class).getFieldObject("byName");
            for (String key : byName.keySet())
                if (key.equalsIgnoreCase(raw)) {
                    actual = byName.get(key);
                    break;
                }
            if (actual == null) throw new IllegalArgumentException(String.format("Could not find enchantment '%s'", raw));
        }
        return new Tuple<>(actual, enchantment.getLevel());
    }

    /**
     * Gets the corresponding namespaced key if the current version of Minecraft supports it.
     * Uses <i>minecraft</i> as the key for {@link #getNamespacedKey(String, String)}.
     *
     * @param value the value
     * @return the namespaced key
     */
    public static Object getNamespacedKey(final @NotNull String value) {
        return getNamespacedKey("minecraft", value);
    }

    /**
     * Gets the corresponding namespaced key if the current version of Minecraft supports it.
     *
     * @param key   the key
     * @param value the value
     * @return the namespaced key
     */
    public static Object getNamespacedKey(final @NotNull String key, final @NotNull String value) {
        try {
            Class<?> clazz = getNamespacedKeyClass();
            return new Refl<>(clazz, key, value).getObject();
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException("NamespacedKey did not exist in this version of Minecraft");
        }
    }

    private static @NotNull Class<?> getNamespacedKeyClass() throws ClassNotFoundException {
        return Class.forName("org.bukkit.NamespacedKey");
    }

    /**
     * Converts the given Bukkit enchantment to a {@link Enchantment}.
     * Uses {@link org.bukkit.enchantments.Enchantment#getName()} for retro-compatibility purposes.
     *
     * @param enchantment the enchantment
     * @return the enchantment
     */
    public static @NotNull Enchantment enchantToWEnchant(final @NotNull org.bukkit.enchantments.Enchantment enchantment) {
        return enchantToWEnchant(enchantment, 1);
    }

    /**
     * Converts the given Bukkit enchantment to a {@link Enchantment}.
     * Uses {@link org.bukkit.enchantments.Enchantment#getName()} for retro-compatibility purposes.
     *
     * @param enchantment the enchantment
     * @param level       the level
     * @return the enchantment
     */
    public static @NotNull Enchantment enchantToWEnchant(final @NotNull org.bukkit.enchantments.Enchantment enchantment, final int level) {
        return new Enchantment(enchantment.getName(), level);
    }
}