package net.minecraft.nbt;

import com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle;
import com.bergerkiller.generated.net.minecraft.nbt.NBTTagListHandle;
import com.bergerkiller.generated.net.minecraft.nbt.NBTTagCompoundHandle;
import com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle.NBTTagStringHandle;
import com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle.NBTTagByteHandle;
import com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle.NBTTagShortHandle;
import com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle.NBTTagIntHandle;
import com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle.NBTTagLongHandle;
import com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle.NBTTagFloatHandle;
import com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle.NBTTagDoubleHandle;
import com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle.NBTTagByteArrayHandle;
import com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle.NBTTagIntArrayHandle;
import com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle.NBTTagLongArrayHandle;

class NBTBase {
#if version >= 1.18
    public abstract byte getTypeId:getId();
    public abstract (Object) NBTBase raw_clone:copy();
#else
    public abstract byte getTypeId();
    public abstract (Object) NBTBase raw_clone:clone();
#endif

    public static NBTBaseHandle createHandle(Object instance) {
        if (!(instance instanceof NBTBase)) {
            throw new IllegalArgumentException("Input is not an instance of NBTBase");
        }
        return com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle.createHandleForData(instance);
    }

    <code>
    public com.bergerkiller.bukkit.common.nbt.CommonTag toCommonTag() {
        return new com.bergerkiller.bukkit.common.nbt.CommonTag(this);
    }
    public abstract NBTBaseHandle clone();
    public abstract Object getData();

    public final String toPrettyString() {
        StringBuilder str = new StringBuilder(100);
        toPrettyString(str, 0);
        return str.toString();
    }

    public void toPrettyString(StringBuilder str, int indent) {
        while (indent-- > 0) {
            str.append("  ");
        }
        Object data = getData();
        if (data == null) {
            str.append("UNKNOWN[").append(getTypeId()).append("]");
        } else {
            Class<?> unboxedType = com.bergerkiller.mountiplex.reflection.util.BoxedType.getUnboxedType(data.getClass());
            if (unboxedType != null) {
                str.append(unboxedType.getSimpleName());
            } else {
                str.append(data.getClass().getSimpleName());
            }
            str.append(": ");

            if (data instanceof byte[]) {
                byte[] values = (byte[]) data;
                str.append("[");
                for (int i = 0; i < values.length; i++) {
                    if (i > 0) str.append(", ");
                    str.append(values[i]);
                }
                str.append("]");
            } else if (data instanceof int[]) {
                int[] values = (int[]) data;
                str.append("[");
                for (int i = 0; i < values.length; i++) {
                    if (i > 0) str.append(", ");
                    str.append(values[i]);
                }
                str.append("]");
            } else if (data instanceof long[]) {
                long[] values = (long[]) data;
                str.append("[");
                for (int i = 0; i < values.length; i++) {
                    if (i > 0) str.append(", ");
                    str.append(values[i]);
                }
                str.append("]");
            } else {
                str.append(data);
            }
        }
    }

    private static final class TypeInfo {
        public final Class<?> dataType;
        public final Template.Class<? extends NBTBaseHandle> handleClass;
        public final java.util.function.Function<Object, Object> constructor;
        public final java.util.function.Function<Object, Object> get_data;

        public TypeInfo(Class<?> dataType,
                        Template.Class<? extends NBTBaseHandle> handleClass,
                        java.util.function.Function<Object, Object> constructor,
                        java.util.function.Function<Object, Object> get_data)
        {
            this.dataType = dataType;
            this.handleClass = handleClass;
            this.constructor = constructor;
            this.get_data = get_data;
        }
    }

    private static class TypeInfoLookup {
        public final com.bergerkiller.bukkit.common.collections.ClassMap<TypeInfo> byType = new com.bergerkiller.bukkit.common.collections.ClassMap<TypeInfo>();
        public final TypeInfo toStringFallback;

        public TypeInfoLookup() {
            toStringFallback = new TypeInfo(
                    String.class, NBTTagStringHandle.T,
                    data -> NBTTagStringHandle.T.create.raw.invoke(com.bergerkiller.bukkit.common.conversion.Conversion.toString.convert(data, "")),
                    java.util.function.Function.identity()
            );

            registerTypeInfo(String.class, NBTTagStringHandle.T, NBTTagStringHandle.T.create.raw::invoke, NBTTagStringHandle.T.getData::invoke);
            registerTypeInfo(byte.class, NBTTagByteHandle.T, NBTTagByteHandle.T.create.raw::invoke, NBTTagByteHandle.T.getByteData::invoke);
            registerTypeInfo(short.class, NBTTagShortHandle.T, NBTTagShortHandle.T.create.raw::invoke, NBTTagShortHandle.T.getShortData::invoke);
            registerTypeInfo(int.class, NBTTagIntHandle.T, NBTTagIntHandle.T.create.raw::invoke, NBTTagIntHandle.T.getIntegerData::invoke);
            registerTypeInfo(long.class, NBTTagLongHandle.T, NBTTagLongHandle.T.create.raw::invoke, NBTTagLongHandle.T.getLongData::invoke);
            registerTypeInfo(float.class, NBTTagFloatHandle.T, NBTTagFloatHandle.T.create.raw::invoke, NBTTagFloatHandle.T.getFloatData::invoke);
            registerTypeInfo(double.class, NBTTagDoubleHandle.T, NBTTagDoubleHandle.T.create.raw::invoke, NBTTagDoubleHandle.T.getDoubleData::invoke);
            registerTypeInfo(byte[].class, NBTTagByteArrayHandle.T, NBTTagByteArrayHandle.T.create.raw::invoke, NBTTagByteArrayHandle.T.getData::invoke);
            registerTypeInfo(int[].class, NBTTagIntArrayHandle.T, NBTTagIntArrayHandle.T.create.raw::invoke, NBTTagIntArrayHandle.T.getData::invoke);

            if (NBTTagLongArrayHandle.T.isAvailable()) {
                registerTypeInfo(long[].class, NBTTagLongArrayHandle.T, NBTTagLongArrayHandle.T.create.raw::invoke, NBTTagLongArrayHandle.T.getData::invoke);
            }

            registerTypeInfo(java.util.Collection.class, NBTTagListHandle.T, NBTTagListHandle.T.create.raw::invoke, NBTTagListHandle.T.data.raw::get);
            registerTypeInfo(java.util.Map.class, NBTTagCompoundHandle.T, NBTTagCompoundHandle.T.create.raw::invoke, NBTTagCompoundHandle.T.data.raw::get);
        }

        private void registerTypeInfo(
                Class<?> dataType,
                Template.Class<? extends NBTBaseHandle> handleClass,
                java.util.function.Function<Object, Object> constructor,
                java.util.function.Function<Object, Object> get_data)
        {
            TypeInfo data_typeInfo = new TypeInfo(dataType, handleClass, constructor, java.util.function.Function.identity());
            byType.put(dataType, data_typeInfo);
            Class<?> boxedDataType = com.bergerkiller.mountiplex.reflection.util.BoxedType.getBoxedType(dataType);
            if (boxedDataType != null) {
                byType.put(boxedDataType, data_typeInfo);
            }

            byType.put(handleClass.getType(), new TypeInfo(dataType, handleClass,
                    java.util.function.Function.identity(), get_data));

            byType.put(handleClass.getHandleType(), new TypeInfo(dataType, handleClass,
                    handle -> ((Template.Handle) handle).getRaw(),
                    handle -> get_data.apply(((Template.Handle) handle).getRaw())));

            handleClass.createHandle(null, true);
        }
    }

    private static TypeInfoLookup lookup = null;

    private static TypeInfoLookup lookup() {
        TypeInfoLookup lookup;
        if ((lookup = NBTBaseHandle.lookup) != null) {
            return lookup;
        }

        synchronized (NBTBaseHandle.class) {
            if ((lookup = NBTBaseHandle.lookup) != null) {
                return lookup;
            }

            lookup = new TypeInfoLookup();
            NBTBaseHandle.lookup = lookup;
            return lookup;
        }
    }

    private static TypeInfo findTypeInfo(Object data) {
        if (data == null) {
            throw new IllegalArgumentException("Can not find tag type information for null data");
        }

        TypeInfoLookup lookup = lookup();
        TypeInfo info = lookup.byType.get(data.getClass());
        if (info != null) {
            return info;
        }
        if (data instanceof com.bergerkiller.bukkit.common.nbt.CommonTag) {
            final TypeInfo handle_info = findTypeInfo(((com.bergerkiller.bukkit.common.nbt.CommonTag) data).getRawHandle());
            return new TypeInfo(
                handle_info.dataType, handle_info.handleClass,
                tag -> ((com.bergerkiller.bukkit.common.nbt.CommonTag) data).getRawHandle(),
                tag -> handle_info.get_data.apply(((com.bergerkiller.bukkit.common.nbt.CommonTag) data).getRawHandle())
            );
        }
        return lookup.toStringFallback;
    }

    public static boolean isDataSupportedNatively(Object data) {
        TypeInfoLookup lookup = lookup();
        return lookup.byType.get(data) != null || data instanceof com.bergerkiller.bukkit.common.nbt.CommonTag;
    }

    public static Object getDataForHandle(Object handle) {
        return findTypeInfo(handle).get_data.apply(handle);
    }

    public static Object createRawHandleForData(Object data) {
        return findTypeInfo(data).constructor.apply(data);
    }

    public static NBTBaseHandle createHandleForData(Object data) {
        TypeInfo info = findTypeInfo(data);
        return info.handleClass.createHandle(info.constructor.apply(data));
    }

    // Used for decoding records/values using Codecs
    public static java.util.function.Consumer<String> createPartialErrorLogger(Object nbtBase) {
        return (s) -> {
            String nbtToStr = (nbtBase == null) ? "[null]" : nbtBase.toString();
            com.bergerkiller.bukkit.common.Logging.LOGGER.severe(
                    "Failed to read (" + nbtToStr + "): " + s);
        };
    }
    </code>

    class NBTTagString extends NBTBase {
#if version >= 1.15
        public static (NBTBaseHandle.NBTTagStringHandle) NBTTagString create:valueOf(String data);
#else
        public static (NBTBaseHandle.NBTTagStringHandle) NBTTagString create(String data) { return new NBTTagString(data); }
#endif

        // Overrides getData() in NBTBase
        public String getData:getAsString();

        <code>
        public NBTBaseHandle.NBTTagStringHandle clone() {
            return com.bergerkiller.bukkit.common.internal.CommonCapabilities.IMMUTABLE_NBT_PRIMITIVES ? this : createHandle(raw_clone());
        }
        </code>
    }

    class NBTTagByte extends NBTBase {
#if version >= 1.15
        public static (NBTBaseHandle.NBTTagByteHandle) NBTTagByte create:valueOf(byte data);
#else
        public static (NBTBaseHandle.NBTTagByteHandle) NBTTagByte create(byte data) { return new NBTTagByte(data); }
#endif

        public byte getByteData:getAsByte();

        <code>
        public NBTBaseHandle.NBTTagByteHandle clone() {
            return com.bergerkiller.bukkit.common.internal.CommonCapabilities.IMMUTABLE_NBT_PRIMITIVES ? this : createHandle(raw_clone());
        }
        public Byte getData() { return Byte.valueOf(getByteData()); }
        </code>
    }

    class NBTTagShort extends NBTBase {
#if version >= 1.15
        public static (NBTBaseHandle.NBTTagShortHandle) NBTTagShort create:valueOf(short data);
#else
        public static (NBTBaseHandle.NBTTagShortHandle) NBTTagShort create(short data) { return new NBTTagShort(data); }
#endif

        public short getShortData:getAsShort();

        <code>
        public static Object createRaw(Object data) { return T.create.raw.invoke(data); }
        public NBTBaseHandle.NBTTagShortHandle clone() {
            return com.bergerkiller.bukkit.common.internal.CommonCapabilities.IMMUTABLE_NBT_PRIMITIVES ? this : createHandle(raw_clone());
        }
        public Short getData() { return Short.valueOf(getShortData()); }
        </code>
    }

    class NBTTagInt extends NBTBase {
#if version >= 1.15
        public static (NBTBaseHandle.NBTTagIntHandle) NBTTagInt create:valueOf(int data);
#else
        public static (NBTBaseHandle.NBTTagIntHandle) NBTTagInt create(int data) { return new NBTTagInt(data); }
#endif
        public int getIntegerData:getAsInt();

        <code>
        public NBTBaseHandle.NBTTagIntHandle clone() {
            return com.bergerkiller.bukkit.common.internal.CommonCapabilities.IMMUTABLE_NBT_PRIMITIVES ? this : createHandle(raw_clone());
        }
        public Integer getData() { return Integer.valueOf(getIntegerData()); }
        </code>
    }

    class NBTTagLong extends NBTBase {
#if version >= 1.15
        public static (NBTBaseHandle.NBTTagLongHandle) NBTTagLong create:valueOf(long data);
#else
        public static (NBTBaseHandle.NBTTagLongHandle) NBTTagLong create(long data) { return new NBTTagLong(data); }
#endif

        public long getLongData:getAsLong();

        <code>
        public NBTBaseHandle.NBTTagLongHandle clone() {
            return com.bergerkiller.bukkit.common.internal.CommonCapabilities.IMMUTABLE_NBT_PRIMITIVES ? this : createHandle(raw_clone());
        }
        public Long getData() { return Long.valueOf(getLongData()); }
        </code>
    }

    class NBTTagFloat extends NBTBase {
#if version >= 1.15
        public static (NBTBaseHandle.NBTTagFloatHandle) NBTTagFloat create:valueOf(float data);
#else
        public static (NBTBaseHandle.NBTTagFloatHandle) NBTTagFloat create(float data) { return new NBTTagFloat(data); }
#endif

        public float getFloatData:getAsFloat();

        <code>
        public NBTBaseHandle.NBTTagFloatHandle clone() {
            return com.bergerkiller.bukkit.common.internal.CommonCapabilities.IMMUTABLE_NBT_PRIMITIVES ? this : createHandle(raw_clone());
        }
        public Float getData() { return Float.valueOf(getFloatData()); }
        </code>
    }

    class NBTTagDouble extends NBTBase {
#if version >= 1.15
        public static (NBTBaseHandle.NBTTagDoubleHandle) NBTTagDouble create:valueOf(double data);
#else
        public static (NBTBaseHandle.NBTTagDoubleHandle) NBTTagDouble create(double data) { return new NBTTagDouble(data); }
#endif

        public double getDoubleData:getAsDouble();

        <code>
        public NBTBaseHandle.NBTTagDoubleHandle clone() {
            return com.bergerkiller.bukkit.common.internal.CommonCapabilities.IMMUTABLE_NBT_PRIMITIVES ? this : createHandle(raw_clone());
        }
        public Double getData() { return Double.valueOf(getDoubleData()); }
        </code>
    }

    class NBTTagByteArray extends NBTBase {
        public static (NBTBaseHandle.NBTTagByteArrayHandle) NBTTagByteArray create(byte[] data) { return new NBTTagByteArray(data); }
#if version >= 1.18
        public byte[] getData:getAsByteArray();
#elseif version >= 1.14
        public byte[] getData:getBytes();
#else
        public byte[] getData:c();
#endif
    }

    class NBTTagIntArray extends NBTBase {
        public static (NBTBaseHandle.NBTTagIntArrayHandle) NBTTagIntArray create(int[] data) { return new NBTTagIntArray(data); }
#if version >= 1.18
        public int[] getData:getAsIntArray();
#elseif version >= 1.14
        public int[] getData:getInts();
#elseif version >= 1.10.2
        public int[] getData:d();
#else
        public int[] getData:c();
#endif
    }

    // Since MC 1.12
    optional class NBTTagLongArray extends NBTBase {
        public static (NBTBaseHandle.NBTTagLongArrayHandle) NBTTagLongArray create(long[] data) { return new NBTTagLongArray(data); }
#if version >= 1.18
        public long[] getData:getAsLongArray();
#elseif version >= 1.14
        public long[] getData:getLongs();
#elseif version >= 1.13
        public long[] getData:d();
#elseif version >= 1.12
        public long[] getData() {
            #require net.minecraft.nbt.NBTTagLongArray private long[] data_field:b;
            return instance#data_field;
        }
#else
        public long[] getData() {
            throw new UnsupportedOperationException("NBTTagLongArray is not available");
        }
#endif
    }
}

class NBTTagList extends NBTBase {
#if version >= 1.18
    #require net.minecraft.nbt.NBTBase public abstract byte getTagTypeId:getId();
#else
    #require net.minecraft.nbt.NBTBase public abstract byte getTagTypeId:getTypeId();
#endif

    // Results in getData() being added, which overrides the one in NBTBase
    private readonly (List<NBTBaseHandle>) List<NBTBase> data:list;

    public static (NBTTagListHandle) NBTTagList createEmpty() {
        return new NBTTagList();
    }

    public static (NBTTagListHandle) NBTTagList create(java.util.Collection<?> data) {
        NBTTagList result = new NBTTagList();
        if (!data.isEmpty()) {
            java.util.Iterator iter = data.iterator();
            com.bergerkiller.mountiplex.reflection.declarations.Template.Method add_method;
            add_method = (com.bergerkiller.mountiplex.reflection.declarations.Template.Method) com.bergerkiller.generated.net.minecraft.nbt.NBTTagListHandle.T.add.raw;
            while (iter.hasNext()) {
                Object element = iter.next();
                if (!(element instanceof NBTBase)) {
                    element = com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle.createRawHandleForData(element);
                }
                add_method.invoke(result, element);
            }
        }
        return result;
    }

    <code>
    public NBTTagListHandle clone() {
        return createHandle(raw_clone());
    }
    </code>

    public int size();
    public boolean isEmpty();

#if version >= 1.21.5
    public (byte) byte getElementTypeId:identifyRawElementType();
#elseif version >= 1.18
    public (byte) byte getElementTypeId:getElementType();
#elseif version >= 1.17
    public (byte) byte getElementTypeId:e();
#elseif version >= 1.16
    public (byte) byte getElementTypeId:d_();
#elseif version >= 1.14
    public (byte) int getElementTypeId:a_();
#elseif version >= 1.13
    public (byte) int getElementTypeId:d();
#elseif version >= 1.10.2
    public (byte) int getElementTypeId:g();
#elseif version >= 1.9
    public (byte) int getElementTypeId:d();
#else
    public (byte) int getElementTypeId:f();
#endif

    public (NBTBaseHandle) NBTBase get_at:get(int index);

#if version >= 1.14
    public void clear();

  #if version >= 1.16.5 && forge == mohist
    // Mohist 1.16.5+ remapping bug
    public (NBTBaseHandle) NBTBase set_at:d(int index, (NBTBaseHandle) NBTBase nbt_value);
    public (NBTBaseHandle) NBTBase remove_at:c(int index);
    public void add_at:c(int index, (NBTBaseHandle) NBTBase value);
  #elseif version >= 1.16.5 && forge == magma
    // Magma 1.16.5+ remapping bug
    public (NBTBaseHandle) NBTBase set_at:d(int index, (NBTBaseHandle) NBTBase nbt_value);
    public (NBTBaseHandle) NBTBase remove_at:c(int index);
    public void add_at:c(int index, (NBTBaseHandle) NBTBase value);
  #else
    public (NBTBaseHandle) NBTBase set_at:set(int index, (NBTBaseHandle) NBTBase nbt_value);
    public (NBTBaseHandle) NBTBase remove_at:remove(int index);
    public void add_at:add(int index, (NBTBaseHandle) NBTBase value);
  #endif

    public boolean add((NBTBaseHandle) NBTBase value) {
        instance.add(value);
        return true;
    }
#else
    #require net.minecraft.nbt.NBTTagList private java.util.List list;
    #require net.minecraft.nbt.NBTTagList private byte type;

    public void clear() {
        java.util.List list = instance#list;
        list.clear();
        instance#type = (byte) 0;
    }

    public (NBTBaseHandle) NBTBase set_at(int index, (NBTBaseHandle) NBTBase nbt_value) {
        byte list_type = instance#type;
        if (list_type != 0 && list_type != nbt_value#getTagTypeId()) {
            throw new UnsupportedOperationException("Trying to set tag of type " +
                nbt_value#getTagTypeId() + " in list of " + list_type);
        }
        NBTBase old_value = instance.get(index);
  #if version >= 1.18
        instance.setTag(index, nbt_value);
  #else
        instance.a(index, nbt_value);
  #endif
        return old_value;
    }

    public void add_at(int index, (NBTBaseHandle) NBTBase value) {
        byte list_type = instance#type;
        if (list_type == 0) {
            instance#type = value#getTagTypeId();
        } else if (list_type != value#getTagTypeId()) {
            throw new UnsupportedOperationException("Trying to add tag of type " +
                value#getTagTypeId() + " to list of " + list_type);
        }
        java.util.List list = instance#list;
        list.add(index, value);
        return true;
    }

    public (NBTBaseHandle) NBTBase remove_at(int index) {
  #if version >= 1.9
        NBTBase removed = instance.remove(index);
  #else
        NBTBase removed = instance.a(index);
  #endif
        if (instance.isEmpty()) {
            instance#type = (byte) 0;
        }
        return removed;
    }

    public boolean add((NBTBaseHandle) NBTBase value) {
  #if version >= 1.13
        if (!instance.add(value)) {
            byte list_type = instance#type;
            throw new UnsupportedOperationException("Trying to add tag of type " +
                value#getTagTypeId() + " to list of " + list_type);
        }
        return true;
  #else
        byte list_type = instance#type;
        if (list_type != 0 && list_type != value.getTypeId()) {
            throw new UnsupportedOperationException("Trying to add tag of type " +
                value#getTagTypeId() + " to list of " + list_type);
        }
        instance.add(value);
        return true;
  #endif
    }
#endif

    <code>
    public com.bergerkiller.bukkit.common.nbt.CommonTagList toCommonTag() {
        return new com.bergerkiller.bukkit.common.nbt.CommonTagList(this);
    }

    @Override
    public void toPrettyString(StringBuilder str, int indent) {
        for (int i = 0; i < indent; i++) {
            str.append("  ");
        }
        List<NBTBaseHandle> values = getData();
        str.append("TagList: ").append(values.size()).append(" entries [");
        for (NBTBaseHandle value : values) {
            str.append('\n');
            value.toPrettyString(str, indent + 1);
        }
        if (!values.isEmpty()) {
            str.append('\n');
            for (int i = 0; i < indent; i++) {
                str.append("  ");
            }
        }
        str.append(']');
    }
    </code>
}

class NBTTagCompound extends NBTBase {
    // Results in getData() being added, which overrides the one in NBTBase
#if version >= 1.17
    private final readonly (Map<String, NBTBaseHandle>) Map<String, NBTBase> data:tags;
#elseif exists net.minecraft.nbt.NBTTagCompound private final it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<String, NBTBase> map;
    // Nachospigot / Azurite
    private final readonly (Map<String, NBTBaseHandle>) it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<String, NBTBase> data:map;
#else
    private final readonly (Map<String, NBTBaseHandle>) Map<String, NBTBase> data:map;
#endif

    public static (NBTTagCompoundHandle) NBTTagCompound createEmpty() { return new NBTTagCompound(); }

    public static (NBTTagCompoundHandle) NBTTagCompound create(java.util.Map<String, ?> map) {
        NBTTagCompound result = new NBTTagCompound();
        if (!map.isEmpty()) {
            java.util.Iterator iter = map.entrySet().iterator();
            while (iter.hasNext()) {
                java.util.Map.Entry entry = (java.util.Map.Entry) iter.next();
                Object value = entry.getValue();
                NBTBase nbt_value;
                if (value instanceof NBTBase) {
                    nbt_value = (NBTBase) value;
                } else {
                    nbt_value = (NBTBase) com.bergerkiller.generated.net.minecraft.nbt.NBTBaseHandle.createRawHandleForData(value);
                }
                result.put((String) entry.getKey(), nbt_value);
            }
        }
        return result;
    }

    <code>
    public NBTTagCompoundHandle clone() {
        return createHandle(raw_clone());
    }
    </code>

    public boolean isEmpty();

#if version >= 1.18
    public int size();
#elseif version >= 1.15
    public int size:e();
#elseif version >= 1.9
    public int size:d();
#else
    public int size() {
        return instance.c().size();
    }
#endif

#if version >= 1.21.5
    public Set<String> getKeys:keySet();
#elseif version >= 1.18
    public Set<String> getKeys:getAllKeys();
#elseif version >= 1.13
    public Set<String> getKeys();
#else
    public Set<String> getKeys:c();
#endif

    public void remove(String key);

#if version >= 1.14
    public (NBTBaseHandle) NBTBase put(String key, (NBTBaseHandle) NBTBase value);
#else
    public (NBTBaseHandle) NBTBase put(String key, (NBTBaseHandle) NBTBase value) {
        NBTBase prev_value = instance.get(key);
        instance.put(key, value);
        return prev_value;
    }
#endif

    public (NBTBaseHandle) NBTBase get(String key);

#if version >= 1.18
    public boolean containsKey:contains(String key);
#else
    public boolean containsKey:hasKey(String key);
#endif

    <code>
    public com.bergerkiller.bukkit.common.nbt.CommonTagCompound toCommonTag() {
        return new com.bergerkiller.bukkit.common.nbt.CommonTagCompound(this);
    }

    @Override
    public void toPrettyString(StringBuilder str, int indent) {
        for (int i = 0; i < indent; i++) {
            str.append("  ");
        }
        Map<String, NBTBaseHandle> values = getData();
        str.append("TagCompound: ").append(values.size()).append(" entries {");
        for (Map.Entry<String, NBTBaseHandle> entry : values.entrySet()) {
            str.append('\n');
            for (int i = 0; i <= indent; i++) {
                str.append("  ");
            }
            str.append(entry.getKey()).append(" = ");
            int startOffset = str.length();
            entry.getValue().toPrettyString(str, indent + 1);
            str.delete(startOffset, startOffset + 2 * (indent + 1));
        }
        if (!values.isEmpty()) {
            str.append('\n');
            for (int i = 0; i < indent; i++) {
                str.append("  ");
            }
        }
        str.append('}');
    }
    </code>
}

class NBTCompressedStreamTools {

#if version >= 1.21.5
    #require MojangsonParser public static NBTTagCompound snbtParseCompoundFully:parseCompoundFully(String snbtContent);
#elseif version >= 1.18
    #require MojangsonParser public static NBTTagCompound snbtParseCompoundFully:parseTag(String snbtContent);
#else
    #require MojangsonParser public static NBTTagCompound snbtParseCompoundFully:parse(String snbtContent);
#endif

    public static (NBTTagCompoundHandle) NBTTagCompound parseTagCompoundFromSNBT(String snbtContent) {
        return #snbtParseCompoundFully(snbtContent);
    }

    public static (NBTBaseHandle) NBTBase parseTagFromSNBT(String snbtContent) {
        // Is the input an ordinary compound? If so, we can use the
        // static public method to parse it into an NBTTagCompound.
        boolean isCompound = false;
        for (int i = 0; i < snbtContent.length(); i++) {
            char c = snbtContent.charAt(i);
            // Note: putting a curly bracket char here bricks macro parser
            if (c == 123) {
                isCompound = true;
                break;
            } else if (c != ' ') {
                break;
            }
        }

        if (isCompound) {
            return #snbtParseCompoundFully(snbtContent);
        }

#if version >= 1.21.5
        #require MojangsonParser private static final MojangsonParser<NBTBase> NBT_OPS_PARSER;
        MojangsonParser parser = #NBT_OPS_PARSER;
        return (NBTBase) parser.parseFully(new com.mojang.brigadier.StringReader(snbtContent));
#elseif version >= 1.18
        MojangsonParser parser = new MojangsonParser(new com.mojang.brigadier.StringReader(snbtContent));
        return parser.readValue();
#elseif version >= 1.14
        MojangsonParser parser = new MojangsonParser(new com.mojang.brigadier.StringReader(snbtContent));
        return parser.d();
#elseif version >= 1.13
        MojangsonParser parser = new MojangsonParser(new com.mojang.brigadier.StringReader(snbtContent));
        #require MojangsonParser protected NBTBase readValue:d();
        return parser#readValue();
#elseif version >= 1.12
        #require MojangsonParser MojangsonParser createParser:<init>(String content);
        #require MojangsonParser protected NBTBase readValue:d();
        MojangsonParser parser = #createParser(snbtContent);
        return parser#readValue();
#else
        #require MojangsonParser static MojangsonParser.MojangsonTypeParser createParserFor:a(String key, String content);
        #require MojangsonParser.MojangsonTypeParser public abstract NBTBase completeParse:a();
        MojangsonParser$MojangsonTypeParser parser = #createParserFor("tag", snbtContent);
        return parser#completeParse();
#endif
    }

    public static String handleSNBTParseError(String snbtContent, Throwable exception) {
        if (exception instanceof com.bergerkiller.mountiplex.reflection.UnhandledInvokerCheckedException) {
            exception = exception.getCause();
        }

#if version >= 1.13
        if (exception instanceof com.mojang.brigadier.exceptions.CommandSyntaxException) {
            return exception.getMessage();
        }
#else
        if (exception instanceof net.minecraft.nbt.MojangsonParseException) {
            return exception.getMessage();
        }
#endif

        com.bergerkiller.bukkit.common.Logging.LOGGER.log(java.util.logging.Level.WARNING, "Error parsing SNBT: " + snbtContent, exception);
        return "Unhandled exception: " + exception.getMessage();
    }

#if version >= 1.18
    // Uncompressed tag
    public static void uncompressed_writeTag:writeUnnamedTag((NBTBaseHandle) NBTBase nbtbase, java.io.DataOutput dataoutput);
    public static (NBTBaseHandle) NBTBase uncompressed_readTag(java.io.DataInput datainput) {
  #if version >= 1.20.2
        #require net.minecraft.nbt.NBTCompressedStreamTools private static NBTBase readUnnamedTag(java.io.DataInput datainput, NBTReadLimiter nbtreadlimiter);
        return #readUnnamedTag(datainput, NBTReadLimiter.unlimitedHeap());
  #else
        #require net.minecraft.nbt.NBTCompressedStreamTools private static NBTBase readUnnamedTag(java.io.DataInput datainput, int i, NBTReadLimiter nbtreadlimiter);
        return #readUnnamedTag(datainput, 0, NBTReadLimiter.a);
  #endif
    }

    // Uncompressed tag compound
    public static void uncompressed_writeTagCompound:write((NBTTagCompoundHandle) NBTTagCompound nbttagcompound, java.io.DataOutput dataoutput);
    public static (NBTTagCompoundHandle) NBTTagCompound uncompressed_readTagCompound(java.io.DataInput datainput) {
  #if version >= 1.20.2
        return NBTCompressedStreamTools.read(datainput, NBTReadLimiter.unlimitedHeap());
  #else
        return NBTCompressedStreamTools.read(datainput, NBTReadLimiter.a);
  #endif
    }

    // Compressed tag compound
  #if version >= 1.20.3
    public static (NBTTagCompoundHandle) NBTTagCompound compressed_readTagCompound(java.io.InputStream inputstream) {
        return NBTCompressedStreamTools.readCompressed(inputstream, NBTReadLimiter.unlimitedHeap());
    }
  #else
    public static (NBTTagCompoundHandle) NBTTagCompound compressed_readTagCompound:readCompressed(java.io.InputStream inputstream);
  #endif
    public static void compressed_writeTagCompound:writeCompressed((NBTTagCompoundHandle) NBTTagCompound nbttagcompound, java.io.OutputStream outputstream);
#else
    // Uncompressed tag
    private static void uncompressed_writeTag:a((NBTBaseHandle) NBTBase nbtbase, java.io.DataOutput dataoutput);
    public static (NBTBaseHandle) NBTBase uncompressed_readTag(java.io.DataInput datainput) {
        #require net.minecraft.nbt.NBTCompressedStreamTools private static NBTBase readTag:a(java.io.DataInput datainput, int i, NBTReadLimiter nbtreadlimiter);
        return #readTag(datainput, 0, NBTReadLimiter.a);
    }

    // Uncompressed tag compound
    public static void uncompressed_writeTagCompound:a((NBTTagCompoundHandle) NBTTagCompound nbttagcompound, java.io.DataOutput dataoutput);
    public static (NBTTagCompoundHandle) NBTTagCompound uncompressed_readTagCompound(java.io.DataInput datainput) {
        return NBTCompressedStreamTools.a(datainput, NBTReadLimiter.a);
    }

    // Compressed tag compound
    public static (NBTTagCompoundHandle) NBTTagCompound compressed_readTagCompound:a(java.io.InputStream inputstream);
    public static void compressed_writeTagCompound:a((NBTTagCompoundHandle) NBTTagCompound nbttagcompound, java.io.OutputStream outputstream);
#endif
}