package com.loganmagnan.eventcore.utils;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class NBTEditor {

    private static final Map< String, Class< ? > > classCache;
    private static final Map< String, Method > methodCache;
    private static final Map< Class< ? >, Constructor< ? > > constructorCache;
    private static final Map< Class< ? >, Class< ? > > NBTClasses;
    private static final Map< Class< ? >, Field > NBTTagFieldCache;
    private static Field NBTListData;
    private static Field NBTCompoundMap;
    private static final String VERSION;
    private static final MinecraftVersion LOCAL_VERSION;

    public static final Type COMPOUND = Type.COMPOUND;
    public static final Type LIST = Type.LIST;
    public static final Type NEW_ELEMENT = Type.NEW_ELEMENT;
    public static final Type DELETE = Type.DELETE;

    static {
        VERSION = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
        LOCAL_VERSION = MinecraftVersion.get( VERSION );

        classCache = new HashMap< String, Class<?> >();
        try {
            if ( LOCAL_VERSION.lessThanOrEqualTo( MinecraftVersion.v1_16 ) ) {
                classCache.put( "NBTBase", Class.forName( "net.minecraft.server." + VERSION + "." + "NBTBase" ) );
                classCache.put( "NBTTagCompound", Class.forName( "net.minecraft.server." + VERSION + "." + "NBTTagCompound" ) );
                classCache.put( "NBTTagList", Class.forName( "net.minecraft.server." + VERSION + "." + "NBTTagList" ) );
                classCache.put( "NBTTagEnd", Class.forName( "net.minecraft.server." + VERSION + "." + "NBTTagEnd" ) );
                classCache.put( "MojangsonParser", Class.forName( "net.minecraft.server." + VERSION + "." + "MojangsonParser" ) );

                classCache.put( "ItemStack", Class.forName( "net.minecraft.server." + VERSION + "." + "ItemStack" ) );

                classCache.put( "Entity", Class.forName( "net.minecraft.server." + VERSION + "." + "Entity" ) );
                classCache.put( "EntityLiving", Class.forName( "net.minecraft.server." + VERSION + "." + "EntityLiving" ) );

                classCache.put( "BlockPosition", Class.forName( "net.minecraft.server." + VERSION + "." + "BlockPosition" ) );
                classCache.put( "TileEntity", Class.forName( "net.minecraft.server." + VERSION + "." + "TileEntity" ) );
                classCache.put( "World", Class.forName( "net.minecraft.server." + VERSION + "." + "World" ) );
                classCache.put( "IBlockData", Class.forName( "net.minecraft.server." + VERSION + "." + "IBlockData" ) );

                classCache.put( "TileEntitySkull", Class.forName( "net.minecraft.server." + VERSION + "." + "TileEntitySkull" ) );

            } else {
                classCache.put( "BlockPosition", Class.forName( "net.minecraft.core.BlockPosition" ) );

                classCache.put( "NBTBase", Class.forName( "net.minecraft.nbt.NBTBase" ) );
                classCache.put( "NBTTagCompound", Class.forName( "net.minecraft.nbt.NBTTagCompound" ) );
                classCache.put( "NBTTagList", Class.forName( "net.minecraft.nbt.NBTTagList" ) );
                classCache.put( "NBTTagEnd", Class.forName( "net.minecraft.nbt.NBTTagEnd" ) );
                classCache.put( "MojangsonParser", Class.forName( "net.minecraft.nbt.MojangsonParser" ) );

                classCache.put( "ItemStack", Class.forName( "net.minecraft.world.item.ItemStack" ) );

                classCache.put( "Entity", Class.forName( "net.minecraft.world.entity.Entity" ) );
                classCache.put( "EntityLiving", Class.forName( "net.minecraft.world.entity.EntityLiving" ) );

                classCache.put( "World", Class.forName( "net.minecraft.world.level.World" ) );
                classCache.put( "IBlockData", Class.forName( "net.minecraft.world.level.block.state.IBlockData" ) );
                classCache.put( "TileEntity", Class.forName( "net.minecraft.world.level.block.entity.TileEntity" ) );
                classCache.put( "TileEntitySkull", Class.forName( "net.minecraft.world.level.block.entity.TileEntitySkull" ) );
            }

            classCache.put( "CraftItemStack", Class.forName( "org.bukkit.craftbukkit." + VERSION + ".inventory." + "CraftItemStack" ) );
            classCache.put( "CraftMetaSkull", Class.forName( "org.bukkit.craftbukkit." + VERSION + ".inventory." + "CraftMetaSkull" ) );

            classCache.put( "CraftEntity", Class.forName( "org.bukkit.craftbukkit." + VERSION + ".entity." + "CraftEntity" ) );

            classCache.put( "CraftWorld", Class.forName( "org.bukkit.craftbukkit." + VERSION + "." + "CraftWorld" ) );
            classCache.put( "CraftBlockState", Class.forName( "org.bukkit.craftbukkit." + VERSION + ".block." + "CraftBlockState" ) );

            classCache.put( "GameProfile", Class.forName( "com.mojang.authlib.GameProfile" ) );
            classCache.put( "Property", Class.forName( "com.mojang.authlib.properties.Property" ) );
            classCache.put( "PropertyMap", Class.forName( "com.mojang.authlib.properties.PropertyMap" ) );
        } catch ( ClassNotFoundException e ) {
            e.printStackTrace();
        }

        NBTClasses = new HashMap< Class< ? >, Class< ? > >();
        try {
            if ( LOCAL_VERSION.lessThanOrEqualTo( MinecraftVersion.v1_16 ) ) {
                NBTClasses.put( Byte.class, Class.forName( "net.minecraft.server." + VERSION + "." + "NBTTagByte" ) );
                NBTClasses.put( Boolean.class, Class.forName( "net.minecraft.server." + VERSION + "." + "NBTTagByte" ) );
                NBTClasses.put( String.class, Class.forName( "net.minecraft.server." + VERSION + "." + "NBTTagString" ) );
                NBTClasses.put( Double.class, Class.forName( "net.minecraft.server." + VERSION + "." + "NBTTagDouble" ) );
                NBTClasses.put( Integer.class, Class.forName( "net.minecraft.server." + VERSION + "." + "NBTTagInt" ) );
                NBTClasses.put( Long.class, Class.forName( "net.minecraft.server." + VERSION + "." + "NBTTagLong" ) );
                NBTClasses.put( Short.class, Class.forName( "net.minecraft.server." + VERSION + "." + "NBTTagShort" ) );
                NBTClasses.put( Float.class, Class.forName( "net.minecraft.server." + VERSION + "." + "NBTTagFloat" ) );
                NBTClasses.put( Class.forName( "[B" ), Class.forName( "net.minecraft.server." + VERSION + "." + "NBTTagByteArray" ) );
                NBTClasses.put( Class.forName( "[I" ), Class.forName( "net.minecraft.server." + VERSION + "." + "NBTTagIntArray" ) );
            } else {
                NBTClasses.put( Byte.class, Class.forName( "net.minecraft.nbt.NBTTagByte" ) );
                NBTClasses.put( Boolean.class, Class.forName( "net.minecraft.nbt.NBTTagByte" ) );
                NBTClasses.put( String.class, Class.forName( "net.minecraft.nbt.NBTTagString" ) );
                NBTClasses.put( Double.class, Class.forName( "net.minecraft.nbt.NBTTagDouble" ) );
                NBTClasses.put( Integer.class, Class.forName( "net.minecraft.nbt.NBTTagInt" ) );
                NBTClasses.put( Long.class, Class.forName( "net.minecraft.nbt.NBTTagLong" ) );
                NBTClasses.put( Short.class, Class.forName( "net.minecraft.nbt.NBTTagShort" ) );
                NBTClasses.put( Float.class, Class.forName( "net.minecraft.nbt.NBTTagFloat" ) );
                NBTClasses.put( Class.forName( "[B" ), Class.forName( "net.minecraft.nbt.NBTTagByteArray" ) );
                NBTClasses.put( Class.forName( "[I" ), Class.forName( "net.minecraft.nbt.NBTTagIntArray" ) );
            }
        } catch ( ClassNotFoundException e ) {
            e.printStackTrace();
        }

        methodCache = new HashMap< String, Method >();
        try {
            if ( LOCAL_VERSION.lessThanOrEqualTo( MinecraftVersion.v1_17 ) ) {
                methodCache.put( "get", getNMSClass( "NBTTagCompound" ).getMethod( "get", String.class ) );
                methodCache.put( "set", getNMSClass( "NBTTagCompound" ).getMethod( "set", String.class, getNMSClass( "NBTBase" ) ) );
                methodCache.put( "hasKey", getNMSClass( "NBTTagCompound" ).getMethod( "hasKey", String.class ) );
            } else {
                methodCache.put( "get", getNMSClass( "NBTTagCompound" ).getMethod( "c", String.class ) );
                methodCache.put( "set", getNMSClass( "NBTTagCompound" ).getMethod( "a", String.class, getNMSClass( "NBTBase" ) ) );
                methodCache.put( "hasKey", getNMSClass( "NBTTagCompound" ).getMethod( "e", String.class ) );
            }
            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                methodCache.put( "setIndex", getNMSClass( "NBTTagList" ).getMethod( "d", int.class, getNMSClass( "NBTBase" ) ) );
            } else if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_17 ) ) {
                methodCache.put( "setIndex", getNMSClass( "NBTTagList" ).getMethod( "set", int.class, getNMSClass( "NBTBase" ) ) );
            } else {
                methodCache.put( "setIndex", getNMSClass( "NBTTagList" ).getMethod( "a", int.class, getNMSClass( "NBTBase" ) ) );
            }
            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                methodCache.put( "add", getNMSClass( "NBTTagList" ).getMethod( "c", int.class, getNMSClass( "NBTBase" ) ) );
            } else if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_14 ) ) {
                methodCache.put( "add", getNMSClass( "NBTTagList" ).getMethod( "add", int.class, getNMSClass( "NBTBase" ) ) );
            } else {
                methodCache.put( "add", getNMSClass( "NBTTagList" ).getMethod( "add", getNMSClass( "NBTBase" ) ) );
            }
            methodCache.put( "size", getNMSClass( "NBTTagList" ).getMethod( "size" ) );

            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                methodCache.put( "listRemove", getNMSClass( "NBTTagList" ).getMethod( "c", int.class )  );
            } else if ( LOCAL_VERSION == MinecraftVersion.v1_8 ) {
                methodCache.put( "listRemove", getNMSClass( "NBTTagList" ).getMethod( "a", int.class )  );
            } else {
                methodCache.put( "listRemove", getNMSClass( "NBTTagList" ).getMethod( "remove", int.class )  );
            }
            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                methodCache.put( "remove", getNMSClass( "NBTTagCompound" ).getMethod( "r", String.class ) );
            } else {
                methodCache.put( "remove", getNMSClass( "NBTTagCompound" ).getMethod( "remove", String.class ) );
            }
            if ( LOCAL_VERSION.lessThanOrEqualTo( MinecraftVersion.v1_12 ) ) {
                methodCache.put( "getKeys", getNMSClass( "NBTTagCompound" ).getMethod( "c" ) );
            } else if ( LOCAL_VERSION.lessThanOrEqualTo( MinecraftVersion.v1_17 ) ) {
                methodCache.put( "getKeys", getNMSClass( "NBTTagCompound" ).getMethod( "getKeys" ) );
            } else {
                methodCache.put( "getKeys", getNMSClass( "NBTTagCompound" ).getMethod( "d" ) );
            }

            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_19 ) ) {
                methodCache.put( "hasTag", getNMSClass( "ItemStack" ).getMethod( "t" ) );
                methodCache.put( "getTag", getNMSClass( "ItemStack" ).getMethod( "u" ) );
                methodCache.put( "setTag", getNMSClass( "ItemStack" ).getMethod( "c", getNMSClass( "NBTTagCompound" ) ) );
            } else if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R2 ) ) {
                methodCache.put( "hasTag", getNMSClass( "ItemStack" ).getMethod( "s" ) );
                methodCache.put( "getTag", getNMSClass( "ItemStack" ).getMethod( "t" ) );
                methodCache.put( "setTag", getNMSClass( "ItemStack" ).getMethod( "c", getNMSClass( "NBTTagCompound" ) ) );
            } else if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                methodCache.put( "hasTag", getNMSClass( "ItemStack" ).getMethod( "r" ) );
                methodCache.put( "getTag", getNMSClass( "ItemStack" ).getMethod( "s" ) );
                methodCache.put( "setTag", getNMSClass( "ItemStack" ).getMethod( "c", getNMSClass( "NBTTagCompound" ) ) );
            } else {
                methodCache.put( "hasTag", getNMSClass( "ItemStack" ).getMethod( "hasTag" ) );
                methodCache.put( "getTag", getNMSClass( "ItemStack" ).getMethod( "getTag" ) );
                methodCache.put( "setTag", getNMSClass( "ItemStack" ).getMethod( "setTag", getNMSClass( "NBTTagCompound" ) ) );
            }
            methodCache.put( "asNMSCopy", getNMSClass( "CraftItemStack" ).getMethod( "asNMSCopy", ItemStack.class ) );
            methodCache.put( "asBukkitCopy", getNMSClass( "CraftItemStack" ).getMethod( "asBukkitCopy", getNMSClass( "ItemStack" ) ) );

            methodCache.put( "getEntityHandle", getNMSClass( "CraftEntity" ).getMethod( "getHandle" ) );
            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                methodCache.put( "getEntityTag", getNMSClass( "Entity" ).getMethod( "f", getNMSClass( "NBTTagCompound" ) ) );
                methodCache.put( "setEntityTag", getNMSClass( "Entity" ).getMethod( "g", getNMSClass( "NBTTagCompound" ) ) );
            } else if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_16 ) ) {
                methodCache.put( "getEntityTag", getNMSClass( "Entity" ).getMethod( "save", getNMSClass( "NBTTagCompound" ) ) );
                methodCache.put( "setEntityTag", getNMSClass( "Entity" ).getMethod( "load", getNMSClass( "NBTTagCompound" ) ) );
            } else {
                methodCache.put( "getEntityTag", getNMSClass( "Entity" ).getMethod( "c", getNMSClass( "NBTTagCompound" ) ) );
                methodCache.put( "setEntityTag", getNMSClass( "Entity" ).getMethod( "f", getNMSClass( "NBTTagCompound" ) ) );
            }

            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                methodCache.put( "save", getNMSClass( "ItemStack" ).getMethod( "b", getNMSClass( "NBTTagCompound" ) ) );
            } else {
                methodCache.put( "save", getNMSClass( "ItemStack" ).getMethod( "save", getNMSClass( "NBTTagCompound" ) ) );
            }

            if ( LOCAL_VERSION.lessThanOrEqualTo( MinecraftVersion.v1_10 ) ) {
                methodCache.put( "createStack", getNMSClass( "ItemStack" ).getMethod( "createStack", getNMSClass( "NBTTagCompound" ) ) );
            } else if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_13 ) ) {
                methodCache.put( "createStack", getNMSClass( "ItemStack" ).getMethod( "a", getNMSClass( "NBTTagCompound" ) ) );
            }

            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                methodCache.put( "setTileTag", getNMSClass( "TileEntity" ).getMethod( "a", getNMSClass( "NBTTagCompound" ) ) );
            } else if ( LOCAL_VERSION == MinecraftVersion.v1_16 ) {
                methodCache.put( "setTileTag", getNMSClass( "TileEntity" ).getMethod( "load", getNMSClass( "IBlockData" ), getNMSClass( "NBTTagCompound" ) ) );
                methodCache.put( "getType", getNMSClass( "World" ).getMethod( "getType", getNMSClass( "BlockPosition" ) ) );
            } else if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_12 ) ) {
                methodCache.put( "setTileTag", getNMSClass( "TileEntity" ).getMethod( "load", getNMSClass( "NBTTagCompound" ) ) );
            } else {
                methodCache.put( "setTileTag", getNMSClass( "TileEntity" ).getMethod( "a", getNMSClass( "NBTTagCompound" ) ) );
            }

            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                methodCache.put( "getTileTag", getNMSClass( "TileEntity" ).getMethod( "m" ) );
            } else if ( LOCAL_VERSION == MinecraftVersion.v1_8 ) {
                methodCache.put( "getTileTag", getNMSClass( "TileEntity" ).getMethod( "b", getNMSClass( "NBTTagCompound" ) ) );
            } else {
                methodCache.put( "getTileTag", getNMSClass( "TileEntity" ).getMethod( "save", getNMSClass( "NBTTagCompound" ) ) );
            }

            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                methodCache.put( "getTileEntity", getNMSClass( "World" ).getMethod( "c_", getNMSClass( "BlockPosition" ) ) );
            } else {
                methodCache.put( "getTileEntity", getNMSClass( "World" ).getMethod( "getTileEntity", getNMSClass( "BlockPosition" ) ) );
            }
            methodCache.put( "getWorldHandle", getNMSClass( "CraftWorld" ).getMethod( "getHandle" ) );

            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                methodCache.put( "setGameProfile", getNMSClass( "TileEntitySkull" ).getMethod( "a", getNMSClass( "GameProfile" ) ) );
            } else {
                methodCache.put( "setGameProfile", getNMSClass( "TileEntitySkull" ).getMethod( "setGameProfile", getNMSClass( "GameProfile" ) ) );
            }
            methodCache.put( "getProperties", getNMSClass( "GameProfile" ).getMethod( "getProperties" ) );
            methodCache.put( "getName", getNMSClass( "Property" ).getMethod( "getName" ) );
            methodCache.put( "getValue", getNMSClass( "Property" ).getMethod( "getValue" ) );
            methodCache.put( "values", getNMSClass( "PropertyMap" ).getMethod( "values" ) );
            methodCache.put( "put", getNMSClass( "PropertyMap" ).getMethod( "put", Object.class, Object.class ) );

            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                methodCache.put( "loadNBTTagCompound", getNMSClass( "MojangsonParser" ).getMethod( "a", String.class ) );
            } else {
                methodCache.put( "loadNBTTagCompound", getNMSClass( "MojangsonParser" ).getMethod( "parse", String.class ) );
            }
        } catch( Exception e ) {
            e.printStackTrace();
        }

        try {
            methodCache.put( "setProfile", getNMSClass( "CraftMetaSkull" ).getDeclaredMethod( "setProfile", getNMSClass( "GameProfile" ) ) );
            methodCache.get( "setProfile" ).setAccessible( true );
        } catch( NoSuchMethodException exception ) {
            // The method doesn't exist, so it's before 1.15.2
        }

        constructorCache = new HashMap< Class< ? >, Constructor< ? > >();
        try {
            constructorCache.put( getNBTTag( Byte.class ), getNBTTag( Byte.class ).getDeclaredConstructor( byte.class ) );
            constructorCache.put( getNBTTag( Boolean.class ), getNBTTag( Boolean.class ).getDeclaredConstructor( byte.class ) );
            constructorCache.put( getNBTTag( String.class ), getNBTTag( String.class ).getDeclaredConstructor( String.class ) );
            constructorCache.put( getNBTTag( Double.class ), getNBTTag( Double.class ).getDeclaredConstructor( double.class ) );
            constructorCache.put( getNBTTag( Integer.class ), getNBTTag( Integer.class ).getDeclaredConstructor( int.class ) );
            constructorCache.put( getNBTTag( Long.class ), getNBTTag( Long.class ).getDeclaredConstructor( long.class ) );
            constructorCache.put( getNBTTag( Float.class ), getNBTTag( Float.class ).getDeclaredConstructor( float.class ) );
            constructorCache.put( getNBTTag( Short.class ), getNBTTag( Short.class ).getDeclaredConstructor( short.class ) );
            constructorCache.put( getNBTTag( Class.forName( "[B" ) ), getNBTTag( Class.forName( "[B" ) ).getDeclaredConstructor( Class.forName( "[B" ) ) );
            constructorCache.put( getNBTTag( Class.forName( "[I" ) ), getNBTTag( Class.forName( "[I" ) ).getDeclaredConstructor( Class.forName( "[I" ) ) );

            // This is for 1.15 since Mojang decided to make the constructors private
            for ( Constructor< ? > cons : constructorCache.values() ) {
                cons.setAccessible( true );
            }

            constructorCache.put( getNMSClass( "BlockPosition" ), getNMSClass( "BlockPosition" ).getConstructor( int.class, int.class, int.class ) );

            constructorCache.put( getNMSClass( "GameProfile" ), getNMSClass( "GameProfile" ).getConstructor( UUID.class, String.class ) );
            constructorCache.put( getNMSClass( "Property" ), getNMSClass( "Property" ).getConstructor( String.class, String.class ) );

            if ( LOCAL_VERSION == MinecraftVersion.v1_11 || LOCAL_VERSION == MinecraftVersion.v1_12 ) {
                constructorCache.put( getNMSClass( "ItemStack" ), getNMSClass( "ItemStack" ).getConstructor( getNMSClass( "NBTTagCompound" ) ) );
            }
        } catch( Exception e ) {
            e.printStackTrace();
        }

        NBTTagFieldCache = new HashMap< Class< ? >, Field >();
        try {
            if ( LOCAL_VERSION.lessThanOrEqualTo( MinecraftVersion.v1_16 ) ) {
                for ( Class< ? > clazz : NBTClasses.values() ) {
                    Field data = clazz.getDeclaredField( "data" );
                    data.setAccessible( true );
                    NBTTagFieldCache.put( clazz, data );
                }
            } else {
                NBTTagFieldCache.put( NBTClasses.get( Byte.class ), NBTClasses.get( Byte.class ).getDeclaredField( "x" ) );
                NBTTagFieldCache.put( NBTClasses.get( Boolean.class ), NBTClasses.get( Boolean.class ).getDeclaredField( "x" ) );
                NBTTagFieldCache.put( NBTClasses.get( String.class ), NBTClasses.get( String.class ).getDeclaredField( "A" ) );
                NBTTagFieldCache.put( NBTClasses.get( Double.class ), NBTClasses.get( Double.class ).getDeclaredField( "w" ) );
                NBTTagFieldCache.put( NBTClasses.get( Integer.class ), NBTClasses.get( Integer.class ).getDeclaredField( "c" ) );
                NBTTagFieldCache.put( NBTClasses.get( Long.class ), NBTClasses.get( Long.class ).getDeclaredField( "c" ) );
                NBTTagFieldCache.put( NBTClasses.get( Float.class ), NBTClasses.get( Float.class ).getDeclaredField( "w" ) );
                NBTTagFieldCache.put( NBTClasses.get( Short.class ), NBTClasses.get( Short.class ).getDeclaredField( "c" ) );
                NBTTagFieldCache.put( NBTClasses.get( Class.forName( "[B" ) ), NBTClasses.get( Class.forName( "[B" ) ).getDeclaredField( "c" ) );
                NBTTagFieldCache.put( NBTClasses.get( Class.forName( "[I" ) ), NBTClasses.get( Class.forName( "[I" ) ).getDeclaredField( "c" ) );

                for ( Field field : NBTTagFieldCache.values() ) {
                    field.setAccessible( true );
                }
            }
        } catch( Exception e ) {
            e.printStackTrace();
        }

        try {
            if ( LOCAL_VERSION.lessThanOrEqualTo( MinecraftVersion.v1_16 ) ) {
                NBTListData = getNMSClass( "NBTTagList" ).getDeclaredField( "list" );
                NBTCompoundMap = getNMSClass( "NBTTagCompound" ).getDeclaredField( "map" );
            } else {
                NBTListData = getNMSClass( "NBTTagList" ).getDeclaredField( "c" );
                NBTCompoundMap = getNMSClass( "NBTTagCompound" ).getDeclaredField( "x" );
            }
            NBTListData.setAccessible( true );
            NBTCompoundMap.setAccessible( true );
        } catch( Exception e ) {
            e.printStackTrace();
        }
    }

    private static Class< ? > getNBTTag( Class< ? > primitiveType ) {
        if ( NBTClasses.containsKey( primitiveType ) )
            return NBTClasses.get( primitiveType );
        return primitiveType;
    }

    private static Object getNBTVar( Object object ) {
        if ( object == null ) {
            return null;
        }
        Class< ? > clazz = object.getClass();
        try {
            if ( NBTTagFieldCache.containsKey( clazz ) ) {
                return NBTTagFieldCache.get( clazz ).get( object );
            }
        } catch ( Exception exception ) {
            exception.printStackTrace();
        }
        return null;
    }

    private static Method getMethod( String name ) {
        return methodCache.containsKey( name ) ? methodCache.get( name ) : null;
    }

    private static Constructor< ? > getConstructor( Class< ? > clazz ) {
        return constructorCache.containsKey( clazz ) ? constructorCache.get( clazz ) : null;
    }

    private static Class<?> getNMSClass(String name) {
        if ( classCache.containsKey( name ) ) {
            return classCache.get( name );
        }

        try {
            return Class.forName("net.minecraft.server." + VERSION + "." + name);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }

    private static String getMatch( String string, String regex ) {
        Pattern pattern = Pattern.compile( regex );
        Matcher matcher = pattern.matcher( string );
        if ( matcher.find() ) {
            return matcher.group( 1 );
        } else {
            return null;
        }
    }

    private static Object createItemStack( Object compound ) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
        if ( LOCAL_VERSION == MinecraftVersion.v1_11 || LOCAL_VERSION == MinecraftVersion.v1_12 ) {
            return getConstructor( getNMSClass( "ItemStack" ) ).newInstance( compound );
        }
        return getMethod( "createStack" ).invoke( null, compound );
    }

    /**
     * Gets the Bukkit version
     *
     * @return
     * The Bukkit version in standard package format
     */
    public static String getVersion() {
        return VERSION;
    }

    public static MinecraftVersion getMinecraftVersion() {
        return LOCAL_VERSION;
    }

    /**
     * Creates a skull with the given url as the skin
     *
     * @param skinURL
     * The URL of the skin, must be from mojang
     * @return
     * An item stack with count of 1
     */
    public static ItemStack getHead( String skinURL ) {
        Material material = Material.getMaterial( "SKULL_ITEM" );
        if ( material == null ) {
            // Most likely 1.13 materials
            material = Material.getMaterial( "PLAYER_HEAD" );
        }
        ItemStack head = new ItemStack( material, 1, ( short ) 3 );
        if ( skinURL == null || skinURL.isEmpty() ) {
            return head;
        }
        ItemMeta headMeta = head.getItemMeta();
        Object profile = null;
        try {
            profile = getConstructor( getNMSClass( "GameProfile" ) ).newInstance( UUID.randomUUID(), null );
            Object propertyMap = getMethod( "getProperties" ).invoke( profile );
            Object textureProperty = getConstructor( getNMSClass( "Property" ) ).newInstance( "textures", new String( Base64.getEncoder().encode( String.format( "{textures:{SKIN:{\"url\":\"%s\"}}}", skinURL ).getBytes() ) ) );
            getMethod( "put" ).invoke( propertyMap, "textures", textureProperty );
        } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException e1 ) {
            e1.printStackTrace();
        }

        if ( methodCache.containsKey( "setProfile" ) ) {
            try {
                getMethod( "setProfile" ).invoke( headMeta, profile );
            } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
                e.printStackTrace();
            }
        } else {
            Field profileField = null;
            try {
                profileField = headMeta.getClass().getDeclaredField("profile");
            } catch ( NoSuchFieldException | SecurityException e ) {
                e.printStackTrace();
            }
            profileField.setAccessible(true);
            try {
                profileField.set(headMeta, profile);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        head.setItemMeta(headMeta);
        return head;
    }

    /**
     * Fetches the texture of a skull
     *
     * @param head
     * The item stack itself
     * @return
     * The URL of the texture
     */
    public static String getTexture( ItemStack head ) {
        ItemMeta meta = head.getItemMeta();
        Field profileField = null;
        try {
            profileField = meta.getClass().getDeclaredField("profile");
        } catch ( NoSuchFieldException | SecurityException e ) {
            e.printStackTrace();
            throw new IllegalArgumentException( "Item is not a player skull!" );
        }
        profileField.setAccessible(true);
        try {
            Object profile = profileField.get( meta );
            if ( profile == null ) {
                return null;
            }

            Collection< Object > properties = ( Collection< Object > ) getMethod( "values" ).invoke( getMethod( "getProperties" ).invoke( profile ) );
            for ( Object prop : properties ) {
                if ( "textures".equals( getMethod( "getName" ).invoke( prop ) ) ) {
                    String texture = new String( Base64.getDecoder().decode( ( String ) getMethod( "getValue" ).invoke( prop ) ) );
                    return getMatch( texture, "\\{\"url\":\"(.*?)\"\\}" );
                }
            }
            return null;
        } catch ( IllegalArgumentException | IllegalAccessException | SecurityException | InvocationTargetException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Gets an NBT tag in a given item with the specified keys
     *
     * @param item
     * The itemstack to get the keys from
     * @param keys
     * The keys to fetch; an integer after a key value indicates that it should get the nth place of
     * the previous compound because it is a list;
     * @return
     * The item represented by the keys, and an integer if it is showing how long a list is.
     */
    private static Object getItemTag( ItemStack item, Object... keys ) {
        try {
            return getTag( getCompound( item ), keys );
        } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
            e.printStackTrace();
            return null;
        }
    }

    // Gets the NBTTagCompound
    private static Object getCompound( ItemStack item ) {
        if ( item == null ) {
            return null;
        }
        try {
            Object stack = null;
            stack = getMethod( "asNMSCopy" ).invoke( null, item );

            Object tag = null;

            if ( getMethod( "hasTag" ).invoke( stack ).equals( true ) ) {
                tag = getMethod( "getTag" ).invoke( stack );
            } else {
                tag = getNMSClass( "NBTTagCompound" ).newInstance();
            }

            return tag;
        } catch ( Exception exception ) {
            exception.printStackTrace();
            return null;
        }
    }

    /**
     * Gets an NBTCompound from the item provided. Use {@link #getNBTCompound(Object, Object...)} instead.
     *
     * @param item
     * Itemstack
     * @param keys
     * Keys in descending order
     * @return
     * An NBTCompound
     */
    private static NBTCompound getItemNBTTag( ItemStack item, Object... keys ) {
        if ( item == null ) {
            return null;
        }
        try {
            Object stack = null;
            stack = getMethod( "asNMSCopy" ).invoke( null, item );

            Object tag = getNMSClass( "NBTTagCompound" ).newInstance();

            tag = getMethod( "save" ).invoke( stack, tag );

            return getNBTTag( tag, keys );
        } catch ( Exception exception ) {
            exception.printStackTrace();
            return null;
        }
    }

    /**
     * Sets an NBT tag in an item with the provided keys and value
     * Should use the {@link #set(Object, Object, Object...)} method instead
     *
     * @param item
     * The itemstack to set
     * @param value
     * The value to set
     * @param keys
     * The keys to set, String for NBTCompound, int or null for an NBTTagList
     * @return
     * A new ItemStack with the updated NBT tags
     */
    private static ItemStack setItemTag( ItemStack item, Object value, Object... keys ) {
        if ( item == null ) {
            return null;
        }
        try {
            Object stack = getMethod( "asNMSCopy" ).invoke( null, item );

            Object tag = null;

            if ( getMethod( "hasTag" ).invoke( stack ).equals( true ) ) {
                tag = getMethod( "getTag" ).invoke( stack );
            } else {
                tag = getNMSClass( "NBTTagCompound" ).newInstance();
            }

            if ( keys.length == 0 && value instanceof NBTCompound ) {
                tag = ( ( NBTCompound ) value ).tag;
            } else {
                setTag( tag, value, keys );
            }

            getMethod( "setTag" ).invoke( stack, tag );
            return ( ItemStack ) getMethod( "asBukkitCopy" ).invoke( null, stack );
        } catch ( Exception exception ) {
            exception.printStackTrace();
            return null;
        }
    }

    /**
     * Constructs an ItemStack from a given NBTCompound
     *
     * @param compound
     * An NBTCompound following an ItemStack structure
     * @return
     * A new ItemStack
     */
    public static ItemStack getItemFromTag( NBTCompound compound ) {
        if ( compound == null ) {
            return null;
        }
        try {
            Object tag = compound.tag;
            Object count = getTag( tag, "Count" );
            Object id = getTag( tag, "id" );
            if ( count == null || id == null ) {
                return null;
            }
            if ( count instanceof Byte && id instanceof String ) {
                return ( ItemStack ) getMethod( "asBukkitCopy" ).invoke( null, createItemStack( tag ) );
            }
            return null;
        } catch ( Exception exception ) {
            exception.printStackTrace();
            return null;
        }
    }

    /**
     * Gets an NBT tag in a given entity with the specified keys
     *
     * @param entity
     * The entity to get the keys from
     * @param keys
     * The keys to fetch; an integer after a key value indicates that it should get the nth place of
     * the previous compound because it is a list;
     * @return
     * The item represented by the keys, and an integer if it is showing how long a list is.
     */
    private static Object getEntityTag( Entity entity, Object... keys ) {
        try {
            return getTag( getCompound( entity ), keys );
        } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
            e.printStackTrace();
            return null;
        }
    }

    // Gets the NBTTagCompound
    private static Object getCompound( Entity entity ) {
        if ( entity == null ) {
            return entity;
        }
        try {
            Object NMSEntity = getMethod( "getEntityHandle" ).invoke( entity );

            Object tag = getNMSClass( "NBTTagCompound" ).newInstance();

            getMethod( "getEntityTag" ).invoke( NMSEntity, tag );

            return tag;
        } catch ( Exception exception ) {
            exception.printStackTrace();
            return null;
        }
    }

    /**
     * Gets an NBTCompound from the entity provided. Use {@link #getNBTCompound(Object, Object...)} instead.
     *
     * @param entity
     * The Bukkit entity provided
     * @param keys
     * Keys in descending order
     * @return
     * An NBTCompound
     */
    private static NBTCompound getEntityNBTTag( Entity entity, Object...keys ) {
        if ( entity == null ) {
            return null;
        }
        try {
            Object NMSEntity = getMethod( "getEntityHandle" ).invoke( entity );

            Object tag = getNMSClass( "NBTTagCompound" ).newInstance();

            getMethod( "getEntityTag" ).invoke( NMSEntity, tag );

            return getNBTTag( tag, keys );
        } catch ( Exception exception ) {
            exception.printStackTrace();
            return null;
        }
    }

    /**
     * Sets an NBT tag in an entity with the provided keys and value
     * Should use the {@link #set(Object, Object, Object...)} method instead
     *
     * @param entity
     * The entity to set
     * @param value
     * The value to set
     * @param keys
     * The keys to set, String for NBTCompound, int or null for an NBTTagList
     */
    private static void setEntityTag( Entity entity, Object value, Object... keys ) {
        if ( entity == null ) {
            return;
        }
        try {
            Object NMSEntity = getMethod( "getEntityHandle" ).invoke( entity );

            Object tag = getNMSClass( "NBTTagCompound" ).newInstance() ;

            getMethod( "getEntityTag" ).invoke( NMSEntity, tag );

            if ( keys.length == 0 && value instanceof NBTCompound ) {
                tag = ( ( NBTCompound ) value ).tag;
            } else {
                setTag( tag, value, keys );
            }

            getMethod( "setEntityTag" ).invoke( NMSEntity, tag );
        } catch ( Exception exception ) {
            exception.printStackTrace();
            return;
        }
    }

    /**
     * Gets an NBT tag in a given block with the specified keys. Use {@link #getNBTCompound(Object, Object...)} instead.
     *
     * @param block
     * The block to get the keys from
     * @param keys
     * The keys to fetch; an integer after a key value indicates that it should get the nth place of
     * the previous compound because it is a list;
     * @return
     * The item represented by the keys, and an integer if it is showing how long a list is.
     */
    private static Object getBlockTag( Block block, Object... keys ) {
        try {
            return getTag( getCompound( block ), keys );
        } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
            e.printStackTrace();
            return null;
        }
    }

    // Gets the NBTTagCompound
    private static Object getCompound( Block block ) {
        try {
            if ( block == null || !getNMSClass( "CraftBlockState" ).isInstance( block.getState() ) ) {
                return null;
            }
            Location location = block.getLocation();

            Object blockPosition = getConstructor( getNMSClass( "BlockPosition" ) ).newInstance( location.getBlockX(), location.getBlockY(), location.getBlockZ() );

            Object nmsWorld = getMethod( "getWorldHandle" ).invoke( location.getWorld() );

            Object tileEntity = getMethod( "getTileEntity" ).invoke( nmsWorld, blockPosition );

            Object tag;
            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                tag = getMethod( "getTileTag" ).invoke( tileEntity );
            } else {
                tag = getNMSClass( "NBTTagCompound" ).newInstance();
                getMethod( "getTileTag" ).invoke( tileEntity, tag );
            }

            return tag;
        } catch( Exception exception ) {
            exception.printStackTrace();
            return null;
        }
    }

    /**
     * Gets an NBTCompound from the block provided
     *
     * @param block
     * The block provided
     * @param keys
     * Keys in descending order
     * @return
     * An NBTCompound
     */
    private static NBTCompound getBlockNBTTag( Block block, Object... keys ) {
        try {
            if ( block == null || !getNMSClass( "CraftBlockState" ).isInstance( block.getState() ) ) {
                return null;
            }
            Location location = block.getLocation();

            Object blockPosition = getConstructor( getNMSClass( "BlockPosition" ) ).newInstance( location.getBlockX(), location.getBlockY(), location.getBlockZ() );

            Object nmsWorld = getMethod( "getWorldHandle" ).invoke( location.getWorld() );

            Object tileEntity = getMethod( "getTileEntity" ).invoke( nmsWorld, blockPosition );

            Object tag;
            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                tag = getMethod( "getTileTag" ).invoke( tileEntity );
            } else {
                tag = getNMSClass( "NBTTagCompound" ).newInstance();
                getMethod( "getTileTag" ).invoke( tileEntity, tag );
            }

            return getNBTTag( tag, keys );
        } catch( Exception exception ) {
            exception.printStackTrace();
            return null;
        }
    }

    /**
     * Sets an NBT tag in an block with the provided keys and value
     * Should use the {@link #set(Object, Object, Object...)} method instead
     *
     * @param block
     * The block to set
     * @param value
     * The value to set
     * @param keys
     * The keys to set, String for NBTCompound, int or null for an NBTTagList
     */
    private static void setBlockTag( Block block, Object value, Object... keys ) {
        try {
            if ( block == null || !getNMSClass( "CraftBlockState" ).isInstance( block.getState() ) ) {
                return;
            }
            Location location = block.getLocation();

            Object blockPosition = getConstructor( getNMSClass( "BlockPosition" ) ).newInstance( location.getBlockX(), location.getBlockY(), location.getBlockZ() );

            Object nmsWorld = getMethod( "getWorldHandle" ).invoke( location.getWorld() );

            Object tileEntity = getMethod( "getTileEntity" ).invoke( nmsWorld, blockPosition );

            Object tag;
            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_18_R1 ) ) {
                tag = getMethod( "getTileTag" ).invoke( tileEntity );
            } else {
                tag = getNMSClass( "NBTTagCompound" ).newInstance();
                getMethod( "getTileTag" ).invoke( tileEntity, tag );
            }

            if ( keys.length == 0 && value instanceof NBTCompound ) {
                tag = ( ( NBTCompound ) value ).tag;
            } else {
                setTag( tag, value, keys );
            }

            if ( LOCAL_VERSION == MinecraftVersion.v1_16 ) {
                getMethod( "setTileTag" ).invoke( tileEntity, getMethod( "getType" ).invoke( nmsWorld, blockPosition ), tag );
            } else {
                getMethod( "setTileTag" ).invoke( tileEntity, tag );
            }
        } catch( Exception exception ) {
            exception.printStackTrace();
            return;
        }
    }

    /**
     * Sets the texture of a skull block
     *
     * @param block
     * The block, must be a skull
     * @param texture
     * The URL of the skin
     */
    public static void setSkullTexture( Block block, String texture ) {
        try {
            Object profile = getConstructor( getNMSClass( "GameProfile" ) ).newInstance( UUID.randomUUID(), null );
            Object propertyMap = getMethod( "getProperties" ).invoke( profile );
            Object textureProperty = getConstructor( getNMSClass( "Property" ) ).newInstance( "textures", new String( Base64.getEncoder().encode( String.format( "{textures:{SKIN:{\"url\":\"%s\"}}}", texture ).getBytes() ) ) );
            getMethod( "put" ).invoke( propertyMap, "textures", textureProperty );

            Location location = block.getLocation();

            Object blockPosition = getConstructor( getNMSClass( "BlockPosition" ) ).newInstance( location.getBlockX(), location.getBlockY(), location.getBlockZ() );

            Object nmsWorld = getMethod( "getWorldHandle" ).invoke( location.getWorld() );

            Object tileEntity = getMethod( "getTileEntity" ).invoke( nmsWorld, blockPosition );

            getMethod( "setGameProfile" ).invoke( tileEntity, profile );
        } catch( Exception exception ) {
            exception.printStackTrace();
        }
    }

    private static Object getValue( Object object, Object... keys ) {
        if ( object instanceof ItemStack ) {
            return getItemTag( ( ItemStack ) object, keys );
        } else if ( object instanceof Entity ) {
            return getEntityTag( ( Entity ) object, keys );
        } else if ( object instanceof Block ) {
            return getBlockTag( ( Block ) object, keys );
        } else if ( object instanceof NBTCompound ) {
            try {
                return getTag( ( ( NBTCompound ) object ).tag, keys );
            } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
                e.printStackTrace();
                return null;
            }
        } else {
            throw new IllegalArgumentException( "Object provided must be of type ItemStack, Entity, Block, or NBTCompound!" );
        }
    }

    /**
     * Gets an NBTCompound from an object
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * An NBTCompound, or null if none is stored at the provided location
     */
    public static NBTCompound getNBTCompound( Object object, Object... keys ) {
        if ( object instanceof ItemStack ) {
            return getItemNBTTag( ( ItemStack ) object, keys );
        } else if ( object instanceof Entity ) {
            return getEntityNBTTag( ( Entity ) object, keys );
        } else if ( object instanceof Block ) {
            return getBlockNBTTag( ( Block ) object, keys );
        } else if ( object instanceof NBTCompound ) {
            try {
                return getNBTTag( ( ( NBTCompound ) object ).tag, keys );
            } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
                e.printStackTrace();
                return null;
            }
        } else if ( getNMSClass( "NBTTagCompound" ).isInstance( object ) ) {
            try {
                return getNBTTag( object, keys );
            } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
                e.printStackTrace();
                return null;
            }
        } else {
            throw new IllegalArgumentException( "Object provided must be of type ItemStack, Entity, Block, or NBTCompound!" );
        }
    }

    /**
     * Gets a string from an object
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * A string, or null if none is stored at the provided location
     */
    public static String getString( Object object, Object... keys ) {
        Object result = getValue( object, keys );
        return result instanceof String ? ( String ) result : null;
    }

    /**
     * Gets an int from an object
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * An integer, or 0 if none is stored at the provided location
     */
    public static int getInt( Object object, Object... keys ) {
        Object result = getValue( object, keys );
        return result instanceof Integer ? ( int ) result : 0;
    }

    /**
     * Gets a double from an object
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * A double, or 0 if none is stored at the provided location
     */
    public static double getDouble( Object object, Object... keys ) {
        Object result = getValue( object, keys );
        return result instanceof Double ? ( double ) result : 0;
    }

    /**
     * Gets a long from an object
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * A long, or 0 if none is stored at the provided location
     */
    public static long getLong( Object object, Object... keys ) {
        Object result = getValue( object, keys );
        return result instanceof Long ? ( long ) result : 0;
    }

    /**
     * Gets a float from an object
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * A float, or 0 if none is stored at the provided location
     */
    public static float getFloat( Object object, Object... keys ) {
        Object result = getValue( object, keys );
        return result instanceof Float ? ( float ) result : 0;
    }

    /**
     * Gets a short from an object
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * A short, or 0 if none is stored at the provided location
     */
    public static short getShort( Object object, Object... keys ) {
        Object result = getValue( object, keys );
        return result instanceof Short ? ( short ) result : 0;
    }

    /**
     * Gets a byte from an object
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * A byte, or 0 if none is stored at the provided location
     */
    public static byte getByte( Object object, Object... keys ) {
        Object result = getValue( object, keys );
        return result instanceof Byte ? ( byte ) result : 0;
    }

    /**
     * Gets a boolean from an object
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * A boolean or false if none is stored at the provided location
     */
    public static boolean getBoolean( Object object, Object... keys ) {
        return getByte( object, keys ) == 1;
    }

    /**
     * Gets a byte array from an object
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * A byte array, or null if none is stored at the provided location
     */
    public static byte[] getByteArray( Object object, Object... keys ) {
        Object result = getValue( object, keys );
        return result instanceof byte[] ? ( byte[] ) result : null;
    }

    /**
     * Gets an int array from an object
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * An int array, or null if none is stored at the provided location
     */
    public static int[] getIntArray( Object object, Object... keys ) {
        Object result = getValue( object, keys );
        return result instanceof int[] ? ( int[] ) result : null;
    }

    /**
     * Checks if the object contains the given key
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * Whether or not the particular tag exists, may not be a primitive
     */
    public static boolean contains( Object object, Object... keys ) {
        Object result = getValue( object, keys );
        return result != null;
    }

    /**
     * Get the keys at the specific location, if it is a compound.
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * A set of keys
     */
    public static Collection< String > getKeys( Object object, Object... keys ) {
        Object compound;
        if ( object instanceof ItemStack ) {
            compound = getCompound( ( ItemStack ) object );
        } else if ( object instanceof Entity ) {
            compound = getCompound( ( Entity ) object );
        } else if ( object instanceof Block ) {
            compound = getCompound( ( Block ) object );
        } else if ( object instanceof NBTCompound ) {
            compound = ( ( NBTCompound ) object ).tag;
        } else {
            throw new IllegalArgumentException( "Object provided must be of type ItemStack, Entity, Block, or NBTCompound!" );
        }

        try {
            NBTCompound nbtCompound = getNBTTag( compound, keys );

            Object tag = nbtCompound.tag;
            if ( getNMSClass( "NBTTagCompound" ).isInstance( tag ) ) {
                return ( Collection< String > ) getMethod( "getKeys" ).invoke( tag );
            } else {
                return null;
            }

        } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * Gets the size of the list or NBTCompound at the given location.
     *
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param keys
     * Keys in descending order
     * @return
     * The size of the list or compound at the given location.
     */
    public static int getSize( Object object, Object... keys ) {
        Object compound;
        if ( object instanceof ItemStack ) {
            compound = getCompound( ( ItemStack ) object );
        } else if ( object instanceof Entity ) {
            compound = getCompound( ( Entity ) object );
        } else if ( object instanceof Block ) {
            compound = getCompound( ( Block ) object );
        } else if ( object instanceof NBTCompound ) {
            compound = ( ( NBTCompound ) object ).tag;
        } else {
            throw new IllegalArgumentException( "Object provided must be of type ItemStack, Entity, Block, or NBTCompound!" );
        }

        try {
            NBTCompound nbtCompound = getNBTTag( compound, keys );
            if ( getNMSClass( "NBTTagCompound" ).isInstance( nbtCompound.tag ) ) {
                return getKeys( nbtCompound ).size();
            } else if ( getNMSClass( "NBTTagList" ).isInstance( nbtCompound.tag ) ) {
                return ( int ) getMethod( "size" ).invoke( nbtCompound.tag );
            }
        } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
            e.printStackTrace();
            return 0;
        }

        throw new IllegalArgumentException( "Value is not a compound or list!" );
    }

    /**
     * Sets the value in the object with the given keys
     *
     * @param <T>
     * ItemStack, Entity, Block, or NBTCompound.
     * @param object
     * Must be an ItemStack, Entity, Block, or NBTCompound
     * @param value
     * The value to set, can be an NBTCompound
     * @param keys
     * The keys in descending order
     * @return
     * The new item stack if the object provided is an item, else original object
     */
    public static < T > T set( T object, Object value, Object... keys ) {
        if ( object instanceof ItemStack ) {
            return ( T ) setItemTag( ( ItemStack ) object, value, keys );
        } else if ( object instanceof Entity ) {
            setEntityTag( ( Entity ) object, value, keys );
        } else if ( object instanceof Block ) {
            setBlockTag( ( Block ) object, value, keys );
        } else if ( object instanceof NBTCompound ) {
            try {
                setTag( ( ( NBTCompound ) object ).tag, value, keys );
            } catch ( InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
                e.printStackTrace();
            }
        } else {
            throw new IllegalArgumentException( "Object provided must be of type ItemStack, Entity, Block, or NBTCompound!" );
        }
        return object;
    }

    /**
     * Load an NBTCompound from a String.
     *
     * @param json
     * A String in json format.
     * @return
     * An NBTCompound from the String provided. May or may not be a valid ItemStack.
     */
    public static NBTCompound getNBTCompound( String json ) {
        return NBTCompound.fromJson( json );
    }

    /**
     * Get an empty NBTCompound.
     *
     * @return
     * A new NBTCompound that contains a NBTTagCompound object.
     */
    public static NBTCompound getEmptyNBTCompound() {
        try {
            return new NBTCompound( getNMSClass( "NBTTagCompound" ).newInstance() );
        } catch ( InstantiationException | IllegalAccessException e ) {
            e.printStackTrace();
            return null;
        }
    }

    private static void setTag( Object tag, Object value, Object... keys ) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Object wrappedValue;
        // Get the real value of what we want to set here
        if ( value != null && value != Type.DELETE ) {
            if ( value instanceof NBTCompound ) {
                wrappedValue = ( ( NBTCompound ) value ).tag;
            } else if ( getNMSClass( "NBTTagList" ).isInstance( value ) || getNMSClass( "NBTTagCompound" ).isInstance( value ) ) {
                wrappedValue = value;
            } else if ( value == Type.COMPOUND ) {
                wrappedValue = getNMSClass( "NBTTagCompound" ).newInstance();
            } else if ( value == Type.LIST ) {
                wrappedValue = getNMSClass( "NBTTagList" ).newInstance();
            } else {
                if ( value instanceof Boolean ) {
                    value = ( byte ) ( ( Boolean ) value == true ? 1 : 0 );
                }
                Constructor< ? > cons = getConstructor( getNBTTag( value.getClass() ) );
                if ( cons != null ) {
                    wrappedValue = getConstructor( getNBTTag( value.getClass() ) ).newInstance( value );
                } else {
                    throw new IllegalArgumentException( "Provided value type(" + value.getClass() + ") is not supported!" );
                }
            }
        } else {
            wrappedValue = Type.DELETE;
        }

        Object compound = tag;
        for ( int index = 0; index < keys.length - 1; index++ ) {
            Object key = keys[ index ];
            Object prevCompound = compound;
            if ( key instanceof Integer ) {
                int keyIndex = ( int ) key;
                List< ? > tagList = ( List< ? > ) NBTListData.get( compound );
                if ( keyIndex >= 0 && keyIndex < tagList.size() ) {
                    compound = tagList.get( keyIndex );
                } else {
                    compound = null;
                }
            } else if ( key != null && key != Type.NEW_ELEMENT ) {
                compound = getMethod( "get" ).invoke( compound, ( String ) key );
            }
            if ( compound == null || key == null || key == Type.NEW_ELEMENT ) {
                if ( keys[ index + 1 ] == null || keys[ index + 1 ] instanceof Integer || keys[ index + 1 ] == Type.NEW_ELEMENT ) {
                    compound = getNMSClass( "NBTTagList" ).newInstance();
                } else {
                    compound = getNMSClass( "NBTTagCompound" ).newInstance();
                }
                if ( prevCompound.getClass().getSimpleName().equals( "NBTTagList" ) ) {
                    if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_14 ) ) {
                        getMethod( "add" ).invoke( prevCompound, getMethod( "size" ).invoke( prevCompound ), compound );
                    } else {
                        getMethod( "add" ).invoke( prevCompound, compound );
                    }
                } else {
                    getMethod( "set" ).invoke( prevCompound, ( String ) key, compound );
                }
            }
        }
        if ( keys.length > 0 ) {
            Object lastKey = keys[ keys.length - 1 ];
            if ( lastKey == null || lastKey == Type.NEW_ELEMENT ) {
                if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_14 ) ) {
                    getMethod( "add" ).invoke( compound, getMethod( "size" ).invoke( compound ), wrappedValue );
                } else {
                    getMethod( "add" ).invoke( compound, wrappedValue );
                }
            } else if ( lastKey instanceof Integer ) {
                if ( wrappedValue == Type.DELETE ) {
                    getMethod( "listRemove" ).invoke( compound, ( int ) lastKey );
                } else {
                    getMethod( "setIndex" ).invoke( compound, ( int ) lastKey, wrappedValue );
                }
            } else {
                if ( wrappedValue == Type.DELETE ) {
                    getMethod( "remove" ).invoke( compound, ( String ) lastKey );
                } else {
                    getMethod( "set" ).invoke( compound, ( String ) lastKey, wrappedValue );
                }
            }
        } else {
            // Add and replace all tags
            if ( wrappedValue != null ) {
                // Only if they're both an NBTTagCompound
                // Can't do anything if its a list or something
                if ( getNMSClass( "NBTTagCompound" ).isInstance( wrappedValue ) && getNMSClass( "NBTTagCompound" ).isInstance( compound ) )
                    for ( String key : getKeys( wrappedValue ) ) {
                        getMethod( "set" ).invoke( compound, key, getMethod( "get" ).invoke( wrappedValue, key ) );
                    }
            } else {
                // Did someone make an error?
                // NBTEditor.set( something, null );
                // Not sure what to do here
            }
        }
    }

    private static NBTCompound getNBTTag( Object tag, Object...keys ) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Object compound = tag;

        for ( Object key : keys ) {
            if ( compound == null ) {
                return null;
            } else if ( getNMSClass( "NBTTagCompound" ).isInstance( compound ) ) {
                compound = getMethod( "get" ).invoke( compound, ( String ) key );
            } else if ( getNMSClass( "NBTTagList" ).isInstance( compound ) ) {
                int keyIndex = ( int ) key;
                List< ? > tagList = ( List< ? > ) NBTListData.get( compound );
                if ( keyIndex >= 0 && keyIndex < tagList.size() ) {
                    compound = tagList.get( keyIndex );
                } else {
                    compound = null;
                }
            }
        }
        return new NBTCompound( compound );
    }

    private static Object getTag( Object tag, Object... keys ) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        if ( keys.length == 0 ) {
            return getTags( tag );
        }

        Object nbtObj = tag;

        for ( Object key : keys ) {
            if ( nbtObj == null ) {
                return null;
            } else if ( getNMSClass( "NBTTagCompound" ).isInstance( nbtObj ) ) {
                nbtObj = getMethod( "get" ).invoke( nbtObj, ( String ) key );
            } else if ( getNMSClass( "NBTTagList" ).isInstance( nbtObj ) ) {
                int keyIndex = ( int ) key;
                List< ? > tagList = ( List< ? > ) NBTListData.get( nbtObj );
                if ( keyIndex >= 0 && keyIndex < tagList.size() ) {
                    nbtObj = tagList.get( keyIndex );
                } else {
                    nbtObj = null;
                }
            } else {
                return getNBTVar( nbtObj );
            }
        }
        if ( nbtObj == null ) {
            return null;
        } else if ( getNMSClass( "NBTTagList" ).isInstance( nbtObj ) ) {
            return getTags( nbtObj );
        } else if ( getNMSClass( "NBTTagCompound" ).isInstance( nbtObj ) ) {
            return getTags( nbtObj );
        } else {
            return getNBTVar( nbtObj );
        }
    }

    @SuppressWarnings( "unchecked" )
    private static Object getTags( Object tag ) {
        Map< Object, Object > tags = new HashMap< Object, Object >();
        try {
            if ( getNMSClass( "NBTTagCompound" ).isInstance( tag ) ) {
                Map< String, Object > tagCompound = ( Map< String, Object > ) NBTCompoundMap.get( tag );
                for ( String key : tagCompound.keySet() ) {
                    Object value = tagCompound.get( key );
                    if ( getNMSClass( "NBTTagEnd" ).isInstance( value ) ) {
                        continue;
                    }
                    tags.put( key, getTag( value ) );
                }
            } else if ( getNMSClass( "NBTTagList" ).isInstance( tag ) ) {
                List< Object > tagList = ( List< Object > ) NBTListData.get( tag );
                for ( int index = 0; index < tagList.size(); index++ ) {
                    Object value = tagList.get( index );
                    if ( getNMSClass( "NBTTagEnd" ).isInstance( value ) ) {
                        continue;
                    }
                    tags.put( index, getTag( value ) );
                }
            } else {
                return getNBTVar( tag );
            }
            return tags;
        } catch ( Exception e ) {
            e.printStackTrace();
            return tags;
        }
    }

    /**
     * A class for holding NBTTagCompounds
     */
    public static final class NBTCompound {
        protected final Object tag;

        protected NBTCompound( Object tag ) {
            this.tag = tag;
        }

        public void set( Object value, Object... keys ) {
            try {
                setTag( tag, value, keys );
            } catch ( Exception e ) {
                e.printStackTrace();
            }
        }

        /**
         * The exact same as the toString method
         *
         * @return
         * Convert the compound to a string.
         */
        public String toJson() {
            return tag.toString();
        }

        public static NBTCompound fromJson( String json ) {
            try {
                return new NBTCompound( getMethod( "loadNBTTagCompound" ).invoke( null, json ) );
            } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
                e.printStackTrace();
                return null;
            }
        }

        @Override
        public String toString() {
            return tag.toString();
        }

        @Override
        public int hashCode() {
            return tag.hashCode();
        }

        @Override
        public boolean equals( Object obj ) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            NBTCompound other = (NBTCompound) obj;
            if (tag == null) {
                if (other.tag != null)
                    return false;
            } else if (!tag.equals(other.tag))
                return false;
            return true;
        }
    }

    /**
     * Minecraft versions as enums
     *
     * @author BananaPuncher714
     */
    public enum MinecraftVersion {
        v1_8,
        v1_9,
        v1_10,
        v1_11,
        v1_12,
        v1_13,
        v1_14,
        v1_15,
        v1_16,
        v1_17,
        v1_18_R1,
        v1_18_R2,
        v1_19,
        v1_20,
        v1_21,
        v1_22;

        // Would be really cool if we could overload operators here
        public boolean greaterThanOrEqualTo( MinecraftVersion other ) {
            return ordinal() >= other.ordinal();
        }

        public boolean lessThanOrEqualTo( MinecraftVersion other ) {
            return ordinal() <= other.ordinal();
        }

        public static MinecraftVersion get( String v ) {
            for ( MinecraftVersion k : MinecraftVersion.values() ) {
                if ( v.contains( k.name().substring( 1 ) ) ) {
                    return k;
                }
            }
            return null;
        }
    }

    private enum Type {
        COMPOUND, LIST, NEW_ELEMENT, DELETE;
    }
}