/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.plc4x.language.java;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.text.WordUtils;
import org.apache.plc4x.plugins.codegenerator.language.mspec.model.terms.DefaultStringLiteral;
import org.apache.plc4x.plugins.codegenerator.protocol.freemarker.BaseFreemarkerLanguageTemplateHelper;
import org.apache.plc4x.plugins.codegenerator.protocol.freemarker.FreemarkerException;
import org.apache.plc4x.plugins.codegenerator.protocol.freemarker.Tracer;
import org.apache.plc4x.plugins.codegenerator.types.definitions.*;
import org.apache.plc4x.plugins.codegenerator.types.fields.*;
import org.apache.plc4x.plugins.codegenerator.types.references.*;
import org.apache.plc4x.plugins.codegenerator.types.terms.*;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.function.Function;

@SuppressWarnings({"unused", "WeakerAccess"})
public class JavaLanguageTemplateHelper extends BaseFreemarkerLanguageTemplateHelper {

    private final Map<String, Object> options;

    public JavaLanguageTemplateHelper(TypeDefinition thisType, String protocolName, String flavorName, Map<String, TypeDefinition> types,
                                      Map<String, Object> options) {
        super(thisType, protocolName, flavorName, types);
        this.options = options;
    }

    public String packageName() {
        return packageName(protocolName, "java", flavorName);
    }

    public String packageName(String protocolName, String languageName, String languageFlavorName) {
        return Optional.ofNullable((String) options.get("package")).orElseGet(() ->
            "org.apache.plc4x." + String.join("", languageName.split("-")) + "." +
                String.join("", protocolName.split("-")) + "." +
                String.join("", languageFlavorName.split("-")));
    }

    @Override
    public String getLanguageTypeNameForField(Field field) {
        // If the referenced type is a DataIo type, the value is of type PlcValue.
        if (field.isPropertyField()) {
            PropertyField propertyField = field.asPropertyField().orElseThrow(IllegalStateException::new);
            if (propertyField.getType().isComplexTypeReference()) {
                ComplexTypeReference complexTypeReference = propertyField.getType().asComplexTypeReference().orElseThrow(IllegalStateException::new);
                final TypeDefinition typeDefinition = getTypeDefinitions().get(complexTypeReference.getName());
                if (typeDefinition instanceof DataIoTypeDefinition) {
                    return "PlcValue";
                }
            }
        }
        return getLanguageTypeNameForTypeReference(((TypedField) field).getType(), !field.isOptionalField());
    }

    public String getNonPrimitiveLanguageTypeNameForField(TypedField field) {
        return getLanguageTypeNameForTypeReference(field.getType(), false);
    }

    public String getLanguageTypeNameForSpecType(TypeReference typeReference) {
        return getLanguageTypeNameForTypeReference(typeReference, true);
    }

    @Override
    public String getLanguageTypeNameForTypeReference(TypeReference typeReference) {
        return getLanguageTypeNameForTypeReference(typeReference, false);
    }

    public String getLanguageTypeNameForTypeReference(TypeReference typeReference, boolean allowPrimitive) {
        Objects.requireNonNull(typeReference);
        if (typeReference instanceof ArrayTypeReference) {
            final ArrayTypeReference arrayTypeReference = (ArrayTypeReference) typeReference;
            if (arrayTypeReference.getElementTypeReference().isByteBased()) {
                return getLanguageTypeNameForTypeReference(arrayTypeReference.getElementTypeReference(), allowPrimitive) + "[]";
            } else {
                return "List<" + getLanguageTypeNameForTypeReference(arrayTypeReference.getElementTypeReference(), false) + ">";
            }
        }
        // DataIo data-types always have properties of type PlcValue
        if (typeReference.isDataIoTypeReference()) {
            return "PlcValue";
        }
        if (typeReference.isNonSimpleTypeReference()) {
            return typeReference.asNonSimpleTypeReference().orElseThrow().getName();
        }
        SimpleTypeReference simpleTypeReference = (SimpleTypeReference) typeReference;
        switch (simpleTypeReference.getBaseType()) {
            case BIT:
                return allowPrimitive ? boolean.class.getSimpleName() : Boolean.class.getSimpleName();
            case BYTE:
                return allowPrimitive ? byte.class.getSimpleName() : Byte.class.getSimpleName();
            case UINT:
                IntegerTypeReference unsignedIntegerTypeReference = (IntegerTypeReference) simpleTypeReference;
                if (unsignedIntegerTypeReference.getSizeInBits() <= 7) {
                    return allowPrimitive ? byte.class.getSimpleName() : Byte.class.getSimpleName();
                }
                if (unsignedIntegerTypeReference.getSizeInBits() <= 15) {
                    return allowPrimitive ? short.class.getSimpleName() : Short.class.getSimpleName();
                }
                if (unsignedIntegerTypeReference.getSizeInBits() <= 31) {
                    return allowPrimitive ? int.class.getSimpleName() : Integer.class.getSimpleName();
                }
                if (unsignedIntegerTypeReference.getSizeInBits() <= 63) {
                    return allowPrimitive ? long.class.getSimpleName() : Long.class.getSimpleName();
                }
                return BigInteger.class.getSimpleName();
            case INT:
                IntegerTypeReference integerTypeReference = (IntegerTypeReference) simpleTypeReference;
                if (integerTypeReference.getSizeInBits() <= 8) {
                    return allowPrimitive ? byte.class.getSimpleName() : Byte.class.getSimpleName();
                }
                if (integerTypeReference.getSizeInBits() <= 16) {
                    return allowPrimitive ? short.class.getSimpleName() : Short.class.getSimpleName();
                }
                if (integerTypeReference.getSizeInBits() <= 32) {
                    return allowPrimitive ? int.class.getSimpleName() : Integer.class.getSimpleName();
                }
                if (integerTypeReference.getSizeInBits() <= 64) {
                    return allowPrimitive ? long.class.getSimpleName() : Long.class.getSimpleName();
                }
                return BigInteger.class.getSimpleName();
            case FLOAT:
            case UFLOAT:
                FloatTypeReference floatTypeReference = (FloatTypeReference) simpleTypeReference;
                int sizeInBits = floatTypeReference.getSizeInBits();
                if (sizeInBits <= 32) {
                    return allowPrimitive ? float.class.getSimpleName() : Float.class.getSimpleName();
                }
                if (sizeInBits <= 64) {
                    return allowPrimitive ? double.class.getSimpleName() : Double.class.getSimpleName();
                }
                return BigDecimal.class.getSimpleName();
            case STRING:
            case VSTRING:
                return String.class.getSimpleName();
            case TIME:
                return LocalTime.class.getSimpleName();
            case DATE:
                return LocalDate.class.getSimpleName();
            case DATETIME:
                return LocalDateTime.class.getSimpleName();

        }
        throw new FreemarkerException("Unsupported simple type");
    }

    public String getPlcValueTypeForTypeReference(TypeReference typeReference) {
        if (typeReference.isArrayTypeReference() && typeReference.asArrayTypeReference().orElseThrow().getElementTypeReference().isByteBased()) {
            return "PlcRawByteArray";
        }
        if (!(typeReference instanceof SimpleTypeReference)) {
            return "PlcStruct";
        }
        SimpleTypeReference simpleTypeReference = (SimpleTypeReference) typeReference;
        int sizeInBits = simpleTypeReference.getSizeInBits();
        switch (simpleTypeReference.getBaseType()) {
            case BIT:
                return "PlcBOOL";
            case BYTE:
                return "PlcSINT";
            case UINT:
                if (sizeInBits <= 8) {
                    return "PlcUSINT";
                }
                if (sizeInBits <= 16) {
                    return "PlcUINT";
                }
                if (sizeInBits <= 32) {
                    return "PlcUDINT";
                }
                if (sizeInBits <= 64) {
                    return "PlcULINT";
                }
                throw new FreemarkerException("Unsupported UINT with bit length " + sizeInBits);
            case INT:
                if (sizeInBits <= 8) {
                    return "PlcSINT";
                }
                if (sizeInBits <= 16) {
                    return "PlcINT";
                }
                if (sizeInBits <= 32) {
                    return "PlcDINT";
                }
                if (sizeInBits <= 64) {
                    return "PlcLINT";
                }
                throw new FreemarkerException("Unsupported INT with bit length " + sizeInBits);
            case FLOAT:
            case UFLOAT:
                if (sizeInBits <= 32) {
                    return "PlcREAL";
                }
                if (sizeInBits <= 64) {
                    return "PlcLREAL";
                }
                throw new FreemarkerException("Unsupported REAL with bit length " + sizeInBits);
            case STRING:
            case VSTRING:
                return "PlcSTRING";
            case TIME:
            case DATE:
            case DATETIME:
                return "PlcTIME";
        }
        throw new FreemarkerException("Unsupported simple type");
    }

    @Override
    public String getNullValueForTypeReference(TypeReference typeReference) {
        if (typeReference instanceof SimpleTypeReference) {
            SimpleTypeReference simpleTypeReference = (SimpleTypeReference) typeReference;
            switch (simpleTypeReference.getBaseType()) {
                case BIT:
                    return "false";
                case BYTE:
                    return "0";
                case UINT:
                    IntegerTypeReference unsignedIntegerTypeReference = (IntegerTypeReference) simpleTypeReference;
                    if (unsignedIntegerTypeReference.getSizeInBits() <= 31) {
                        return "0";
                    }
                    if (unsignedIntegerTypeReference.getSizeInBits() <= 63) {
                        return "0l";
                    }
                    return "null";
                case INT:
                    IntegerTypeReference integerTypeReference = (IntegerTypeReference) simpleTypeReference;
                    if (integerTypeReference.getSizeInBits() <= 32) {
                        return "0";
                    }
                    if (integerTypeReference.getSizeInBits() <= 64) {
                        return "0l";
                    }
                    return "null";
                case FLOAT:
                    FloatTypeReference floatTypeReference = (FloatTypeReference) simpleTypeReference;
                    int sizeInBits = floatTypeReference.getSizeInBits();
                    if (sizeInBits <= 32) {
                        return "0.0f";
                    }
                    if (sizeInBits <= 64) {
                        return "0.0";
                    }
                    return "null";
                case STRING:
                case VSTRING:
                    return "null";
            }
            throw new FreemarkerException("Unmapped base-type" + simpleTypeReference.getBaseType());
        } else {
            return "null";
        }
    }

    public int getNumBits(SimpleTypeReference simpleTypeReference) {
        switch (simpleTypeReference.getBaseType()) {
            case BIT:
                return 1;
            case BYTE:
                return 8;
            case UINT:
            case INT:
                IntegerTypeReference integerTypeReference = (IntegerTypeReference) simpleTypeReference;
                return integerTypeReference.getSizeInBits();
            case FLOAT:
                FloatTypeReference floatTypeReference = (FloatTypeReference) simpleTypeReference;
                return floatTypeReference.getSizeInBits();
            case STRING:
                StringTypeReference stringTypeReference = (StringTypeReference) simpleTypeReference;
                return stringTypeReference.getSizeInBits();
            case VSTRING:
                throw new IllegalArgumentException("getSizeInBits doesn't work for 'vstring' fields");
            default:
                return 0;
        }
    }

    @Deprecated
    @Override
    public String getReadBufferReadMethodCall(SimpleTypeReference simpleTypeReference, String valueString, TypedField field) {
        return "/*TODO: migrate me*/" + getReadBufferReadMethodCall("", simpleTypeReference, valueString, field);
    }

    @Deprecated
    public String getReadBufferReadMethodCall(String logicalName, SimpleTypeReference simpleTypeReference, String valueString, TypedField field) {
        switch (simpleTypeReference.getBaseType()) {
            case BIT:
                String bitType = "Bit";
                return "/*TODO: migrate me*/" + "readBuffer.read" + bitType + "(\"" + logicalName + "\")";
            case BYTE:
                String byteType = "Byte";
                return "/*TODO: migrate me*/" + "readBuffer.read" + byteType + "(\"" + logicalName + "\")";
            case UINT:
                String unsignedIntegerType;
                IntegerTypeReference unsignedIntegerTypeReference = (IntegerTypeReference) simpleTypeReference;
                if (unsignedIntegerTypeReference.getSizeInBits() <= 7) {
                    unsignedIntegerType = "UnsignedByte";
                } else if (unsignedIntegerTypeReference.getSizeInBits() <= 15) {
                    unsignedIntegerType = "UnsignedShort";
                } else if (unsignedIntegerTypeReference.getSizeInBits() <= 31) {
                    unsignedIntegerType = "UnsignedInt";
                } else if (unsignedIntegerTypeReference.getSizeInBits() <= 63) {
                    unsignedIntegerType = "UnsignedLong";
                } else {
                    unsignedIntegerType = "UnsignedBigInteger";
                }
                return "/*TODO: migrate me*/" + "readBuffer.read" + unsignedIntegerType + "(\"" + logicalName + "\", " + simpleTypeReference.getSizeInBits() + ")";
            case INT:
                String integerType;
                if (simpleTypeReference.getSizeInBits() <= 8) {
                    integerType = "SignedByte";
                } else if (simpleTypeReference.getSizeInBits() <= 16) {
                    integerType = "Short";
                } else if (simpleTypeReference.getSizeInBits() <= 32) {
                    integerType = "Int";
                } else if (simpleTypeReference.getSizeInBits() <= 64) {
                    integerType = "Long";
                } else {
                    integerType = "BigInteger";
                }
                return "/*TODO: migrate me*/" + "readBuffer.read" + integerType + "(\"" + logicalName + "\", " + simpleTypeReference.getSizeInBits() + ")";
            case FLOAT:
                String floatType = (simpleTypeReference.getSizeInBits() <= 32) ? "Float" : "Double";
                return "/*TODO: migrate me*/" + "readBuffer.read" + floatType + "(\"" + logicalName + "\", " + simpleTypeReference.getSizeInBits() + ")";
            case STRING:
            case VSTRING:
                String stringType = "String";
                final Term encodingTerm = field.getEncoding().orElse(new DefaultStringLiteral("UTF-8"));
                if (!(encodingTerm instanceof StringLiteral)) {
                    throw new IllegalArgumentException("Encoding must be a quoted string value");
                }
                String encoding = ((StringLiteral) encodingTerm).getValue();
                String length = Integer.toString(simpleTypeReference.getSizeInBits());
                if (simpleTypeReference.getBaseType() == SimpleTypeReference.SimpleBaseType.VSTRING) {
                    VstringTypeReference vstringTypeReference = (VstringTypeReference) simpleTypeReference;
                    length = toParseExpression(field, INT_TYPE_REFERENCE, vstringTypeReference.getLengthExpression(), null);
                }
                return "/*TODO: migrate me*/" + "readBuffer.read" + stringType + "(\"" + logicalName + "\", " + length + ", WithOption.WithEncoding(\"" +
                    encoding + "\"))";
        }
        return "/*TODO: migrate me*/" + "";
    }

    public String getDataReaderCall(TypeReference typeReference) {
        return getDataReaderCall(typeReference, "enumForValue");
    }

    public String getDataReaderCall(TypeReference typeReference, String resolverMethod) {
        if (typeReference.isEnumTypeReference()) {
            final String languageTypeName = getLanguageTypeNameForTypeReference(typeReference);
            final SimpleTypeReference enumBaseTypeReference = getEnumBaseTypeReference(typeReference);
            return "readEnum(" + languageTypeName + "::" + resolverMethod + ", " + getDataReaderCall(enumBaseTypeReference) + ")";
        } else if (typeReference.isArrayTypeReference()) {
            final ArrayTypeReference arrayTypeReference = typeReference.asArrayTypeReference().orElseThrow();
            return getDataReaderCall(arrayTypeReference.getElementTypeReference(), resolverMethod);
        } else if (typeReference.isSimpleTypeReference()) {
            SimpleTypeReference simpleTypeReference = typeReference.asSimpleTypeReference().orElseThrow(IllegalStateException::new);
            return getDataReaderCall(simpleTypeReference);
        } else if (typeReference.isComplexTypeReference()) {
            StringBuilder paramsString = new StringBuilder();
            ComplexTypeReference complexTypeReference = typeReference.asComplexTypeReference().orElseThrow(IllegalStateException::new);
            ComplexTypeDefinition typeDefinition = complexTypeReference.getTypeDefinition();
            String parserCallString = getLanguageTypeNameForTypeReference(typeReference);
            // In case of DataIo we actually need to use the type name and not what above returns.
            // (In this case the mspec type name and the result type name differ)
            if (typeReference.isDataIoTypeReference()) {
                parserCallString = typeReference.asDataIoTypeReference().orElseThrow().getName();
            }
            if (typeDefinition.isDiscriminatedChildTypeDefinition()) {
                parserCallString = "(" + getLanguageTypeNameForTypeReference(typeReference) + ") " + typeDefinition.getParentType().orElseThrow().getName();
            }
            List<Term> paramTerms = complexTypeReference.getParams().orElse(Collections.emptyList());
            for (int i = 0; i < paramTerms.size(); i++) {
                Term paramTerm = paramTerms.get(i);
                final TypeReference argumentType = getArgumentType(complexTypeReference, i);
                paramsString
                    .append(", (")
                    .append(getLanguageTypeNameForTypeReference(argumentType, true))
                    .append(") (")
                    .append(toParseExpression(null, argumentType, paramTerm, null))
                    .append(")");
            }
            return "readComplex(() -> " + parserCallString + ".staticParse(readBuffer" + paramsString + "), readBuffer)";
        } else {
            throw new IllegalStateException("What is this type? " + typeReference);
        }
    }

    public String getDataReaderCall(SimpleTypeReference simpleTypeReference) {
        final int sizeInBits = simpleTypeReference.getSizeInBits();
        switch (simpleTypeReference.getBaseType()) {
            case BIT:
                return "readBoolean(readBuffer)";
            case BYTE:
                return "readByte(readBuffer, " + sizeInBits + ")";
            case UINT:
                if (sizeInBits <= 7) return "readUnsignedByte(readBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 15) return "readUnsignedShort(readBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 31) return "readUnsignedInt(readBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 63) return "readUnsignedLong(readBuffer, " + sizeInBits + ")";
                return "readUnsignedBigInteger(readBuffer, " + sizeInBits + ")";
            case INT:
                if (sizeInBits <= 8) return "readSignedByte(readBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 16) return "readSignedShort(readBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 32) return "readSignedInt(readBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 64) return "readSignedLong(readBuffer, " + sizeInBits + ")";
                return "readSignedBigInteger(readBuffer, " + sizeInBits + ")";
            case FLOAT:
                if (sizeInBits <= 32) return "readFloat(readBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 64) return "readDouble(readBuffer, " + sizeInBits + ")";
                return "readBigDecimal(readBuffer, " + sizeInBits + ")";
            case STRING:
                return "readString(readBuffer, " + sizeInBits + ")";
            case VSTRING:
                VstringTypeReference vstringTypeReference = (VstringTypeReference) simpleTypeReference;
                return "readString(readBuffer, " + toParseExpression(null, INT_TYPE_REFERENCE, vstringTypeReference.getLengthExpression(), null) + ")";
            case TIME:
                return "readTime(readBuffer)";
            case DATE:
                return "readDate(readBuffer)";
            case DATETIME:
                return "readDateTime(readBuffer)";
            default:
                throw new UnsupportedOperationException("Unsupported type " + simpleTypeReference.getBaseType());
        }
    }

    public String getDataWriterCall(TypeReference typeReference, String fieldName) {
        if (typeReference.isSimpleTypeReference()) {
            SimpleTypeReference simpleTypeReference = typeReference.asSimpleTypeReference().orElseThrow(IllegalStateException::new);
            return getDataWriterCall(simpleTypeReference);
        } else if (typeReference.isArrayTypeReference()) {
            final ArrayTypeReference arrayTypeReference = typeReference.asArrayTypeReference().orElseThrow();
            return getDataWriterCall(arrayTypeReference.getElementTypeReference(), fieldName);
        } else if (typeReference.isComplexTypeReference()) {
            return "writeComplex(writeBuffer)";
        } else {
            throw new IllegalStateException("What is this type? " + typeReference);
        }
    }

    public String getEnumDataWriterCall(EnumTypeReference typeReference, String fieldName, String attributeName) {
        if (!typeReference.isEnumTypeReference()) {
            throw new IllegalArgumentException("this method should only be called for enum types");
        }
        final String languageTypeName = getLanguageTypeNameForTypeReference(typeReference);
        SimpleTypeReference outputTypeReference;
        if ("value".equals(attributeName)) {
            outputTypeReference = getEnumBaseTypeReference(typeReference);
        } else {
            outputTypeReference = getEnumFieldSimpleTypeReference(typeReference.asNonSimpleTypeReference().orElseThrow(), attributeName);
        }
        return "writeEnum(" + languageTypeName + "::get" + StringUtils.capitalize(attributeName) + ", " + languageTypeName + "::name, " + getDataWriterCall(outputTypeReference, fieldName) + ")";
    }

    public String getDataWriterCall(SimpleTypeReference simpleTypeReference) {
        final int sizeInBits = simpleTypeReference.getSizeInBits();
        switch (simpleTypeReference.getBaseType()) {
            case BIT:
                return "writeBoolean(writeBuffer)";
            case BYTE:
                return "writeByte(writeBuffer, " + sizeInBits + ")";
            case UINT:
                if (sizeInBits <= 7) return "writeUnsignedByte(writeBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 15) return "writeUnsignedShort(writeBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 31) return "writeUnsignedInt(writeBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 63) return "writeUnsignedLong(writeBuffer, " + sizeInBits + ")";
                return "writeUnsignedBigInteger(writeBuffer, " + sizeInBits + ")";
            case INT:
                if (sizeInBits <= 8) return "writeSignedByte(writeBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 16) return "writeSignedShort(writeBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 32) return "writeSignedInt(writeBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 64) return "writeSignedLong(writeBuffer, " + sizeInBits + ")";
                return "writeSignedBigInteger(writeBuffer, " + sizeInBits + ")";
            case FLOAT:
                if (sizeInBits <= 32) return "writeFloat(writeBuffer, " + sizeInBits + ")";
                if (sizeInBits <= 64) return "writeDouble(writeBuffer, " + sizeInBits + ")";
                return "writeBigDecimal(writeBuffer, " + sizeInBits + ")";
            case STRING:
                return "writeString(writeBuffer, " + sizeInBits + ")";
            case VSTRING:
                VstringTypeReference vstringTypeReference = (VstringTypeReference) simpleTypeReference;
                return "writeString(writeBuffer, " + toParseExpression(null, INT_TYPE_REFERENCE, vstringTypeReference.getLengthExpression(), null) + ")";
            case TIME:
                return "writeTime(writeBuffer)";
            case DATE:
                return "writeDate(writeBuffer)";
            case DATETIME:
                return "writeDateTime(writeBuffer)";
            default:
                throw new UnsupportedOperationException("Unsupported type " + simpleTypeReference.getBaseType());
        }
    }

    @Deprecated
    @Override
    public String getWriteBufferWriteMethodCall(SimpleTypeReference simpleTypeReference, String fieldName, TypedField field) {
        return "/*TODO: migrate me*/" + getWriteBufferWriteMethodCall("", simpleTypeReference, fieldName, field);
    }

    @Deprecated
    public String getWriteBufferWriteMethodCall(String logicalName, SimpleTypeReference simpleTypeReference, String fieldName, TypedField field, String... writerArgs) {
        String writerArgsString = "";
        if (writerArgs.length > 0) {
            writerArgsString += ", " + StringUtils.join(writerArgs, ", ");
        }
        switch (simpleTypeReference.getBaseType()) {
            case BIT:
                return "/*TODO: migrate me*/" + "writeBuffer.writeBit(\"" + logicalName + "\", (boolean) " + fieldName + "" + writerArgsString + ")";
            case BYTE:
                ByteTypeReference byteTypeReference = (ByteTypeReference) simpleTypeReference;
                return "/*TODO: migrate me*/" + "writeBuffer.writeByte(\"" + logicalName + "\", ((Number) " + fieldName + ").byteValue()" + writerArgsString + ")";
            case UINT:
                IntegerTypeReference unsignedIntegerTypeReference = (IntegerTypeReference) simpleTypeReference;
                if (unsignedIntegerTypeReference.getSizeInBits() <= 7) {
                    return "/*TODO: migrate me*/" + "writeBuffer.writeUnsignedByte(\"" + logicalName + "\", " + unsignedIntegerTypeReference.getSizeInBits() + ", ((Number) " + fieldName + ").byteValue()" + writerArgsString + ")";
                }
                if (unsignedIntegerTypeReference.getSizeInBits() <= 15) {
                    return "/*TODO: migrate me*/" + "writeBuffer.writeUnsignedShort(\"" + logicalName + "\", " + unsignedIntegerTypeReference.getSizeInBits() + ", ((Number) " + fieldName + ").shortValue()" + writerArgsString + ")";
                }
                if (unsignedIntegerTypeReference.getSizeInBits() <= 31) {
                    return "/*TODO: migrate me*/" + "writeBuffer.writeUnsignedInt(\"" + logicalName + "\", " + unsignedIntegerTypeReference.getSizeInBits() + ", ((Number) " + fieldName + ").intValue()" + writerArgsString + ")";
                }
                if (unsignedIntegerTypeReference.getSizeInBits() <= 63) {
                    return "/*TODO: migrate me*/" + "writeBuffer.writeUnsignedLong(\"" + logicalName + "\", " + unsignedIntegerTypeReference.getSizeInBits() + ", ((Number) " + fieldName + ").longValue()" + writerArgsString + ")";
                }
                return "/*TODO: migrate me*/" + "writeBuffer.writeUnsignedBigInteger(\"" + logicalName + "\", " + unsignedIntegerTypeReference.getSizeInBits() + ", (BigInteger) " + fieldName + "" + writerArgsString + ")";
            case INT:
                IntegerTypeReference integerTypeReference = (IntegerTypeReference) simpleTypeReference;
                if (integerTypeReference.getSizeInBits() <= 8) {
                    return "/*TODO: migrate me*/" + "writeBuffer.writeSignedByte(\"" + logicalName + "\", " + integerTypeReference.getSizeInBits() + ", ((Number) " + fieldName + ").byteValue()" + writerArgsString + ")";
                }
                if (integerTypeReference.getSizeInBits() <= 16) {
                    return "/*TODO: migrate me*/" + "writeBuffer.writeShort(\"" + logicalName + "\", " + integerTypeReference.getSizeInBits() + ", ((Number) " + fieldName + ").shortValue()" + writerArgsString + ")";
                }
                if (integerTypeReference.getSizeInBits() <= 32) {
                    return "/*TODO: migrate me*/" + "writeBuffer.writeInt(\"" + logicalName + "\", " + integerTypeReference.getSizeInBits() + ", ((Number) " + fieldName + ").intValue()" + writerArgsString + ")";
                }
                if (integerTypeReference.getSizeInBits() <= 64) {
                    return "/*TODO: migrate me*/" + "writeBuffer.writeLong(\"" + logicalName + "\", " + integerTypeReference.getSizeInBits() + ", ((Number) " + fieldName + ").longValue()" + writerArgsString + ")";
                }
                return "/*TODO: migrate me*/" + "writeBuffer.writeBigInteger(\"" + logicalName + "\", " + integerTypeReference.getSizeInBits() + ", BigInteger.valueOf( " + fieldName + ")" + writerArgsString + ")";
            case FLOAT:
            case UFLOAT:
                FloatTypeReference floatTypeReference = (FloatTypeReference) simpleTypeReference;
                if (floatTypeReference.getSizeInBits() <= 32) {
                    return "/*TODO: migrate me*/" + "writeBuffer.writeFloat(\"" + logicalName + "\", " + floatTypeReference.getSizeInBits() + "," + fieldName + "" + writerArgsString + ")";
                } else if (floatTypeReference.getSizeInBits() <= 64) {
                    return "/*TODO: migrate me*/" + "writeBuffer.writeDouble(\"" + logicalName + "\", " + floatTypeReference.getSizeInBits() + "," + fieldName + "" + writerArgsString + ")";
                } else {
                    throw new RuntimeException("Unsupported float type");
                }
            case STRING:
            case VSTRING:
                final Term encodingTerm = field.getEncoding().orElse(new DefaultStringLiteral("UTF-8"));
                if (!(encodingTerm instanceof StringLiteral)) {
                    throw new RuntimeException("Encoding must be a quoted string value");
                }
                String encoding = ((StringLiteral) encodingTerm).getValue();
                String length = Integer.toString(simpleTypeReference.getSizeInBits());
                if (simpleTypeReference.getBaseType() == SimpleTypeReference.SimpleBaseType.VSTRING) {
                    VstringTypeReference vstringTypeReference = (VstringTypeReference) simpleTypeReference;
                    length = toSerializationExpression(field, INT_TYPE_REFERENCE, vstringTypeReference.getLengthExpression(), thisType.getParserArguments().orElse(Collections.emptyList()));
                }
                return "/*TODO: migrate me*/" + "writeBuffer.writeString(\"" + logicalName + "\", " + length + ", (String) " + fieldName + "" + writerArgsString + ", WithOption.WithEncoding(\"" + encoding + "\"))";
        }
        throw new FreemarkerException("Unmapped basetype" + simpleTypeReference.getBaseType());
    }

    public boolean isRawByteArray(DiscriminatedComplexTypeDefinition currentCase) {
        Optional<Field> valueFieldOptional = currentCase.getFields().stream().filter(field -> field.isNamedField() && field.asNamedField().orElseThrow().getName().equals("value")).findFirst();
        if (valueFieldOptional.isPresent()) {
            Field valueField = valueFieldOptional.get();
            if (valueField.isTypedField()) {
                TypedField typedField = valueField.asTypedField().orElseThrow();
                return typedField.getType().isArrayTypeReference() && typedField.getType().asArrayTypeReference().orElseThrow().getElementTypeReference().isByteBased();
            }
        }
        return false;
    }

    public String getDataIoPropertyValue(PropertyField propertyField) {
        TypeReference propertyFieldType = propertyField.getType();
        if (propertyFieldType.isSimpleTypeReference()) {
            SimpleTypeReference simpleTypeReference = propertyFieldType.asSimpleTypeReference().orElseThrow();
            switch (propertyField.getName()) {
                case "value":
                    return "_value.get" + getLanguageTypeNameForTypeReference(simpleTypeReference) + "()";
                case "year":
                    return "_value.getDate().getYear()";
                case "month":
                    return "_value.getDate().getMonthValue()";
                case "day":
                case "dayOfMonth":
                    return "_value.getDate().getDayOfMonth()";
                case "dayOfWeek":
                    return "_value.getDate().getDayOfWeek().getValue()";
                case "hour":
                    return "_value.getTime().getHour()";
                case "minutes":
                    return "_value.getTime().getMinute()";
                case "seconds":
                    return "_value.getTime().getSecond()";
                case "secondsSinceEpoch":
                    return "_value.getDateTime().toEpochSecond(ZoneOffset.UTC)";
                case "milliseconds":
                    return "_value.getDuration().toMillis()";
                case "millisecondsOfSecond":
                    return "_value.getTime().get(ChronoField.MILLI_OF_SECOND)";
                case "millisecondsSinceMidnight":
                    if (simpleTypeReference.getSizeInBits() <= 63) {
                        return "_value.getTime().getLong(ChronoField.MILLI_OF_DAY)";
                    } else {
                        return "BigInteger.valueOf(_value.getTime().getLong(ChronoField.MILLI_OF_DAY))";
                    }
                case "nanoseconds":
                    if (simpleTypeReference.getSizeInBits() <= 63) {
                        return "_value.getDuration().toNanos()";
                    } else {
                        return "BigInteger.valueOf(_value.getDuration().toNanos())";
                    }
                case "nanosecondsSinceMidnight":
                    if (simpleTypeReference.getSizeInBits() <= 63) {
                        return "_value.getTime().getLong(ChronoField.NANO_OF_DAY)";
                    } else {
                        return "BigInteger.valueOf(_value.getTime().getLong(ChronoField.NANO_OF_DAY))";
                    }
                case "nannosecondsOfSecond":
                    if (simpleTypeReference.getSizeInBits() <= 63) {
                        return "_value.getTime().getLong(ChronoField.NANO_OF_SECOND)";
                    } else {
                        return "BigInteger.valueOf(_value.getTime().getLong(ChronoField.NANO_OF_SECOND))";
                    }
                case "nanosecondsSinceEpoch":
                    return "BigInteger.valueOf(_value.getDateTime().toEpochSecond(ZoneOffset.UTC)).multiply(BigInteger.valueOf(1000000000)).add(BigInteger.valueOf(_value.getDateTime().getNano()))";
                default:
                    throw new UnsupportedOperationException(String.format("Unsupported field name %s.", propertyField.getName()));
            }
        }
        throw new UnsupportedOperationException("Non Simple types not yet supported.");
    }

    public String getReservedValue(ReservedField reservedField) {
        final String languageTypeName = getLanguageTypeNameForTypeReference(reservedField.getType(), true);
        if ("BigInteger".equals(languageTypeName)) {
            return "BigInteger.valueOf(" + reservedField.getReferenceValue() + ")";
        } else {
            return "(" + languageTypeName + ") " + reservedField.getReferenceValue();
        }
    }

    /**
     * @param field           this generally only is needed in order to access field attributes such as encoding etc.
     * @param resultType      the type the resulting expression should have
     * @param term            the term representing the expression
     * @param parserArguments any parser arguments, which could be referenced in expressions (Needed for getting the type)
     * @return Java code which does the things defined in 'term'
     */
    public String toParseExpression(Field field, TypeReference resultType, Term term, List<Argument> parserArguments) {
        Tracer tracer = Tracer.start("toParseExpression");
        return tracer + toExpression(field, resultType, term, variableLiteral -> tracer.dive("variableExpressionGenerator") + toVariableParseExpression(field, resultType, variableLiteral, parserArguments));
    }

    /**
     * @param field               this generally only is needed in order to access field attributes such as encoding etc.
     * @param resultType          the type the resulting expression should have
     * @param term                the term representing the expression
     * @param serializerArguments any serializer arguments, which could be referenced in expressions (Needed for getting the type)
     * @return Java code which does the things defined in 'term'
     */
    public String toSerializationExpression(Field field, TypeReference resultType, Term term, List<Argument> serializerArguments) {
        Tracer tracer = Tracer.start("toSerializationExpression");
        return tracer + toExpression(field, resultType, term, variableLiteral -> tracer.dive("variableExpressionGenerator") + toVariableSerializationExpression(field, resultType, variableLiteral, serializerArguments));
    }

    private String toExpression(Field field, TypeReference resultType, Term term, Function<VariableLiteral, String> variableExpressionGenerator) {
        Tracer tracer = Tracer.start("toExpression");
        if (term == null) {
            return tracer + "";
        }
        if (term instanceof Literal) {
            return toLiteralTermExpression(field, resultType, (Literal) term, variableExpressionGenerator, tracer);
        } else if (term instanceof UnaryTerm) {
            return toUnaryTermExpression(field, resultType, (UnaryTerm) term, variableExpressionGenerator, tracer);
        } else if (term instanceof BinaryTerm) {
            return toBinaryTermExpression(field, resultType, (BinaryTerm) term, variableExpressionGenerator, tracer);
        } else if (term instanceof TernaryTerm) {
            return toTernaryTermExpression(field, resultType, (TernaryTerm) term, variableExpressionGenerator, tracer);
        } else {
            throw new RuntimeException("Unsupported Term type " + term.getClass().getName() + ". Actual type " + resultType);
        }
    }

    private String toLiteralTermExpression(Field field, TypeReference resultType, Literal literal, Function<VariableLiteral, String> variableExpressionGenerator, Tracer tracer) {
        tracer = tracer.dive("literal term instanceOf");
        if (literal instanceof NullLiteral) {
            tracer = tracer.dive("null literal instanceOf");
            return tracer + "null";
        } else if (literal instanceof BooleanLiteral) {
            tracer = tracer.dive("boolean literal instanceOf");
            return tracer + Boolean.toString(((BooleanLiteral) literal).getValue());
        } else if (literal instanceof NumericLiteral) {
            tracer = tracer.dive("numeric literal instanceOf");
            final String numberString = ((NumericLiteral) literal).getNumber().toString();
            if (resultType.isIntegerTypeReference()) {
                final IntegerTypeReference integerTypeReference = resultType.asIntegerTypeReference().orElseThrow(RuntimeException::new);
                if (integerTypeReference.getBaseType() == SimpleTypeReference.SimpleBaseType.UINT && integerTypeReference.getSizeInBits() >= 32) {
                    tracer = tracer.dive("uint >= 32bit");
                    return tracer + numberString + "L";
                } else if (integerTypeReference.getBaseType() == SimpleTypeReference.SimpleBaseType.INT && integerTypeReference.getSizeInBits() > 32) {
                    tracer = tracer.dive("int > 32bit");
                    return tracer + numberString + "L";
                }
            } else if (resultType.isFloatTypeReference()) {
                final FloatTypeReference floatTypeReference = resultType.asFloatTypeReference().orElseThrow(RuntimeException::new);
                if (floatTypeReference.getSizeInBits() <= 32) {
                    tracer = tracer.dive("float < 32bit");
                    return tracer + numberString + "F";
                }
            }
            return tracer + numberString;
        } else if (literal instanceof HexadecimalLiteral) {
            tracer = tracer.dive("hexadecimal literal instanceOf");
            final String hexString = ((HexadecimalLiteral) literal).getHexString();
            if (resultType.isIntegerTypeReference()) {
                final IntegerTypeReference integerTypeReference = resultType.asIntegerTypeReference().orElseThrow(RuntimeException::new);
                if (integerTypeReference.getBaseType() == SimpleTypeReference.SimpleBaseType.UINT && integerTypeReference.getSizeInBits() >= 32) {
                    tracer = tracer.dive("uint >= 32bit");
                    return tracer + hexString + "L";
                } else if (integerTypeReference.getBaseType() == SimpleTypeReference.SimpleBaseType.INT && integerTypeReference.getSizeInBits() > 32) {
                    tracer = tracer.dive("int > 32bit");
                    return tracer + hexString + "L";
                }
            }
            return tracer + hexString;
        } else if (literal instanceof StringLiteral) {
            tracer = tracer.dive("string literal instanceOf");
            return tracer + "\"" + ((StringLiteral) literal).getValue() + "\"";
        } else if (literal instanceof VariableLiteral) {
            tracer = tracer.dive("variable literal instanceOf");
            VariableLiteral variableLiteral = (VariableLiteral) literal;
            if ("curPos".equals(((VariableLiteral) literal).getName())) {
                return "(positionAware.getPos() - startPos)";
            }
            // If this literal references an Enum type, then we have to output it differently.
            if (getTypeDefinitions().get(variableLiteral.getName()) instanceof EnumTypeDefinition) {
                tracer = tracer.dive("enum definition instanceOf");
                VariableLiteral enumDefinitionChild = variableLiteral.getChild()
                    .orElseThrow(() -> new RuntimeException("enum definitions should have childs"));
                return tracer + variableLiteral.getName() + "." + enumDefinitionChild.getName() +
                    enumDefinitionChild.getChild().map(child -> "." + toVariableExpressionRest(field, resultType, child)).orElse("");
            } else {
                return tracer + variableExpressionGenerator.apply(variableLiteral);
            }
        } else {
            throw new RuntimeException("Unsupported Literal type " + literal.getClass().getName());
        }
    }

    private String toUnaryTermExpression(Field field, TypeReference resultType, UnaryTerm unaryTerm, Function<VariableLiteral, String> variableExpressionGenerator, Tracer tracer) {
        tracer = tracer.dive("unary term instanceOf");
        Term a = unaryTerm.getA();
        switch (unaryTerm.getOperation()) {
            case "!":
                tracer = tracer.dive("case !");
                if ((resultType != getAnyTypeReference()) && !resultType.isBooleanTypeReference()) {
                    throw new IllegalArgumentException("'!(...)' expression requires boolean type. Actual type " + resultType);
                }
                return tracer + "!(" + toExpression(field, resultType, a, variableExpressionGenerator) + ")";
            case "-":
                tracer = tracer.dive("case -");
                if ((resultType != getAnyTypeReference()) && !resultType.isIntegerTypeReference() && !resultType.isFloatTypeReference()) {
                    throw new IllegalArgumentException("'-(...)' expression requires integer or floating-point type. Actual type " + resultType);
                }
                return tracer + "-(" + toExpression(field, resultType, a, variableExpressionGenerator) + ")";
            case "()":
                tracer = tracer.dive("case ()");
                return tracer + "(" + toExpression(field, resultType, a, variableExpressionGenerator) + ")";
            default:
                throw new RuntimeException("Unsupported unary operation type " + unaryTerm.getOperation() + ". Actual type " + resultType);
        }
    }

    private String toBinaryTermExpression(Field field, TypeReference resultType, BinaryTerm binaryTerm, Function<VariableLiteral, String> variableExpressionGenerator, Tracer tracer) {
        tracer = tracer.dive("binary term instanceOf");
        Term a = binaryTerm.getA();
        Term b = binaryTerm.getB();
        String operation = binaryTerm.getOperation();
        switch (operation) {
            case "^": {
                tracer = tracer.dive(operation);
                if ((resultType != getAnyTypeReference()) && !resultType.isIntegerTypeReference() && !resultType.isFloatTypeReference()) {
                    throw new IllegalArgumentException("'A^B' expression requires numeric result type. Actual type " + resultType);
                }
                return tracer + "Math.pow((" + toExpression(field, resultType, a, variableExpressionGenerator) + "), (" + toExpression(field, resultType, b, variableExpressionGenerator) + "))";
            }
            case "*":
            case "/":
            case "%":
            case "+":
            case "-": {
                tracer = tracer.dive(operation);
                if ((resultType != getAnyTypeReference()) && !resultType.isIntegerTypeReference() && !resultType.isFloatTypeReference()) {
                    throw new IllegalArgumentException("'A" + operation + "B' expression requires numeric result type. Actual type " + resultType);
                }
                return tracer + "(" + toExpression(field, resultType, a, variableExpressionGenerator) + ") " + operation + " (" + toExpression(field, resultType, b, variableExpressionGenerator) + ")";
            }
            case ">>":
            case "<<": {
                tracer = tracer.dive(operation);
                return tracer + "(" + toExpression(field, resultType, a, variableExpressionGenerator) + ") " + operation + " (" + toExpression(field, INT_TYPE_REFERENCE, b, variableExpressionGenerator) + ")";
            }
            case ">=":
            case "<=":
            case ">":
            case "<":
            case "==":
            case "!=":
                if ((resultType != getAnyTypeReference()) && !resultType.isBooleanTypeReference()) {
                    throw new IllegalArgumentException("'A" + operation + "B' expression requires boolean result type. Actual type " + resultType);
                }
                // TODO: Try to infer the types of the arguments in this case
                return tracer + "(" + toExpression(field, ANY_TYPE_REFERENCE, a, variableExpressionGenerator) + ") " + operation + " (" + toExpression(field, ANY_TYPE_REFERENCE, b, variableExpressionGenerator) + ")";
            case "&&":
            case "||":
                if ((resultType != getAnyTypeReference()) && !resultType.isBooleanTypeReference()) {
                    throw new IllegalArgumentException("'A" + operation + "B' expression requires boolean result type. Actual type " + resultType);
                }
                return tracer + "(" + toExpression(field, resultType, a, variableExpressionGenerator) + ") " + operation + " (" + toExpression(field, resultType, b, variableExpressionGenerator) + ")";
            case "&":
            case "|":
                if ((resultType != getAnyTypeReference()) && !resultType.isIntegerTypeReference() && !resultType.isByteTypeReference()) {
                    throw new IllegalArgumentException("'A" + operation + "B' expression requires byte or integer result type. Actual type " + resultType);
                }
                return tracer + "(" + toExpression(field, resultType, a, variableExpressionGenerator) + ") " + operation + " (" + toExpression(field, resultType, b, variableExpressionGenerator) + ")";
            default:
                throw new IllegalArgumentException("Unsupported ternary operation type " + operation);
        }
    }

    private String toTernaryTermExpression(Field field, TypeReference resultType, TernaryTerm ternaryTerm, Function<VariableLiteral, String> variableExpressionGenerator, Tracer tracer) {
        tracer = tracer.dive("ternary term instanceOf");
        if ("if".equals(ternaryTerm.getOperation())) {
            Term a = ternaryTerm.getA();
            Term b = ternaryTerm.getB();
            Term c = ternaryTerm.getC();
            return tracer +
                "(" +
                "(" + toExpression(field, BOOL_TYPE_REFERENCE, a, variableExpressionGenerator) + ") ? " +
                toExpression(field, resultType, b, variableExpressionGenerator) + " : " +
                toExpression(field, resultType, c, variableExpressionGenerator) + "" +
                ")";
        } else {
            throw new IllegalArgumentException("Unsupported ternary operation type " + ternaryTerm.getOperation() + ". Actual type " + resultType);
        }
    }

    public String toVariableEnumAccessExpression(VariableLiteral variableLiteral) {
        return variableLiteral.getName();
    }

    private String toVariableParseExpression(Field field, TypeReference resultType, VariableLiteral variableLiteral, List<Argument> parserArguments) {
        Tracer tracer = Tracer.start("toVariableParseExpression");
        // CAST expressions are special as we need to add a ".class" to the second parameter in Java.
        if ("CAST".equals(variableLiteral.getName())) {
            return toCastVariableParseExpression(field, resultType, variableLiteral, parserArguments, tracer);
        }
        // Special handling for ByteOrder enums (Built in enums)
        else if ("BIG_ENDIAN".equals(variableLiteral.getName())) {
            return "ByteOrder.BIG_ENDIAN";
        } else if ("LITTLE_ENDIAN".equals(variableLiteral.getName())) {
            return "ByteOrder.LITTLE_ENDIAN";
        }
        // If we're referencing an implicit field, we need to handle that differently.
        else if (isVariableLiteralImplicitField(variableLiteral)) { // If we are accessing implicit fields, we need to rely on a local variable instead.
            return toImplicitVariableParseExpression(field, resultType, variableLiteral, tracer);
        }
        // Call a static function in the drivers StaticHelper
        else if ("STATIC_CALL".equals(variableLiteral.getName())) {
            return toStaticCallParseExpression(field, resultType, variableLiteral, parserArguments, tracer);
        }
        // Call a built-in global static function
        else if (variableLiteral.getName().equals(variableLiteral.getName().toUpperCase())) { // All uppercase names are not fields, but utility methods.
            return toFunctionCallParseExpression(field, resultType, variableLiteral, parserArguments, tracer);
        }
        // The synthetic checksumRawData is a local field and should not be accessed as bean property.
        boolean isParserArg = "readBuffer".equals(variableLiteral.getName());
        boolean isTypeArg = "_type".equals(variableLiteral.getName());
        if (!isParserArg && !isTypeArg && parserArguments != null) {
            for (Argument serializerArgument : parserArguments) {
                if (serializerArgument.getName().equals(variableLiteral.getName())) {
                    isParserArg = true;
                    break;
                }
            }
        }
        if (isParserArg) {
            tracer = tracer.dive("parser arg");
            return tracer + variableLiteral.getName() + variableLiteral.getChild().map(child -> "." + toVariableExpressionRest(field, resultType, child)).orElse("");
        } else if (isTypeArg) {
            tracer = tracer.dive("type arg");
            String part = variableLiteral.getChild().map(VariableLiteral::getName).orElse("");
            switch (part) {
                case "name":
                    return tracer + "\"" + field.getTypeName() + "\"";
                case "length":
                    return tracer + "\"" + ((SimpleTypeReference) field).getSizeInBits() + "\"";
                case "encoding":
                    String encoding = ((StringLiteral) field.getEncoding().orElse(new DefaultStringLiteral("UTF-8"))).getValue();
                    return tracer + "\"" + encoding + "\"";
                default:
                    return tracer + "";
            }
        } else {
            String indexAddon = "";
            if (variableLiteral.getIndex().isPresent()) {
                indexAddon = ".get(" + variableLiteral.getIndex().orElseThrow() + ")";
            }
            return tracer + variableLiteral.getName() + indexAddon + variableLiteral.getChild().map(child -> "." + toVariableExpressionRest(field, resultType, child)).orElse("");
        }
    }

    private String toCastVariableParseExpression(Field field, TypeReference resultType, VariableLiteral variableLiteral, List<Argument> parserArguments, Tracer tracer) {
        tracer = tracer.dive("CAST");
        List<Term> arguments = variableLiteral.getArgs().orElseThrow(() -> new RuntimeException("A Cast expression needs arguments"));
        if (arguments.size() != 2) {
            throw new RuntimeException("A CAST expression expects exactly two arguments.");
        }
        VariableLiteral firstArgument = arguments.get(0).asLiteral()
            .orElseThrow(() -> new RuntimeException("First argument should be a literal"))
            .asVariableLiteral()
            .orElseThrow(() -> new RuntimeException("First argument should be a Variable literal"));
        StringLiteral typeArgument = arguments.get(1).asLiteral().orElseThrow(() -> new RuntimeException("Second argument should be a String literal"))
            .asStringLiteral()
            .orElseThrow(() -> new RuntimeException("Second argument should be a String literal"));
        String sb = "CAST" + "(" +
            toVariableParseExpression(field, ANY_TYPE_REFERENCE, firstArgument, parserArguments) +
            ", " +
            typeArgument.getValue() + ".class)";
        return tracer + sb + variableLiteral.getChild().map(child -> "." + toVariableExpressionRest(field, resultType, child)).orElse("");
    }

    private String toImplicitVariableParseExpression(Field field, TypeReference resultType, VariableLiteral variableLiteral, Tracer tracer) {
        tracer = tracer.dive("implicit");
        return tracer + variableLiteral.getName();
    }

    private String toStaticCallParseExpression(Field field, TypeReference resultType, VariableLiteral variableLiteral, List<Argument> parserArguments, Tracer tracer) {
        tracer = tracer.dive("STATIC_CALL");
        List<Term> arguments = variableLiteral.getArgs().orElseThrow(() -> new RuntimeException("A STATIC_CALL expression needs arguments"));
        if (arguments.size() < 1) {
            throw new RuntimeException("A STATIC_CALL expression expects at least one argument.");
        }
        // TODO: make it as static import with a emitImport so if a static call is present a "utils" package must be present in the import
        StringBuilder sb = new StringBuilder();
        sb.append(packageName()).append(".utils.StaticHelper.");
        // Get the class and method name
        String methodName = arguments.get(0).asLiteral()
            .orElseThrow(() -> new RuntimeException("First argument should be a literal"))
            .asStringLiteral()
            .orElseThrow(() -> new RuntimeException("Expecting the first argument of a 'STATIC_CALL' to be a StringLiteral")).
            getValue();
        sb.append(methodName).append("(");
        for (int i = 1; i < arguments.size(); i++) {
            Term arg = arguments.get(i);
            if (i > 1) {
                sb.append(", ");
            }
            sb.append(toParseExpression(field, ANY_TYPE_REFERENCE, arg, parserArguments));
           /*if (arg instanceof VariableLiteral) {
                VariableLiteral variableLiteralArg = (VariableLiteral) arg;
                // "readBuffer" is the default name of the reader argument which is always available.
                boolean isParserArg = "readBuffer".equals(variableLiteralArg.getName());
                boolean isTypeArg = "_type".equals(variableLiteralArg.getName());
                if (!isParserArg && !isTypeArg && parserArguments != null) {
                    for (Argument parserArgument : parserArguments) {
                        if (parserArgument.getName().equals(variableLiteralArg.getName())) {
                            isParserArg = true;
                            break;
                        }
                    }
                }
                if (isParserArg) {
                    sb.append(variableLiteralArg.getName()).append(variableLiteralArg.getChild().map(child -> "." + toVariableExpressionRest(child)).orElse(""));
                } else if (isTypeArg) {// We have to manually evaluate the type information at code-generation time.
                    String part = variableLiteralArg.getChild().map(VariableLiteral::getName).orElse("");
                    switch (part) {
                        case "name":
                            sb.append("\"").append(field.getTypeName()).append("\"");
                            break;
                        case "length":
                            sb.append("\"").append(((SimpleTypeReference) field).getSizeInBits()).append("\"");
                            break;
                        case "encoding":
                            String encoding = ((StringLiteral) field.getEncoding().orElse(new DefaultStringLiteral("UTF-8"))).getValue();
                            sb.append("\"").append(encoding).append("\"");
                            break;
                    }
                } else {
                    sb.append(toVariableParseExpression(field, variableLiteralArg, null));
                }
            } else if (arg instanceof StringLiteral) {
                sb.append(((StringLiteral) arg).getValue());
            }*/
        }
        sb.append(")");
        if (variableLiteral.getIndex().isPresent()) {
            // TODO: If this is a byte typed field, this needs to be an array accessor instead.
            sb.append(".get(").append(variableLiteral.getIndex().orElseThrow()).append(")");
        }
        return tracer + sb.toString();
    }

    private String toFunctionCallParseExpression(Field field, TypeReference resultType, VariableLiteral variableLiteral, List<Argument> parserArguments, Tracer tracer) {
        tracer = tracer.dive("FunctionCall");
        StringBuilder sb = new StringBuilder(variableLiteral.getName());
        Optional<List<Term>> args = variableLiteral.getArgs();
        if (args.isPresent()) {
            sb.append("(");
            boolean firstArg = true;
            for (Term arg : args.get()) {
                if (!firstArg) {
                    sb.append(", ");
                }
                // TODO: Try to infer the type of the argument ...
                sb.append(toParseExpression(field, ANY_TYPE_REFERENCE, arg, parserArguments));
                firstArg = false;
            }
            sb.append(")");
        }
        if (variableLiteral.getIndex().isPresent()) {
            // TODO: If this is a byte typed field, this needs to be an array accessor instead.
            sb.append(".get(").append(variableLiteral.getIndex().orElseThrow()).append(")");
        }
        return tracer + sb.toString() + variableLiteral.getChild().map(child -> "." + toVariableExpressionRest(field, resultType, child)).orElse("");
    }

    private String toVariableSerializationExpression(Field field, TypeReference resultType, VariableLiteral variableLiteral, List<Argument> serialzerArguments) {
        Tracer tracer = Tracer.start("variable serialization expression");
        if ("STATIC_CALL".equals(variableLiteral.getName())) {
            return toStaticCallSerializationExpression(field, resultType, variableLiteral, serialzerArguments, tracer);
        }
        // All uppercase names are not fields, but utility methods.
        else if (variableLiteral.getName().equals(variableLiteral.getName().toUpperCase())) {
            return toGlobalFunctionCallSerializationExpression(field, resultType, variableLiteral, serialzerArguments, tracer);
        } else if (isVariableLiteralImplicitField(variableLiteral)) { // If we are accessing implicit fields, we need to rely on a local variable instead.
            tracer = tracer.dive("implicit field");
            final ImplicitField referencedImplicitField = getReferencedImplicitField(variableLiteral);
            return tracer + toSerializationExpression(referencedImplicitField, referencedImplicitField.getType(), getReferencedImplicitField(variableLiteral).getSerializeExpression(), serialzerArguments);
        } else if (isVariableLiteralVirtualField(variableLiteral)) {
            tracer = tracer.dive("virtual field");
            return tracer + toVariableExpressionRest(field, resultType, variableLiteral);
        }
        // The synthetic checksumRawData is a local field and should not be accessed as bean property.
        boolean isSerializerArg = "writeBuffer".equals(variableLiteral.getName()) || "checksumRawData".equals(variableLiteral.getName()) || "_value".equals(variableLiteral.getName()) || "element".equals(variableLiteral.getName()) || "size".equals(variableLiteral.getName());
        boolean isTypeArg = "_type".equals(variableLiteral.getName());
        if (!isSerializerArg && !isTypeArg && serialzerArguments != null) {
            for (Argument serializerArgument : serialzerArguments) {
                if (serializerArgument.getName().equals(variableLiteral.getName())) {
                    isSerializerArg = true;
                    break;
                }
            }
        }
        if (isSerializerArg) {
            tracer = tracer.dive("serializer arg");
            return tracer + variableLiteral.getName() + variableLiteral.getChild().map(child -> "." + toVariableExpressionRest(field, resultType, child)).orElse("");
        } else if (isTypeArg) {
            tracer = tracer.dive("type arg");
            String part = variableLiteral.getChild().map(VariableLiteral::getName).orElse("");
            switch (part) {
                case "name":
                    return tracer + "\"" + field.getTypeName() + "\"";
                case "length":
                    return tracer + "\"" + ((SimpleTypeReference) field).getSizeInBits() + "\"";
                case "encoding":
                    String encoding = ((StringLiteral) field.getEncoding().orElse(new DefaultStringLiteral("UTF-8"))).getValue();
                    return tracer + "\"" + encoding + "\"";
                default:
                    return tracer + "";
            }
        } else {
            return tracer + toVariableExpressionRest(field, resultType, variableLiteral);
        }
    }

    private String toGlobalFunctionCallSerializationExpression(Field field, TypeReference resultType, VariableLiteral variableLiteral, List<Argument> serialzerArguments, Tracer tracer) {
        tracer = tracer.dive("GLOBAL_FUNCTION_CALL");
        StringBuilder sb = new StringBuilder(variableLiteral.getName());
        Optional<List<Term>> args = variableLiteral.getArgs();
        if (args.isPresent()) {
            sb.append("(");
            boolean firstArg = true;
            for (Term arg : args.get()) {
                if (!firstArg) {
                    sb.append(", ");
                }
                sb.append(toSerializationExpression(field, ANY_TYPE_REFERENCE, arg, serialzerArguments));
                firstArg = false;
                /*if (arg instanceof VariableLiteral) {
                    VariableLiteral va = (VariableLiteral) arg;
                    boolean isSerializerArg = "readBuffer".equals(va.getName()) || "writeBuffer".equals(va.getName());
                    boolean isTypeArg = "_type".equals(va.getName());
                    if (!isSerializerArg && !isTypeArg && serialzerArguments != null) {
                        for (Argument serializerArgument : serialzerArguments) {
                            if (serializerArgument.getName().equals(va.getName())) {
                                isSerializerArg = true;
                                break;
                            }
                        }
                    }
                    if (isSerializerArg) {
                        sb.append(va.getName()).append(va.getChild().map(child -> "." + toVariableExpressionRest(child)).orElse(""));
                    } else if (isTypeArg) {
                        String part = va.getChild().map(VariableLiteral::getName).orElse("");
                        switch (part) {
                            case "name":
                                sb.append("\"").append(field.getTypeName()).append("\"");
                                break;
                            case "length":
                                sb.append("\"").append(((SimpleTypeReference) field).getSizeInBits()).append("\"");
                                break;
                            case "encoding":
                                String encoding = ((StringLiteral) field.getEncoding().orElse(new DefaultStringLiteral("UTF-8"))).getValue();
                                sb.append("\"").append(encoding).append("\"");
                                break;
                        }
                    } else {
                        sb.append(toVariableSerializationExpression(field, va, serialzerArguments));
                    }
                } else if (arg instanceof StringLiteral) {
                    sb.append(((StringLiteral) arg).getValue());
                }*/
            }
            sb.append(")");
        }
        return tracer + sb.toString();
    }

    private String toStaticCallSerializationExpression(Field field, TypeReference resultType, VariableLiteral variableLiteral, List<Argument> serialzerArguments, Tracer tracer) {
        tracer = tracer.dive("STATIC_CALL");
        StringBuilder sb = new StringBuilder();
        List<Term> arguments = variableLiteral.getArgs().orElseThrow(() -> new RuntimeException("A STATIC_CALL expression needs arguments"));
        if (arguments.size() < 1) {
            throw new RuntimeException("A STATIC_CALL expression expects at least one argument.");
        }
        // TODO: make it as static import with a emitImport so if a static call is present a "utils" package must be present in the import
        sb.append(packageName()).append(".utils.StaticHelper.");
        // Get the class and method name
        String methodName = arguments.get(0).asLiteral()
            .orElseThrow(() -> new RuntimeException("First argument should be a literal"))
            .asStringLiteral()
            .orElseThrow(() -> new RuntimeException("Expecting the first argument of a 'STATIC_CALL' to be a StringLiteral")).
            getValue();
        //methodName = methodName.substring(1, methodName.length() - 1);
        sb.append(methodName).append("(");
        for (int i = 1; i < arguments.size(); i++) {
            Term arg = arguments.get(i);
            if (i > 1) {
                sb.append(", ");
            }
            sb.append(toSerializationExpression(field, ANY_TYPE_REFERENCE, arg, serialzerArguments));
            /*if (arg instanceof VariableLiteral) {
                VariableLiteral va = (VariableLiteral) arg;
                // "readBuffer" and "_value" are always available in every parser.
                boolean isSerializerArg = "readBuffer".equals(va.getName()) || "writeBuffer".equals(va.getName()) || "_value".equals(va.getName()) || "element".equals(va.getName());
                boolean isTypeArg = "_type".equals(va.getName());
                if (!isSerializerArg && !isTypeArg && serialzerArguments != null) {
                    for (Argument serializerArgument : serialzerArguments) {
                        if (serializerArgument.getName().equals(va.getName())) {
                            isSerializerArg = true;
                            break;
                        }
                    }
                }
                if (isSerializerArg) {
                    sb.append(va.getName()).append(va.getChild().map(child -> "." + toVariableExpressionRest(child)).orElse(""));
                } else if (isTypeArg) {
                    String part = va.getChild().map(VariableLiteral::getName).orElse("");
                    switch (part) {
                        case "name":
                            sb.append("\"").append(field.getTypeName()).append("\"");
                            break;
                        case "length":
                            sb.append("\"").append(((SimpleTypeReference) field).getSizeInBits()).append("\"");
                            break;
                        case "encoding":
                            String encoding = ((StringLiteral) field.getEncoding().orElse(new DefaultStringLiteral("UTF-8"))).getValue();
                            sb.append("\"").append(encoding).append("\"");
                            break;
                    }
                } else {
                    sb.append(toVariableSerializationExpression(field, va, serialzerArguments));
                }
            } else if (arg instanceof StringLiteral) {
                sb.append(((StringLiteral) arg).getValue());
            }*/
        }
        sb.append(")");
        return tracer + sb.toString();
    }

    private String toVariableExpressionRest(Field field, TypeReference resultType, VariableLiteral variableLiteral) {
        Tracer tracer = Tracer.start("variable expression rest");
        // length is kind of a keyword in mspec, so we shouldn't be naming variables length. if we ask for the length of a object we can just return length().
        // This way we can get the length of a string when serializing
        String variableLiteralName = variableLiteral.getName();
        if (variableLiteralName.equals("length")) {
            tracer = tracer.dive("length");
            return tracer + variableLiteralName + "()" + ((variableLiteral.getIndex().isPresent() ? ".get(" + variableLiteral.getIndex().orElseThrow() + ")" : "") +
                variableLiteral.getChild().map(child -> "." + toVariableExpressionRest(field, resultType, child)).orElse(""));
        }
        return tracer + "get" + WordUtils.capitalize(variableLiteralName) + "()" + ((variableLiteral.getIndex().isPresent() ? ".get(" + variableLiteral.getIndex().orElseThrow() + ")" : "") +
            variableLiteral.getChild().map(child -> "." + toVariableExpressionRest(field, resultType, child)).orElse(""));
    }

    public String getSizeInBits(ComplexTypeDefinition complexTypeDefinition, List<Argument> parserArguments) {
        int sizeInBits = 0;
        StringBuilder sb = new StringBuilder();
        for (Field field : complexTypeDefinition.getFields()) {
            if (field instanceof ArrayField) {
                ArrayField arrayField = (ArrayField) field;
                final SimpleTypeReference type = (SimpleTypeReference) arrayField.getType();
                switch (arrayField.getLoopType()) {
                    case COUNT:
                        sb.append("(").append(toSerializationExpression(null, INT_TYPE_REFERENCE, arrayField.getLoopExpression(), parserArguments)).append(" * ").append(type.getSizeInBits()).append(") + ");
                        break;
                    case LENGTH:
                        sb.append("(").append(toSerializationExpression(null, INT_TYPE_REFERENCE, arrayField.getLoopExpression(), parserArguments)).append(" * 8) + ");
                        break;
                    case TERMINATED:
                        // No terminated.
                        break;
                }
            } else if (field instanceof TypedField) {
                TypedField typedField = (TypedField) field;
                final TypeReference type = typedField.getType();
                if (field instanceof ManualField) {
                    ManualField manualField = (ManualField) field;
                    sb.append("(").append(toSerializationExpression(null, INT_TYPE_REFERENCE, manualField.getLengthExpression(), parserArguments)).append(") + ");
                } else if (type instanceof SimpleTypeReference) {
                    SimpleTypeReference simpleTypeReference = (SimpleTypeReference) type;
                    if (simpleTypeReference instanceof VstringTypeReference) {
                        sb.append(toSerializationExpression(null, INT_TYPE_REFERENCE, ((VstringTypeReference) simpleTypeReference).getLengthExpression(), parserArguments)).append(" + ");
                    } else {
                        sizeInBits += simpleTypeReference.getSizeInBits();
                    }
                }
            }
        }
        return sb.toString() + sizeInBits;
    }

    public boolean requiresCurPos() {
        if (thisType instanceof ComplexTypeDefinition) {
            ComplexTypeDefinition complexTypeDefinition = (ComplexTypeDefinition) this.thisType;
            for (Field curField : complexTypeDefinition.getFields()) {
                if (requiresVariable(curField, "curPos")) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean requiresStartPos() {
        if (thisType instanceof ComplexTypeDefinition) {
            ComplexTypeDefinition complexTypeDefinition = (ComplexTypeDefinition) this.thisType;
            for (Field curField : complexTypeDefinition.getFields()) {
                if (requiresVariable(curField, "startPos")) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean requiresVariable(Field curField, String variable) {
        if (curField.isArrayField()) {
            ArrayField arrayField = (ArrayField) curField;
            if (arrayField.getLoopExpression().contains(variable)) {
                return true;
            }
        } else if (curField.isOptionalField()) {
            OptionalField optionalField = (OptionalField) curField;
            if (optionalField.getConditionExpression().isPresent() && optionalField.getConditionExpression().orElseThrow(IllegalStateException::new).contains(variable)) {
                return true;
            }
        }
        return curField.asTypedField()
            .map(typedField -> typedField.getType().asNonSimpleTypeReference()
                .map(nonSimpleTypeReference -> nonSimpleTypeReference.getParams()
                    .map(params -> params.stream()
                        .anyMatch(param -> param.contains(variable))
                    )
                    .orElse(false)
                )
                .orElse(false)
            )
            .orElse(false);
    }

    public String escapeValue(TypeReference typeReference, String valueString) {
        if (valueString == null) {
            return null;
        }
        if (typeReference instanceof SimpleTypeReference) {
            SimpleTypeReference simpleTypeReference = (SimpleTypeReference) typeReference;
            switch (simpleTypeReference.getBaseType()) {
                case UINT:
                case INT:
                    // If it's a one character string and is numeric, output it as char.
                    if (!NumberUtils.isParsable(valueString) && (valueString.length() == 1)) {
                        return "'" + valueString + "'";
                    }
                    break;
                case STRING:
                case VSTRING:
                    return "\"" + valueString + "\"";
            }
        }
        return valueString;
    }

    public String getFieldOptions(TypedField field, List<Argument> parserArguments) {
        StringBuilder sb = new StringBuilder();
        field.getEncoding().ifPresent(term -> {
            final String encoding = toParseExpression(field, field.getType(), term, parserArguments);
            sb.append(", WithOption.WithEncoding(").append(encoding).append(")");
        });

        field.getByteOrder().ifPresent(term -> {
            final String byteOrder = toParseExpression(field, field.getType(), term, parserArguments);
            sb.append(", WithOption.WithByteOrder(").append(byteOrder).append(")");
        });

        field.getAttribute("nullBytesHex").ifPresent(term -> {
            final String nullBytesHex = toParseExpression(field, field.getType(), term, parserArguments);
            sb.append(", WithOption.WithNullBytesHex(\"").append(nullBytesHex).append("\")");
        });
        return sb.toString();
    }

    public boolean isBigIntegerSource(Term term) {
        boolean isBigInteger = term.asLiteral()
            .flatMap(LiteralConversions::asVariableLiteral)
            .flatMap(VariableLiteral::getChild)
            .map(Term.class::cast)
            .map(this::isBigIntegerSource)
            .orElse(false);
        return isBigInteger || term.asLiteral()
            .flatMap(LiteralConversions::asVariableLiteral)
            .map(VariableLiteral::getTypeReference)
            .flatMap(TypeReferenceConversions::asIntegerTypeReference)
            .map(integerTypeReference -> integerTypeReference.getSizeInBits() >= 64)
            .orElse(false);
    }

    public boolean needsLongMarker(Optional<SimpleTypeReference> baseTypeReference) {
        return baseTypeReference.isPresent() && baseTypeReference.get().isIntegerTypeReference() && baseTypeReference.get().asIntegerTypeReference().orElseThrow().getSizeInBits() >= 32;
    }

    public boolean isGeneratePropertiesForParserArguments() {
        return options.getOrDefault("generate-properties-for-parser-arguments", "false").equals("true");
    }

    public boolean isGeneratePropertiesForReservedFields() {
        return options.getOrDefault("generate-properties-for-reserved-fields", "false").equals("true");
    }

    public String getExternalTypeImports() {
        StringBuilder imports = new StringBuilder();
        if(options.containsKey("externalTypes")) {
            Object externalTypes = options.get("externalTypes");
            if(externalTypes instanceof Map) {
                Map<String, Object> externalTypesMap = (Map<String, Object>) externalTypes;
                for (String mspecTypeName : externalTypesMap.keySet()) {
                    Object obj = externalTypesMap.get(mspecTypeName);
                    if(obj instanceof String) {
                        imports.append("import ").append(obj).append(";\n");
                    } else {
                        throw new IllegalArgumentException("Type definition for " + mspecTypeName + " is invalid");
                    }
                }
            }
        }
        return imports.toString();
    }

    public boolean isVarduintField(Field field) {
        Optional<Term> encoding = field.getEncoding();
        if (encoding.isPresent()) {
            String encodingName = encoding.get().asLiteral().orElseThrow().asStringLiteral().orElseThrow().getValue();
            return encodingName.equalsIgnoreCase("VARUDINT");
        }
        return false;
    }

    public boolean isVardintField(Field field) {
        Optional<Term> encoding = field.getEncoding();
        if (encoding.isPresent()) {
            String encodingName = encoding.get().asLiteral().orElseThrow().asStringLiteral().orElseThrow().getValue();
            return encodingName.equalsIgnoreCase("VARDINT");
        }
        return false;
    }

}
