001/*
002 * Copyright 2020 zml
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *    http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package ca.stellardrift.confabricate;
017
018import java.io.IOException;
019import java.util.List;
020import java.util.Map;
021import java.util.Set;
022import net.minecraft.nbt.ByteArrayTag;
023import net.minecraft.nbt.ByteTag;
024import net.minecraft.nbt.CompoundTag;
025import net.minecraft.nbt.DoubleTag;
026import net.minecraft.nbt.EndTag;
027import net.minecraft.nbt.FloatTag;
028import net.minecraft.nbt.IntArrayTag;
029import net.minecraft.nbt.IntTag;
030import net.minecraft.nbt.ListTag;
031import net.minecraft.nbt.LongArrayTag;
032import net.minecraft.nbt.LongTag;
033import net.minecraft.nbt.ShortTag;
034import net.minecraft.nbt.StringTag;
035import net.minecraft.nbt.Tag;
036import org.checkerframework.checker.nullness.qual.NonNull;
037import org.spongepowered.configurate.BasicConfigurationNode;
038import org.spongepowered.configurate.ConfigurationNode;
039import org.spongepowered.configurate.ConfigurationNodeFactory;
040import org.spongepowered.configurate.ConfigurationOptions;
041
042/**
043 * A configuration adapter that will convert Minecraft NBT data into a
044 * Configurate {@link ConfigurationNode}.
045 *
046 * @since 1.0.0
047 */
048public final class NbtNodeAdapter {
049
050    private static final ConfigurationNodeFactory<BasicConfigurationNode> FACTORY = new ConfigurationNodeFactory<>() {
051
052        @Override
053        public BasicConfigurationNode createNode(final ConfigurationOptions options) {
054            return BasicConfigurationNode.root(options
055                    .nativeTypes(Set.of(Map.class, List.class, Byte.class,
056                            Short.class, Integer.class, Long.class, Float.class, Double.class,
057                            long[].class, byte[].class, int[].class, String.class)));
058        }
059
060    };
061
062    private NbtNodeAdapter() {}
063
064    /**
065     * Given a tag, convert it to a node.
066     *
067     * <p>Depending on the configuration of the provided node, the conversion
068     * may lose some data when roundtripped back. For example, array tags may
069     * be converted to lists if the node provided does not support arrays.
070     *
071     * @param tag the tag to convert
072     * @param node the node to populate
073     * @throws IOException if invalid tags are provided
074     * @since 1.0.0
075     */
076    public static void tagToNode(final Tag tag, final ConfigurationNode node) throws IOException {
077        if (tag instanceof final CompoundTag compoundTag) {
078            for (final String key : compoundTag.getAllKeys()) {
079                tagToNode(compoundTag.get(key), node.node(key));
080            }
081        } else if (tag instanceof final ListTag list) {
082            for (final Tag value : list) {
083                tagToNode(value, node.appendListNode());
084            }
085        } else if (tag instanceof StringTag) {
086            node.raw(tag.getAsString());
087        } else if (tag instanceof final ByteTag b) {
088            node.raw(b.getAsByte());
089        } else if (tag instanceof final ShortTag s) {
090            node.raw(s.getAsShort());
091        } else if (tag instanceof final IntTag i) {
092            node.raw(i.getAsInt());
093        } else if (tag instanceof final LongTag l) {
094            node.raw(l.getAsLong());
095        } else if (tag instanceof final FloatTag f) {
096            node.raw(f.getAsFloat());
097        } else if (tag instanceof final DoubleTag d) {
098            node.raw(d.getAsDouble());
099        } else if (tag instanceof final ByteArrayTag arr) {
100            if (node.options().acceptsType(byte[].class)) {
101                node.raw(arr.getAsByteArray());
102            } else {
103                node.raw(null);
104                for (final byte b : arr.getAsByteArray()) {
105                    node.appendListNode().raw(b);
106                }
107            }
108        } else if (tag instanceof final IntArrayTag arr) {
109            if (node.options().acceptsType(int[].class)) {
110                node.raw(arr.getAsIntArray());
111            } else {
112                node.raw(null);
113                for (final int i : arr.getAsIntArray()) {
114                    node.appendListNode().raw(i);
115                }
116            }
117
118        } else if (tag instanceof final LongArrayTag arr) {
119            if (node.options().acceptsType(long[].class)) {
120                node.raw(arr.getAsLongArray());
121            } else {
122                node.raw(null);
123                for (final long l : arr.getAsLongArray()) {
124                    node.appendListNode().raw(l);
125                }
126            }
127        } else if (tag instanceof EndTag) {
128            // no-op
129        } else {
130            throw new IOException("Unknown tag type: " + tag.getClass());
131        }
132    }
133
134    /**
135     * Convert a node to tag. Because NBT is strongly typed and does not permit
136     * lists with mixed types, some configuration nodes will not be convertible
137     * to Tags.
138     *
139     * @param node the configuration node
140     * @return the converted tag object
141     * @throws IOException if an IO error occurs while converting the tag
142     * @since 1.0.0
143     */
144    public static Tag nodeToTag(final ConfigurationNode node) throws IOException {
145        if (node.isMap()) {
146            final CompoundTag tag = new CompoundTag();
147            for (final Map.Entry<Object, ? extends ConfigurationNode> ent : node.childrenMap().entrySet()) {
148                tag.put(ent.getKey().toString(), nodeToTag(ent.getValue()));
149            }
150            return tag;
151        } else if (node.isList()) {
152            final ListTag list = new ListTag();
153            for (final ConfigurationNode child : node.childrenList()) {
154                list.add(nodeToTag(child));
155            }
156            return list;
157        } else {
158            final Object obj = node.raw();
159            if (obj instanceof final byte[] arr) {
160                return new ByteArrayTag(arr);
161            } else if (obj instanceof final int[] arr) {
162                return new IntArrayTag(arr);
163            } else if (obj instanceof final long[] arr) {
164                return new LongArrayTag(arr);
165            } else if (obj instanceof final Byte b) {
166                return ByteTag.valueOf(b);
167            } else if (obj instanceof final Short s) {
168                return ShortTag.valueOf(s);
169            } else if (obj instanceof final Integer i) {
170                return IntTag.valueOf(i);
171            } else if (obj instanceof final Long l) {
172                return LongTag.valueOf(l);
173            } else if (obj instanceof final Float f) {
174                return FloatTag.valueOf(f);
175            } else if (obj instanceof final Double d) {
176                return DoubleTag.valueOf(d);
177            } else if (obj instanceof final String s) {
178                return StringTag.valueOf(s);
179            } else {
180                throw new IOException("Unsupported object type " + (obj == null ? null : obj.getClass()));
181            }
182        }
183    }
184
185    /**
186     * Create an empty node with options appropriate for handling NBT data.
187     *
188     * @return the new node
189     * @since 1.0.0
190     */
191    public static ConfigurationNode createEmptyNode() {
192        return FACTORY.createNode(Confabricate.confabricateOptions());
193    }
194
195    /**
196     * Create an empty node with options appropriate for handling NBT data.
197     *
198     * @param options options to work with
199     * @return the new node
200     * @since 1.0.0
201     */
202    public static ConfigurationNode createEmptyNode(final @NonNull ConfigurationOptions options) {
203        return FACTORY.createNode(options);
204    }
205
206    /**
207     * Get a factory for nodes prepared to handle NBT data.
208     *
209     * @return the factory
210     * @since 3.0.0
211     */
212    public static ConfigurationNodeFactory<BasicConfigurationNode> nodeFactory() {
213        return FACTORY;
214    }
215
216}