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