package io.github.bananapuncher714.nbteditor;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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 org.bukkit.inventory.meta.SkullMeta;

/**
 * Sets/Gets NBT tags from ItemStacks 
 * Supports 1.8-1.21
 * 
 * Github: https://github.com/BananaPuncher714/NBTEditor
 * Spigot: https://www.spigotmc.org/threads/269621/
 * 
 * @version 7.20.2
 * @author BananaPuncher714
 */
public final class NBTEditor {
    private static final Set< ReflectionTarget > reflectionTargets;
    
    private static final Map< ClassId, Class< ? > > classCache;
    private static final Map< MethodId, Method > methodCache;
    private static final Map< ConstructorKey, Constructor< ? > > constructorCache;
    private static final Map< Class< ? >, Constructor< ? > > NBTConstructors;
    private static final Map< Class< ? >, Class< ? > > NBTClasses;
    private static final Map< Class< ? >, Field > NBTTagFieldCache;
    private static Field NBTListData;
    private static Field NBTCompoundMap;
    private static Field skullProfile;
    private static final String VERSION;
    private static final MinecraftVersion LOCAL_VERSION;
    private static final String BUKKIT_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;
    public static final Type CUSTOM_DATA = Type.CUSTOM_DATA;
    public static final Type ITEMSTACK_COMPONENTS = Type.ITEMSTACK_COMPONENTS;
    
    static {
        String cbPackage = Bukkit.getServer().getClass().getPackage().getName();
        String detectedVersion = cbPackage.substring( cbPackage.lastIndexOf( '.' ) + 1 );
        String bukkitVersion = Bukkit.getServer().getBukkitVersion();
        if ( !detectedVersion.startsWith( "v" ) ) {
            // Paper or something...
            detectedVersion = bukkitVersion;
        }
        
        VERSION = detectedVersion;
        LOCAL_VERSION = MinecraftVersion.get( VERSION );
        BUKKIT_VERSION = bukkitVersion;

        classCache = new HashMap< ClassId, Class< ? > >();
        methodCache = new HashMap< MethodId, Method >();
        constructorCache = new HashMap< ConstructorKey, Constructor< ? > >();
        
        reflectionTargets = new TreeSet< ReflectionTarget >();
        reflectionTargets.addAll( Arrays.asList(
                new ReflectionTarget.v1_8().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_9().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_11().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_12().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_13().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_15().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_16().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_17().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_18_R1().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_18_R2().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_19_R1().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_19_R2().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_20_R1().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_20_R2().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_20_R3().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_20_R4().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_21_R1().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_21_R4().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_21_R5().setClassFetcher( NBTEditor::getNMSClass ),
                new ReflectionTarget.v1_21_R6().setClassFetcher( NBTEditor::getNMSClass )
        ) );
        
        NBTClasses = new HashMap< Class< ? >, Class< ? > >();
        try {
            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_17 ) ) {
                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" ) );
            } else {
                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" ) );
            }
        } catch ( ClassNotFoundException e ) {
            e.printStackTrace();
        }

        NBTConstructors = new HashMap< Class< ? >, Constructor< ? > >();
        try {
            NBTConstructors.put( getNBTTag( Byte.class ), getNBTTag( Byte.class ).getDeclaredConstructor( byte.class ) );
            NBTConstructors.put( getNBTTag( Boolean.class ), getNBTTag( Boolean.class ).getDeclaredConstructor( byte.class ) );
            NBTConstructors.put( getNBTTag( String.class ), getNBTTag( String.class ).getDeclaredConstructor( String.class ) );
            NBTConstructors.put( getNBTTag( Double.class ), getNBTTag( Double.class ).getDeclaredConstructor( double.class ) );
            NBTConstructors.put( getNBTTag( Integer.class ), getNBTTag( Integer.class ).getDeclaredConstructor( int.class ) );
            NBTConstructors.put( getNBTTag( Long.class ), getNBTTag( Long.class ).getDeclaredConstructor( long.class ) );
            NBTConstructors.put( getNBTTag( Float.class ), getNBTTag( Float.class ).getDeclaredConstructor( float.class ) );
            NBTConstructors.put( getNBTTag( Short.class ), getNBTTag( Short.class ).getDeclaredConstructor( short.class ) );
            NBTConstructors.put( getNBTTag( Class.forName( "[B" ) ), getNBTTag( Class.forName( "[B" ) ).getDeclaredConstructor( Class.forName( "[B" ) ) );
            NBTConstructors.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 : NBTConstructors.values() ) {
                cons.setAccessible( true );
            }
        } catch( ClassNotFoundException | NoSuchMethodException | SecurityException e ) {
            e.printStackTrace();
        }

        NBTTagFieldCache = new HashMap< Class< ? >, Field >();
        try {
            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_21_R4 ) ) {
                NBTTagFieldCache.put( NBTClasses.get( Byte.class ), NBTClasses.get( Byte.class ).getDeclaredField( "v" ) );
                NBTTagFieldCache.put( NBTClasses.get( Boolean.class ), NBTClasses.get( Boolean.class ).getDeclaredField( "v" ) );
                NBTTagFieldCache.put( NBTClasses.get( String.class ), NBTClasses.get( String.class ).getDeclaredField( "b" ) );
                NBTTagFieldCache.put( NBTClasses.get( Double.class ), NBTClasses.get( Double.class ).getDeclaredField( "c" ) );
                NBTTagFieldCache.put( NBTClasses.get( Integer.class ), NBTClasses.get( Integer.class ).getDeclaredField( "b" ) );
                NBTTagFieldCache.put( NBTClasses.get( Long.class ), NBTClasses.get( Long.class ).getDeclaredField( "b" ) );
                NBTTagFieldCache.put( NBTClasses.get( Float.class ), NBTClasses.get( Float.class ).getDeclaredField( "c" ) );
                NBTTagFieldCache.put( NBTClasses.get( Short.class ), NBTClasses.get( Short.class ).getDeclaredField( "b" ) );
                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 );
                }                
            } else if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_17 ) ) {
                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 );
                }
            } else {
                for ( Class< ? > clazz : NBTClasses.values() ) {
                    Field data = clazz.getDeclaredField( "data" );
                    data.setAccessible( true );
                    NBTTagFieldCache.put( clazz, data );
                }
            }
        } catch( ClassNotFoundException | NoSuchFieldException | SecurityException e ) {
            e.printStackTrace();
        }

        try {
            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_21_R4 ) ) {
                try {
                    NBTListData    = getNMSClass(ClassId.NBTTagList).getDeclaredField("list");
                    NBTCompoundMap = getNMSClass(ClassId.NBTTagCompound).getDeclaredField("tags");
                } catch (NoSuchFieldException ex) {
                    NBTListData    = getNMSClass(ClassId.NBTTagList).getDeclaredField("v");
                    NBTCompoundMap = getNMSClass(ClassId.NBTTagCompound).getDeclaredField("x");
                }
            } else if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_17 ) ) {
                NBTListData = getNMSClass( ClassId.NBTTagList ).getDeclaredField( "c" );
                NBTCompoundMap = getNMSClass( ClassId.NBTTagCompound ).getDeclaredField( "x" );
            } else {
                NBTListData = getNMSClass( ClassId.NBTTagList ).getDeclaredField( "list" );
                NBTCompoundMap = getNMSClass( ClassId.NBTTagCompound ).getDeclaredField( "map" );
            }
            NBTListData.setAccessible( true );
            NBTCompoundMap.setAccessible( true );
            
            skullProfile = getNMSClass( ClassId.CraftMetaSkull ).getDeclaredField( "profile" );
            skullProfile.setAccessible( true );
        } catch( Exception e ) {
            e.printStackTrace();
        }
    }
    
    private static Constructor< ? > getNBTTagConstructor( Class< ? > primitiveType ) {
        return NBTConstructors.get( getNBTTag( primitiveType ) );
    }
    
    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( MethodId name ) {
        if ( methodCache.containsKey( name ) ) {
            return methodCache.get( name );
        }
        
        for ( ReflectionTarget target : reflectionTargets ) {
            // Only check targets that are of the correct version
            if ( target.getVersion().lessThanOrEqualTo( LOCAL_VERSION ) ) {
                try {
                    Method method = target.fetchMethod( name );
                    if ( method != null ) {
                        methodCache.put( name, method );
                        
                        return method;
                    }
                } catch ( ClassNotFoundException | NoSuchMethodException | SecurityException e ) {
                    e.printStackTrace();
                }
            }
        }
        
        // Prevent any additional reflection lookups, since we know it doesn't exist
        methodCache.put( name, null );
        
        return null;
    }

    private static Constructor< ? > getConstructor( ClassId id ) {
        return getConstructor( new ConstructorKey( id ) );
    }

    private static Constructor< ? > getConstructor( ConstructorId id ) {
        return getConstructor( new ConstructorKey( id ) );
    }

    private static Constructor< ? > getConstructor( ConstructorKey key ) {
        if ( constructorCache.containsKey( key ) ) {
            return constructorCache.get( key );
        }
        
        for ( ReflectionTarget target : reflectionTargets ) {
            // Only check targets that are of the correct version
            if ( target.getVersion().lessThanOrEqualTo( LOCAL_VERSION ) ) {
                try {
                    Constructor< ? > cons = target.fetchConstructor( key );
                    if ( cons != null ) {
                        constructorCache.put( key, cons );
                    
                        return cons;
                    }
                } catch ( ClassNotFoundException | NoSuchMethodException | SecurityException e ) {
                    e.printStackTrace();
                }
            }
        }
        
        return null;
    }

    private static Class< ? > getNMSClass( ClassId id ) {
        if ( classCache.containsKey( id ) ) {
            return classCache.get( id );
        }
        
        for ( ReflectionTarget target : reflectionTargets ) {
            // Only check targets that are of the correct version
            if ( target.getVersion().lessThanOrEqualTo( LOCAL_VERSION ) ) {
                try {
                    Class< ? > clazz = target.fetchClass( id );
                    if ( clazz != null ) {
                        classCache.put( id, clazz );
                        
                        return clazz;
                    }
                } 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;
        }
    }

    // For some reason, 1.11 and 1.12 have a constructor for ItemStack that accepts an NBTTagCompound
    private static Object createItemStack( Object compound ) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException, NoSuchFieldException, SecurityException {
        if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_21_R5 ) ) {
            return ReflectionTarget.v1_21_R5.getItemFrom( registryAccess(), compound );
        } else if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_21_4 ) ) {
            Optional< ? > optional = ( Optional< ? > ) getMethod( MethodId.createStackOptional ).invoke( null, registryAccess(), compound );
            return optional.get();
        } else if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_20_R4 ) ) {
            Method createStack = getMethod( MethodId.createStack );
            if ( createStack.getParameterCount() == 2 ) {
                // RegistryAccess, NBTagCompound
                return getMethod( MethodId.createStack ).invoke( null, registryAccess(), compound );
            } else {
                // NBTTagCompound
                return getMethod( MethodId.createStack ).invoke( null, compound );
            }
        } else if ( LOCAL_VERSION == MinecraftVersion.v1_11 || LOCAL_VERSION == MinecraftVersion.v1_12 ) {
            return getConstructor( ClassId.ItemStack ).newInstance( compound );
        } else {
            return getMethod( MethodId.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;
        if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_21_R6 ) ) {
            try {
                // 1.21.10 switches to using records for GameProfiles, and the property map is immutable,
                // so we need to construct a new property map first before creating the game profile
                final Object multimap = getMethod( MethodId.createHashMultimap ).invoke( null );
                final Object textureProperty = getConstructor( ClassId.Property ).newInstance( "textures", new String( Base64.getEncoder().encode( String.format( "{textures:{SKIN:{\"url\":\"%s\"}}}", skinURL ).getBytes() ) ) );
                getMethod( MethodId.multimapPut ).invoke( multimap, "textures", textureProperty );
                final Object propertyMap = getConstructor( ClassId.PropertyMap ).newInstance( multimap );
                profile = getConstructor( ConstructorId.GameProfileWithPropertyMap ).newInstance( UUID.fromString( "069a79f4-44e9-4726-a5be-fca90e38aaf5" ), "Notch", propertyMap );
            } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException e1 ) {
                e1.printStackTrace();
            }
        } else {
            try {
                // Use a non-random UUID so heads will stack; in this case, Notch.
                profile = getConstructor( ClassId.GameProfile ).newInstance( UUID.fromString( "069a79f4-44e9-4726-a5be-fca90e38aaf5" ), "Notch" );
                Object propertyMap = getMethod( MethodId.getProperties ).invoke( profile );
                Object textureProperty = getConstructor( ClassId.Property ).newInstance( "textures", new String( Base64.getEncoder().encode( String.format( "{textures:{SKIN:{\"url\":\"%s\"}}}", skinURL ).getBytes() ) ) );
                getMethod( MethodId.putProperty ).invoke( propertyMap, "textures", textureProperty );
            } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException e1 ) {
                e1.printStackTrace();
            }
        }

        final Method setSkullResolvableProfile = getMethod( MethodId.setCraftMetaSkullResolvableProfile );
        Method setSkullProfile = getMethod( MethodId.setCraftMetaSkullProfile );
        if ( setSkullResolvableProfile != null ) {
            try {
                Object resolvableProfile;
                if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_21_R6 ) ) {
                    resolvableProfile = getMethod( MethodId.constructResolvableProfileFromGameProfile ).invoke( null, profile );
                } else {
                    resolvableProfile = getConstructor( ClassId.ResolvableProfile ).newInstance( profile );
                }
                setSkullResolvableProfile.invoke( headMeta, resolvableProfile );
            } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException e ) {
                e.printStackTrace();
            }
        } else if ( setSkullProfile != null ) {
            try {
                setSkullProfile.invoke( headMeta, profile );
            } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
                e.printStackTrace();
            }
        } else {
            try {
                skullProfile.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
     */
    @SuppressWarnings( "unchecked" )
    public static String getTexture( ItemStack head ) {
        ItemMeta meta = head.getItemMeta();
        if ( !( meta instanceof SkullMeta ) ) {
            throw new IllegalArgumentException( "Item is not a player skull!" );
        }
        try {
            Object profile = skullProfile.get( meta );
            
            Class< ? > resolvableProfile = getNMSClass( ClassId.ResolvableProfile );
            if ( resolvableProfile != null && resolvableProfile.isInstance( profile ) ) {
                final Method getGameProfile = getMethod( MethodId.getResolvableProfileGameProfile );
                profile = getGameProfile.invoke( profile );
            }
            
            if ( profile == null ) {
                return null;
            }
            
            final Collection< Object > properties = ( Collection< Object > ) getMethod( MethodId.propertyValues ).invoke( getMethod( MethodId.getProperties ).invoke( profile ) );
            for ( Object prop : properties ) {
                if ( "textures".equals( getMethod( MethodId.getPropertyName ).invoke( prop ) ) ) {
                    String texture = new String( Base64.getDecoder().decode( ( String ) getMethod( MethodId.getPropertyValue ).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 {
            Object compound = getCompound( item );
            
            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_20_R4 ) && !BUKKIT_VERSION.startsWith( "1.20.4" ) && !BUKKIT_VERSION.startsWith( "1.20.5" ) ) {
                // Apply the components tag since all items should contain this if not already present, since 1.20.6
                if ( keys.length > 0 && keys[ 0 ] != Type.ITEMSTACK_COMPONENTS ) {
                    compound = getMethod( MethodId.compoundGet ).invoke( compound, "components" );
                }
            }
            
            return getTag( compound, keys );
        } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException e ) {
            e.printStackTrace();
            return null;
        }
    }

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

            Object tag = null;

            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_21_R5 ) ) {
                tag = ReflectionTarget.v1_21_R5.save( stack, registryAccess() );
            } else {
                Method saveOptional = null;
                if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_20_R4 ) ) {
                    saveOptional = getMethod( MethodId.saveOptional );
                }
                
                if ( saveOptional != null ) {
                    tag = saveOptional.invoke( stack, registryAccess() );
                } else {
                    if ( getMethod( MethodId.itemHasTag ).invoke( stack ).equals( true ) ) {
                        tag = getMethod( MethodId.getItemTag ).invoke( stack );
                    } else {
                        tag = getNMSClass( ClassId.NBTTagCompound ).newInstance();
                    }
                }
            }

            return tag;
        } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException | NoSuchFieldException | SecurityException 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
     * @throws SecurityException 
     * @throws NoSuchFieldException 
     */
    @SuppressWarnings( "deprecation" )
    private static NBTCompound getItemNBTTag( ItemStack item, Object... keys ) {
        if ( item == null ) {
            return null;
        }
        try {
            Object stack = null;
            stack = getMethod( MethodId.asNMSCopy ).invoke( null, item );

            Object tag = getNMSClass( ClassId.NBTTagCompound ).newInstance();
            
            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_21_R5 ) ) {
                tag = ReflectionTarget.v1_21_R5.save( stack, registryAccess() );
            } else {
                Method saveOptional = null;
                if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_20_R4 ) ) {
                    saveOptional = getMethod( MethodId.saveOptional );
                }
                
                if ( saveOptional != null ) {
                    tag = saveOptional.invoke( stack, registryAccess() );
                } else {
                    tag = getMethod( MethodId.itemSave ).invoke( stack, tag );                                
                }
            }

            return getNBTTag( tag, keys );
        } catch ( IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException | NoSuchFieldException | SecurityException 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
     * @throws SecurityException 
     * @throws NoSuchFieldException 
     */
    @SuppressWarnings( "deprecation" )
    private static ItemStack setItemTag( ItemStack item, Object value, Object... keys ) {
        if ( item == null ) {
            return null;
        }
        try {
            Object stack = getMethod( MethodId.asNMSCopy ).invoke( null, item );

            Object tag = null;

            if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_21_R5 ) ) {
                tag = ReflectionTarget.v1_21_R5.save( stack, registryAccess() );
            } else {
                Method saveOptional = null;
                if ( LOCAL_VERSION.greaterThanOrEqualTo( MinecraftVersion.v1_20_R4 ) ) {
                    saveOptional = getMethod( MethodId.saveOptional );
                }
                
                if ( saveOptional != null ) {
                    tag = saveOptional.invoke( stack, registryAccess() );
                } else {
                    if ( getMethod( MethodId.itemHasTag ).invoke( stack ).equals( true ) ) {
                        tag = getMethod( MethodId.getItemTag ).invoke( stack );
                    } else {
                        tag = getNMSClass( ClassId.NBTTagCompound ).newInstance();
                    }
                }
            }

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