package net.minestom.server.tag;
import net.kyori.adventure.text.Component;
import net.minestom.server.item.ItemStack;
import net.minestom.server.utils.collection.AutoIncrementMap;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jglrxavpok.hephaistos.nbt.NBT;
import org.jglrxavpok.hephaistos.nbt.NBTCompoundLike;
import org.jglrxavpok.hephaistos.nbt.NBTList;
import org.jglrxavpok.hephaistos.nbt.NBTType;
import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
/**
* Represents a key to retrieve or change a value.
*
* All tags are serializable.
*
* @param the tag type
*/
@ApiStatus.NonExtendable
public class Tag {
private static final AutoIncrementMap INDEX_MAP = new AutoIncrementMap<>();
record PathEntry(String name, int index) {
}
final int index;
private final String key;
final Serializers.Entry entry;
private final Supplier defaultValue;
final Function, ?> readComparator;
// Optional properties
final PathEntry[] path;
final UnaryOperator copy;
final int listScope;
Tag(int index, String key,
Function, ?> readComparator,
Serializers.Entry entry,
Supplier defaultValue, PathEntry[] path, UnaryOperator copy, int listScope) {
assert index == INDEX_MAP.get(key);
this.index = index;
this.key = key;
this.readComparator = readComparator;
this.entry = entry;
this.defaultValue = defaultValue;
this.path = path;
this.copy = copy;
this.listScope = listScope;
}
static Tag tag(@NotNull String key, @NotNull Serializers.Entry entry) {
return new Tag<>(INDEX_MAP.get(key), key, entry.reader(), (Serializers.Entry) entry,
null, null, null, 0);
}
static Tag fromSerializer(@NotNull String key, @NotNull TagSerializer serializer) {
if (serializer instanceof TagRecord.Serializer recordSerializer) {
// Allow fast retrieval
//noinspection unchecked
return tag(key, recordSerializer.serializerEntry);
}
return tag(key, Serializers.fromTagSerializer(serializer));
}
/**
* Returns the key used to navigate inside the holder nbt.
*
* @return the tag key
*/
public @NotNull String getKey() {
return key;
}
@Contract(value = "_ -> new", pure = true)
public Tag defaultValue(@NotNull Supplier defaultValue) {
return new Tag<>(index, key, readComparator, entry, defaultValue, path, copy, listScope);
}
@Contract(value = "_ -> new", pure = true)
public Tag defaultValue(@NotNull T defaultValue) {
return defaultValue(() -> defaultValue);
}
@Contract(value = "_, _ -> new", pure = true)
public Tag map(@NotNull Function readMap,
@NotNull Function writeMap) {
var entry = this.entry;
final Function readFunction = entry.reader().andThen(t -> {
if (t == null) return null;
return readMap.apply(t);
});
final Function writeFunction = writeMap.andThen(entry.writer());
return new Tag<>(index, key, readMap,
new Serializers.Entry<>(entry.nbtType(), readFunction, writeFunction),
// Default value
() -> {
T defaultValue = createDefault();
if (defaultValue == null) return null;
return readMap.apply(defaultValue);
},
path, null, listScope);
}
@ApiStatus.Experimental
@Contract(value = "-> new", pure = true)
public Tag> list() {
var entry = this.entry;
var readFunction = entry.reader();
var writeFunction = entry.writer();
var listEntry = new Serializers.Entry, NBTList>>(
NBTType.TAG_List,
read -> {
if (read.isEmpty()) return List.of();
return read.asListView().stream().map(readFunction).toList();
},
write -> {
if (write.isEmpty())
return NBT.List(NBTType.TAG_String); // String is the default type for lists
final List list = write.stream().map(writeFunction).toList();
final NBTType> type = list.get(0).getID();
return NBT.List(type, list);
});
UnaryOperator> co = this.copy != null ? ts -> {
final int size = ts.size();
T[] array = (T[]) new Object[size];
boolean shallowCopy = true;
for (int i = 0; i < size; i++) {
final T t = ts.get(i);
final T copy = this.copy.apply(t);
if (shallowCopy && copy != t) shallowCopy = false;
array[i] = copy;
}
return shallowCopy ? List.copyOf(ts) : List.of(array);
} : List::copyOf;
return new Tag<>(index, key, readComparator, Serializers.Entry.class.cast(listEntry),
null, path, co, listScope + 1);
}
@ApiStatus.Experimental
@Contract(value = "_ -> new", pure = true)
public Tag path(@NotNull String @Nullable ... path) {
if (path == null || path.length == 0) {
return new Tag<>(index, key, readComparator, entry, defaultValue, null, copy, listScope);
}
PathEntry[] pathEntries = new PathEntry[path.length];
for (int i = 0; i < path.length; i++) {
final String name = path[i];
if (name == null || name.isEmpty())
throw new IllegalArgumentException("Path must not be empty: " + Arrays.toString(path));
pathEntries[i] = new PathEntry(name, INDEX_MAP.get(name));
}
return new Tag<>(index, key, readComparator, entry, defaultValue, pathEntries, copy, listScope);
}
public @Nullable T read(@NotNull NBTCompoundLike nbt) {
final NBT readable = isView() ? nbt.toCompound() : nbt.get(key);
final T result;
try {
if (readable == null || (result = entry.read(readable)) == null)
return createDefault();
return result;
} catch (ClassCastException e) {
return createDefault();
}
}
public void write(@NotNull MutableNBTCompound nbtCompound, @Nullable T value) {
if (value != null) {
final NBT nbt = entry.write(value);
if (isView()) nbtCompound.copyFrom((NBTCompoundLike) nbt);
else nbtCompound.set(key, nbt);
} else {
if (isView()) nbtCompound.clear();
else nbtCompound.remove(key);
}
}
public void writeUnsafe(@NotNull MutableNBTCompound nbtCompound, @Nullable Object value) {
//noinspection unchecked
write(nbtCompound, (T) value);
}
final boolean isView() {
return key.isEmpty();
}
final boolean shareValue(@NotNull Tag> other) {
if (this == other) return true;
// Tags are not strictly the same, compare readers
if (this.listScope != other.listScope)
return false;
return this.readComparator == other.readComparator;
}
final T createDefault() {
final Supplier supplier = defaultValue;
return supplier != null ? supplier.get() : null;
}
final T copyValue(@NotNull T value) {
final UnaryOperator copier = copy;
return copier != null ? copier.apply(value) : value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Tag> tag)) return false;
return index == tag.index &&
listScope == tag.listScope &&
readComparator.equals(tag.readComparator) &&
Objects.equals(defaultValue, tag.defaultValue) &&
Arrays.equals(path, tag.path) && Objects.equals(copy, tag.copy);
}
@Override
public int hashCode() {
int result = Objects.hash(index, readComparator, defaultValue, copy, listScope);
result = 31 * result + Arrays.hashCode(path);
return result;
}
public static @NotNull Tag Byte(@NotNull String key) {
return tag(key, Serializers.BYTE);
}
public static @NotNull Tag Boolean(@NotNull String key) {
return tag(key, Serializers.BOOLEAN);
}
public static @NotNull Tag Short(@NotNull String key) {
return tag(key, Serializers.SHORT);
}
public static @NotNull Tag Integer(@NotNull String key) {
return tag(key, Serializers.INT);
}
public static @NotNull Tag Long(@NotNull String key) {
return tag(key, Serializers.LONG);
}
public static @NotNull Tag Float(@NotNull String key) {
return tag(key, Serializers.FLOAT);
}
public static @NotNull Tag Double(@NotNull String key) {
return tag(key, Serializers.DOUBLE);
}
public static @NotNull Tag String(@NotNull String key) {
return tag(key, Serializers.STRING);
}
@ApiStatus.Experimental
public static @NotNull Tag UUID(@NotNull String key) {
return tag(key, Serializers.UUID);
}
public static @NotNull Tag ItemStack(@NotNull String key) {
return tag(key, Serializers.ITEM);
}
public static @NotNull Tag Component(@NotNull String key) {
return tag(key, Serializers.COMPONENT);
}
/**
* Creates a flexible tag able to read and write any {@link NBT} objects.
*
* Specialized tags are recommended if the type is known as conversion will be required both way (read and write).
*/
public static @NotNull Tag NBT(@NotNull String key) {
return tag(key, Serializers.NBT_ENTRY);
}
/**
* Creates a tag containing multiple fields.
*
* Those fields cannot be modified from an outside tag. (This is to prevent the backed object from becoming out of sync)
*
* @param key the tag key
* @param serializer the tag serializer
* @param the tag type
* @return the created tag
*/
public static @NotNull Tag Structure(@NotNull String key, @NotNull TagSerializer serializer) {
return fromSerializer(key, serializer);
}
/**
* Specialized Structure tag affecting the src of the handler (i.e. overwrite all its data).
*
* Must be used with care.
*/
public static @NotNull Tag View(@NotNull TagSerializer serializer) {
return Structure("", serializer);
}
@ApiStatus.Experimental
public static @NotNull Tag Structure(@NotNull String key, @NotNull Class type) {
return Structure(key, TagRecord.serializer(type));
}
@ApiStatus.Experimental
public static @NotNull Tag View(@NotNull Class type) {
return View(TagRecord.serializer(type));
}
}