package io.smallrye.config;

import static org.objectweb.asm.Opcodes.AASTORE;
import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_INTERFACE;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACONST_NULL;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ANEWARRAY;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASM7;
import static org.objectweb.asm.Opcodes.ASTORE;
import static org.objectweb.asm.Opcodes.BIPUSH;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.DCMPL;
import static org.objectweb.asm.Opcodes.DRETURN;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.FCMPL;
import static org.objectweb.asm.Opcodes.FRETURN;
import static org.objectweb.asm.Opcodes.F_SAME;
import static org.objectweb.asm.Opcodes.GETFIELD;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.GOTO;
import static org.objectweb.asm.Opcodes.ICONST_0;
import static org.objectweb.asm.Opcodes.ICONST_1;
import static org.objectweb.asm.Opcodes.IFEQ;
import static org.objectweb.asm.Opcodes.IFNE;
import static org.objectweb.asm.Opcodes.IFNULL;
import static org.objectweb.asm.Opcodes.IF_ACMPEQ;
import static org.objectweb.asm.Opcodes.IF_ACMPNE;
import static org.objectweb.asm.Opcodes.IF_ICMPNE;
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.IRETURN;
import static org.objectweb.asm.Opcodes.LCMP;
import static org.objectweb.asm.Opcodes.LRETURN;
import static org.objectweb.asm.Opcodes.NEW;
import static org.objectweb.asm.Opcodes.POP;
import static org.objectweb.asm.Opcodes.PUTFIELD;
import static org.objectweb.asm.Opcodes.PUTSTATIC;
import static org.objectweb.asm.Opcodes.RETURN;
import static org.objectweb.asm.Opcodes.SIPUSH;
import static org.objectweb.asm.Opcodes.SWAP;
import static org.objectweb.asm.Opcodes.V1_8;
import static org.objectweb.asm.Type.getDescriptor;
import static org.objectweb.asm.Type.getInternalName;
import static org.objectweb.asm.Type.getMethodDescriptor;
import static org.objectweb.asm.Type.getType;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.regex.Pattern;

import org.eclipse.microprofile.config.inject.ConfigProperties;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import io.smallrye.config.ConfigMapping.NamingStrategy;
import io.smallrye.config.ConfigMappingContext.ObjectCreator;
import io.smallrye.config.ConfigMappingInterface.CollectionProperty;
import io.smallrye.config.ConfigMappingInterface.LeafProperty;
import io.smallrye.config.ConfigMappingInterface.MapProperty;
import io.smallrye.config.ConfigMappingInterface.MayBeOptionalProperty;
import io.smallrye.config.ConfigMappingInterface.PrimitiveProperty;
import io.smallrye.config.ConfigMappingInterface.Property;

public class ConfigMappingGenerator {
    static final boolean usefulDebugInfo;
    /**
     * The regular expression allowing to detect arrays in a full type name.
     */
    private static final Pattern ARRAY_FORMAT_REGEX = Pattern.compile("([<;])L(.*)\\[];");

    static {
        usefulDebugInfo = Boolean.parseBoolean(AccessController.doPrivileged(
                (PrivilegedAction<String>) () -> System.getProperty("io.smallrye.config.mapper.useful-debug-info")));
    }

    private static final String I_CLASS = getInternalName(Class.class);
    private static final String I_FIELD = getInternalName(Field.class);

    private static final String I_MAPPING_CONTEXT = getInternalName(ConfigMappingContext.class);
    private static final String I_OBJECT_CREATOR = getInternalName(ConfigMappingContext.ObjectCreator.class);
    private static final String I_NAMING_STRATEGY = getInternalName(NamingStrategy.class);
    private static final String I_STRING_BUILDER = getInternalName(StringBuilder.class);

    private static final String I_RUNTIME_EXCEPTION = getInternalName(RuntimeException.class);
    private static final String I_OBJECT = getInternalName(Object.class);
    private static final String I_STRING = getInternalName(String.class);
    private static final String I_ITERABLE = getInternalName(Iterable.class);

    private static final int V_THIS = 0;
    private static final int V_MAPPING_CONTEXT = 1;

    /**
     * Generates the backing implementation of an interface annotated with the {@link ConfigMapping} annotation.
     *
     * @param mapping information about a configuration interface.
     * @return the class bytes representing the implementation of the configuration interface.
     */
    static byte[] generate(final ConfigMappingInterface mapping) {
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        ClassVisitor visitor = usefulDebugInfo ? new Debugging.ClassVisitorImpl(writer) : writer;

        visitor.visit(V1_8, ACC_PUBLIC, mapping.getClassInternalName(), null, I_OBJECT,
                new String[] { getInternalName(mapping.getInterfaceType()) });
        visitor.visitSource(null, null);

        // No Args Constructor - To use for proxies
        MethodVisitor noArgsCtor = visitor.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        noArgsCtor.visitVarInsn(ALOAD, V_THIS);
        noArgsCtor.visitMethodInsn(INVOKESPECIAL, I_OBJECT, "<init>", "()V", false);
        noArgsCtor.visitInsn(RETURN);
        noArgsCtor.visitEnd();
        noArgsCtor.visitMaxs(0, 0);

        ObjectCreatorMethodVisitor ctor = new ObjectCreatorMethodVisitor(
                visitor.visitMethod(ACC_PUBLIC, "<init>", "(L" + I_MAPPING_CONTEXT + ";)V", null, null));
        ctor.visitParameter("context", ACC_FINAL);
        Label ctorStart = new Label();
        ctor.visitLabel(ctorStart);
        ctor.visitVarInsn(ALOAD, V_THIS);
        ctor.visitMethodInsn(INVOKESPECIAL, I_OBJECT, "<init>", "()V", false);

        if (mapping.hasConfigMapping()) {
            ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT);
            ctor.visitFieldInsn(GETSTATIC, I_NAMING_STRATEGY, mapping.getNamingStrategy().name(),
                    "L" + I_NAMING_STRATEGY + ";");
            ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "applyNamingStrategy", "(L" + I_NAMING_STRATEGY + ";)V",
                    false);

            ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT);
            ctor.visitInsn(mapping.isBeanStyleGetters() ? ICONST_1 : ICONST_0);
            ctor.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false);
            ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "applyBeanStyleGetters", "(Ljava/lang/Boolean;)V", false);
        }

        addProperties(visitor, ctor, mapping);

        ctor.visitInsn(RETURN);
        Label ctorEnd = new Label();
        ctor.visitLabel(ctorEnd);
        ctor.visitLocalVariable("mc", 'L' + I_MAPPING_CONTEXT + ';', null, ctorStart, ctorEnd, V_MAPPING_CONTEXT);
        ctor.visitEnd();
        ctor.visitMaxs(0, 0);
        visitor.visitEnd();

        generateStaticInit(visitor, mapping);
        generateEquals(visitor, mapping);
        generateHashCode(visitor, mapping);
        generateToString(visitor, mapping);

        return writer.toByteArray();
    }

    /**
     * Generates a configuration interface to act as a middle ground between a configuration class and the backing
     * implementation of a configuration interface.
     * <p>
     *
     * MicroProfile Config 2.0 added support for configuration classes and the
     * {@link org.eclipse.microprofile.config.inject.ConfigProperties} annotation. Since SmallRye Config only supports
     * configuration interfaces, the expected interface is generated from the MicroProfile Config configuration
     * class annotated with {@link org.eclipse.microprofile.config.inject.ConfigProperties}.
     * <p>
     *
     * The generated configuration interface implements {@link ConfigMappingClassMapper} which provides the bridge
     * between the instance of the configuration class and the implementation of the configuration interface provided by
     * {@link ConfigMappingGenerator#generate(ConfigMappingInterface)} to retrieve the configuration values.
     *
     * @param classType the configuration class.
     * @param interfaceName the generated interface class name.
     * @return the class bytes representing the interface of the configuration class.
     */
    static byte[] generate(final Class<?> classType, final String interfaceName) {
        String classInternalName = getInternalName(classType);
        String interfaceInternalName = interfaceName.replace('.', '/');

        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        writer.visit(V1_8, ACC_PUBLIC | ACC_INTERFACE | ACC_ABSTRACT, interfaceInternalName, null, I_OBJECT,
                new String[] { getInternalName(ConfigMappingClassMapper.class) });

        {
            AnnotationVisitor av = writer.visitAnnotation("L" + getInternalName(ConfigMapping.class) + ";", true);
            av.visitEnum("namingStrategy", "L" + getInternalName(NamingStrategy.class) + ";",
                    NamingStrategy.VERBATIM.toString());

            if (classType.isAnnotationPresent(ConfigProperties.class)) {
                av.visit("prefix", classType.getAnnotation(ConfigProperties.class).prefix());
            }

            av.visitEnd();
        }

        Object classInstance;
        try {
            classInstance = classType.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }

        Field[] declaredFields = classType.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            MethodVisitor mv = writer.visitMethod(ACC_PUBLIC | ACC_ABSTRACT, declaredField.getName(),
                    getMethodDescriptor(getType(declaredField.getType())), getSignature(declaredField),
                    null);

            boolean hasDefault = false;

            if (declaredField.isAnnotationPresent(WithName.class)) {
                AnnotationVisitor av = mv.visitAnnotation("L" + getInternalName(WithName.class) + ";", true);
                av.visit("value", declaredField.getAnnotation(WithName.class).value());
                av.visitEnd();
            }

            if (declaredField.isAnnotationPresent(WithDefault.class)) {
                AnnotationVisitor av = mv.visitAnnotation("L" + getInternalName(WithDefault.class) + ";", true);
                av.visit("value", declaredField.getAnnotation(WithDefault.class).value());
                av.visitEnd();
                hasDefault = true;
            }

            if (declaredField.isAnnotationPresent(WithConverter.class)) {
                AnnotationVisitor av = mv.visitAnnotation("L" + getInternalName(WithConverter.class) + ";", true);
                av.visit("value", declaredField.getAnnotation(WithConverter.class).value());
                av.visitEnd();
            }

            if (declaredField.isAnnotationPresent(ConfigProperty.class)) {
                ConfigProperty configProperty = declaredField.getAnnotation(ConfigProperty.class);
                {
                    if (!configProperty.name().isEmpty()) {
                        AnnotationVisitor av = mv.visitAnnotation("L" + getInternalName(WithName.class) + ";", true);
                        av.visit("value", configProperty.name());
                        av.visitEnd();
                    }
                }
                {
                    if (!configProperty.defaultValue().equals(ConfigProperty.UNCONFIGURED_VALUE)) {
                        AnnotationVisitor av = mv.visitAnnotation("L" + getInternalName(WithDefault.class) + ";", true);
                        av.visit("value", configProperty.defaultValue());
                        av.visitEnd();
                        hasDefault = true;
                    }
                }
            }

            if (!hasDefault) {
                try {
                    declaredField.setAccessible(true);
                    Object defaultValue = declaredField.get(classInstance);
                    if (hasDefaultValue(declaredField.getType(), defaultValue)) {
                        AnnotationVisitor av = mv.visitAnnotation("L" + getInternalName(WithDefault.class) + ";", true);
                        av.visit("value", defaultValue.toString());
                        av.visitEnd();
                    }
                } catch (IllegalAccessException e) {
                    // Ignore
                }
            }

            mv.visitEnd();
        }

        MethodVisitor ctor = writer.visitMethod(ACC_PUBLIC, "map", "()L" + I_OBJECT + ";", null, null);
        Label ctorStart = new Label();
        ctor.visitLabel(ctorStart);
        ctor.visitTypeInsn(NEW, classInternalName);
        ctor.visitInsn(DUP);
        ctor.visitMethodInsn(INVOKESPECIAL, classInternalName, "<init>", "()V", false);
        ctor.visitVarInsn(ASTORE, 1);

        for (Field declaredField : declaredFields) {
            if (Modifier.isStatic(declaredField.getModifiers()) || Modifier.isVolatile(declaredField.getModifiers())
                    || Modifier.isFinal(declaredField.getModifiers())) {
                continue;
            }

            String name = declaredField.getName();
            Class<?> type = declaredField.getType();

            if (Modifier.isPublic(declaredField.getModifiers())) {
                ctor.visitVarInsn(ALOAD, 1);
                ctor.visitVarInsn(ALOAD, 0);
                ctor.visitMethodInsn(INVOKEINTERFACE, interfaceInternalName, name, getMethodDescriptor(getType(type)),
                        true);
                ctor.visitFieldInsn(PUTFIELD, classInternalName, name, getDescriptor(type));
            } else {
                ctor.visitLdcInsn(getType(classType));
                ctor.visitLdcInsn(name);
                ctor.visitMethodInsn(INVOKEVIRTUAL, I_CLASS, "getDeclaredField",
                        getMethodDescriptor(getType(Field.class), getType(String.class)), false);
                ctor.visitVarInsn(ASTORE, 2);
                ctor.visitVarInsn(ALOAD, 2);
                ctor.visitInsn(ICONST_1);
                ctor.visitMethodInsn(INVOKEVIRTUAL, I_FIELD, "setAccessible", "(Z)V", false);

                ctor.visitVarInsn(ALOAD, 2);
                ctor.visitVarInsn(ALOAD, 1);
                ctor.visitVarInsn(ALOAD, 0);
                ctor.visitMethodInsn(INVOKEINTERFACE, interfaceInternalName, name, getMethodDescriptor(getType(type)), true);

                switch (Type.getType(type).getSort()) {
                    case Type.BOOLEAN:
                        ctor.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false);
                        break;
                    case Type.BYTE:
                        ctor.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false);
                        break;
                    case Type.CHAR:
                        ctor.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false);
                        break;
                    case Type.SHORT:
                        ctor.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false);
                        break;
                    case Type.INT:
                        ctor.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
                        break;
                    case Type.FLOAT:
                        ctor.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false);
                        break;
                    case Type.LONG:
                        ctor.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
                        break;
                    case Type.DOUBLE:
                        ctor.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false);
                        break;
                }

                ctor.visitMethodInsn(INVOKEVIRTUAL, I_FIELD, "set",
                        getMethodDescriptor(getType(void.class), getType(Object.class), getType(Object.class)), false);
            }
        }

        ctor.visitVarInsn(ALOAD, 1);
        ctor.visitInsn(ARETURN);
        ctor.visitMaxs(2, 2);
        writer.visitEnd();

        return writer.toByteArray();
    }

    private static void addProperties(
            final ClassVisitor cv,
            final ObjectCreatorMethodVisitor ctor,
            final ConfigMappingInterface mapping) {

        for (Property property : mapping.getProperties()) {
            Method method = property.getMethod();
            String memberName = method.getName();

            // Field Declaration
            String fieldType = getInternalName(method.getReturnType());
            String fieldDesc = getDescriptor(method.getReturnType());
            cv.visitField(ACC_PRIVATE, memberName, fieldDesc, null, null);

            // Getter
            MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, memberName, "()" + fieldDesc, null, null);
            mv.visitVarInsn(ALOAD, V_THIS);
            mv.visitFieldInsn(GETFIELD, mapping.getClassInternalName(), memberName, fieldDesc);
            mv.visitInsn(getReturnInstruction(property));
            mv.visitEnd();
            mv.visitMaxs(0, 0);

            if (property.isDefaultMethod()) {
                continue;
            }

            // Constructor field init
            Label _try = new Label();
            Label _catch = new Label();
            Label _continue = new Label();
            ctor.visitTryCatchBlock(_try, _catch, _catch, I_RUNTIME_EXCEPTION);

            // try
            ctor.visitLabel(_try);

            ctor.visitVarInsn(ALOAD, V_THIS);
            generateProperty(ctor, property);

            ctor.visitCast(property, fieldType);
            ctor.visitFieldInsn(PUTFIELD, mapping.getClassInternalName(), memberName, fieldDesc);
            ctor.visitJumpInsn(GOTO, _continue);

            // catch
            ctor.visitLabel(_catch);
            ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT);
            ctor.visitInsn(SWAP);
            ctor.visitMethodInsn(INVOKEVIRTUAL, I_MAPPING_CONTEXT, "problem", "(L" + I_RUNTIME_EXCEPTION + ";)V", false);
            ctor.visitJumpInsn(GOTO, _continue);

            ctor.visitLabel(_continue);
        }

        // We don't know the order in the constructor and the default method may require call to other
        // properties that may not be initialized yet, so we add them last
        for (Property property : mapping.getProperties()) {
            Method method = property.getMethod();
            String memberName = method.getName();
            String fieldDesc = getDescriptor(method.getReturnType());

            if (property.isDefaultMethod()) {
                ctor.visitVarInsn(ALOAD, V_THIS);
                Method defaultMethod = property.asDefaultMethod().getDefaultMethod();
                ctor.visitVarInsn(ALOAD, V_THIS);
                ctor.visitMethodInsn(INVOKESTATIC, getInternalName(defaultMethod.getDeclaringClass()), defaultMethod.getName(),
                        "(" + getType(mapping.getInterfaceType()) + ")" + fieldDesc, false);
                ctor.visitFieldInsn(PUTFIELD, mapping.getClassInternalName(), memberName, fieldDesc);
            }
        }
    }

    private static void generateProperty(final ObjectCreatorMethodVisitor ctor, final Property property) {
        if (property.isPrimitive()) {
            PrimitiveProperty primitiveProperty = property.asPrimitive();
            ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT);
            ctor.visitPropertyName(property);
            if (!primitiveProperty.hasConvertWith() && primitiveProperty.getPrimitiveType() == int.class) {
                ctor.visitMethod(PrimitiveMethodInvocation.intValue);
            } else if (!primitiveProperty.hasConvertWith() && primitiveProperty.getPrimitiveType() == boolean.class) {
                ctor.visitMethod(PrimitiveMethodInvocation.boolValue);
            } else {
                ctor.visitLdcInsn(Type.getType(primitiveProperty.getBoxType()));
                ctor.visitConverter(primitiveProperty);
                ctor.visitMethod(PrimitiveMethodInvocation.value);
            }
        } else if (property.isLeaf() && !property.isOptional()) {
            LeafProperty leafProperty = property.asLeaf();
            ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT);
            ctor.visitPropertyName(property);
            if (!leafProperty.hasConvertWith() && leafProperty.getValueType() == String.class) {
                ctor.visitMethod(ObjectMethodInvocation.stringValue);
            } else if (!leafProperty.hasConvertWith() && leafProperty.getValueRawType() == Integer.class) {
                ctor.visitMethod(ObjectMethodInvocation.integerValue);
            } else if (!leafProperty.hasConvertWith() && leafProperty.getValueRawType() == Boolean.class) {
                ctor.visitMethod(ObjectMethodInvocation.booleanValue);
            } else {
                ctor.visitLdcInsn(Type.getType(leafProperty.getValueRawType()));
                ctor.visitConverter(leafProperty);
                ctor.visitMethod(property.isSecret() ? ObjectMethodInvocation.secretValue : ObjectMethodInvocation.value);
            }
        } else if (property.isOptional() && property.isLeaf()) {
            LeafProperty optionalProperty = property.asLeaf();
            ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT);
            ctor.visitPropertyName(property);
            if (!optionalProperty.hasConvertWith() && !optionalProperty.isSecret()
                    && optionalProperty.getValueRawType() == String.class) {
                ctor.visitMethod(ObjectMethodInvocation.optionalStringValue);
            } else {
                ctor.visitLdcInsn(Type.getType(optionalProperty.getValueRawType()));
                ctor.visitConverter(optionalProperty);
                ctor.visitMethod(optionalProperty.isSecret() ? ObjectMethodInvocation.optionalSecretValue
                        : ObjectMethodInvocation.optionalValue);
            }
        } else if (property.isMap() && property.asMap().getValueProperty().isLeaf()) {
            MapProperty mapProperty = property.asMap();
            Property valueProperty = mapProperty.getValueProperty();
            ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT);
            ctor.visitPropertyName(property);
            ctor.visitLdcInsn(getType(mapProperty.getKeyRawType()));
            ctor.visitKeyConverter(mapProperty);
            ctor.visitInsn(valueProperty.isOptional() ? ICONST_1 : ICONST_0);
            ctor.visitLdcInsn(getType(valueProperty.asLeaf().getValueRawType()));
            ctor.visitConverter(valueProperty.asLeaf());
            ctor.visitKeyProvider(mapProperty);
            ctor.visitDefault(mapProperty);
            ctor.visitMethod(valueProperty.isSecret() ? MapMethodInvocation.secretValues : MapMethodInvocation.values);
        } else if (property.isMap() && property.asMap().getValueProperty().isCollection()
                && property.asMap().getValueProperty().asCollection().getElement().isLeaf()) {
            MapProperty mapProperty = property.asMap();
            Property valueProperty = mapProperty.getValueProperty();
            ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT);
            ctor.visitPropertyName(property);
            ctor.visitLdcInsn(getType(mapProperty.getKeyRawType()));
            ctor.visitKeyConverter(mapProperty);
            LeafProperty elementProperty = valueProperty.asCollection().getElement().asLeaf();
            ctor.visitLdcInsn(getType(elementProperty.getValueRawType()));
            ctor.visitConverter(elementProperty);
            ctor.visitLdcInsn(getType(valueProperty.asCollection().getCollectionRawType()));
            ctor.visitKeyProvider(mapProperty);
            ctor.visitDefault(mapProperty);
            ctor.visitMethod(
                    elementProperty.isSecret() ? MapCollectionInvocation.secretValues : MapCollectionInvocation.values);
        } else if (property.isCollection() && property.asCollection().getElement().isLeaf()) {
            CollectionProperty collectionProperty = property.asCollection();
            LeafProperty elementProperty = collectionProperty.getElement().asLeaf();
            ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT);
            ctor.visitPropertyName(property);
            ctor.visitLdcInsn(getType(elementProperty.getValueRawType()));
            ctor.visitConverter(elementProperty);
            ctor.visitLdcInsn(getType(collectionProperty.getCollectionRawType()));
            ctor.visitMethod(
                    elementProperty.isSecret() ? CollectionMethodInvocation.secretValues : CollectionMethodInvocation.values);
        } else if (property.isOptional() && property.asOptional().getNestedProperty().isCollection()
                && property.asOptional().getNestedProperty().asCollection().getElement().isLeaf()) {
            CollectionProperty collectionProperty = property.asOptional().getNestedProperty().asCollection();
            LeafProperty elementProperty = collectionProperty.getElement().asLeaf();
            ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT);
            ctor.visitPropertyName(property);
            ctor.visitLdcInsn(getType(elementProperty.getValueRawType()));
            ctor.visitConverter(elementProperty);
            ctor.visitLdcInsn(getType(collectionProperty.getCollectionRawType()));
            ctor.visitMethod(elementProperty.isSecret() ? CollectionMethodInvocation.optionalSecretValues
                    : CollectionMethodInvocation.optionalValues);
        } else {
            ctor.visitTypeInsn(NEW, I_OBJECT_CREATOR);
            ctor.visitInsn(DUP);
            ctor.visitVarInsn(ALOAD, V_MAPPING_CONTEXT);
            ctor.visitLdcInsn(property.getPropertyName());
            ctor.visitInsn(property.hasPropertyName() ? ICONST_0 : ICONST_1);
            ctor.visitMethodInsn(INVOKESPECIAL, I_OBJECT_CREATOR, "<init>", "(" + D_MAPPING_CONTEXT + D_STRING + "Z" + ")V",
                    false);
            generateNestedProperty(ctor, property);
        }
    }

    private static void generateNestedProperty(final ObjectCreatorMethodVisitor ctor, final Property property) {
        if (property.isGroup()) {
            ctor.visitLdcInsn(getType(property.asGroup().getGroupType().getInterfaceType()));
            ctor.visitMethod(ObjectCreatorInvocation.group);
            ctor.visitMethod(ObjectCreatorInvocation.get);
        } else if (property.isMap() && property.asMap().getValueProperty().isLeaf()) {
            MapProperty mapProperty = property.asMap();
            Property valueProperty = mapProperty.getValueProperty();
            ctor.visitLdcInsn(getType(mapProperty.getKeyRawType()));
            ctor.visitKeyConverter(mapProperty);
            LeafProperty leafProperty = valueProperty.asLeaf();
            ctor.visitLdcInsn(getType(leafProperty.getValueRawType()));
            ctor.visitConverter(leafProperty);
            ctor.visitKeyProvider(mapProperty);
            ctor.visitDefault(mapProperty);
            ctor.visitMethod(ObjectCreatorMapInvocation.values);
            ctor.visitMethod(ObjectCreatorInvocation.get);
        } else if (property.isMap() && property.asMap().getValueProperty().isCollection()
                && property.asMap().getValueProperty().asCollection().getElement().isLeaf()) {
            MapProperty mapProperty = property.asMap();
            Property valueProperty = mapProperty.getValueProperty();
            ctor.visitLdcInsn(getType(mapProperty.getKeyRawType()));
            ctor.visitKeyConverter(mapProperty);
            LeafProperty leafProperty = valueProperty.asCollection().getElement().asLeaf();
            ctor.visitLdcInsn(getType(leafProperty.getValueRawType()));
            ctor.visitConverter(leafProperty);
            ctor.visitLdcInsn(getType(valueProperty.asCollection().getCollectionRawType()));
            ctor.visitKeyProvider(mapProperty);
            ctor.visitDefault(mapProperty);
            ctor.visitMethod(ObjectCreatorInvocation.values);
            ctor.visitMethod(ObjectCreatorInvocation.get);
        } else if (property.isOptional() && property.asOptional().getNestedProperty().isCollection()
                && property.asOptional().getNestedProperty().asCollection().getElement().isLeaf()) {
            CollectionProperty collectionProperty = property.asOptional().getNestedProperty().asCollection();
            ctor.visitLdcInsn(getType(collectionProperty.getElement().asLeaf().getValueRawType()));
            ctor.visitConverter(collectionProperty.getElement().asLeaf());
            ctor.visitLdcInsn(getType(collectionProperty.getCollectionRawType()));
            ctor.visitMethod(ObjectCreatorInvocation.optionalValues);
            ctor.visitMethod(ObjectCreatorInvocation.get);
        } else if (property.isMap()) {
            MapProperty mapProperty = property.asMap();
            Property valueProperty = mapProperty.getValueProperty();
            if (valueProperty.isGroup()) {
                ctor.visitLdcInsn(getType(mapProperty.getKeyRawType()));
                ctor.visitKeyConverter(mapProperty);
                ctor.visitKeyUnnamed(mapProperty);
                ctor.visitKeyProvider(mapProperty);
                if (mapProperty.hasDefaultValue()) {
                    ctor.visitLdcInsn(getType(valueProperty.asGroup().getGroupType().getInterfaceType()));
                } else {
                    ctor.visitInsn(ACONST_NULL);
                }
                ctor.visitMethod(ObjectCreatorMapGroupInvocation.map);
                ctor.visitLdcInsn(getType(valueProperty.asGroup().getGroupType().getInterfaceType()));
                ctor.visitMethod(
                        mapProperty.hasKeyProvider() ? ObjectCreatorInvocation.group : ObjectCreatorInvocation.lazyGroup);
                ctor.visitMethod(ObjectCreatorInvocation.get);
            } else {
                unwrapNestedProperty(ctor, property);
            }
        } else if (property.isCollection()) {
            unwrapNestedProperty(ctor, property);
        } else if (property.isOptional()) {
            final MayBeOptionalProperty nestedProperty = property.asOptional().getNestedProperty();
            if (nestedProperty.isGroup()) {
                ctor.visitLdcInsn(getType(nestedProperty.asGroup().getGroupType().getInterfaceType()));
                ctor.visitMethod(ObjectCreatorInvocation.optionalGroup);
                ctor.visitMethod(ObjectCreatorInvocation.get);
            } else if (nestedProperty.isCollection()) {
                CollectionProperty collectionProperty = nestedProperty.asCollection();
                ctor.visitLdcInsn(getType(collectionProperty.getCollectionRawType()));
                ctor.visitMethod(ObjectCreatorInvocation.optionalCollection);
                generateNestedProperty(ctor, collectionProperty.getElement());
            } else {
                throw new UnsupportedOperationException();
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

    private static void unwrapNestedProperty(final ObjectCreatorMethodVisitor ctor, final Property property) {
        if (property.isMap()) {
            MapProperty mapProperty = property.asMap();
            ctor.visitLdcInsn(getType(mapProperty.getKeyRawType()));
