// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.codec.builder;

import java.util.function.Consumer;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.function.BiFunction;
import com.hypixel.hytale.function.consumer.TriConsumer;
import java.util.function.Function;
import com.hypixel.hytale.codec.schema.config.NullSchema;
import com.hypixel.hytale.codec.schema.SchemaConvertable;
import com.hypixel.hytale.codec.EmptyExtraInfo;
import com.hypixel.hytale.codec.schema.metadata.ui.UIDisplayMode;
import com.hypixel.hytale.codec.schema.config.Schema;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import com.hypixel.hytale.codec.schema.config.ObjectSchema;
import com.hypixel.hytale.codec.schema.SchemaContext;
import java.util.Set;
import java.io.IOException;
import com.hypixel.hytale.codec.util.RawJsonReader;
import com.hypixel.hytale.codec.VersionedExtraInfo;
import org.bson.BsonDocument;
import com.hypixel.hytale.codec.exception.CodecException;
import org.bson.BsonValue;
import java.util.Iterator;
import java.util.Comparator;
import java.util.Collections;
import java.util.Objects;
import com.hypixel.hytale.codec.schema.metadata.Metadata;
import com.hypixel.hytale.codec.ExtraInfo;
import com.hypixel.hytale.codec.validation.ValidationResults;
import java.util.function.BiConsumer;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import java.util.function.Supplier;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.codec.validation.ValidatableCodec;
import com.hypixel.hytale.codec.InheritCodec;
import com.hypixel.hytale.codec.RawJsonCodec;
import com.hypixel.hytale.codec.DirectDecodeCodec;
import com.hypixel.hytale.codec.Codec;

public class BuilderCodec<T> implements Codec<T>, DirectDecodeCodec<T>, RawJsonCodec<T>, InheritCodec<T>, ValidatableCodec<T>
{
    public static final int UNSET_VERSION = Integer.MIN_VALUE;
    public static final int UNSET_MAX_VERSION = Integer.MAX_VALUE;
    public static final int INITIAL_VERSION = 0;
    public static final BuilderCodec<?>[] EMPTY_ARRAY;
    private static final KeyedCodec<Integer> VERSION;
    protected final Class<T> tClass;
    protected final Supplier<T> supplier;
    @Nullable
    protected final BuilderCodec<? super T> parentCodec;
    @Nonnull
    protected final Map<String, List<BuilderField<T, ?>>> entries;
    @Nonnull
    protected final Map<String, List<BuilderField<T, ?>>> unmodifiableEntries;
    protected final BiConsumer<T, ValidationResults> validator;
    protected final BiConsumer<T, ExtraInfo> afterDecode;
    protected final boolean hasNonNullValidator;
    protected final String documentation;
    protected final List<Metadata> metadata;
    protected final int codecVersion;
    protected final int minCodecVersion;
    protected final boolean versioned;
    @Deprecated
    protected final boolean useLegacyVersion;
    @Nonnull
    protected final StringTreeMap<KeyEntry<T>> stringTreeMap;
    
    protected BuilderCodec(@Nonnull final BuilderBase<T, ?> builder) {
        this.tClass = builder.tClass;
        this.supplier = builder.supplier;
        this.parentCodec = builder.parentCodec;
        this.entries = Objects.requireNonNull(builder.entries, "entries parameter can't be null");
        this.unmodifiableEntries = Collections.unmodifiableMap((Map<? extends String, ? extends List<BuilderField<T, ?>>>)builder.entries);
        this.stringTreeMap = builder.stringTreeMap;
        this.validator = builder.validator;
        this.afterDecode = builder.afterDecode;
        this.documentation = builder.documentation;
        this.metadata = builder.metadata;
        boolean hasNonNullValidator = false;
        for (final List<BuilderField<T, ?>> fields : this.entries.values()) {
            fields.sort(Comparator.comparingInt(BuilderField::getMinVersion));
            for (final BuilderField<T, ?> field : fields) {
                hasNonNullValidator |= field.hasNonNullValidator();
            }
        }
        this.hasNonNullValidator = hasNonNullValidator;
        int codecVersion;
        if (builder.codecVersion != Integer.MIN_VALUE) {
            codecVersion = builder.codecVersion;
        }
        else {
            int highestFieldVersion = Integer.MIN_VALUE;
            for (final List<BuilderField<T, ?>> fields2 : this.entries.values()) {
                for (final BuilderField<T, ?> field2 : fields2) {
                    highestFieldVersion = Math.max(highestFieldVersion, field2.getHighestSupportedVersion());
                }
            }
            codecVersion = highestFieldVersion;
        }
        int minCodecVersion;
        if (builder.minCodecVersion != Integer.MAX_VALUE) {
            minCodecVersion = builder.minCodecVersion;
        }
        else {
            int lowestFieldVersion = Integer.MAX_VALUE;
            for (final List<BuilderField<T, ?>> fields3 : this.entries.values()) {
                for (final BuilderField<T, ?> field3 : fields3) {
                    final int min = field3.getMinVersion();
                    if (min != Integer.MIN_VALUE) {
                        lowestFieldVersion = Math.min(lowestFieldVersion, min);
                    }
                }
            }
            minCodecVersion = lowestFieldVersion;
        }
        if (this.parentCodec != null) {
            codecVersion = Math.max(codecVersion, this.parentCodec.codecVersion);
            minCodecVersion = Math.min(minCodecVersion, this.parentCodec.minCodecVersion);
            this.versioned = (builder.versioned || this.parentCodec.versioned);
            this.useLegacyVersion = (builder.useLegacyVersion || this.parentCodec.useLegacyVersion);
        }
        else {
            this.versioned = builder.versioned;
            this.useLegacyVersion = builder.useLegacyVersion;
        }
        this.codecVersion = codecVersion;
        this.minCodecVersion = minCodecVersion;
    }
    
    public Class<T> getInnerClass() {
        return this.tClass;
    }
    
    public Supplier<T> getSupplier() {
        return this.supplier;
    }
    
    public T getDefaultValue() {
        return this.getDefaultValue(ExtraInfo.THREAD_LOCAL.get());
    }
    
    public T getDefaultValue(final ExtraInfo extraInfo) {
        final T t = this.supplier.get();
        this.afterDecode(t, extraInfo);
        return t;
    }
    
    @Nonnull
    public Map<String, List<BuilderField<T, ?>>> getEntries() {
        return this.unmodifiableEntries;
    }
    
    public BiConsumer<T, ExtraInfo> getAfterDecode() {
        return this.afterDecode;
    }
    
    @Nullable
    public BuilderCodec<? super T> getParent() {
        return this.parentCodec;
    }
    
    public String getDocumentation() {
        return this.documentation;
    }
    
    public int getCodecVersion() {
        return this.codecVersion;
    }
    
    public void inherit(final T t, @Nonnull final T parent, @Nonnull final ExtraInfo extraInfo) {
        if (this.parentCodec != null) {
            this.parentCodec.inherit((Object)t, (Object)parent, extraInfo);
        }
        if (this.getInnerClass().isAssignableFrom(parent.getClass())) {
            for (final List<BuilderField<T, ?>> entry : this.entries.values()) {
                final BuilderField<T, ?> field = findField(entry, extraInfo);
                if (field != null) {
                    field.inherit(t, parent, extraInfo);
                }
            }
        }
    }
    
    public void afterDecode(final T t, final ExtraInfo extraInfo) {
        if (this.parentCodec != null) {
            this.parentCodec.afterDecode(t, extraInfo);
        }
        if (this.afterDecode != null) {
            this.afterDecode.accept(t, extraInfo);
        }
    }
    
    public void afterDecodeAndValidate(final T t, @Nonnull final ExtraInfo extraInfo) {
        if (this.parentCodec != null) {
            this.parentCodec.afterDecodeAndValidate(t, extraInfo);
        }
        if (this.afterDecode != null) {
            this.afterDecode.accept(t, extraInfo);
        }
        final ValidationResults results = extraInfo.getValidationResults();
        if (this.hasNonNullValidator) {
            for (final List<BuilderField<T, ?>> entry : this.entries.values()) {
                final BuilderField<T, ?> field = findField(entry, extraInfo);
                if (field != null) {
                    extraInfo.pushKey(field.codec.getKey());
                    try {
                        field.nullValidate(t, results, extraInfo);
                    }
                    finally {
                        extraInfo.popKey();
                    }
                }
            }
            if (this.validator != null) {
                this.validator.accept(t, results);
            }
            results._processValidationResults();
        }
        else if (this.validator != null) {
            this.validator.accept(t, results);
            results._processValidationResults();
        }
    }
    
    @Override
    public T decode(@Nonnull final BsonValue bsonValue, @Nonnull final ExtraInfo extraInfo) {
        if (this.supplier == null) {
            throw new CodecException("This BuilderCodec is for an abstract or direct codec. To use this codec you must specify an existing object to decode into.");
        }
        final T t = this.supplier.get();
        this.decode(bsonValue.asDocument(), t, extraInfo);
        return t;
    }
    
    @Nonnull
    @Override
    public BsonDocument encode(final T t, @Nonnull ExtraInfo extraInfo) {
        final BsonDocument document = new BsonDocument();
        if (this.versioned) {
            if (this.codecVersion != 0) {
                BuilderCodec.VERSION.put(document, this.codecVersion, extraInfo);
            }
            extraInfo = new VersionedExtraInfo(this.codecVersion, extraInfo);
        }
        return this.encode0(t, document, extraInfo);
    }
    
    @Override
    public void decode(@Nonnull final BsonValue bsonValue, final T t, @Nonnull final ExtraInfo extraInfo) {
        this.decode0(bsonValue.asDocument(), t, extraInfo);
        this.afterDecodeAndValidate(t, extraInfo);
    }
    
    protected void decode0(@Nonnull final BsonDocument document, final T t, ExtraInfo extraInfo) {
        if (this.versioned) {
            extraInfo = this.decodeVersion(document, extraInfo);
        }
        for (final Map.Entry<String, BsonValue> entry : document.entrySet()) {
            final String key = entry.getKey();
            final BuilderField<? super T, ?> field = findEntry((BuilderCodec<? super Object>)this, key, extraInfo);
            if (field != null) {
                field.decode(document, t, extraInfo);
            }
            else {
                extraInfo.addUnknownKey(key);
            }
        }
    }
    
    @Nonnull
    protected BsonDocument encode0(final T t, @Nonnull final BsonDocument document, @Nonnull final ExtraInfo extraInfo) {
        if (this.parentCodec != null) {
            this.parentCodec.encode0(t, document, extraInfo);
        }
        for (final List<BuilderField<T, ?>> entry : this.entries.values()) {
            final BuilderField<T, ?> field = findField(entry, extraInfo);
            if (field != null) {
                field.encode(document, t, extraInfo);
            }
        }
        return document;
    }
    
    @Override
    public T decodeJson(@Nonnull final RawJsonReader reader, @Nonnull final ExtraInfo extraInfo) throws IOException {
        if (this.supplier == null) {
            throw new CodecException("This BuilderCodec is for an abstract or direct codec. To use this codec you must specify an existing object to decode into.");
        }
        final T t = this.supplier.get();
        this.decodeJson0(reader, t, extraInfo);
        this.afterDecodeAndValidate(t, extraInfo);
        return t;
    }
    
    public void decodeJson0(@Nonnull final RawJsonReader reader, final T t, ExtraInfo extraInfo) throws IOException {
        if (this.versioned) {
            extraInfo = this.decodeVersion(reader, extraInfo);
        }
        reader.expect('{');
        reader.consumeWhiteSpace();
        if (reader.tryConsume('}')) {
            return;
        }
        while (true) {
            this.readEntry(reader, t, extraInfo);
            reader.consumeWhiteSpace();
            if (reader.tryConsumeOrExpect('}', ',')) {
                break;
            }
            reader.consumeWhiteSpace();
        }
    }
    
    protected void readEntry(@Nonnull final RawJsonReader reader, final T t, @Nonnull final ExtraInfo extraInfo) throws IOException {
        reader.mark();
        final StringTreeMap<KeyEntry<T>> treeMapEntry = this.stringTreeMap.findEntry(reader);
        final KeyEntry<T> keyEntry;
        if (treeMapEntry == null || (keyEntry = treeMapEntry.getValue()) == null) {
            reader.reset();
            this.readUnknownField(reader, extraInfo);
            return;
        }
        switch (keyEntry.getType().ordinal()) {
            case 0: {
                final String key = treeMapEntry.getKey();
                final List<BuilderField<T, ?>> fields = keyEntry.getFields();
                this.readField(reader, t, extraInfo, key, fields);
                break;
            }
            case 1: {
                reader.unmark();
                this.skipField(reader);
                break;
            }
            case 2: {
                if (extraInfo.getKeysSize() == 0) {
                    reader.unmark();
                    this.skipField(reader);
                    break;
                }
                reader.reset();
                this.readUnknownField(reader, extraInfo);
                break;
            }
            default: {
                throw new IllegalArgumentException("Unknown field entry type: " + String.valueOf(keyEntry.getType()));
            }
        }
    }
    
    private void skipField(@Nonnull final RawJsonReader reader) throws IOException {
        reader.consumeWhiteSpace();
        reader.expect(':');
        reader.consumeWhiteSpace();
        reader.skipValue();
    }
    
    private void readField(@Nonnull final RawJsonReader reader, final T t, @Nonnull final ExtraInfo extraInfo, final String key, @Nonnull final List<BuilderField<T, ?>> fields) throws IOException {
        BuilderField<T, ?> entry = null;
        for (final BuilderField<T, ?> field : fields) {
            if (field.supportsVersion(extraInfo.getVersion())) {
                entry = field;
                break;
            }
        }
        if (entry == null) {
            reader.reset();
            this.readUnknownField(reader, extraInfo);
            return;
        }
        reader.unmark();
        reader.consumeWhiteSpace();
        reader.expect(':');
        reader.consumeWhiteSpace();
        extraInfo.pushKey(key, reader);
        try {
            entry.decodeJson(reader, t, extraInfo);
        }
        catch (final Exception e) {
            throw new CodecException("Failed to decode", reader, extraInfo, e);
        }
        finally {
            extraInfo.popKey();
        }
    }
    
    private void readUnknownField(@Nonnull final RawJsonReader reader, @Nonnull final ExtraInfo extraInfo) throws IOException {
        extraInfo.readUnknownKey(reader);
        reader.consumeWhiteSpace();
        reader.expect(':');
        reader.consumeWhiteSpace();
        reader.skipValue();
    }
    
    public void decodeJson(@Nonnull final RawJsonReader reader, final T t, @Nonnull final ExtraInfo extraInfo) throws IOException {
        this.decodeJson0(reader, t, extraInfo);
        this.afterDecodeAndValidate(t, extraInfo);
    }
    
    @Override
    public T decodeAndInherit(@Nonnull final BsonDocument document, final T parent, final ExtraInfo extraInfo) {
        final T t = this.supplier.get();
        this.decodeAndInherit(document, t, parent, extraInfo);
        return t;
    }
    
    @Override
    public void decodeAndInherit(@Nonnull final BsonDocument document, final T t, @Nullable final T parent, ExtraInfo extraInfo) {
        if (this.versioned) {
            extraInfo = this.decodeVersion(document, extraInfo);
        }
        if (parent != null) {
            this.inherit(t, parent, extraInfo);
        }
        this.decodeAndInherit0(document, t, parent, extraInfo);
        this.afterDecodeAndValidate(t, extraInfo);
    }
    
    protected void decodeAndInherit0(@Nonnull final BsonDocument document, final T t, final T parent, @Nonnull final ExtraInfo extraInfo) {
        for (final Map.Entry<String, BsonValue> entry : document.entrySet()) {
            final String key = entry.getKey();
            final BuilderField<? super T, ?> field = findEntry((BuilderCodec<? super Object>)this, key, extraInfo);
            if (field != null) {
                if (field.codec.getChildCodec() instanceof BuilderCodec) {
                    decodeAndInherit(field, document, (Object)t, (Object)parent, extraInfo);
                }
                else {
                    field.decodeAndInherit(document, (Object)t, (Object)parent, extraInfo);
                }
            }
            else {
                extraInfo.addUnknownKey(key);
            }
        }
    }
    
    private static <Type, FieldType> void decodeAndInherit(@Nonnull final BuilderField<Type, FieldType> entry, @Nonnull final BsonDocument document, final Type t, @Nullable final Type parent, @Nonnull final ExtraInfo extraInfo) {
        final KeyedCodec<FieldType> codec = entry.codec;
        final String key = codec.getKey();
        final BsonValue bsonValue = document.get(key);
        if (Codec.isNullBsonValue(bsonValue)) {
            if (bsonValue != null && bsonValue.isNull()) {
                entry.setValue(t, null, extraInfo);
            }
            return;
        }
        extraInfo.pushKey(key);
        try {
            final BuilderCodec<FieldType> inheritCodec = (BuilderCodec)codec.getChildCodec();
            final FieldType value = inheritCodec.getSupplier().get();
            final FieldType parentValue = (parent != null) ? entry.getter.apply(parent, extraInfo) : null;
            inheritCodec.decodeAndInherit(bsonValue.asDocument(), value, parentValue, extraInfo);
            entry.setValue(t, value, extraInfo);
        }
        catch (final Exception e) {
            throw new CodecException("Failed to decode", bsonValue, extraInfo, e);
        }
        finally {
            extraInfo.popKey();
        }
    }
    
    @Override
    public T decodeAndInheritJson(@Nonnull final RawJsonReader reader, final T parent, final ExtraInfo extraInfo) throws IOException {
        final T t = this.supplier.get();
        this.decodeAndInheritJson(reader, t, parent, extraInfo);
        return t;
    }
    
    @Override
    public void decodeAndInheritJson(@Nonnull final RawJsonReader reader, final T t, @Nullable final T parent, ExtraInfo extraInfo) throws IOException {
        if (this.versioned) {
            extraInfo = this.decodeVersion(reader, extraInfo);
        }
        if (parent != null) {
            this.inherit(t, parent, extraInfo);
        }
        this.decodeAndInheritJson0(reader, t, parent, extraInfo);
        this.afterDecodeAndValidate(t, extraInfo);
    }
    
    public void decodeAndInheritJson0(@Nonnull final RawJsonReader reader, final T t, final T parent, @Nonnull final ExtraInfo extraInfo) throws IOException {
        reader.expect('{');
        reader.consumeWhiteSpace();
        if (reader.tryConsume('}')) {
            return;
        }
        while (true) {
            this.readAndInheritEntry(reader, t, parent, extraInfo);
            reader.consumeWhiteSpace();
            if (reader.tryConsumeOrExpect('}', ',')) {
                break;
            }
            reader.consumeWhiteSpace();
        }
    }
    
    protected void readAndInheritEntry(@Nonnull final RawJsonReader reader, final T t, final T parent, @Nonnull final ExtraInfo extraInfo) throws IOException {
        reader.mark();
        final StringTreeMap<KeyEntry<T>> treeMapEntry = this.stringTreeMap.findEntry(reader);
        final KeyEntry<T> keyEntry;
        if (treeMapEntry == null || (keyEntry = treeMapEntry.getValue()) == null) {
            reader.reset();
            this.readUnknownField(reader, extraInfo);
            return;
        }
        switch (keyEntry.getType().ordinal()) {
            case 0: {
                final String key = treeMapEntry.getKey();
                final List<BuilderField<T, ?>> fields = keyEntry.getFields();
                this.readAndInheritField(reader, t, parent, extraInfo, key, fields);
                break;
            }
            case 1: {
                reader.unmark();
                this.skipField(reader);
                break;
            }
            case 2: {
                if (extraInfo.getKeysSize() == 0) {
                    reader.unmark();
                    this.skipField(reader);
                    break;
                }
                reader.reset();
                this.readUnknownField(reader, extraInfo);
                break;
            }
        }
    }
    
    private void readAndInheritField(@Nonnull final RawJsonReader reader, final T t, final T parent, @Nonnull final ExtraInfo extraInfo, final String key, @Nonnull final List<BuilderField<T, ?>> fields) throws IOException {
        BuilderField<T, ?> entry = null;
        for (final BuilderField<T, ?> field : fields) {
            if (field.supportsVersion(extraInfo.getVersion())) {
                entry = field;
                break;
            }
        }
        if (entry == null) {
            reader.reset();
            this.readUnknownField(reader, extraInfo);
            return;
        }
        reader.unmark();
        reader.consumeWhiteSpace();
        reader.expect(':');
        reader.consumeWhiteSpace();
        extraInfo.pushKey(key, reader);
        try {
            if (entry.codec.getChildCodec() instanceof BuilderCodec) {
                decodeAndInheritJson(entry, reader, t, parent, extraInfo);
            }
            else {
                entry.decodeAndInheritJson(reader, t, parent, extraInfo);
            }
        }
        catch (final Exception e) {
            throw new CodecException("Failed to decode", reader, extraInfo, e);
        }
        finally {
            extraInfo.popKey();
        }
    }
    
    private static <Type, FieldType> void decodeAndInheritJson(@Nonnull final BuilderField<Type, FieldType> entry, @Nonnull final RawJsonReader reader, final Type t, @Nullable final Type parent, @Nonnull final ExtraInfo extraInfo) throws IOException {
        final int read = reader.peek();
        if (read == -1) {
            throw new IOException("Unexpected EOF!");
        }
        switch (read) {
            case 78:
            case 110: {
                reader.readNullValue();
                entry.setValue(t, null, extraInfo);
                return;
            }
            default: {
                final BuilderCodec<FieldType> inheritCodec = (BuilderCodec)entry.codec.getChildCodec();
                final FieldType value = inheritCodec.getSupplier().get();
                final FieldType parentValue = (parent != null) ? entry.getter.apply(parent, extraInfo) : null;
                inheritCodec.decodeAndInheritJson(reader, value, parentValue, extraInfo);
                entry.setValue(t, value, extraInfo);
            }
        }
    }
    
    @Nonnull
    protected ExtraInfo decodeVersion(final BsonDocument document, @Nonnull final ExtraInfo extraInfo) {
        if (this.useLegacyVersion && extraInfo.getLegacyVersion() != Integer.MAX_VALUE) {
            return new VersionedExtraInfo(extraInfo.getLegacyVersion(), extraInfo);
        }
        final int version = BuilderCodec.VERSION.get(document, extraInfo).orElse(0);
        if (version > this.codecVersion) {
            throw new IllegalArgumentException("Version " + version + " is newer than expected version " + this.codecVersion);
        }
        if (this.minCodecVersion != Integer.MAX_VALUE && version < this.minCodecVersion) {
            throw new IllegalArgumentException("Version " + version + " is older than min supported version " + this.minCodecVersion);
        }
        return new VersionedExtraInfo(version, extraInfo);
    }
    
    @Nonnull
    protected ExtraInfo decodeVersion(@Nonnull final RawJsonReader reader, @Nonnull final ExtraInfo extraInfo) throws IOException {
        if (this.useLegacyVersion && extraInfo.getLegacyVersion() != Integer.MAX_VALUE) {
            return new VersionedExtraInfo(extraInfo.getLegacyVersion(), extraInfo);
        }
        reader.mark();
        int version = 0;
        if (RawJsonReader.seekToKey(reader, BuilderCodec.VERSION.getKey())) {
            version = reader.readIntValue();
        }
        if (version > this.codecVersion) {
            throw new IllegalArgumentException("Version " + version + " is newer than expected version " + this.codecVersion);
        }
        if (this.minCodecVersion != Integer.MAX_VALUE && version < this.minCodecVersion) {
            throw new IllegalArgumentException("Version " + version + " is older than min supported version " + this.minCodecVersion);
        }
        reader.reset();
        extraInfo.ignoreUnusedKey(BuilderCodec.VERSION.getKey());
        return new VersionedExtraInfo(version, extraInfo);
    }
    
    @Override
    public void validate(final T t, @Nonnull final ExtraInfo extraInfo) {
        if (this.parentCodec != null) {
            this.parentCodec.validate(t, extraInfo);
        }
        for (final List<BuilderField<T, ?>> entry : this.entries.values()) {
            final BuilderField<T, ?> field = findField(entry, extraInfo);
            if (field != null) {
                extraInfo.pushKey(field.codec.getKey());
                try {
                    field.validate(t, extraInfo);
                }
                finally {
                    extraInfo.popKey();
                }
            }
        }
    }
    
    @Override
    public void validateDefaults(@Nonnull final ExtraInfo extraInfo, @Nonnull final Set<Codec<?>> tested) {
        if (!tested.add(this)) {
            return;
        }
        final T t = this.supplier.get();
        this.afterDecode(t, extraInfo);
        for (BuilderCodec<T> codec = this; codec != null; codec = (BuilderCodec<T>)codec.parentCodec) {
            for (final List<BuilderField<T, ?>> entry : codec.entries.values()) {
                final BuilderField<T, ?> field = findField(entry, extraInfo);
                if (field != null) {
                    extraInfo.pushKey(field.codec.getKey());
                    try {
                        field.validateDefaults(t, extraInfo, tested);
                    }
                    finally {
                        extraInfo.popKey();
                    }
                }
            }
        }
    }
    
    @Nonnull
    @Override
    public ObjectSchema toSchema(@Nonnull final SchemaContext context) {
        final T t = this.getDefaultValue();
        return this.toSchema(context, t);
    }
    
    @Nonnull
    @Override
    public ObjectSchema toSchema(@Nonnull final SchemaContext context, @Nullable final T def) {
        final ObjectSchema schema = new ObjectSchema();
        schema.setAdditionalProperties(false);
        schema.setTitle(this.tClass.getSimpleName());
        schema.setMarkdownDescription(this.documentation);
        schema.getHytale().setMergesProperties(true);
        final Map<String, Schema> properties = new Object2ObjectLinkedOpenHashMap<String, Schema>();
        if (this.versioned) {
            properties.put(BuilderCodec.VERSION.getKey(), BuilderCodec.VERSION.getChildCodec().toSchema(context));
        }
        final Schema comment = new Schema();
        comment.getHytale().setUiPropertyTitle("Comment");
        comment.setDoNotSuggest(true);
        comment.setDescription("Comments don't have any function other than allowing users to add certain internal comments or notes to an asset");
        UIDisplayMode.HIDDEN.modify(comment);
        properties.put("$Title", comment);
        properties.put("$Comment", comment);
        properties.put("$Author", comment);
        properties.put("$TODO", comment);
        properties.put("$Position", comment);
        properties.put("$FloatingFunctionNodes", comment);
        properties.put("$Groups", comment);
        properties.put("$WorkspaceID", comment);
        properties.put("$NodeId", comment);
        properties.put("$NodeEditorMetadata", comment);
        schema.setProperties(properties);
        createSchemaFields(context, def, this, properties);
        if (this.metadata != null) {
            for (int i = 0; i < this.metadata.size(); ++i) {
                final Metadata meta = this.metadata.get(i);
                meta.modify(schema);
            }
        }
        return schema;
    }
    
    private static <T> void createSchemaFields(@Nonnull final SchemaContext context, @Nullable final T def, @Nonnull final BuilderCodec<T> codec, @Nonnull final Map<String, Schema> properties) {
        if (codec.parentCodec != null) {
            createSchemaFields(context, def, codec.parentCodec, properties);
        }
        for (final Map.Entry<String, List<BuilderField<T, ?>>> entry : codec.getEntries().entrySet()) {
            final String key = entry.getKey();
            final List<BuilderField<T, ?>> fields = entry.getValue();
            final BuilderField<T, ?> field = fields.getLast();
            final Codec c = field.getCodec().getChildCodec();
            Object defC;
            try {
                defC = field.getter.apply(def, EmptyExtraInfo.EMPTY);
            }
            catch (final UnsupportedOperationException e) {
                continue;
            }
            final Schema fieldSchema = context.refDefinition(c, defC);
            field.updateSchema(context, fieldSchema);
            Schema finalSchema = fieldSchema;
            final String type = Schema.CODEC.getIdFor((Class<?>)fieldSchema.getClass());
            if (!type.isEmpty()) {
                if (!field.hasNonNullValidator() && !field.isPrimitive) {
                    fieldSchema.setTypes(new String[] { type, "null" });
                }
                properties.put(key, fieldSchema);
            }
            else if (field.hasNonNullValidator()) {
                properties.put(key, fieldSchema);
            }
            else {
                properties.put(key, finalSchema = Schema.anyOf(fieldSchema, NullSchema.INSTANCE));
            }
            finalSchema.setMarkdownDescription(field.getDocumentation());
        }
    }
    
    @Nonnull
    @Override
    public String toString() {
        return "BuilderCodec{supplier=" + String.valueOf(this.supplier) + ", parentCodec=" + String.valueOf(this.parentCodec) + ", entries=" + String.valueOf(this.entries) + ", afterDecodeAndValidate=" + String.valueOf(this.afterDecode);
    }
    
    @Nullable
    protected static <T> BuilderField<? super T, ?> findEntry(@Nonnull BuilderCodec<? super T> current, final String key, @Nonnull final ExtraInfo extraInfo) {
        final List<? extends BuilderField<? super T, ?>> fields = current.entries.get(key);
        if (fields != null && fields.size() == 1) {
            final BuilderField<? super T, ?> field = (BuilderField<? super T, ?>)fields.getFirst();
            if (field.supportsVersion(extraInfo.getVersion())) {
                return field;
            }
        }
        BuilderField<? super T, ?> entry = null;
        while (current != null) {
            entry = findField(current.entries.get(key), extraInfo);
            if (entry != null) {
                return entry;
            }
            current = current.parentCodec;
        }
        return entry;
    }
    
    @Nullable
    protected static <T, F extends BuilderField<T, ?>> F findField(@Nullable final List<F> entry, @Nonnull final ExtraInfo extraInfo) {
        if (entry == null) {
            return null;
        }
        for (int i = 0, size = entry.size(); i < size; ++i) {
            final F field = entry.get(i);
            if (field.supportsVersion(extraInfo.getVersion())) {
                return field;
            }
        }
        return null;
    }
    
    @Nonnull
    public static <T> Builder<T> builder(final Class<T> tClass, final Supplier<T> supplier) {
        return new Builder<T>(tClass, supplier);
    }
    
    @Nonnull
    public static <T> Builder<T> builder(final Class<T> tClass, final Supplier<T> supplier, final BuilderCodec<? super T> parentCodec) {
        return new Builder<T>(tClass, supplier, parentCodec);
    }
    
    @Nonnull
    public static <T> Builder<T> abstractBuilder(final Class<T> tClass) {
        return new Builder<T>(tClass, null);
    }
    
    @Nonnull
    public static <T> Builder<T> abstractBuilder(final Class<T> tClass, final BuilderCodec<? super T> parentCodec) {
        return new Builder<T>(tClass, null, parentCodec);
    }
    
    static {
        EMPTY_ARRAY = new BuilderCodec[0];
        VERSION = new KeyedCodec<Integer>("Version", BuilderCodec.INTEGER);
    }
    
    public static class Builder<T> extends BuilderBase<T, Builder<T>>
    {
        protected Builder(final Class<T> tClass, final Supplier<T> supplier) {
            super(tClass, supplier);
        }
        
        protected Builder(final Class<T> tClass, final Supplier<T> supplier, @Nullable final BuilderCodec<? super T> parentCodec) {
            super(tClass, supplier, parentCodec);
        }
    }
    
    public abstract static class BuilderBase<T, S extends BuilderBase<T, S>>
    {
        protected final Class<T> tClass;
        protected final Supplier<T> supplier;
        @Nullable
        protected final BuilderCodec<? super T> parentCodec;
        protected final Map<String, List<BuilderField<T, ?>>> entries;
        @Nonnull
        protected final StringTreeMap<KeyEntry<T>> stringTreeMap;
        protected BiConsumer<T, ValidationResults> validator;
        protected BiConsumer<T, ExtraInfo> afterDecode;
        protected String documentation;
        protected List<Metadata> metadata;
        protected int codecVersion;
        protected int minCodecVersion;
        protected boolean versioned;
        protected boolean useLegacyVersion;
        
        protected BuilderBase(final Class<T> tClass, final Supplier<T> supplier) {
            this(tClass, supplier, null);
        }
        
        protected BuilderBase(final Class<T> tClass, final Supplier<T> supplier, @Nullable final BuilderCodec<? super T> parentCodec) {
            this.entries = new Object2ObjectLinkedOpenHashMap<String, List<BuilderField<T, ?>>>();
            this.codecVersion = Integer.MIN_VALUE;
            this.minCodecVersion = Integer.MAX_VALUE;
            this.versioned = false;
            this.useLegacyVersion = false;
            this.tClass = tClass;
            this.supplier = supplier;
            this.parentCodec = parentCodec;
            if (parentCodec != null) {
                this.stringTreeMap = new StringTreeMap<KeyEntry<T>>((StringTreeMap<KeyEntry<T>>)parentCodec.stringTreeMap);
            }
            else {
                this.stringTreeMap = new StringTreeMap<KeyEntry<T>>(Map.ofEntries(Map.entry("$Title", new KeyEntry(EntryType.IGNORE)), Map.entry("$Comment", new KeyEntry(EntryType.IGNORE)), Map.entry("$TODO", new KeyEntry(EntryType.IGNORE)), Map.entry("$Author", new KeyEntry(EntryType.IGNORE)), Map.entry("$Position", new KeyEntry(EntryType.IGNORE)), Map.entry("$FloatingFunctionNodes", new KeyEntry(EntryType.IGNORE)), Map.entry("$Groups", new KeyEntry(EntryType.IGNORE)), Map.entry("$WorkspaceID", new KeyEntry(EntryType.IGNORE)), Map.entry("$NodeEditorMetadata", new KeyEntry(EntryType.IGNORE)), Map.entry("$NodeId", new KeyEntry(EntryType.IGNORE)), Map.entry("Parent", new KeyEntry(EntryType.IGNORE_IN_BASE_OBJECT))));
            }
        }
        
        private S self() {
            return (S)this;
        }
        
        @Nonnull
        public S documentation(final String doc) {
            this.documentation = doc;
            return this.self();
        }
        
        @Nonnull
        public S versioned() {
            this.versioned = true;
            return this.self();
        }
        
        @Nonnull
        @Deprecated
        public S legacyVersioned() {
            this.versioned = true;
            this.useLegacyVersion = true;
            return this.self();
        }
        
        @Nonnull
        @Deprecated
        public <FieldType> S addField(@Nonnull final KeyedCodec<FieldType> codec, @Nonnull final BiConsumer<T, FieldType> setter, @Nonnull final Function<T, FieldType> getter) {
            return this.addField(new BuilderField<T, Object>((KeyedCodec<Object>)codec, (t, fieldType, extraInfo) -> setter.accept(t, fieldType), (t1, extraInfo1) -> getter.apply(t1), (TriConsumer<T, T, ExtraInfo>)null));
        }
        
        @Nonnull
        public <FieldType> BuilderField.FieldBuilder<T, FieldType, S> append(final KeyedCodec<FieldType> codec, @Nonnull final BiConsumer<T, FieldType> setter, @Nonnull final Function<T, FieldType> getter) {
            return (BuilderField.FieldBuilder<T, FieldType, S>)this.append(codec, (t, fieldType, extraInfo) -> setter.accept(t, fieldType), (t, extraInfo) -> getter.apply(t));
        }
        
        @Nonnull
        public <FieldType> BuilderField.FieldBuilder<T, FieldType, S> append(final KeyedCodec<FieldType> codec, final TriConsumer<T, FieldType, ExtraInfo> setter, final BiFunction<T, ExtraInfo, FieldType> getter) {
            return new BuilderField.FieldBuilder<T, FieldType, S>(this.self(), codec, setter, getter, null);
        }
        
        @Nonnull
        public <FieldType> BuilderField.FieldBuilder<T, FieldType, S> appendInherited(final KeyedCodec<FieldType> codec, @Nonnull final BiConsumer<T, FieldType> setter, @Nonnull final Function<T, FieldType> getter, @Nonnull final BiConsumer<T, T> inherit) {
            return (BuilderField.FieldBuilder<T, FieldType, S>)this.appendInherited(codec, (t, fieldType, extraInfo) -> setter.accept(t, fieldType), (t, extraInfo) -> getter.apply(t), (t, parent, extraInfo) -> inherit.accept(t, parent));
        }
        
        @Nonnull
        public <FieldType> BuilderField.FieldBuilder<T, FieldType, S> appendInherited(final KeyedCodec<FieldType> codec, final TriConsumer<T, FieldType, ExtraInfo> setter, final BiFunction<T, ExtraInfo, FieldType> getter, final TriConsumer<T, T, ExtraInfo> inherit) {
            return new BuilderField.FieldBuilder<T, FieldType, S>(this.self(), codec, setter, getter, inherit);
        }
        
        @Nonnull
        public <FieldType> S addField(@Nonnull final BuilderField<T, FieldType> entry) {
            if (entry.getMinVersion() > entry.getMaxVersion()) {
                throw new IllegalArgumentException("Min version must be less than the max version: " + String.valueOf(entry));
            }
            final List<BuilderField<T, ?>> fields = this.entries.computeIfAbsent(entry.getCodec().getKey(), k -> new ObjectArrayList());
            for (final BuilderField<T, ?> field : fields) {
                if (entry.getMaxVersion() >= field.getMinVersion() && entry.getMinVersion() <= field.getMaxVersion()) {
                    throw new IllegalArgumentException("Field already defined for this version range!");
                }
            }
            fields.add(entry);
            this.stringTreeMap.put(entry.getCodec().getKey(), new KeyEntry<T>(fields));
            return this.self();
        }
        
        @Nonnull
        public S afterDecode(@Nonnull final Consumer<T> afterDecode) {
            Objects.requireNonNull(afterDecode, "afterDecodeAndValidate can't be null!");
            return this.afterDecode((t, extraInfo) -> afterDecode.accept(t));
        }
        
        @Nonnull
        public S afterDecode(final BiConsumer<T, ExtraInfo> afterDecode) {
            this.afterDecode = Objects.requireNonNull(afterDecode, "afterDecodeAndValidate can't be null!");
            return this.self();
        }
        
        @Nonnull
        @Deprecated
        public S validator(final BiConsumer<T, ValidationResults> validator) {
            this.validator = Objects.requireNonNull(validator, "validator can't be null!");
            return this.self();
        }
        
        @Nonnull
        public S metadata(final Metadata metadata) {
            if (this.metadata == null) {
                this.metadata = new ObjectArrayList<Metadata>();
            }
            this.metadata.add(metadata);
            return this.self();
        }
        
        @Nonnull
        public S codecVersion(final int minCodecVersion, final int codecVersion) {
            this.minCodecVersion = minCodecVersion;
            this.codecVersion = codecVersion;
            return this.self();
        }
        
        @Nonnull
        public S codecVersion(final int codecVersion) {
            this.minCodecVersion = 0;
            this.codecVersion = codecVersion;
            return this.self();
        }
        
        @Nonnull
        public BuilderCodec<T> build() {
            return new BuilderCodec<T>(this);
        }
    }
    
    protected static class KeyEntry<T>
    {
        private final EntryType type;
        @Nullable
        private final List<BuilderField<T, ?>> fields;
        
        public KeyEntry(final EntryType type) {
            this.type = type;
            this.fields = null;
        }
        
        public KeyEntry(final List<BuilderField<T, ?>> fields) {
            this.type = EntryType.FIELD;
            this.fields = fields;
        }
        
        public EntryType getType() {
            return this.type;
        }
        
        @Nullable
        public List<BuilderField<T, ?>> getFields() {
            return this.fields;
        }
    }
    
    protected enum EntryType
    {
        FIELD, 
        IGNORE, 
        IGNORE_IN_BASE_OBJECT;
    }
}
