package net.minecraft.nbt;

import net.minecraft.nbt.TagParser;

import com.bergerkiller.generated.net.minecraft.nbt.TagHandle;
import com.bergerkiller.generated.net.minecraft.nbt.ListTagHandle;
import com.bergerkiller.generated.net.minecraft.nbt.CompoundTagHandle;
import com.bergerkiller.generated.net.minecraft.nbt.TagHandle.StringTagHandle;
import com.bergerkiller.generated.net.minecraft.nbt.TagHandle.ByteTagHandle;
import com.bergerkiller.generated.net.minecraft.nbt.TagHandle.ShortTagHandle;
import com.bergerkiller.generated.net.minecraft.nbt.TagHandle.IntTagHandle;
import com.bergerkiller.generated.net.minecraft.nbt.TagHandle.LongTagHandle;
import com.bergerkiller.generated.net.minecraft.nbt.TagHandle.FloatTagHandle;
import com.bergerkiller.generated.net.minecraft.nbt.TagHandle.DoubleTagHandle;
import com.bergerkiller.generated.net.minecraft.nbt.TagHandle.ByteArrayTagHandle;
import com.bergerkiller.generated.net.minecraft.nbt.TagHandle.IntArrayTagHandle;
import com.bergerkiller.generated.net.minecraft.nbt.TagHandle.LongArrayTagHandle;

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

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

    <code>
    public com.bergerkiller.bukkit.common.nbt.CommonTag toCommonTag() {
        return new com.bergerkiller.bukkit.common.nbt.CommonTag(this);
    }
    public abstract TagHandle 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 TagHandle> 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 TagHandle> 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, StringTagHandle.T,
                    data -> StringTagHandle.T.create.raw.invoke(com.bergerkiller.bukkit.common.conversion.Conversion.toString.convert(data, "")),
                    java.util.function.Function.identity()
            );

            registerTypeInfo(String.class, StringTagHandle.T, StringTagHandle.T.create.raw::invoke, StringTagHandle.T.getData::invoke);
            registerTypeInfo(byte.class, ByteTagHandle.T, ByteTagHandle.T.create.raw::invoke, ByteTagHandle.T.getByteData::invoke);
            registerTypeInfo(short.class, ShortTagHandle.T, ShortTagHandle.T.create.raw::invoke, ShortTagHandle.T.getShortData::invoke);
            registerTypeInfo(int.class, IntTagHandle.T, IntTagHandle.T.create.raw::invoke, IntTagHandle.T.getIntegerData::invoke);
            registerTypeInfo(long.class, LongTagHandle.T, LongTagHandle.T.create.raw::invoke, LongTagHandle.T.getLongData::invoke);
            registerTypeInfo(float.class, FloatTagHandle.T, FloatTagHandle.T.create.raw::invoke, FloatTagHandle.T.getFloatData::invoke);
            registerTypeInfo(double.class, DoubleTagHandle.T, DoubleTagHandle.T.create.raw::invoke, DoubleTagHandle.T.getDoubleData::invoke);
            registerTypeInfo(byte[].class, ByteArrayTagHandle.T, ByteArrayTagHandle.T.create.raw::invoke, ByteArrayTagHandle.T.getData::invoke);
            registerTypeInfo(int[].class, IntArrayTagHandle.T, IntArrayTagHandle.T.create.raw::invoke, IntArrayTagHandle.T.getData::invoke);

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

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

        private void registerTypeInfo(
                Class<?> dataType,
                Template.Class<? extends TagHandle> 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 = TagHandle.lookup) != null) {
            return lookup;
        }

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

            lookup = new TypeInfoLookup();
            TagHandle.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 TagHandle 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 StringTag extends Tag {
#if version >= 1.15
        public static (TagHandle.StringTagHandle) StringTag create:valueOf(String data);
#else
        public static (TagHandle.StringTagHandle) StringTag create(String data) { return new StringTag(data); }
#endif

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

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

    class ByteTag extends Tag {
#if version >= 1.15
        public static (TagHandle.ByteTagHandle) ByteTag create:valueOf(byte data);
#else
        public static (TagHandle.ByteTagHandle) ByteTag create(byte data) { return new ByteTag(data); }
#endif

        public byte getByteData:getAsByte();

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

    class ShortTag extends Tag {
#if version >= 1.15
        public static (TagHandle.ShortTagHandle) ShortTag create:valueOf(short data);
#else
        public static (TagHandle.ShortTagHandle) ShortTag create(short data) { return new ShortTag(data); }
#endif

        public short getShortData:getAsShort();

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

    class IntTag extends Tag {
#if version >= 1.15
        public static (TagHandle.IntTagHandle) IntTag create:valueOf(int data);
#else
        public static (TagHandle.IntTagHandle) IntTag create(int data) { return new IntTag(data); }
#endif
        public int getIntegerData:getAsInt();

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

    class LongTag extends Tag {
#if version >= 1.15
        public static (TagHandle.LongTagHandle) LongTag create:valueOf(long data);
#else
        public static (TagHandle.LongTagHandle) LongTag create(long data) { return new LongTag(data); }
#endif

        public long getLongData:getAsLong();

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

    class FloatTag extends Tag {
#if version >= 1.15
        public static (TagHandle.FloatTagHandle) FloatTag create:valueOf(float data);
#else
        public static (TagHandle.FloatTagHandle) FloatTag create(float data) { return new FloatTag(data); }
#endif

        public float getFloatData:getAsFloat();

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

    class DoubleTag extends Tag {
#if version >= 1.15
        public static (TagHandle.DoubleTagHandle) DoubleTag create:valueOf(double data);
#else
        public static (TagHandle.DoubleTagHandle) DoubleTag create(double data) { return new DoubleTag(data); }
#endif

        public double getDoubleData:getAsDouble();

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

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

    class IntArrayTag extends Tag {
        public static (TagHandle.IntArrayTagHandle) IntArrayTag create(int[] data) { return new IntArrayTag(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 LongArrayTag extends Tag {
        public static (TagHandle.LongArrayTagHandle) LongArrayTag create(long[] data) { return new LongArrayTag(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.LongArrayTag private long[] data_field:b;
            return instance#data_field;
        }
#else
        public long[] getData() {
            throw new UnsupportedOperationException("LongArrayTag is not available");
        }
#endif
    }
}

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

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

    public static (ListTagHandle) ListTag createEmpty() {
        return new ListTag();
    }

    public static (ListTagHandle) ListTag create(java.util.Collection<?> data) {
        ListTag result = new ListTag();
        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.ListTagHandle.T.add.raw;
            while (iter.hasNext()) {
                Object element = iter.next();
                if (!(element instanceof Tag)) {
                    element = com.bergerkiller.generated.net.minecraft.nbt.TagHandle.createRawHandleForData(element);
                }
                add_method.invoke(result, element);
            }
        }
        return result;
    }

    <code>
    public ListTagHandle 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 (TagHandle) Tag 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 (TagHandle) Tag set_at:d(int index, (TagHandle) Tag nbt_value);
    public (TagHandle) Tag remove_at:c(int index);
    public void add_at:c(int index, (TagHandle) Tag value);
  #elseif version >= 1.16.5 && forge == magma
    // Magma 1.16.5+ remapping bug
    public (TagHandle) Tag set_at:d(int index, (TagHandle) Tag nbt_value);
    public (TagHandle) Tag remove_at:c(int index);
    public void add_at:c(int index, (TagHandle) Tag value);
  #else
    public (TagHandle) Tag set_at:set(int index, (TagHandle) Tag nbt_value);
    public (TagHandle) Tag remove_at:remove(int index);
    public void add_at:add(int index, (TagHandle) Tag value);
  #endif

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

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

    public (TagHandle) Tag set_at(int index, (TagHandle) Tag 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);
        }
        Tag 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, (TagHandle) Tag 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 (TagHandle) Tag remove_at(int index) {
  #if version >= 1.9
        Tag removed = instance.remove(index);
  #else
        Tag removed = instance.a(index);
  #endif
        if (instance.isEmpty()) {
            instance#type = (byte) 0;
        }
        return removed;
    }

    public boolean add((TagHandle) Tag 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<TagHandle> values = getData();
        str.append("TagList: ").append(values.size()).append(" entries [");
        for (TagHandle 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 CompoundTag extends Tag {
    // Results in getData() being added, which overrides the one in Tag
#if version >= 1.17
    private final readonly (Map<String, TagHandle>) Map<String, Tag> data:tags;
#elseif exists net.minecraft.nbt.CompoundTag private final it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<String, Tag> map;
    // Nachospigot / Azurite
    private final readonly (Map<String, TagHandle>) it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<String, Tag> data:map;
#else
    private final readonly (Map<String, TagHandle>) Map<String, Tag> data:map;
#endif

    public static (CompoundTagHandle) CompoundTag createEmpty() { return new CompoundTag(); }

    public static (CompoundTagHandle) CompoundTag create(java.util.Map<String, ?> map) {
        CompoundTag result = new CompoundTag();
        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();
                Tag nbt_value;
                if (value instanceof Tag) {
                    nbt_value = (Tag) value;
                } else {
                    nbt_value = (Tag) com.bergerkiller.generated.net.minecraft.nbt.TagHandle.createRawHandleForData(value);
                }
                result.put((String) entry.getKey(), nbt_value);
            }
        }
        return result;
    }

    <code>
    public CompoundTagHandle 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

#if version >= 1.21.11
    public (void) Tag remove(String key);
#else
    public (void) void remove(String key);
#endif

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

    public (TagHandle) Tag 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, TagHandle> values = getData();
        str.append("TagCompound: ").append(values.size()).append(" entries {");
        for (Map.Entry<String, TagHandle> 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 NbtIo {

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

    public static (CompoundTagHandle) CompoundTag parseTagCompoundFromSNBT(String snbtContent) {
        return #snbtParseCompoundFully(snbtContent);
    }

    public static (TagHandle) Tag parseTagFromSNBT(String snbtContent) {
        // Is the input an ordinary compound? If so, we can use the
        // static public method to parse it into an CompoundTag.
        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 TagParser private static final TagParser<Tag> NBT_OPS_PARSER;
        TagParser parser = #NBT_OPS_PARSER;
        return (Tag) parser.parseFully(new com.mojang.brigadier.StringReader(snbtContent));
#elseif version >= 1.18
        TagParser parser = new TagParser(new com.mojang.brigadier.StringReader(snbtContent));
        return parser.readValue();
#elseif version >= 1.14
        TagParser parser = new TagParser(new com.mojang.brigadier.StringReader(snbtContent));
        return parser.d();
#elseif version >= 1.13
        TagParser parser = new TagParser(new com.mojang.brigadier.StringReader(snbtContent));
        #require TagParser protected Tag readValue:d();
        return parser#readValue();
#elseif version >= 1.12
        #require TagParser TagParser createParser:<init>(String content);
        #require TagParser protected Tag readValue:d();
        TagParser parser = #createParser(snbtContent);
        return parser#readValue();
#else
        #require TagParser static TagParser.TagTypeParser createParserFor:a(String key, String content);
        #require TagParser.TagTypeParser public abstract Tag completeParse:a();
        TagParser$TagTypeParser 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.TagParseException) {
            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((TagHandle) Tag nbtbase, java.io.DataOutput dataoutput);
    public static (TagHandle) Tag uncompressed_readTag(java.io.DataInput datainput) {
  #if version >= 1.20.2
        #require net.minecraft.nbt.NbtIo private static Tag readUnnamedTag(java.io.DataInput datainput, NbtAccounter nbtreadlimiter);
        return #readUnnamedTag(datainput, NbtAccounter.unlimitedHeap());
  #else
        #require net.minecraft.nbt.NbtIo private static Tag readUnnamedTag(java.io.DataInput datainput, int i, NbtAccounter nbtreadlimiter);
        return #readUnnamedTag(datainput, 0, NbtAccounter.a);
  #endif
    }

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

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

    // Uncompressed tag compound
    public static void uncompressed_writeTagCompound:a((CompoundTagHandle) CompoundTag nbttagcompound, java.io.DataOutput dataoutput);
    public static (CompoundTagHandle) CompoundTag uncompressed_readTagCompound(java.io.DataInput datainput) {
        return NbtIo.a(datainput, NbtAccounter.a);
    }

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