/*
 * ProGuardCORE -- library to process Java bytecode.
 *
 * Copyright (c) 2002-2020 Guardsquare NV
 *
 * Licensed 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
 *
 *      http://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 proguard.classfile.util;

import proguard.classfile.*;
import proguard.classfile.attribute.*;
import proguard.classfile.attribute.annotation.*;
import proguard.classfile.attribute.annotation.visitor.*;
import proguard.classfile.attribute.visitor.*;
import proguard.classfile.constant.*;
import proguard.classfile.constant.visitor.ConstantVisitor;
import proguard.classfile.editor.NamedAttributeDeleter;
import proguard.classfile.kotlin.*;
import proguard.classfile.kotlin.reflect.util.KotlinCallableReferenceInitializer;
import proguard.classfile.kotlin.visitor.*;
import proguard.classfile.visitor.*;

import java.util.*;
import java.util.stream.Collectors;

import static proguard.classfile.TypeConstants.INNER_CLASS_SEPARATOR;
import static proguard.classfile.kotlin.KotlinConstants.DEFAULT_IMPLEMENTATIONS_SUFFIX;
import static proguard.classfile.kotlin.KotlinConstants.DEFAULT_METHOD_SUFFIX;

/**
 * This {@link ClassVisitor} initializes the references of all classes that
 * it visits.
 * <p/>
 * All class constant pool entries get direct references to the corresponding
 * classes. These references make it more convenient to travel up and across
 * the class hierarchy.
 * <p/>
 * All field and method reference constant pool entries get direct references
 * to the corresponding classes, fields, and methods.
 * <p/>
 * All name and type constant pool entries get a list of direct references to
 * the classes listed in the type.
 * <p/>
 * All Kotlin metadata elements get references to their corresponding Java
 * implementation elements.
 * <p/>
 * This visitor optionally prints warnings if some items can't be found.
 * <p/>
 * The class hierarchy must be initialized before using this visitor.
 *
 * @author Eric Lafortune
 */
public class ClassReferenceInitializer
implements   ClassVisitor,

             // Implementation interfaces.
             MemberVisitor,
             ConstantVisitor,
             AttributeVisitor,
             LocalVariableInfoVisitor,
             LocalVariableTypeInfoVisitor,
             AnnotationVisitor,
             ElementValueVisitor
{
    private final ClassPool      programClassPool;
    private final ClassPool      libraryClassPool;
    private final boolean        checkAccessRules;
    private final WarningPrinter missingClassWarningPrinter;
    private final WarningPrinter missingProgramMemberWarningPrinter;
    private final WarningPrinter missingLibraryMemberWarningPrinter;
    private final WarningPrinter dependencyWarningPrinter;

    private final MemberFinder memberFinder       = new MemberFinder();
    private final MemberFinder strictMemberFinder = new MemberFinder(false);

    private final KotlinReferenceInitializer kotlinReferenceInitializer = new KotlinReferenceInitializer();


    /**
     * Creates a new ClassReferenceInitializer that initializes the references
     * of all visited class files.
     */
    public ClassReferenceInitializer(ClassPool programClassPool,
                                     ClassPool libraryClassPool)
    {
        this(programClassPool,
             libraryClassPool,
             true);
    }


    /**
     * Creates a new ClassReferenceInitializer that initializes the references
     * of all visited class files.
     */
    public ClassReferenceInitializer(ClassPool programClassPool,
                                     ClassPool libraryClassPool,
                                     boolean   checkAccessRules)
    {
        this(programClassPool,
             libraryClassPool,
             checkAccessRules,
             null,
             null,
             null,
             null);
    }


    /**
     * Creates a new ClassReferenceInitializer that initializes the references
     * of all visited class files, optionally printing warnings if some classes
     * or class members can't be found or if they are in the program class pool.
     */
    public ClassReferenceInitializer(ClassPool      programClassPool,
                                     ClassPool      libraryClassPool,
                                     WarningPrinter missingClassWarningPrinter,
                                     WarningPrinter missingProgramMemberWarningPrinter,
                                     WarningPrinter missingLibraryMemberWarningPrinter,
                                     WarningPrinter dependencyWarningPrinter)
    {
        this(programClassPool,
             libraryClassPool,
             true,
             missingClassWarningPrinter,
             missingProgramMemberWarningPrinter,
             missingLibraryMemberWarningPrinter,
             dependencyWarningPrinter);
    }


    /**
     * Creates a new ClassReferenceInitializer that initializes the references
     * of all visited class files, optionally printing warnings if some classes
     * or class members can't be found or if they are in the program class pool.
     */
    public ClassReferenceInitializer(ClassPool      programClassPool,
                                     ClassPool      libraryClassPool,
                                     boolean        checkAccessRules,
                                     WarningPrinter missingClassWarningPrinter,
                                     WarningPrinter missingProgramMemberWarningPrinter,
                                     WarningPrinter missingLibraryMemberWarningPrinter,
                                     WarningPrinter dependencyWarningPrinter)
    {
        this.programClassPool                   = programClassPool;
        this.libraryClassPool                   = libraryClassPool;
        this.checkAccessRules                   = checkAccessRules;
        this.missingClassWarningPrinter         = missingClassWarningPrinter;
        this.missingProgramMemberWarningPrinter = missingProgramMemberWarningPrinter;
        this.missingLibraryMemberWarningPrinter = missingLibraryMemberWarningPrinter;
        this.dependencyWarningPrinter           = dependencyWarningPrinter;
    }

    // Implementations for ClassVisitor.

    @Override
    public void visitAnyClass(Clazz clazz)
    {
        throw new UnsupportedOperationException(this.getClass().getName() + " does not support " + clazz.getClass().getName());
    }


    @Override
    public void visitProgramClass(ProgramClass programClass)
    {
        // Initialize the constant pool entries.
        programClass.constantPoolEntriesAccept(this);

        // Initialize all fields and methods.
        programClass.fieldsAccept(this);
        programClass.methodsAccept(this);

        // Initialize the attributes.
        programClass.attributesAccept(this);

        // Initialize the Kotlin metadata.
        programClass.kotlinMetadataAccept(kotlinReferenceInitializer);
    }


    @Override
    public void visitLibraryClass(LibraryClass libraryClass)
    {
        // Initialize all fields and methods.
        libraryClass.fieldsAccept(this);
        libraryClass.methodsAccept(this);

        // Initialize the Kotlin metadata.
        libraryClass.kotlinMetadataAccept(kotlinReferenceInitializer);
    }


    // Implementations for MemberVisitor.
    @Override
    public void visitProgramField(ProgramClass programClass, ProgramField programField)
    {
        programField.referencedClass =
            findReferencedClass(programClass,
                                programField.getDescriptor(programClass));

        // Initialize the attributes.
        programField.attributesAccept(programClass, this);
    }

    @Override
    public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod)
    {
        programMethod.referencedClasses =
            findReferencedClasses(programClass,
                                  programMethod.getDescriptor(programClass));

        // Initialize the attributes.
        programMethod.attributesAccept(programClass, this);
    }

    @Override
    public void visitLibraryField(LibraryClass libraryClass, LibraryField libraryField)
    {
        libraryField.referencedClass =
            findReferencedClass(libraryClass,
                                libraryField.getDescriptor(libraryClass));
    }

    @Override
    public void visitLibraryMethod(LibraryClass libraryClass, LibraryMethod libraryMethod)
    {
        libraryMethod.referencedClasses =
            findReferencedClasses(libraryClass,
                                  libraryMethod.getDescriptor(libraryClass));
    }


    // Implementations for ConstantVisitor.
    @Override
    public void visitAnyConstant(Clazz clazz, Constant constant) {}

    @Override
    public void visitStringConstant(Clazz clazz, StringConstant stringConstant)
    {
        // Fill out the String class.
        stringConstant.javaLangStringClass =
            findClass(clazz, ClassConstants.NAME_JAVA_LANG_STRING);
    }

    @Override
    public void visitDynamicConstant(Clazz clazz, DynamicConstant dynamicConstant)
    {
        dynamicConstant.referencedClasses =
            findReferencedClasses(clazz,
                                  dynamicConstant.getType(clazz));
    }

    @Override
    public void visitInvokeDynamicConstant(Clazz clazz, InvokeDynamicConstant invokeDynamicConstant)
    {
        invokeDynamicConstant.referencedClasses =
            findReferencedClasses(clazz,
                                  invokeDynamicConstant.getType(clazz));
    }

    @Override
    public void visitMethodHandleConstant(Clazz clazz, MethodHandleConstant methodHandleConstant)
    {
        // Fill out the MethodHandle class.
        methodHandleConstant.javaLangInvokeMethodHandleClass =
            findClass(clazz, ClassConstants.NAME_JAVA_LANG_INVOKE_METHOD_HANDLE);
    }

    @Override
    public void visitFieldrefConstant(Clazz clazz, FieldrefConstant fieldrefConstant)
    {
        String className = fieldrefConstant.getClassName(clazz);

        // Methods for array types should be found in the Object class.
        if (ClassUtil.isInternalArrayType(className))
        {
            className = ClassConstants.NAME_JAVA_LANG_OBJECT;
        }

        // See if we can find the referenced class.
        // Unresolved references are assumed to refer to library classes
        // that will not change anyway.
        Clazz referencedClass = findClass(clazz, className);

        if (referencedClass != null)
        {
            String name = fieldrefConstant.getName(clazz);
            String type = fieldrefConstant.getType(clazz);

            // See if we can find the referenced class member somewhere in the
            // hierarchy.
            Clazz referencingClass = checkAccessRules ? clazz : null;

            fieldrefConstant.referencedField = memberFinder.findField(referencingClass,
                                                                      referencedClass,
                                                                      name,
                                                                      type);
            fieldrefConstant.referencedClass = memberFinder.correspondingClass();

            if (fieldrefConstant.referencedField == null)
            {
                // We haven't found the class member anywhere in the hierarchy.
                boolean isProgramClass = referencedClass instanceof ProgramClass;

                WarningPrinter missingMemberWarningPrinter = isProgramClass ?
                    missingProgramMemberWarningPrinter :
                    missingLibraryMemberWarningPrinter;

                if (missingMemberWarningPrinter != null)
                {
                    missingMemberWarningPrinter.print(clazz.getName(),
                                                      className,
                                                      "Warning: " +
                                                      ClassUtil.externalClassName(clazz.getName()) +
                                                      ": can't find referenced field '"  +
                                                      ClassUtil.externalFullFieldDescription(0, name, type) +
                                                      "' in " +
                                                      (isProgramClass ?
                                                          "program" :
                                                          "library") +
                                                      " class " +
                                                      ClassUtil.externalClassName(className));
                }
            }
        }
    }

    @Override
    public void visitAnyMethodrefConstant(Clazz clazz, AnyMethodrefConstant anyMethodrefConstant)
    {
        String className = anyMethodrefConstant.getClassName(clazz);

        // Methods for array types should be found in the Object class.
        if (ClassUtil.isInternalArrayType(className))
        {
            className = ClassConstants.NAME_JAVA_LANG_OBJECT;
        }

        // See if we can find the referenced class.
        // Unresolved references are assumed to refer to library classes
        // that will not change anyway.
        Clazz referencedClass = findClass(clazz, className);

        if (referencedClass != null)
        {
            String name = anyMethodrefConstant.getName(clazz);
            String type = anyMethodrefConstant.getType(clazz);

            boolean isFieldRef = anyMethodrefConstant.getTag() == Constant.FIELDREF;

            // See if we can find the referenced class member somewhere in the
            // hierarchy.
            Clazz referencingClass = checkAccessRules ? clazz : null;

            anyMethodrefConstant.referencedMethod = memberFinder.findMethod(referencingClass,
                                                                            referencedClass,
                                                                            name,
                                                                            type);
            anyMethodrefConstant.referencedClass  = memberFinder.correspondingClass();

            if (anyMethodrefConstant.referencedMethod == null)
            {
                // We haven't found the class member anywhere in the hierarchy.
                boolean isProgramClass = referencedClass instanceof ProgramClass;

                WarningPrinter missingMemberWarningPrinter = isProgramClass ?
                    missingProgramMemberWarningPrinter :
                    missingLibraryMemberWarningPrinter;

                if (missingMemberWarningPrinter != null)
                {
                    missingMemberWarningPrinter.print(clazz.getName(),
                                                      className,
                                                      "Warning: " +
                                                      ClassUtil.externalClassName(clazz.getName()) +
                                                      ": can't find referenced method '" +
                                                      ClassUtil.externalFullMethodDescription(className, 0, name, type) +
                                                      "' in " +
                                                      (isProgramClass ?
                                                          "program" :
                                                          "library") +
                                                      " class " +
                                                      ClassUtil.externalClassName(className));
                }
            }
        }
    }

    @Override
    public void visitClassConstant(Clazz clazz, ClassConstant classConstant)
    {
        // Fill out the referenced class.
        classConstant.referencedClass =
            findClass(clazz, ClassUtil.internalClassNameFromClassType(classConstant.getName(clazz)));

        // Fill out the Class class.
        classConstant.javaLangClassClass =
            findClass(clazz, ClassConstants.NAME_JAVA_LANG_CLASS);
    }

    @Override
    public void visitMethodTypeConstant(Clazz clazz, MethodTypeConstant methodTypeConstant)
    {
        // Fill out the MethodType class.
        methodTypeConstant.javaLangInvokeMethodTypeClass =
            findClass(clazz, ClassConstants.NAME_JAVA_LANG_INVOKE_METHOD_TYPE);

        methodTypeConstant.referencedClasses =
            findReferencedClasses(clazz,
                                  methodTypeConstant.getType(clazz));
    }


    // Implementations for AttributeVisitor.
    @Override
    public void visitAnyAttribute(Clazz clazz, Attribute attribute) {}

    @Override
    public void visitEnclosingMethodAttribute(Clazz clazz, EnclosingMethodAttribute enclosingMethodAttribute)
    {
        String enclosingClassName = enclosingMethodAttribute.getClassName(clazz);

        // See if we can find the referenced class.
        enclosingMethodAttribute.referencedClass =
            findClass(clazz, enclosingClassName);

        if (enclosingMethodAttribute.referencedClass != null)
        {
            // Is there an enclosing method? Otherwise it's just initialization
            // code outside of the constructors.
            if (enclosingMethodAttribute.u2nameAndTypeIndex != 0)
            {
                String name = enclosingMethodAttribute.getName(clazz);
                String type = enclosingMethodAttribute.getType(clazz);

                // See if we can find the method in the referenced class.
                enclosingMethodAttribute.referencedMethod =
                    enclosingMethodAttribute.referencedClass.findMethod(name, type);

                if (enclosingMethodAttribute.referencedMethod == null &&
                    missingProgramMemberWarningPrinter != null)
                {
                    // We couldn't find the enclosing method.
                    String className = clazz.getName();

                    missingProgramMemberWarningPrinter.print(className,
                                                             enclosingClassName,
                                                             "Warning: " +
                                                             ClassUtil.externalClassName(className) +
                                                             ": can't find enclosing method '" +
                                                             ClassUtil.externalFullMethodDescription(enclosingClassName, 0, name, type) +
                                                             "' in program class " +
                                                             ClassUtil.externalClassName(enclosingClassName));
                }
            }
        }
        else
        {
            if (enclosingClassName                                != null &&
                enclosingClassName.indexOf(INNER_CLASS_SEPARATOR) != -1)
            {
                String myEnclosingClassName = enclosingClassName;

                do
                {
                    // [DGD-1462] Truncate the name until we find the first
                    // non-anonymous class, i.e. the first class that exists in
                    // a class pool.
                    myEnclosingClassName = myEnclosingClassName.substring(0, myEnclosingClassName.lastIndexOf("$"));

                    enclosingMethodAttribute.referencedClass =
                        findClass(clazz, myEnclosingClassName, false);
                }
                while (enclosingMethodAttribute.referencedClass            == null &&
                       myEnclosingClassName.indexOf(INNER_CLASS_SEPARATOR) != -1);

                if (enclosingMethodAttribute.referencedClass == null)
                {
                    reportMissingClass(clazz.getName(), enclosingClassName);
                }
            }
        }
    }

    @Override
    public void visitCodeAttribute(Clazz clazz, Method method, CodeAttribute codeAttribute)
    {
        // Initialize the nested attributes.
        codeAttribute.attributesAccept(clazz, method, this);
    }

    @Override
    public void visitLocalVariableTableAttribute(Clazz clazz, Method method, CodeAttribute codeAttribute, LocalVariableTableAttribute localVariableTableAttribute)
    {
        // Initialize the local variables.
        localVariableTableAttribute.localVariablesAccept(clazz, method, codeAttribute, this);
    }

    @Override
    public void visitLocalVariableTypeTableAttribute(Clazz clazz, Method method, CodeAttribute codeAttribute, LocalVariableTypeTableAttribute localVariableTypeTableAttribute)
    {
        // Initialize the local variable types.
        localVariableTypeTableAttribute.localVariablesAccept(clazz, method, codeAttribute, this);
    }


    @Override
    public void visitSignatureAttribute(Clazz clazz, Member member, SignatureAttribute signatureAttribute)
    {
        try
        {
            visitSignatureAttribute(clazz, signatureAttribute);
        }
        catch (Exception corruptSignature)
        {
            // #2468: delete corrupt signature attributes, since they
            // cannot be otherwise worked around.
            member.accept(clazz, new NamedAttributeDeleter(Attribute.SIGNATURE));
        }
    }


    @Override
    public void visitSignatureAttribute(Clazz clazz, SignatureAttribute signatureAttribute)
    {
        signatureAttribute.referencedClasses =
            findReferencedClasses(clazz,
                                  signatureAttribute.getSignature(clazz));
    }

    @Override
    public void visitAnyAnnotationsAttribute(Clazz clazz, AnnotationsAttribute annotationsAttribute)
    {
        // Initialize the annotations.
        annotationsAttribute.annotationsAccept(clazz, this);
    }

    @Override
    public void visitAnyParameterAnnotationsAttribute(Clazz clazz, Method method, ParameterAnnotationsAttribute parameterAnnotationsAttribute)
    {
        // Initialize the annotations.
        parameterAnnotationsAttribute.annotationsAccept(clazz, method, this);
    }

    @Override
    public void visitAnnotationDefaultAttribute(Clazz clazz, Method method, AnnotationDefaultAttribute annotationDefaultAttribute)
    {
        // Initialize the annotation.
        annotationDefaultAttribute.defaultValueAccept(clazz, this);
    }


    // Implementations for LocalVariableInfoVisitor.
    @Override
    public void visitLocalVariableInfo(Clazz clazz, Method method, CodeAttribute codeAttribute, LocalVariableInfo localVariableInfo)
    {
        localVariableInfo.referencedClass =
            findReferencedClass(clazz,
                                localVariableInfo.getDescriptor(clazz));
    }


    // Implementations for LocalVariableTypeInfoVisitor.
    @Override
    public void visitLocalVariableTypeInfo(Clazz clazz, Method method, CodeAttribute codeAttribute, LocalVariableTypeInfo localVariableTypeInfo)
    {
        localVariableTypeInfo.referencedClasses =
            findReferencedClasses(clazz,
                                  localVariableTypeInfo.getSignature(clazz));
    }


    // Implementations for AnnotationVisitor.
    @Override
    public void visitAnnotation(Clazz clazz, Annotation annotation)
    {
        annotation.referencedClasses =
            findReferencedClasses(clazz,
                                  annotation.getType(clazz));

        // Initialize the element values.
        annotation.elementValuesAccept(clazz, this);
    }


    // Implementations for ElementValueVisitor.
    @Override
    public void visitConstantElementValue(Clazz clazz, Annotation annotation, ConstantElementValue constantElementValue)
    {
        initializeElementValue(clazz, annotation, constantElementValue);
    }

    @Override
    public void visitEnumConstantElementValue(Clazz clazz, Annotation annotation, EnumConstantElementValue enumConstantElementValue)
    {
        initializeElementValue(clazz, annotation, enumConstantElementValue);

        enumConstantElementValue.referencedClasses =
            findReferencedClasses(clazz,
                                  enumConstantElementValue.getTypeName(clazz));
    }

    @Override
    public void visitClassElementValue(Clazz clazz, Annotation annotation, ClassElementValue classElementValue)
    {
        initializeElementValue(clazz, annotation, classElementValue);

        classElementValue.referencedClasses =
            findReferencedClasses(clazz,
                                  classElementValue.getClassName(clazz));
    }

    @Override
    public void visitAnnotationElementValue(Clazz clazz, Annotation annotation, AnnotationElementValue annotationElementValue)
    {
        initializeElementValue(clazz, annotation, annotationElementValue);

        // Initialize the annotation.
        annotationElementValue.annotationAccept(clazz, this);
    }

    @Override
    public void visitArrayElementValue(Clazz clazz, Annotation annotation, ArrayElementValue arrayElementValue)
    {
        initializeElementValue(clazz, annotation, arrayElementValue);

        // Initialize the element values.
        arrayElementValue.elementValuesAccept(clazz, annotation, this);
    }

    /**
     * Initializes the referenced method of an element value, if any.
     */
    private void initializeElementValue(Clazz clazz, Annotation annotation, ElementValue elementValue)
    {
        // See if we have a referenced class.
        if (annotation                      != null &&
            annotation.referencedClasses    != null &&
            elementValue.u2elementNameIndex != 0)
        {
            // See if we can find the method in the referenced class
            // (ignoring the descriptor).
            String name = elementValue.getMethodName(clazz);

            Clazz referencedClass = annotation.referencedClasses[0];
            elementValue.referencedClass  = referencedClass;
            elementValue.referencedMethod = referencedClass.findMethod(name, null);
        }
    }

    // Kotlin initializer class, used to initialize Kotlin metadata references.

    private class KotlinReferenceInitializer
    implements    KotlinMetadataVisitor,
                  KotlinPropertyVisitor,
                  KotlinFunctionVisitor,
                  KotlinConstructorVisitor,
                  KotlinTypeVisitor,
                  KotlinTypeAliasVisitor,
                  KotlinValueParameterVisitor,
                  KotlinTypeParameterVisitor
    {
        private final KotlinDefaultImplsInitializer                kotlinDefaultImplsInitializer          = new KotlinDefaultImplsInitializer();
        private final KotlinDefaultMethodInitializer               kotlinDefaultMethodInitializer         = new KotlinDefaultMethodInitializer();
        private final KotlinInterClassSyntheticFunctionInitializer interClassSyntheticFunctionInitializer = new KotlinInterClassSyntheticFunctionInitializer();
        private final KotlinCallableReferenceInitializer           callableReferenceInitializer           = new KotlinCallableReferenceInitializer();

        // Implementations for KotlinMetadataVisitor.
        @Override
        public void visitAnyKotlinMetadata(Clazz clazz, KotlinMetadata kotlinMetadata) {}

        @Override
        public void visitKotlinDeclarationContainerMetadata(Clazz                              clazz,
                                                            KotlinDeclarationContainerMetadata kotlinDeclarationContainerMetadata)
        {
            kotlinDeclarationContainerMetadata.ownerClassName       = clazz.getName();
            kotlinDeclarationContainerMetadata.ownerReferencedClass = clazz;

            kotlinDeclarationContainerMetadata.propertiesAccept(         clazz, this);
            kotlinDeclarationContainerMetadata.delegatedPropertiesAccept(clazz, this);
            kotlinDeclarationContainerMetadata.functionsAccept(          clazz, this);
            kotlinDeclarationContainerMetadata.typeAliasesAccept(        clazz,this);
        }


        @Override
        public void visitKotlinClassMetadata(Clazz clazz, KotlinClassKindMetadata kotlinClassKindMetadata)
        {
            kotlinClassKindMetadata.referencedClass = findClass(clazz, kotlinClassKindMetadata.className);

            if (kotlinClassKindMetadata.anonymousObjectOriginName != null)
            {
                kotlinClassKindMetadata.anonymousObjectOriginClass =
                    findClass(clazz, kotlinClassKindMetadata.anonymousObjectOriginName, false);

                if (kotlinClassKindMetadata.anonymousObjectOriginClass == null)
                {
                    kotlinClassKindMetadata.anonymousObjectOriginName = null;
                }
            }

            if (kotlinClassKindMetadata.companionObjectName != null)
            {
                String name = clazz.getName() + "$" + kotlinClassKindMetadata.companionObjectName;
                kotlinClassKindMetadata.referencedCompanionClass = findClass(clazz, name);
                kotlinClassKindMetadata.referencedCompanionField = memberFinder.findField(clazz,
                                                                                          kotlinClassKindMetadata.companionObjectName,
                                                                                          ClassUtil.internalTypeFromClassName(name));
            }

            kotlinClassKindMetadata.referencedEnumEntries =
                kotlinClassKindMetadata.enumEntryNames
                    .stream()
                    .map(enumEntry ->
                         strictMemberFinder.findField(clazz, enumEntry, null))
                    .collect(Collectors.toList());

            kotlinClassKindMetadata.referencedNestedClasses =
                kotlinClassKindMetadata.nestedClassNames
                    .stream()
                    .map(nestedName ->
                         findClass(clazz, clazz.getName() + "$" + nestedName))
                    .collect(Collectors.toList());

            kotlinClassKindMetadata.referencedSealedSubClasses=
                kotlinClassKindMetadata.sealedSubclassNames
                    .stream()
                    .map(sealedSubName ->
                         findClass(clazz, sealedSubName))
                    .collect(Collectors.toList());


            kotlinClassKindMetadata.typeParametersAccept(clazz, this);
            kotlinClassKindMetadata.superTypesAccept(    clazz, this);
            kotlinClassKindMetadata.constructorsAccept(  clazz, this);

            visitKotlinDeclarationContainerMetadata(clazz, kotlinClassKindMetadata);

            if (kotlinClassKindMetadata.flags.isInterface)
            {
                // Initialize the default implementations class of interfaces.
                // The class will be an inner class and have a name like MyInterface$DefaultImpls.
                kotlinClassKindMetadata.referencedDefaultImplsClass =
                    programClassPool.getClass(
                        kotlinClassKindMetadata.className +
                        KotlinConstants.DEFAULT_IMPLEMENTATIONS_SUFFIX
                    );

                if (kotlinClassKindMetadata.referencedDefaultImplsClass != null)
                {
                    // Initialize references from interface functions to
                    // their default implementations.
                    kotlinDefaultImplsInitializer.defaultImplsClass = kotlinClassKindMetadata.referencedDefaultImplsClass;
                    kotlinClassKindMetadata.functionsAccept(clazz, kotlinDefaultImplsInitializer);

                    // Initialize missing references from interface properties
                    // to their backing field.
                    kotlinClassKindMetadata.accept(clazz,
                                                   new AllKotlinPropertiesVisitor(
                                                   new KotlinInterClassPropertyReferenceInitializer(
                                                       kotlinClassKindMetadata.referencedDefaultImplsClass)));
                }
            }

            // Initialize references from functions to their utility methods
            // for default parameter values.
            kotlinDefaultMethodInitializer.isInterface = kotlinClassKindMetadata.flags.isInterface;
            kotlinClassKindMetadata.functionsAccept(clazz, kotlinDefaultMethodInitializer);

            if (kotlinClassKindMetadata.flags.isCompanionObject)
            {
                // Initialize missing references from properties in the Companion class
                // that have their backing field on the parent class.
                clazz.attributeAccept(Attribute.INNER_CLASSES,
                                      new KotlinCompanionParentPropertyInitializer());
            }
        }

        @Override
        public void visitKotlinFileFacadeMetadata(Clazz clazz, KotlinFileFacadeKindMetadata kotlinFileFacadeKindMetadata)
        {
            visitKotlinDeclarationContainerMetadata(clazz, kotlinFileFacadeKindMetadata);

            // Initialize references from functions to their utility methods
            // for default parameter values.
            kotlinDefaultMethodInitializer.isInterface = false;
            kotlinFileFacadeKindMetadata.functionsAccept(clazz, kotlinDefaultMethodInitializer);
        }

        @Override
        public void visitKotlinSyntheticClassMetadata(Clazz clazz, KotlinSyntheticClassKindMetadata kotlinSyntheticClassKindMetadata)
        {
            kotlinSyntheticClassKindMetadata.functionsAccept(clazz, this);

            // Initialize references from functions to their utility methods
            // for default parameter values.
            kotlinDefaultMethodInitializer.isInterface = false;
            kotlinSyntheticClassKindMetadata.functionsAccept(clazz, kotlinDefaultMethodInitializer);

            // Initialize missing references from synthetic functions by
            // searching in the enclosing method attribute.
            kotlinSyntheticClassKindMetadata.functionsAccept(clazz, interClassSyntheticFunctionInitializer);

            // Initialize callable references.
            kotlinSyntheticClassKindMetadata.accept(clazz, callableReferenceInitializer);
        }

        @Override
        public void visitKotlinMultiFileFacadeMetadata(Clazz clazz, KotlinMultiFileFacadeKindMetadata kotlinMultiFileFacadeKindMetadata)
        {
            kotlinMultiFileFacadeKindMetadata.referencedPartClasses =
                kotlinMultiFileFacadeKindMetadata.partClassNames
                    .stream()
                    .map(partName ->
                         findClass(clazz, partName))
                    .collect(Collectors.toList());
        }

        @Override
        public void visitKotlinMultiFilePartMetadata(Clazz clazz, KotlinMultiFilePartKindMetadata kotlinMultiFilePartKindMetadata)
        {
            kotlinMultiFilePartKindMetadata.referencedFacadeClass = findClass(clazz,
                                                                              kotlinMultiFilePartKindMetadata.facadeName);

            visitKotlinDeclarationContainerMetadata(clazz, kotlinMultiFilePartKindMetadata);

            // Initialize references from functions to their utility methods
            // for default parameter values.
            kotlinDefaultMethodInitializer.isInterface = false;
            kotlinMultiFilePartKindMetadata.functionsAccept(clazz, kotlinDefaultMethodInitializer);

            if (kotlinMultiFilePartKindMetadata.referencedFacadeClass != null)
            {
                // Initialize missing references from properties in multi-file parts
                // that have their backing field on the multi-file facade class.
                kotlinMultiFilePartKindMetadata.accept(clazz,
                                                       new AllKotlinPropertiesVisitor(
                                                       new KotlinInterClassPropertyReferenceInitializer(kotlinMultiFilePartKindMetadata.referencedFacadeClass)));
            }
        }

        // Implementations for KotlinPropertyVisitor.
        @Override
        public void visitAnyProperty(Clazz                              clazz,
                                     KotlinDeclarationContainerMetadata kotlinDeclarationContainerMetadata,
                                     KotlinPropertyMetadata             kotlinPropertyMetadata)
        {
            if (kotlinPropertyMetadata.backingFieldSignature != null)
            {
                kotlinPropertyMetadata.referencedBackingField =
                    strictMemberFinder.findField(clazz,
                                                 kotlinPropertyMetadata.backingFieldSignature.getName(),
                                                 kotlinPropertyMetadata.backingFieldSignature.getDesc());

                kotlinPropertyMetadata.referencedBackingFieldClass = strictMemberFinder.correspondingClass();
            }

            if (kotlinPropertyMetadata.getterSignature != null)
            {
                kotlinPropertyMetadata.referencedGetterMethod =
                    strictMemberFinder.findMethod(clazz,
                                                  kotlinPropertyMetadata.getterSignature.getName(),
                                                  kotlinPropertyMetadata.getterSignature.getDesc());
            }

            if (kotlinPropertyMetadata.setterSignature != null)
            {
                kotlinPropertyMetadata.referencedSetterMethod =
                    strictMemberFinder.findMethod(clazz,
                                                  kotlinPropertyMetadata.setterSignature.getName(),
                                                  kotlinPropertyMetadata.setterSignature.getDesc());
            }

            if (kotlinPropertyMetadata.syntheticMethodForAnnotations != null)
            {
                kotlinPropertyMetadata.referencedSyntheticMethodForAnnotations =
                    strictMemberFinder.findMethod(clazz,
                                                  kotlinPropertyMetadata.syntheticMethodForAnnotations.getName(),
                                                  kotlinPropertyMetadata.syntheticMethodForAnnotations.getDesc());

                kotlinPropertyMetadata.referencedSyntheticMethodClass = strictMemberFinder.correspondingClass();
            }

            kotlinPropertyMetadata.typeParametersAccept(  clazz, kotlinDeclarationContainerMetadata, this);
            kotlinPropertyMetadata.receiverTypeAccept(    clazz, kotlinDeclarationContainerMetadata, this);
            kotlinPropertyMetadata.typeAccept(            clazz, kotlinDeclarationContainerMetadata, this);
            kotlinPropertyMetadata.setterParametersAccept(clazz, kotlinDeclarationContainerMetadata, this);
        }


        // Implementations for KotlinFunctionVisitor.
        @Override
        public void visitAnyFunction(Clazz                  clazz,
                                     KotlinMetadata         kotlinMetadata,
                                     KotlinFunctionMetadata kotlinFunctionMetadata)
        {
            kotlinFunctionMetadata.referencedMethodClass = clazz;

            if (kotlinFunctionMetadata.jvmSignature != null)
            {
                kotlinFunctionMetadata.referencedMethod =
                    strictMemberFinder.findMethod(kotlinFunctionMetadata.referencedMethodClass,
                                                  kotlinFunctionMetadata.jvmSignature.getName(),
                                                  kotlinFunctionMetadata.jvmSignature.getDesc());
            }

            if (kotlinFunctionMetadata.lambdaClassOriginName != null)
            {
                kotlinFunctionMetadata.referencedLambdaClassOrigin =
                    findClass(clazz, kotlinFunctionMetadata.lambdaClassOriginName, false);

                if (kotlinFunctionMetadata.referencedLambdaClassOrigin == null)
                {
                    kotlinFunctionMetadata.lambdaClassOriginName = null;
                }
            }

            kotlinFunctionMetadata.contractsAccept(      clazz, kotlinMetadata, new AllTypeVisitor(this));
            kotlinFunctionMetadata.typeParametersAccept( clazz, kotlinMetadata, this);
            kotlinFunctionMetadata.receiverTypeAccept(   clazz, kotlinMetadata, this);
            kotlinFunctionMetadata.valueParametersAccept(clazz, kotlinMetadata, this);
            kotlinFunctionMetadata.returnTypeAccept(     clazz, kotlinMetadata, this);
        }

        // Implementations for KotlinConstructorVisitor
        @Override
        public void visitConstructor(Clazz                     clazz,
                                     KotlinClassKindMetadata   kotlinClassKindMetadata,
                                     KotlinConstructorMetadata kotlinConstructorMetadata)
        {
            // Annotation constructors don't have a corresponding constructor method.
            if (kotlinConstructorMetadata.jvmSignature != null)
            {
                kotlinConstructorMetadata.referencedMethod =
                    strictMemberFinder.findMethod(clazz,
                                                  kotlinConstructorMetadata.jvmSignature.getName(),
                                                  kotlinConstructorMetadata.jvmSignature.getDesc());
            }

            kotlinConstructorMetadata.valueParametersAccept(clazz, kotlinClassKindMetadata, this);
        }

        // Implementations for KotlinTypeVisitor.
        @Override
        public void visitAnyType(Clazz clazz, KotlinTypeMetadata kotlinTypeMetadata)
        {
            String className = kotlinTypeMetadata.className;

            if (className != null)
            {
                // If this is a dummy Kotlin type, assign a dummy referenced class.
                kotlinTypeMetadata.referencedClass = KotlinConstants.dummyClassPool.getClass(className);

                if (kotlinTypeMetadata.referencedClass == null)
                {
                    kotlinTypeMetadata.referencedClass = findClass(clazz, className);
                }
            }
            else if (kotlinTypeMetadata.aliasName != null)
            {
                // This type is an alias, that refers to an actual type (or another alias).
                // We search for the alias definition to create a reference for this use.

                String aliasName = kotlinTypeMetadata.aliasName;

                int innerClassMarkerIndex = aliasName.lastIndexOf('.');
                String classNameFilter, simpleName;

                if (innerClassMarkerIndex != -1)
                {
                    // Declared inside a class - we know exactly which one.
                    classNameFilter = aliasName.substring(0, innerClassMarkerIndex);
                    simpleName      = aliasName.substring(innerClassMarkerIndex + 1);
                }
                else
                {
                    // Declared in a file facade - we know which package only.
                    classNameFilter = ClassUtil.internalPackagePrefix(aliasName) + "*";
                    simpleName      = ClassUtil.internalSimpleClassName(kotlinTypeMetadata.aliasName);
                }

                programClassPool.classesAccept(
                    classNameFilter,
                    new ReferencedKotlinMetadataVisitor(
                    new KotlinTypeAliasReferenceInitializer(kotlinTypeMetadata, simpleName))
                );
            }

            initializeAnnotations(clazz, kotlinTypeMetadata.annotations);

            kotlinTypeMetadata.typeArgumentsAccept(clazz, this);
            kotlinTypeMetadata.outerClassAccept(   clazz, this);
            kotlinTypeMetadata.upperBoundsAccept(  clazz, this);
            kotlinTypeMetadata.abbreviationAccept( clazz, this);
        }


        // Implementations for KotlinTypeAliasVisitor.
        @Override
        public void visitTypeAlias(Clazz                              clazz,
                                   KotlinDeclarationContainerMetadata kotlinDeclarationContainerMetadata,
                                   KotlinTypeAliasMetadata            kotlinTypeAliasMetadata)
        {
            initializeAnnotations(clazz, kotlinTypeAliasMetadata.annotations);

            kotlinTypeAliasMetadata.referencedDeclarationContainer = kotlinDeclarationContainerMetadata;

            kotlinTypeAliasMetadata.underlyingTypeAccept(clazz, kotlinDeclarationContainerMetadata, this);
            kotlinTypeAliasMetadata.expandedTypeAccept(  clazz, kotlinDeclarationContainerMetadata, this);
            kotlinTypeAliasMetadata.typeParametersAccept(clazz, kotlinDeclarationContainerMetadata, this);
        }


        // Implementations for KotlinValueParameterVisitor.
        @Override
        public void visitAnyValueParameter(Clazz                        clazz,
                                           KotlinValueParameterMetadata kotlinValueParameterMetadata) {}

        @Override
        public void visitFunctionValParameter(Clazz                        clazz,
                                              KotlinMetadata               kotlinMetadata,
                                              KotlinFunctionMetadata       kotlinFunctionMetadata,
                                              KotlinValueParameterMetadata kotlinValueParameterMetadata)
        {
            kotlinValueParameterMetadata.typeAccept(clazz, kotlinMetadata, kotlinFunctionMetadata, this);
        }

        @Override
        public void visitConstructorValParameter(Clazz                        clazz,
                                                 KotlinClassKindMetadata      kotlinClassKindMetadata,
                                                 KotlinConstructorMetadata    kotlinConstructorMetadata,
                                                 KotlinValueParameterMetadata kotlinValueParameterMetadata)
        {
            kotlinValueParameterMetadata.typeAccept(clazz, kotlinClassKindMetadata, kotlinConstructorMetadata, this);
        }

        @Override
        public void visitPropertyValParameter(Clazz                              clazz,
                                              KotlinDeclarationContainerMetadata kotlinDeclarationContainerMetadata,
                                              KotlinPropertyMetadata             kotlinPropertyMetadata,
                                              KotlinValueParameterMetadata       kotlinValueParameterMetadata)
        {
            kotlinValueParameterMetadata.typeAccept(clazz, kotlinDeclarationContainerMetadata, kotlinPropertyMetadata, this);
        }


        // Implementations for KotlinTypeParameterVisitor

        @Override
        public void visitAnyTypeParameter(Clazz clazz, KotlinTypeParameterMetadata kotlinTypeParameterMetadata)
        {
            initializeAnnotations(clazz, kotlinTypeParameterMetadata.annotations);

            kotlinTypeParameterMetadata.upperBoundsAccept(clazz, this);
        }

        /**
         * Initialize Kotlin annotations.
         */
        private void initializeAnnotations(Clazz clazz, List<KotlinMetadataAnnotation> annotations)
        {
            for (KotlinMetadataAnnotation annotation : annotations)
            {
                annotation.referencedAnnotationClass = findClass(clazz, annotation.kmAnnotation.getClassName());
                if (annotation.referencedAnnotationClass != null)
                {
                    Set<String> argumentNames            = annotation.kmAnnotation.getArguments().keySet();
                    Map<String, Method> referencedKeys   = new HashMap<String, Method>();
                    for (String argumentName : argumentNames)
                    {
                        referencedKeys.put(argumentName, strictMemberFinder.findMethod(annotation.referencedAnnotationClass, argumentName, null));
                    }
                    annotation.referencedArgumentMethods = referencedKeys;
                }
                else
                {
                    annotation.referencedArgumentMethods = new HashMap<>();
                }
            }
        }
    }

    // Small utility methods.

    /**
     * Returns the single class referenced by the given descriptor, or
     * <code>null</code> if there isn't any useful reference.
     */
    private Clazz findReferencedClass(Clazz  referencingClass,
                                      String descriptor)
    {
        DescriptorClassEnumeration enumeration =
            new DescriptorClassEnumeration(descriptor);

        enumeration.nextFluff();

        if (enumeration.hasMoreClassNames())
        {
            return findClass(referencingClass, enumeration.nextClassName());
        }

        return null;
    }


    /**
     * Returns an array of classes referenced by the given descriptor, or
     * <code>null</code> if there aren't any useful references.
     */
    private Clazz[] findReferencedClasses(Clazz  referencingClass,
                                          String descriptor)
    {
        DescriptorClassEnumeration enumeration =
            new DescriptorClassEnumeration(descriptor);

        int classCount = enumeration.classCount();
        if (classCount > 0)
        {
            Clazz[] referencedClasses = new Clazz[classCount];

            boolean foundReferencedClasses = false;

            for (int index = 0; index < classCount; index++)
            {
                String fluff = enumeration.nextFluff();
                String name  = enumeration.nextClassName();

                Clazz referencedClass = findClass(referencingClass, name);

                if (referencedClass != null)
                {
                    referencedClasses[index] = referencedClass;
                    foundReferencedClasses = true;
                }
            }

            if (foundReferencedClasses)
            {
                return referencedClasses;
            }
        }

        return null;
    }


    /**
     * Returns the class with the given name, either for the program class pool
     * or from the library class pool, or <code>null</code> if it can't be found.
     */
    private Clazz findClass(Clazz referencingClass, String name)
    {
        return findClass(referencingClass, name, true);
    }


    /**
     * @param report Report if the class is not found. Set to false if the class
     *               doesn't necessarily exist.
     */
    private Clazz findClass(Clazz referencingClass, String name, boolean report)
    {
        // Is it an array type?
        if (ClassUtil.isInternalArrayType(name))
        {
            // Ignore any primitive array types.
            if (!ClassUtil.isInternalClassType(name))
            {
                return null;
            }

            // Strip the array part.
            name = ClassUtil.internalClassNameFromClassType(name);
        }

        // First look for the class in the program class pool.
        Clazz clazz = programClassPool.getClass(name);

        // Otherwise look for the class in the library class pool.
        if (clazz == null)
        {
            clazz = libraryClassPool.getClass(name);

            if (report &&
                clazz == null &&
                missingClassWarningPrinter != null)
            {
                // We didn't find the superclass or interface. Print a warning.
                reportMissingClass(referencingClass.getName(), name);
            }
        }
        else if (dependencyWarningPrinter != null)
        {
            // The superclass or interface was found in the program class pool.
            // Print a warning.
            String referencingClassName = referencingClass.getName();

            dependencyWarningPrinter.print(referencingClassName,
                                           name,
                                           "Warning: library class " +
                                           ClassUtil.externalClassName(referencingClassName) +
                                           " depends on program class " +
                                           ClassUtil.externalClassName(name));
        }

        return clazz;
    }

    private void reportMissingClass(String referencingClassName, String name)
    {
        missingClassWarningPrinter.print(referencingClassName,
                                         name,
                                         "Warning: " +
                                         ClassUtil.externalClassName(referencingClassName) +
                                         ": can't find referenced class " +
                                         ClassUtil.externalClassName(name));
    }


    // Helper classes for KotlinReferenceInitializer.

    public static class KotlinTypeAliasReferenceInitializer
    implements          KotlinMetadataVisitor,
                        KotlinTypeAliasVisitor
    {
        private final KotlinTypeMetadata kotlinTypeMetadata;
        private final String             simpleName;

        KotlinTypeAliasReferenceInitializer(KotlinTypeMetadata kotlinTypeMetadata, String simpleName)
        {
            this.simpleName         = simpleName;
            this.kotlinTypeMetadata = kotlinTypeMetadata;
        }

        // Implementations for KotlinMetadataVisitor.

        @Override
        public void visitAnyKotlinMetadata(Clazz clazz, KotlinMetadata kotlinMetadata) {}

        @Override
        public void visitKotlinDeclarationContainerMetadata(Clazz                              clazz,
                                                            KotlinDeclarationContainerMetadata kotlinDeclarationContainerMetadata)
        {
            kotlinDeclarationContainerMetadata.typeAliasesAccept(clazz, this);
        }

        @Override
        public void visitKotlinClassMetadata(Clazz clazz, KotlinClassKindMetadata kotlinClassKindMetadata)
        {
            // Only if the alias was declared inside this class.
            if (kotlinTypeMetadata.aliasName.equals(clazz.getName() + "." + simpleName))
            {
                kotlinClassKindMetadata.typeAliasesAccept(clazz, this);
            }
        }

        // Implementations for KotlinTypeAliasVisitor.

        @Override
        public void visitTypeAlias(Clazz                              clazz,
                                   KotlinDeclarationContainerMetadata kotlinDeclarationContainerMetadata,
                                   KotlinTypeAliasMetadata            kotlinTypeAliasMetadata)
        {
            if (this.simpleName.equals(kotlinTypeAliasMetadata.name))
            {
                this.kotlinTypeMetadata.referencedTypeAlias = kotlinTypeAliasMetadata;
            }
        }
    }

    /**
     * This KotlinFunctionVisitor initializes references to the default implementations of
     * interface functions.
     */
    private class KotlinDefaultImplsInitializer
    implements KotlinFunctionVisitor
    {
        private Clazz defaultImplsClass;

        @Override
        public void visitAnyFunction(Clazz                  clazz,
                                     KotlinMetadata         kotlinMetadata,
                                     KotlinFunctionMetadata kotlinFunctionMetadata) {}

        @Override
        public void visitFunction(Clazz                              clazz,
                                  KotlinDeclarationContainerMetadata kotlinDeclarationContainerMetadata,
                                  KotlinFunctionMetadata             kotlinFunctionMetadata)
        {
            if (defaultImplsClass != null &&
                !kotlinFunctionMetadata.flags.modality.isAbstract)
            {
                kotlinFunctionMetadata.referencedDefaultImplementationMethod =
                    strictMemberFinder.findMethod(
                        defaultImplsClass,
                        kotlinFunctionMetadata.jvmSignature.getName(),
                        getDescriptor(kotlinDeclarationContainerMetadata, kotlinFunctionMetadata)
                    );

                if (kotlinFunctionMetadata.referencedDefaultImplementationMethod != null)
                {
                    kotlinFunctionMetadata.referencedDefaultImplementationMethodClass = defaultImplsClass;
                }
            }
        }

        // Small helper methods.

        /**
         * Returns the descriptor for the default implementation method.
         */
        private String getDescriptor(KotlinDeclarationContainerMetadata kotlinDeclarationContainerMetadata,
                                     KotlinFunctionMetadata             kotlinFunctionMetadata)
        {
            // Default implementation methods are static and have the interface
            // instance as its first parameter.
            return kotlinFunctionMetadata.jvmSignature.getDesc()
                .replace("(", "(L" + kotlinDeclarationContainerMetadata.ownerClassName + ";");
        }
    }

    /**
     * This KotlinFunctionVisitor initializes references to sibling methods that
     * handle default parameter values.
      */
    private class KotlinDefaultMethodInitializer
    implements KotlinFunctionVisitor
    {
        boolean isInterface = false;
        boolean hasDefaults = false;

        // Implementations for KotlinFunctionVisitor.

        @Override
        public void visitAnyFunction(Clazz                  clazz,
                                     KotlinMetadata         kotlinMetadata,
                                     KotlinFunctionMetadata kotlinFunctionMetadata)
        {
            // During initialization this can be null because the
            // Asserter has not been run yet.
            if (kotlinFunctionMetadata.referencedMethod == null)
            {
                return;
            }

            // Use the jvm name here because the jvm name might not match
            // the metadata name.
            String methodName =
                kotlinFunctionMetadata.referencedMethod.getName(kotlinFunctionMetadata.referencedMethodClass);

            if (methodName.endsWith(DEFAULT_METHOD_SUFFIX))
            {
                return;
            }

            // Check if there are parameters with default values.
            hasDefaults = false;
            kotlinFunctionMetadata.valueParametersAccept(clazz,
                                                         kotlinMetadata,
                                                         (_clazz, vp) -> hasDefaults |= vp.flags.hasDefaultValue);

            if (hasDefaults)
            {
                String defaultMethodName = methodName + DEFAULT_METHOD_SUFFIX;
                String descriptor        = getDescriptor(kotlinFunctionMetadata);

                kotlinFunctionMetadata.referencedDefaultMethod =
                    strictMemberFinder.findMethod(kotlinFunctionMetadata.referencedMethodClass,
                                                  defaultMethodName,
                                                  descriptor);

                kotlinFunctionMetadata.referencedDefaultMethodClass = strictMemberFinder.correspondingClass();

                if (kotlinFunctionMetadata.referencedDefaultMethod == null && isInterface)
                {
                    Clazz defaultImplsClass = programClassPool.getClass(clazz.getName() +
                                                                        DEFAULT_IMPLEMENTATIONS_SUFFIX);

                    if (defaultImplsClass != null)
                    {
                        kotlinFunctionMetadata.referencedDefaultMethod =
                            strictMemberFinder.findMethod(defaultImplsClass, defaultMethodName, descriptor);

                        kotlinFunctionMetadata.referencedDefaultMethodClass = strictMemberFinder.correspondingClass();
                    }
                }
            }
        }

        // Small helper methods.

        /**
         * Return the descriptor for the utility method for default parameter values of
         * the supplied function.
         *
         * The descriptor matches the original method but with 2 or more extra parameters:
         *
         *  - the first parameter is an instance reference if the original method was not static.
         *  - the last 2+ parameters
         *     - int masks for every 32 parameters that encode which parameter values were passed
         *     - an object for future use? seems to be always null for now?
         */
        private String getDescriptor(KotlinFunctionMetadata kotlinFunctionMetadata)
        {
            String originalDescriptor =
                kotlinFunctionMetadata.referencedMethod.getDescriptor(kotlinFunctionMetadata.referencedMethodClass);

            // Each int param encodes up to 32 parameters as being present or not.
            int requiredIntParams = 1 + (ClassUtil.internalMethodParameterCount(originalDescriptor) / 32);

            String descriptor = originalDescriptor.replace(
                ")",
                String.join("", Collections.nCopies(requiredIntParams, "I")) + "Ljava/lang/Object;)"
            );

            // Instance methods will have been made static and have the class
            // instance as its first parameter.
            if ((kotlinFunctionMetadata.referencedMethod.getAccessFlags() & AccessConstants.STATIC) == 0)
            {
                descriptor = descriptor.replace("(", "(L" + kotlinFunctionMetadata.referencedMethodClass.getName() + ";");
            }

            return descriptor;
        }
    }

    /**
     * This KotlinPropertyVisitor tries to initialize missing property references
     * by looking into the supplied class.
     */
    private class KotlinInterClassPropertyReferenceInitializer
    implements KotlinPropertyVisitor
    {
        private final Clazz clazz;

        KotlinInterClassPropertyReferenceInitializer(Clazz clazz)
        {
            this.clazz = clazz;
        }

        // Implementations for KotlinPropertyVisitor.

        @Override
        public void visitAnyProperty(Clazz                              clazz,
                                     KotlinDeclarationContainerMetadata kotlinDeclarationContainerMetadata,
                                     KotlinPropertyMetadata             kotlinPropertyMetadata)
        {
            if (kotlinPropertyMetadata.backingFieldSignature  != null &&
                kotlinPropertyMetadata.referencedBackingField == null)
            {
                kotlinPropertyMetadata.referencedBackingField =
                    strictMemberFinder.findField(this.clazz,
                                           kotlinPropertyMetadata.backingFieldSignature.getName(),
                                           kotlinPropertyMetadata.backingFieldSignature.getDesc());

                kotlinPropertyMetadata.referencedBackingFieldClass = strictMemberFinder.correspondingClass();
            }

            /* Default implementations of methods are stored in the $DefaultImpls class not in the
             * interface class.
             *
             * We fix any synthetic methods for annotations that were not already set (because
             * they weren't found the in interface class) - these will be found in the $DefaultImpls class.
             *
             * Not necessary for getters/setters as they are stored on the interface class itself.
             */
            if (kotlinPropertyMetadata.syntheticMethodForAnnotations           != null &&
                kotlinPropertyMetadata.referencedSyntheticMethodForAnnotations == null)
            {
                kotlinPropertyMetadata.referencedSyntheticMethodForAnnotations =
                    strictMemberFinder.findMethod(this.clazz,
                                            kotlinPropertyMetadata.syntheticMethodForAnnotations.getName(),
                                            kotlinPropertyMetadata.syntheticMethodForAnnotations.getDesc());

                kotlinPropertyMetadata.referencedSyntheticMethodClass = strictMemberFinder.correspondingClass();
            }
        }
    }

    /**
     * This KotlinFunctionVisitor tries to initialize missing function references.
     */
    private static class KotlinInterClassSyntheticFunctionInitializer
    implements           KotlinFunctionVisitor,
                         AttributeVisitor
    {
        private KotlinFunctionMetadata currentFunction;

        @Override
        public void visitAnyFunction(Clazz                  clazz,
                                     KotlinMetadata         kotlinMetadata,
                                     KotlinFunctionMetadata kotlinFunctionMetadata) {}

        @Override
        public void visitSyntheticFunction(Clazz                            clazz,
                                           KotlinSyntheticClassKindMetadata kotlinSyntheticClassKindMetadata,
                                           KotlinFunctionMetadata           kotlinFunctionMetadata)
        {
            if (kotlinFunctionMetadata.referencedMethod == null)
            {
                this.currentFunction = kotlinFunctionMetadata;
                clazz.attributeAccept(Attribute.ENCLOSING_METHOD, this);
            }
        }

        @Override
        public void visitEnclosingMethodAttribute(Clazz clazz, EnclosingMethodAttribute enclosingMethodAttribute)
        {
            Method enclosingMethod      = enclosingMethodAttribute.referencedMethod;
            Clazz  enclosingMethodClass = enclosingMethodAttribute.referencedClass;

            if (enclosingMethod              != null &&
                currentFunction.jvmSignature != null &&
                enclosingMethod.getName(enclosingMethodClass).equals(currentFunction.jvmSignature.getName()))
            {
                currentFunction.referencedMethod      = enclosingMethod;
                currentFunction.referencedMethodClass = enclosingMethodClass;
            }
        }
    }

    /**
     * This AttributeVisitor finds the parent of the visited companion class
     * and passes it to a KotlinInterClassPropertyReferenceInitializer.
     */
    private class KotlinCompanionParentPropertyInitializer
    implements    AttributeVisitor,
                  InnerClassesInfoVisitor,
                  ConstantVisitor
    {
        // Implementations for AttributeVisitor.

        @Override
        public void visitAnyAttribute(Clazz clazz, Attribute attribute) {}

        @Override
        public void visitInnerClassesAttribute(Clazz clazz, InnerClassesAttribute innerClassesAttribute)
        {
            innerClassesAttribute.innerClassEntriesAccept(clazz, this);
        }

        // Implementations for InnerClassesInfoVisitor.

        @Override
        public void visitInnerClassesInfo(Clazz clazz, InnerClassesInfo innerClassesInfo)
        {
            innerClassesInfo.outerClassConstantAccept(clazz, this);
        }

        // Implementations for ConstantVisitor.

        @Override
        public void visitAnyConstant(Clazz clazz, Constant constant) {}

        @Override
        public void visitClassConstant(Clazz clazz, ClassConstant classConstant)
        {
            clazz.kotlinMetadataAccept(new AllKotlinPropertiesVisitor(
                                       new KotlinInterClassPropertyReferenceInitializer(classConstant.referencedClass)));
        }
    }
}
