AopProxyWriter.java

/*
 * Copyright 2017-2020 original authors
 *
 * 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
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.micronaut.aop.writer;

import io.micronaut.aop.HotSwappableInterceptedProxy;
import io.micronaut.aop.Intercepted;
import io.micronaut.aop.InterceptedProxy;
import io.micronaut.aop.Interceptor;
import io.micronaut.aop.InterceptorKind;
import io.micronaut.aop.InterceptorRegistry;
import io.micronaut.aop.Introduced;
import io.micronaut.aop.chain.InterceptorChain;
import io.micronaut.aop.chain.MethodInterceptorChain;
import io.micronaut.aop.internal.intercepted.InterceptedMethodUtil;
import io.micronaut.context.BeanContext;
import io.micronaut.context.BeanDefinitionRegistry;
import io.micronaut.context.BeanLocator;
import io.micronaut.context.BeanRegistration;
import io.micronaut.context.BeanResolutionContext;
import io.micronaut.context.DefaultBeanContext;
import io.micronaut.context.Qualifier;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationUtil;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Generated;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.value.OptionalValues;
import io.micronaut.inject.BeanDefinition;
import io.micronaut.inject.ExecutableMethod;
import io.micronaut.inject.ProxyBeanDefinition;
import io.micronaut.inject.annotation.AnnotationMetadataReference;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.Element;
import io.micronaut.inject.ast.ElementQuery;
import io.micronaut.inject.ast.FieldElement;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.ast.ParameterElement;
import io.micronaut.inject.ast.PrimitiveElement;
import io.micronaut.inject.ast.TypedElement;
import io.micronaut.inject.configuration.ConfigurationMetadataBuilder;
import io.micronaut.inject.qualifiers.Qualified;
import io.micronaut.inject.visitor.VisitorContext;
import io.micronaut.inject.writer.ArgumentExpUtils;
import io.micronaut.inject.writer.BeanDefinitionWriter;
import io.micronaut.inject.writer.ClassOutputWriter;
import io.micronaut.inject.writer.ClassWriterOutputVisitor;
import io.micronaut.inject.writer.ExecutableMethodsDefinitionWriter;
import io.micronaut.inject.writer.MethodGenUtils;
import io.micronaut.inject.writer.OriginatingElements;
import io.micronaut.inject.writer.ProxyingBeanDefinitionVisitor;
import io.micronaut.sourcegen.bytecode.ByteCodeWriter;
import io.micronaut.sourcegen.model.ClassDef;
import io.micronaut.sourcegen.model.ClassTypeDef;
import io.micronaut.sourcegen.model.ExpressionDef;
import io.micronaut.sourcegen.model.FieldDef;
import io.micronaut.sourcegen.model.MethodDef;
import io.micronaut.sourcegen.model.ParameterDef;
import io.micronaut.sourcegen.model.StatementDef;
import io.micronaut.sourcegen.model.TypeDef;
import io.micronaut.sourcegen.model.VariableDef;

import javax.lang.model.element.Modifier;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static io.micronaut.core.annotation.AnnotationUtil.ZERO_ANNOTATION_VALUES;
import static io.micronaut.inject.ast.ParameterElement.ZERO_PARAMETER_ELEMENTS;

/**
 * A class that generates AOP proxy classes at compile time.
 *
 * @author Graeme Rocher
 * @since 1.0
 */
@Internal
public class AopProxyWriter implements ProxyingBeanDefinitionVisitor, ClassOutputWriter {

    public static final int ADDITIONAL_PARAMETERS_COUNT = 5;

    private static final Method METHOD_GET_PROXY_TARGET_BEAN_WITH_BEAN_DEFINITION_AND_CONTEXT = ReflectionUtils.getRequiredInternalMethod(
        DefaultBeanContext.class,
        "getProxyTargetBean",
        BeanResolutionContext.class,
        BeanDefinition.class,
        Argument.class,
        Qualifier.class
    );

    private static final Method METHOD_GET_PROXY_BEAN_DEFINITION = ReflectionUtils.getRequiredInternalMethod(
        BeanDefinitionRegistry.class,
        "getProxyTargetBeanDefinition",
        Argument.class,
        Qualifier.class
    );

    private static final Method METHOD_INTERCEPTED_TARGET = ReflectionUtils.getRequiredInternalMethod(
        InterceptedProxy.class,
        "interceptedTarget"
    );

    private static final Method METHOD_HAS_CACHED_INTERCEPTED_METHOD = ReflectionUtils.getRequiredInternalMethod(
        InterceptedProxy.class,
        "hasCachedInterceptedTarget"
    );

    private static final Method METHOD_BEAN_DEFINITION_GET_REQUIRED_METHOD = ReflectionUtils.getRequiredInternalMethod(
        BeanDefinition.class,
        "getRequiredMethod",
        String.class,
        Class[].class
    );

    private static final Method GET_READ_LOCK_METHOD = ReflectionUtils.getRequiredInternalMethod(
        ReadWriteLock.class,
        "readLock"
    );

    private static final Method GET_WRITE_LOCK_METHOD = ReflectionUtils.getRequiredInternalMethod(
        ReadWriteLock.class,
        "writeLock"
    );

    private static final Method LOCK_METHOD = ReflectionUtils.getRequiredInternalMethod(
        Lock.class,
        "lock"
    );

    private static final Method UNLOCK_METHOD = ReflectionUtils.getRequiredInternalMethod(
        Lock.class,
        "unlock"
    );

    private static final Method SWAP_METHOD = ReflectionUtils.getRequiredInternalMethod(
        HotSwappableInterceptedProxy.class,
        "swap",
        Object.class
    );

    private static final Method WITH_QUALIFIER_METHOD = ReflectionUtils.getRequiredInternalMethod(
        Qualified.class,
        "$withBeanQualifier",
        Qualifier.class
    );

    private static final String FIELD_TARGET = "$target";
    private static final String FIELD_BEAN_RESOLUTION_CONTEXT = "$beanResolutionContext";
    private static final String FIELD_READ_WRITE_LOCK = "$target_rwl";
    private static final String FIELD_READ_LOCK = "$target_rl";
    private static final String FIELD_WRITE_LOCK = "$target_wl";

    private static final Method RESOLVE_INTRODUCTION_INTERCEPTORS_METHOD = ReflectionUtils.getRequiredInternalMethod(InterceptorChain.class, "resolveIntroductionInterceptors", InterceptorRegistry.class, ExecutableMethod.class, List.class);

    private static final Method RESOLVE_AROUND_INTERCEPTORS_METHOD = ReflectionUtils.getRequiredInternalMethod(InterceptorChain.class, "resolveAroundInterceptors", InterceptorRegistry.class, ExecutableMethod.class, List.class);

    private static final Constructor<?> CONSTRUCTOR_METHOD_INTERCEPTOR_CHAIN = ReflectionUtils.findConstructor(MethodInterceptorChain.class, Interceptor[].class, Object.class, ExecutableMethod.class, Object[].class).orElseThrow(() ->
        new IllegalStateException("new MethodInterceptorChain(..) constructor not found. Incompatible version of Micronaut?")
    );

    private static final Constructor<?> CONSTRUCTOR_METHOD_INTERCEPTOR_CHAIN_NO_PARAMS = ReflectionUtils.findConstructor(MethodInterceptorChain.class, Interceptor[].class, Object.class, ExecutableMethod.class).orElseThrow(() ->
        new IllegalStateException("new MethodInterceptorChain(..) constructor not found. Incompatible version of Micronaut?")
    );
    private static final String INTERCEPTORS_PARAMETER = "$interceptors";

    private static final Method METHOD_PROCEED = ReflectionUtils.getRequiredInternalMethod(InterceptorChain.class, "proceed");

    private static final Method COPY_BEAN_CONTEXT_METHOD = ReflectionUtils.getRequiredMethod(BeanResolutionContext.class, "copy");

    private static final String FIELD_INTERCEPTORS = "$interceptors";
    private static final String FIELD_BEAN_LOCATOR = "$beanLocator";
    private static final String FIELD_BEAN_QUALIFIER = "$beanQualifier";
    private static final String FIELD_PROXY_METHODS = "$proxyMethods";
    private static final String FIELD_PROXY_BEAN_DEFINITION = "$proxyBeanDefinition";
    private static final ClassTypeDef METHOD_INTERCEPTOR_CHAIN_TYPE = ClassTypeDef.of(MethodInterceptorChain.class);

    private final String packageName;
    private final String targetClassShortName;
    private final String targetClassFullName;
    private final String proxyFullName;
    private final BeanDefinitionWriter proxyBeanDefinitionWriter;
    private final Set<AnnotationValue<?>> interceptorBinding;
    private final Set<ClassElement> interfaceTypes;
    private final boolean hotswap;
    private final boolean lazy;
    private final boolean cacheLazyTarget;
    private final boolean isInterface;
    private final BeanDefinitionWriter parentWriter;
    private final boolean isIntroduction;
    private final boolean implementInterface;
    private final boolean isProxyTarget;

    private final List<MethodRef> proxiedMethods = new ArrayList<>();
    private final Set<MethodRef> proxiedMethodsRefSet = new HashSet<>();
    private final List<MethodRef> proxyTargetMethods = new ArrayList<>();
    private int proxyMethodCount = 0;
    private int interceptorsListArgumentIndex;
    private int beanResolutionContextArgumentIndex = -1;
    private int beanContextArgumentIndex = -1;
    private int interceptorRegistryArgumentIndex = -1;
    private int qualifierIndex;
    private final List<Runnable> deferredInjectionPoints = new ArrayList<>();
    private boolean constructorRequiresReflection;
    private MethodElement declaredConstructor;
    private MethodElement newConstructor;
    private MethodElement realConstructor;
    private List<Map.Entry<ParameterElement, Integer>> superConstructorParametersBinding;
    private ParameterElement qualifierParameter;
    private ParameterElement interceptorsListParameter;
    private VisitorContext visitorContext;

    private final OriginatingElements originatingElements;

    private final ClassDef.ClassDefBuilder proxyBuilder;
    private final FieldDef interceptorsField;
    private final FieldDef proxyMethodsField;
    private FieldDef targetField;

    /**
     * <p>Constructs a new {@link AopProxyWriter} for the given parent {@link BeanDefinitionWriter} and starting interceptors types.</p>
     *
     * <p>Additional {@link Interceptor} types can be added downstream with {@link #visitInterceptorBinding(AnnotationValue[])} .</p>
     *
     * @param parent             The parent {@link BeanDefinitionWriter}
     * @param settings           optional setting
     * @param visitorContext     The visitor context
     * @param interceptorBinding The interceptor binding of the {@link Interceptor} instances to be injected
     */
    public AopProxyWriter(BeanDefinitionWriter parent,
                          OptionalValues<Boolean> settings,
                          VisitorContext visitorContext,
                          AnnotationValue<?>... interceptorBinding) {

        this.originatingElements = OriginatingElements.of(parent.getOriginatingElements());

        this.isIntroduction = false;
        this.implementInterface = true;
        this.parentWriter = parent;
        this.isProxyTarget = settings.get(Interceptor.PROXY_TARGET).orElse(false) || parent.isInterface();
        parent.setProxiedBean(true, isProxyTarget);
        this.hotswap = isProxyTarget && settings.get(Interceptor.HOTSWAP).orElse(false);
        this.lazy = isProxyTarget && settings.get(Interceptor.LAZY).orElse(false);
        this.cacheLazyTarget = lazy && settings.get(Interceptor.CACHEABLE_LAZY_TARGET).orElse(false);
        this.isInterface = parent.isInterface();
        this.packageName = parent.getPackageName();
        this.targetClassShortName = parent.getBeanSimpleName();
        this.targetClassFullName = packageName + '.' + targetClassShortName;

        this.proxyFullName = parent.getBeanDefinitionName() + PROXY_SUFFIX;
        this.interceptorBinding = toInterceptorBindingMap(interceptorBinding);
        this.interfaceTypes = Collections.emptySet();
        final ClassElement aopElement = ClassElement.of(proxyFullName, isInterface, parent.getAnnotationMetadata());
        this.proxyBeanDefinitionWriter = new BeanDefinitionWriter(
            aopElement,
            parent,
            visitorContext
        );
        proxyBeanDefinitionWriter.setRequiresMethodProcessing(parent.requiresMethodProcessing());
        proxyBeanDefinitionWriter.setInterceptedType(targetClassFullName);

        proxyBuilder = ClassDef.builder(proxyFullName).synthetic();

        interceptorsField = FieldDef.builder(FIELD_INTERCEPTORS, Interceptor[][].class)
            .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
            .build();

        proxyBuilder.addField(interceptorsField);

        proxyMethodsField = FieldDef.builder(FIELD_PROXY_METHODS, ExecutableMethod[].class)
            .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
            .build();

        proxyBuilder.addField(proxyMethodsField);

        if (cacheLazyTarget || hotswap) {
            targetField = FieldDef.builder(FIELD_TARGET, ClassTypeDef.of(targetClassFullName)).addModifiers(Modifier.PRIVATE).build();
            proxyBuilder.addField(targetField);
        } else if (!lazy) {
            targetField = FieldDef.builder(FIELD_TARGET, ClassTypeDef.of(targetClassFullName)).addModifiers(Modifier.PRIVATE, Modifier.FINAL).build();
            proxyBuilder.addField(targetField);
        }

        this.visitorContext = visitorContext;
    }

    /**
     * Constructs a new {@link AopProxyWriter} for the purposes of writing {@link io.micronaut.aop.Introduction} advise.
     *
     * @param packageName        The package name
     * @param className          The class name
     * @param isInterface        Is the target of the advice an interface
     * @param originatingElement The originating element
     * @param annotationMetadata The annotation metadata
     * @param interfaceTypes     The additional interfaces to implement
     * @param visitorContext     The visitor context
     * @param interceptorBinding The interceptor types
     */
    public AopProxyWriter(String packageName,
                          String className,
                          boolean isInterface,
                          Element originatingElement,
                          AnnotationMetadata annotationMetadata,
                          ClassElement[] interfaceTypes,
                          VisitorContext visitorContext,
                          AnnotationValue<?>... interceptorBinding) {
        this(packageName, className, isInterface, true, originatingElement, annotationMetadata, interfaceTypes, visitorContext, interceptorBinding);
    }

    /**
     * Constructs a new {@link AopProxyWriter} for the purposes of writing {@link io.micronaut.aop.Introduction} advise.
     *
     * @param packageName        The package name
     * @param className          The class name
     * @param isInterface        Is the target of the advice an interface
     * @param implementInterface Whether the interface should be implemented. If false the {@code interfaceTypes} argument should contain at least one entry
     * @param originatingElement The originating elements
     * @param annotationMetadata The annotation metadata
     * @param interfaceTypes     The additional interfaces to implement
     * @param visitorContext     The visitor context
     * @param interceptorBinding The interceptor binding
     */
    public AopProxyWriter(String packageName,
                          String className,
                          boolean isInterface,
                          boolean implementInterface,
                          Element originatingElement,
                          AnnotationMetadata annotationMetadata,
                          ClassElement[] interfaceTypes,
                          VisitorContext visitorContext,
                          AnnotationValue<?>... interceptorBinding) {
        this.originatingElements = OriginatingElements.of(originatingElement);
        this.isIntroduction = true;
        this.implementInterface = implementInterface;

        if (!implementInterface && ArrayUtils.isEmpty(interfaceTypes)) {
            throw new IllegalArgumentException("if argument implementInterface is false at least one interface should be provided to the 'interfaceTypes' argument");
        }

        this.packageName = packageName;
        this.isInterface = isInterface;
        this.isProxyTarget = false;
        this.hotswap = false;
        this.lazy = false;
        this.cacheLazyTarget = false;
        this.targetClassShortName = className;
        this.targetClassFullName = packageName + '.' + targetClassShortName;
        this.parentWriter = null;
        this.proxyFullName = targetClassFullName + PROXY_SUFFIX;
        this.interceptorBinding = toInterceptorBindingMap(interceptorBinding);
        this.interfaceTypes = interfaceTypes != null ? new LinkedHashSet<>(Arrays.asList(interfaceTypes)) : Collections.emptySet();
        ClassElement aopElement = ClassElement.of(
            proxyFullName,
            isInterface,
            annotationMetadata
        );
        this.proxyBeanDefinitionWriter = new BeanDefinitionWriter(
            aopElement,
            this,
            visitorContext
        );
        if (isInterface) {
            if (implementInterface) {
                proxyBeanDefinitionWriter.setInterceptedType(targetClassFullName);
            }
        } else {
            proxyBeanDefinitionWriter.setInterceptedType(targetClassFullName);
        }

        proxyBuilder = ClassDef.builder(proxyFullName).synthetic();

        interceptorsField = FieldDef.builder(FIELD_INTERCEPTORS, Interceptor[][].class)
            .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
            .build();

        proxyBuilder.addField(interceptorsField);

        proxyMethodsField = FieldDef.builder(FIELD_PROXY_METHODS, ExecutableMethod[].class)
            .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
            .build();

        proxyBuilder.addField(proxyMethodsField);

        this.visitorContext = visitorContext;
    }

    /**
     * Find the interceptors list constructor parameter index.
     *
     * @param parameters The constructor parameters
     * @return the index
     */
    public static int findInterceptorsListParameterIndex(List<ParameterElement> parameters) {
        return parameters.indexOf(parameters.stream().filter(p -> p.getName().equals(INTERCEPTORS_PARAMETER)).findFirst().orElseThrow());
    }

    @Override
    public boolean isEnabled() {
        return proxyBeanDefinitionWriter.isEnabled();
    }

    /**
     * Is the target bean being proxied.
     *
     * @return True if the target bean is being proxied
     */
    @Override
    public boolean isProxyTarget() {
        return false;
    }

    @Override
    public Element getOriginatingElement() {
        return originatingElements.getOriginatingElements()[0];
    }

    @Override
    public void visitBeanFactoryMethod(ClassElement factoryClass, MethodElement factoryMethod) {
        proxyBeanDefinitionWriter.visitBeanFactoryMethod(factoryClass, factoryMethod);
    }

    @Override
    public void visitBeanFactoryMethod(ClassElement factoryClass, MethodElement factoryMethod, ParameterElement[] parameters) {
        proxyBeanDefinitionWriter.visitBeanFactoryMethod(factoryClass, factoryMethod, parameters);
    }

    @Override
    public void visitBeanFactoryField(ClassElement factoryClass, FieldElement factoryField) {
        proxyBeanDefinitionWriter.visitBeanFactoryField(factoryClass, factoryField);
    }

    @Override
    public boolean isSingleton() {
        return proxyBeanDefinitionWriter.isSingleton();
    }

    @Override
    public boolean isInterface() {
        return isInterface;
    }

    @Override
    public void visitBeanDefinitionInterface(Class<? extends BeanDefinition> interfaceType) {
        proxyBeanDefinitionWriter.visitBeanDefinitionInterface(interfaceType);
    }

    @Override
    public String getBeanTypeName() {
        return proxyBeanDefinitionWriter.getBeanTypeName();
    }

    @Override
    public void setValidated(boolean validated) {
        proxyBeanDefinitionWriter.setValidated(validated);
    }

    @Override
    public void setInterceptedType(String typeName) {
        proxyBeanDefinitionWriter.setInterceptedType(typeName);
    }

    @Override
    public Optional<String> getInterceptedType() {
        return proxyBeanDefinitionWriter.getInterceptedType();
    }

    @Override
    public boolean isValidated() {
        return proxyBeanDefinitionWriter.isValidated();
    }

    @Override
    public String getBeanDefinitionName() {
        return proxyBeanDefinitionWriter.getBeanDefinitionName();
    }

    /**
     * Visits a constructor.
     *
     * @param constructor        The constructor
     * @param requiresReflection Whether reflection is required
     * @param visitorContext     The visitor context
     */
    @Override
    public void visitBeanDefinitionConstructor(
        MethodElement constructor,
        boolean requiresReflection,
        VisitorContext visitorContext) {
        this.constructorRequiresReflection = requiresReflection;
        this.declaredConstructor = constructor;
        this.visitorContext = visitorContext;
        AnnotationValue<?>[] interceptorTypes =
            InterceptedMethodUtil.resolveInterceptorBinding(constructor.getAnnotationMetadata(), InterceptorKind.AROUND_CONSTRUCT);
        visitInterceptorBinding(interceptorTypes);
    }

    @Override
    public void visitDefaultConstructor(AnnotationMetadata annotationMetadata, VisitorContext visitorContext) {
        this.constructorRequiresReflection = false;
        this.visitorContext = visitorContext;
        ClassElement classElement = ClassElement.of(proxyFullName);
        this.declaredConstructor = MethodElement.of(
            classElement,
            annotationMetadata,
            classElement,
            classElement,
            "<init>"
        );
    }

    private void initConstructor(MethodElement constructor) {
        final ClassElement interceptorList = ClassElement.of(List.class, AnnotationMetadata.EMPTY_METADATA, Collections.singletonMap(
            "E", ClassElement.of(BeanRegistration.class, AnnotationMetadata.EMPTY_METADATA, Collections.singletonMap(
                "T", ClassElement.of(Interceptor.class)
            ))
        ));
        this.qualifierParameter = ParameterElement.of(Qualifier.class, "$qualifier");
        this.interceptorsListParameter = ParameterElement.of(interceptorList, INTERCEPTORS_PARAMETER);
        ParameterElement interceptorRegistryParameter = ParameterElement.of(ClassElement.of(InterceptorRegistry.class), "$interceptorRegistry");
        ClassElement proxyClass = ClassElement.of(proxyFullName);
        superConstructorParametersBinding = new ArrayList<>();
        ParameterElement[] constructorParameters = constructor.getParameters();
        List<ParameterElement> newConstructorParameters = new ArrayList<>(constructorParameters.length + 5);
        newConstructorParameters.addAll(Arrays.asList(constructorParameters));
        int superConstructorParameterIndex = 0;
        for (ParameterElement newConstructorParameter : newConstructorParameters) {
            superConstructorParametersBinding.add(Map.entry(newConstructorParameter, superConstructorParameterIndex++));
        }

        ParameterElement beanResolutionContext = ParameterElement.of(BeanResolutionContext.class, "$beanResolutionContext");
        newConstructorParameters.add(beanResolutionContext);
        ParameterElement beanContext = ParameterElement.of(BeanContext.class, "$beanContext");
        newConstructorParameters.add(beanContext);
        newConstructorParameters.add(qualifierParameter);
        newConstructorParameters.add(interceptorsListParameter);
        newConstructorParameters.add(interceptorRegistryParameter);
        superConstructorParameterIndex += 5; // Skip internal parameters
        if (MethodGenUtils.hasKotlinDefaultsParameters(List.of(constructorParameters))) {
            List<ParameterElement> realNewConstructorParameters = new ArrayList<>(newConstructorParameters);
            int count = MethodGenUtils.calculateNumberOfKotlinDefaultsMasks(List.of(constructorParameters));
            for (int j = 0; j < count; j++) {
                ParameterElement mask = ParameterElement.of(PrimitiveElement.INT, "mask" + j);
                realNewConstructorParameters.add(mask);
                superConstructorParametersBinding.add(Map.entry(mask, superConstructorParameterIndex++));
            }
            ParameterElement marker = ParameterElement.of(ClassElement.of("kotlin.jvm.internal.DefaultConstructorMarker"), "marker");
            realNewConstructorParameters.add(marker);
            superConstructorParametersBinding.add(Map.entry(marker, superConstructorParameterIndex));

            this.realConstructor = MethodElement.of(
                proxyClass,
                constructor.getAnnotationMetadata(),
                proxyClass,
                proxyClass,
                "<init>",
                realNewConstructorParameters.toArray(ZERO_PARAMETER_ELEMENTS)
            );
        }
        this.newConstructor = MethodElement.of(
            proxyClass,
            constructor.getAnnotationMetadata(),
            proxyClass,
            proxyClass,
            "<init>",
            newConstructorParameters.toArray(ZERO_PARAMETER_ELEMENTS)
        );
        if (realConstructor == null) {
            realConstructor = newConstructor;
        }

        this.beanResolutionContextArgumentIndex = newConstructorParameters.indexOf(beanResolutionContext);
        this.beanContextArgumentIndex = newConstructorParameters.indexOf(beanContext);
        this.qualifierIndex = newConstructorParameters.indexOf(qualifierParameter);
        this.interceptorsListArgumentIndex = newConstructorParameters.indexOf(interceptorsListParameter);
        this.interceptorRegistryArgumentIndex = newConstructorParameters.indexOf(interceptorRegistryParameter);
    }

    @NonNull
    @Override
    public String getBeanDefinitionReferenceClassName() {
        return proxyBeanDefinitionWriter.getBeanDefinitionReferenceClassName();
    }

    /**
     * Visit an abstract method that is to be implemented.
     *
     * @param declaringBean The declaring bean of the method.
     * @param methodElement The method element
     */
    public void visitIntroductionMethod(TypedElement declaringBean,
                                        MethodElement methodElement) {
        visitAroundMethod(declaringBean, methodElement);
    }

    /**
     * Visit a method that is to be proxied.
     *
     * @param beanType      The bean type.
     * @param methodElement The method element
     **/
    public void visitAroundMethod(TypedElement beanType,
                                  MethodElement methodElement) {

        final Optional<MethodElement> overridden = methodElement.getOwningType()
            .getEnclosedElement(ElementQuery.ALL_METHODS
                .onlyInstance()
                .filter(el -> el.getName().equals(methodElement.getName()) && el.overrides(methodElement)));

        if (overridden.isPresent()) {
            MethodElement overriddenBy = overridden.get();

            String methodElementKey = methodElement.getName() +
                Arrays.stream(methodElement.getSuspendParameters())
                    .map(p -> toTypeString(p.getType()))
                    .collect(Collectors.joining(","));

            String overriddenByKey = overriddenBy.getName() +
                Arrays.stream(methodElement.getSuspendParameters())
                    .map(p -> toTypeString(p.getGenericType()))
                    .collect(Collectors.joining(","));

            if (!methodElementKey.equals(overriddenByKey)) {
                proxyBuilder.addMethod(MethodDef.override(methodElement)
                    .build((aThis, methodParameters) -> aThis.superRef().invoke(overriddenBy, methodParameters).returning())
                );
                return;
            }
        }

        String methodName = methodElement.getName();
        List<ParameterElement> argumentTypeList = Arrays.asList(methodElement.getSuspendParameters());
        ClassElement returnType = methodElement.isSuspend() ? ClassElement.of(Object.class) : methodElement.getReturnType();
        MethodRef methodKey = new MethodRef(methodName, argumentTypeList, TypeDef.erasure(returnType));

        if (!proxiedMethodsRefSet.contains(methodKey)) {

            String interceptedProxyClassName = null;
            String interceptedProxyBridgeMethodName = null;

            if (!isProxyTarget) {
                // if the target is not being proxied then we need to generate a bridge method and executable method that knows about it

                if (!methodElement.isAbstract() || methodElement.isDefault()) {
                    interceptedProxyClassName = proxyFullName;
                    interceptedProxyBridgeMethodName = "$$access$$" + methodName;

                    // now build a bridge to invoke the original method
                    proxyBuilder.addMethod(
                        MethodDef.builder(interceptedProxyBridgeMethodName)
                            .addModifiers(Modifier.PUBLIC)
                            .addParameters(argumentTypeList.stream().map(p -> ParameterDef.of(p.getName(), TypeDef.erasure(p.getType()))).toList())
                            .returns(TypeDef.erasure(returnType))
                            .build((aThis, methodParameters) -> aThis.superRef((ClassTypeDef) TypeDef.erasure(methodElement.getOwningType()))
                                .invoke(methodElement, methodParameters).returning())
                    );
                }
            }

            BeanDefinitionWriter beanDefinitionWriter = parentWriter == null ? proxyBeanDefinitionWriter : parentWriter;
            int methodIndex = beanDefinitionWriter.visitExecutableMethod(
                beanType,
                methodElement,
                interceptedProxyClassName,
                interceptedProxyBridgeMethodName
            );
            int index = proxyMethodCount++;

            methodKey.methodIndex = methodIndex;
            proxiedMethods.add(methodKey);
            proxiedMethodsRefSet.add(methodKey);
            proxyTargetMethods.add(methodKey);

            proxyBuilder.addMethod(
                buildMethodOverride(methodElement, index)
            );
        }
    }

    private MethodDef buildMethodOverride(MethodElement methodElement, int index) {
        return MethodDef.override(methodElement)
            .build((aThis, methodParameters) -> {

                ExpressionDef targetArgument;
                if (isProxyTarget) {
                    if (hotswap || lazy) {
                        targetArgument = aThis.invoke(METHOD_INTERCEPTED_TARGET);
                    } else {
                        targetArgument = aThis.field(targetField);
                    }
                } else {
                    targetArgument = aThis;
                }

                ExpressionDef.InvokeInstanceMethod invocation;
                if (methodParameters.isEmpty()) {
                    // invoke MethodInterceptorChain constructor without parameters
                    invocation = METHOD_INTERCEPTOR_CHAIN_TYPE.instantiate(
                        CONSTRUCTOR_METHOD_INTERCEPTOR_CHAIN_NO_PARAMS,

                        // 1st argument: interceptors
                        aThis.field(interceptorsField).arrayElement(index),
                        // 2nd argument: this or target
                        targetArgument,
                        // 3rd argument: the executable method
                        aThis.field(proxyMethodsField).arrayElement(index)
                        // fourth argument: array of the argument values
                    ).invoke(METHOD_PROCEED);
                } else {
                    // invoke MethodInterceptorChain constructor with parameters
                    invocation = METHOD_INTERCEPTOR_CHAIN_TYPE.instantiate(
                        CONSTRUCTOR_METHOD_INTERCEPTOR_CHAIN,

                        // 1st argument: interceptors
                        aThis.field(interceptorsField).arrayElement(index),
                        // 2nd argument: this or target
                        targetArgument,
                        // 3rd argument: the executable method
                        aThis.field(proxyMethodsField).arrayElement(index),
                        // 4th argument: array of the argument values
                        TypeDef.OBJECT.array().instantiate(methodParameters)
                    ).invoke(METHOD_PROCEED);
                }
                if (!methodElement.getReturnType().isVoid() || methodElement.isSuspend()) {
                    return invocation.returning();
                }
                return invocation;
            });
    }

    /**
     * Finalizes the proxy. This method should be called before writing the proxy to disk with {@link #writeTo(File)}
     */
    @Override
    public void visitBeanDefinitionEnd() {
        ClassTypeDef targetType = ClassTypeDef.of(targetClassFullName);

        if (!isInterface) {
            proxyBuilder.superclass(targetType);
        }
        List<ClassTypeDef> interfaces = new ArrayList<>();
        interfaceTypes.stream().map(typedElement -> (ClassTypeDef) TypeDef.erasure(typedElement)).forEach(interfaces::add);
        if (isInterface && implementInterface) {
            interfaces.add(targetType);
        }
        interfaces.sort(Comparator.comparing(ClassTypeDef::getName));
        interfaces.forEach(proxyBuilder::addSuperinterface);

        proxyBuilder.addAnnotation(Generated.class);

        if (declaredConstructor == null) {
            throw new IllegalStateException("The method visitBeanDefinitionConstructor(..) should be called at least once");
        } else {
            initConstructor(declaredConstructor);
        }

        if (parentWriter != null && !isProxyTarget) {
            processAlreadyVisitedMethods(parentWriter);
        }

        interceptorsListParameter.annotate(AnnotationUtil.ANN_INTERCEPTOR_BINDING_QUALIFIER, builder -> {
            final AnnotationValue<?>[] interceptorBinding = this.interceptorBinding.toArray(ZERO_ANNOTATION_VALUES);
            builder.values(interceptorBinding);
        });
        qualifierParameter.annotate(AnnotationUtil.NULLABLE);

        proxyBeanDefinitionWriter.visitBeanDefinitionConstructor(
            newConstructor,
            constructorRequiresReflection,
            visitorContext
        );

        if (parentWriter != null) {
            proxyBeanDefinitionWriter.visitBeanDefinitionInterface(ProxyBeanDefinition.class);
            proxyBeanDefinitionWriter.generateProxyReference(parentWriter.getBeanDefinitionName(), parentWriter.getBeanTypeName());
        }

        if (isProxyTarget) {
            generateProxyTarget(targetType);
        } else {
            proxyBuilder.addSuperinterface(TypeDef.of(isIntroduction ? Introduced.class : Intercepted.class));
            proxyBuilder.addMethod(MethodDef.constructor()
                .addParameters(Arrays.stream(realConstructor.getParameters()).map(p -> TypeDef.erasure(p.getType())).toList())
                .build((aThis, methodParameters) -> StatementDef.multi(
                    invokeSuperConstructor(aThis, methodParameters),
                    initializeProxyMethodsAndInterceptors(aThis, methodParameters)
                )));
        }

        for (Runnable fieldInjectionPoint : deferredInjectionPoints) {
            fieldInjectionPoint.run();
        }

        proxyBeanDefinitionWriter.visitBeanDefinitionEnd();
    }

    private void generateProxyTarget(ClassTypeDef targetType) {
        List<MethodDef.MethodBodyBuilder> bodyBuilders = new ArrayList<>();

        FieldDef proxyBeanDefinitionField = FieldDef.builder(FIELD_PROXY_BEAN_DEFINITION, BeanDefinition.class)
            .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
            .build();
        proxyBuilder.addField(proxyBeanDefinitionField);
        bodyBuilders.add((aThis, methodParameters) -> aThis.field(proxyBeanDefinitionField).assign(
            methodParameters.get(beanContextArgumentIndex).invoke(
                METHOD_GET_PROXY_BEAN_DEFINITION,

                // 1nd argument: the type
                pushTargetArgument(targetType),
                // 2rd argument: the qualifier
                methodParameters.get(qualifierIndex)
            )
        ));

        FieldDef beanQualifierField = FieldDef.builder(FIELD_BEAN_QUALIFIER, TypeDef.of(Qualifier.class))
            .addModifiers(Modifier.PRIVATE)
            .build();
        proxyBuilder.addField(beanQualifierField);
        proxyBuilder.addMethod(writeWithQualifierMethod(beanQualifierField));
        bodyBuilders.add((aThis, methodParameters) ->
            aThis.field(beanQualifierField).assign(methodParameters.get(qualifierIndex)));

        MethodDef interceptedTargetMethod;
        if (lazy) {
            proxyBuilder.addSuperinterface(TypeDef.of(InterceptedProxy.class));

            FieldDef beanResolutionContextField = FieldDef.builder(FIELD_BEAN_RESOLUTION_CONTEXT, BeanResolutionContext.class)
                .addModifiers(Modifier.PRIVATE)
                .build();

            proxyBuilder.addField(beanResolutionContextField);

            FieldDef beanLocatorField = FieldDef.builder(FIELD_BEAN_LOCATOR, BeanLocator.class)
                .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
                .build();

            proxyBuilder.addField(beanLocatorField);

            if (cacheLazyTarget) {
                interceptedTargetMethod = getCacheLazyTargetInterceptedTargetMethod(
                    targetField,
                    beanLocatorField,
                    beanResolutionContextField,
                    proxyBeanDefinitionField,
                    beanQualifierField
                );
                proxyBuilder.addMethod(
                    getHasCachedInterceptedTargetMethod(targetField)
                );
            } else {
                interceptedTargetMethod = getLazyInterceptedTargetMethod(
                    beanLocatorField,
                    beanResolutionContextField,
                    proxyBeanDefinitionField,
                    beanQualifierField
                );
            }

            bodyBuilders.add((aThis, methodParameters) -> StatementDef.multi(
                aThis.field(beanLocatorField).assign(methodParameters.get(beanContextArgumentIndex)),
                aThis.field(beanResolutionContextField).assign(
                    methodParameters.get(beanResolutionContextArgumentIndex)
                        .invoke(COPY_BEAN_CONTEXT_METHOD)
                )
            ));
        } else {
            if (hotswap) {
                proxyBuilder.addSuperinterface(TypeDef.parameterized(HotSwappableInterceptedProxy.class, targetType));

                ClassTypeDef readWriteLockType = ClassTypeDef.of(ReentrantReadWriteLock.class);
                FieldDef readWriteLockField = FieldDef.builder(FIELD_READ_WRITE_LOCK, readWriteLockType)
                    .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
                    .initializer(readWriteLockType.instantiate())
                    .build();

                proxyBuilder.addField(readWriteLockField);

                ClassTypeDef lockType = ClassTypeDef.of(Lock.class);
                FieldDef readLockField = FieldDef.builder(FIELD_READ_LOCK, lockType)
                    .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
                    .initializer(new VariableDef.This().field(readWriteLockField).invoke(GET_READ_LOCK_METHOD))
                    .build();

                proxyBuilder.addField(readLockField);

                FieldDef writeLockField = FieldDef.builder(FIELD_WRITE_LOCK, lockType)
                    .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
                    .initializer(new VariableDef.This().field(readWriteLockField).invoke(GET_WRITE_LOCK_METHOD))
                    .build();

                proxyBuilder.addField(writeLockField);

                proxyBuilder.addMethod(
                    getSwapMethod(targetField, writeLockField)
                );
                interceptedTargetMethod = getHotSwapInterceptedTargetMethod(targetField, readLockField);
            } else {
                proxyBuilder.addSuperinterface(TypeDef.parameterized(InterceptedProxy.class, targetType));
                interceptedTargetMethod = getSimpleInterceptedTargetMethod(targetField);
            }

            // Non-lazy target
            bodyBuilders.add((aThis, methodParameters) -> aThis.field(targetField).assign(
                    methodParameters.get(beanContextArgumentIndex)
                        .cast(DefaultBeanContext.class).invoke(
                            METHOD_GET_PROXY_TARGET_BEAN_WITH_BEAN_DEFINITION_AND_CONTEXT,

                            // 1st argument: the bean resolution context
                            methodParameters.get(beanResolutionContextArgumentIndex),
                            // 2nd argument: this.$proxyBeanDefinition
                            aThis.field(proxyBeanDefinitionField),
                            // 3rd argument: the type
                            pushTargetArgument(targetType),
                            // 4th argument: the qualifier
                            methodParameters.get(qualifierIndex)
                        ).cast(targetType)
                )
            );
        }

        proxyBuilder.addMethod(interceptedTargetMethod);

        proxyBuilder.addMethod(MethodDef.constructor()
            .addParameters(Arrays.stream(realConstructor.getParameters()).map(p -> TypeDef.erasure(p.getType())).toList())
            .build((aThis, methodParameters) -> {
                List<StatementDef> constructorStatements = new ArrayList<>();
                constructorStatements.add(
                    invokeSuperConstructor(aThis, methodParameters)
                );
                bodyBuilders.forEach(bodyBuilder -> constructorStatements.add(bodyBuilder.apply(aThis, methodParameters)));
                constructorStatements.add(
                    initializeProxyTargetMethodsAndInterceptors(aThis, methodParameters, proxyBeanDefinitionField)
                );
                return StatementDef.multi(constructorStatements);
            }));
    }

    private StatementDef initializeProxyMethodsAndInterceptors(VariableDef.This aThis,
                                                               List<VariableDef.MethodParameter> parameters) {
        if (proxiedMethods.isEmpty()) {
            return StatementDef.multi();
        }
        BeanDefinitionWriter beanDefinitionWriter = parentWriter == null ? proxyBeanDefinitionWriter : parentWriter;
        ExecutableMethodsDefinitionWriter executableMethodsDefinitionWriter = beanDefinitionWriter.getExecutableMethodsWriter();
        ClassTypeDef executableMethodsType = executableMethodsDefinitionWriter.getClassTypeDef();
        ExpressionDef.NewInstance executableMethodsInstance;
        if (executableMethodsDefinitionWriter.isSupportsInterceptedProxy()) {
            executableMethodsInstance = executableMethodsType.instantiate(TypeDef.Primitive.BOOLEAN.constant(true));
        } else {
            executableMethodsInstance = executableMethodsType.instantiate();
        }
        AtomicInteger index = new AtomicInteger();
        return executableMethodsInstance.newLocal("executableMethods", executableMethodsVar -> StatementDef.multi(
            aThis.field(proxyMethodsField).assign(
                ClassTypeDef.of(ExecutableMethod.class).array().instantiate(
                    proxyTargetMethods.stream().map(methodRef ->
                        executableMethodsVar.invoke(
                            ExecutableMethodsDefinitionWriter.GET_EXECUTABLE_AT_INDEX_METHOD,

                            TypeDef.Primitive.INT.constant(methodRef.methodIndex)
                        )).toList()
                )
            ),
            aThis.field(interceptorsField).assign(
                ClassTypeDef.of(Interceptor.class).array(2).instantiate(
                    proxyTargetMethods.stream().map(methodRef -> {
                            int methodIndex = methodRef.methodIndex;
                            boolean introduction = isIntroduction && (
                                executableMethodsDefinitionWriter.isAbstract(methodIndex) || (
                                    executableMethodsDefinitionWriter.isInterface(methodIndex) && !executableMethodsDefinitionWriter.isDefault(methodIndex)));

                            return ClassTypeDef.of(InterceptorChain.class).invokeStatic(
                                (introduction ? RESOLVE_INTRODUCTION_INTERCEPTORS_METHOD : RESOLVE_AROUND_INTERCEPTORS_METHOD),

                                // First argument. The interceptor registry
                                parameters.get(interceptorRegistryArgumentIndex),
                                // Second argument i.e. proxyMethods[0]
                                aThis.field(proxyMethodsField).arrayElement(index.getAndIncrement()),
                                // Third argument i.e. interceptors
                                parameters.get(interceptorsListArgumentIndex)
                            );
                        }
                    ).toList()
                )
            )
        ));
    }

    private StatementDef initializeProxyTargetMethodsAndInterceptors(VariableDef.This aThis,
                                                                     List<VariableDef.MethodParameter> parameters,
                                                                     FieldDef proxyBeanDefinitionField) {
        if (proxiedMethods.size() != proxyMethodCount) {
            throw new IllegalStateException("Expected proxy methods count to match actual methods");
        }
        AtomicInteger index = new AtomicInteger();
        return StatementDef.multi(
            aThis.field(proxyMethodsField).assign(
                ClassTypeDef.of(ExecutableMethod.class).array().instantiate(
                    proxyTargetMethods.stream().map(methodRef ->
                        aThis.field(proxyBeanDefinitionField).invoke(
                            METHOD_BEAN_DEFINITION_GET_REQUIRED_METHOD,

                            ExpressionDef.constant(methodRef.name),
                            TypeDef.CLASS.array().instantiate(
                                methodRef.genericArgumentTypes.stream().map(t -> ExpressionDef.constant(TypeDef.erasure(t))).toList()
                            )
                        )
                    ).toList()
                )
            ),
            aThis.field(interceptorsField).assign(
                ClassTypeDef.of(Interceptor.class).array(2).instantiate(
                    proxyTargetMethods.stream().map(methodRef ->
                        ClassTypeDef.of(InterceptorChain.class).invokeStatic(
                            (isIntroduction ? RESOLVE_INTRODUCTION_INTERCEPTORS_METHOD : RESOLVE_AROUND_INTERCEPTORS_METHOD),

                            // First argument. The interceptor registry
                            parameters.get(interceptorRegistryArgumentIndex),
                            // Second argument i.e. proxyMethods[0]
                            aThis.field(proxyMethodsField).arrayElement(index.getAndIncrement()),
                            // Third argument i.e. interceptors
                            parameters.get(interceptorsListArgumentIndex)
                        )
                    ).toList()
                )
            )
        );
    }

    private ExpressionDef.InvokeInstanceMethod invokeSuperConstructor(VariableDef.This aThis, List<VariableDef.MethodParameter> methodParameters) {
        if (isInterface) {
            return aThis.superRef().invokeConstructor();
        }
        List<ExpressionDef> values = new ArrayList<>();
        List<TypeDef> arguments = new ArrayList<>();
        for (Map.Entry<ParameterElement, Integer> e : superConstructorParametersBinding) {
            values.add(methodParameters.get(e.getValue()));
            arguments.add(TypeDef.erasure(e.getKey().getType()));
        }
        return aThis.superRef().invokeConstructor(arguments, values);
    }

    private ExpressionDef.InvokeInstanceMethod pushResolveLazyProxyTargetBean(VariableDef.This aThis,
                                                                              List<VariableDef.MethodParameter> parameters,
                                                                              FieldDef beanLocatorField,
                                                                              FieldDef beanResolutionContextField,
                                                                              FieldDef proxyBeanDefinitionField,
                                                                              FieldDef beanQualifierField) {
        return aThis.field(beanLocatorField).cast(DefaultBeanContext.class).invoke(
            METHOD_GET_PROXY_TARGET_BEAN_WITH_BEAN_DEFINITION_AND_CONTEXT,

            // 1st argument: the bean resolution context
            aThis.field(beanResolutionContextField),
            // 2nd argument: this.$proxyBeanDefinition
            aThis.field(proxyBeanDefinitionField),
            // 3rd argument: the type
            pushTargetArgument(ClassTypeDef.of(targetClassFullName)),
            // 4th argument: the qualifier
            aThis.field(beanQualifierField)
        );
    }

    private ExpressionDef pushTargetArgument(TypeDef targetType) {
        return ArgumentExpUtils.buildArgumentWithGenerics(
            targetType,
            new AnnotationMetadataReference(
                getBeanDefinitionName(),
                getAnnotationMetadata()
            ),
            parentWriter != null ? parentWriter.getTypeArguments() : proxyBeanDefinitionWriter.getTypeArguments()
        );
    }

    @NonNull
    @Override
    public ClassElement[] getTypeArguments() {
        return proxyBeanDefinitionWriter.getTypeArguments();
    }

    @Override
    public Map<String, ClassElement> getTypeArgumentMap() {
        return proxyBeanDefinitionWriter.getTypeArgumentMap();
    }

    /**
     * Write the class to output via a visitor that manages output destination.
     *
     * @param visitor the writer output visitor
     * @throws IOException If an error occurs
     */
    @Override
    public void accept(ClassWriterOutputVisitor visitor) throws IOException {
        proxyBeanDefinitionWriter.accept(visitor);
        try (OutputStream out = visitor.visitClass(proxyFullName, getOriginatingElements())) {
            out.write(new ByteCodeWriter().write(proxyBuilder.build()));
        }
    }

    @Override
    public void visitSuperBeanDefinition(String name) {
        proxyBeanDefinitionWriter.visitSuperBeanDefinition(name);
    }

    @Override
    public void visitSuperBeanDefinitionFactory(String beanName) {
        proxyBeanDefinitionWriter.visitSuperBeanDefinitionFactory(beanName);
    }

    @Override
    public void visitSetterValue(
        TypedElement declaringType,
        MethodElement methodElement,
        AnnotationMetadata annotationMetadata,
        boolean requiresReflection,
        boolean isOptional) {
        deferredInjectionPoints.add(() ->
            proxyBeanDefinitionWriter.visitSetterValue(
                declaringType,
                methodElement,
                annotationMetadata,
                requiresReflection,
                isOptional
            )
        );
    }

    @Override
    public void visitPostConstructMethod(
        TypedElement declaringType,
        MethodElement methodElement,
        boolean requiresReflection,
        VisitorContext visitorContext) {
        deferredInjectionPoints.add(() -> proxyBeanDefinitionWriter.visitPostConstructMethod(
            declaringType,
            methodElement,
            requiresReflection,
            visitorContext
        ));
    }

    @Override
    public void visitPreDestroyMethod(
        TypedElement declaringType,
        MethodElement methodElement,
        boolean requiresReflection,
        VisitorContext visitorContext) {
        deferredInjectionPoints.add(() ->
            proxyBeanDefinitionWriter.visitPreDestroyMethod(
                declaringType,
                methodElement,
                requiresReflection,
                visitorContext)
        );
    }

    @Override
    public void visitMethodInjectionPoint(TypedElement beanType,
                                          MethodElement methodElement,
                                          boolean requiresReflection,
                                          VisitorContext visitorContext) {
        deferredInjectionPoints.add(() ->
            proxyBeanDefinitionWriter.visitMethodInjectionPoint(
                beanType,
                methodElement,
                requiresReflection,
                visitorContext)
        );
    }

    @Override
    public int visitExecutableMethod(
        TypedElement declaringBean,
        MethodElement methodElement,
        VisitorContext visitorContext) {
        deferredInjectionPoints.add(() ->
            proxyBeanDefinitionWriter.visitExecutableMethod(
                declaringBean,
                methodElement,
                visitorContext
            )
        );
        return -1;
    }

    @Override
    public void visitFieldInjectionPoint(
        TypedElement declaringType,
        FieldElement fieldType,
        boolean requiresReflection, VisitorContext visitorContext) {
        deferredInjectionPoints.add(() ->
            proxyBeanDefinitionWriter.visitFieldInjectionPoint(
                declaringType,
                fieldType,
                requiresReflection,
                visitorContext
            )
        );
    }

    @Override
    public void visitAnnotationMemberPropertyInjectionPoint(TypedElement annotationMemberBeanType,
                                                            String annotationMemberProperty,
                                                            String requiredValue,
                                                            String notEqualsValue) {
        deferredInjectionPoints.add(() ->
            proxyBeanDefinitionWriter.visitAnnotationMemberPropertyInjectionPoint(
                annotationMemberBeanType,
                annotationMemberProperty,
                requiredValue,
                notEqualsValue));
    }

    @Override
    public void visitFieldValue(TypedElement declaringType,
                                FieldElement fieldType,
                                boolean requiresReflection, boolean isOptional) {
        deferredInjectionPoints.add(() ->
            proxyBeanDefinitionWriter.visitFieldValue(
                declaringType,
                fieldType, requiresReflection, isOptional
            )
        );
    }

    @Override
    public String getPackageName() {
        return proxyBeanDefinitionWriter.getPackageName();
    }

    @Override
    public String getBeanSimpleName() {
        return proxyBeanDefinitionWriter.getBeanSimpleName();
    }

    @Override
    public AnnotationMetadata getAnnotationMetadata() {
        return proxyBeanDefinitionWriter.getAnnotationMetadata();
    }

    @Override
    public void visitConfigBuilderField(ClassElement type, String field, AnnotationMetadata annotationMetadata, ConfigurationMetadataBuilder metadataBuilder, boolean isInterface) {
        proxyBeanDefinitionWriter.visitConfigBuilderField(type, field, annotationMetadata, metadataBuilder, isInterface);
    }

    @Override
    public void visitConfigBuilderMethod(ClassElement type, String methodName, AnnotationMetadata annotationMetadata, ConfigurationMetadataBuilder metadataBuilder, boolean isInterface) {
        proxyBeanDefinitionWriter.visitConfigBuilderMethod(type, methodName, annotationMetadata, metadataBuilder, isInterface);
    }

    @Override
    public void visitConfigBuilderMethod(String propertyName, ClassElement returnType, String methodName, ClassElement paramType, Map<String, ClassElement> generics, String propertyPath) {
        proxyBeanDefinitionWriter.visitConfigBuilderMethod(propertyName, returnType, methodName, paramType, generics, propertyPath);
    }

    @Override
    public void visitConfigBuilderDurationMethod(String propertyName, ClassElement returnType, String methodName, String propertyPath) {
        proxyBeanDefinitionWriter.visitConfigBuilderDurationMethod(propertyName, returnType, methodName, propertyPath);
    }

    @Override
    public void visitConfigBuilderEnd() {
        proxyBeanDefinitionWriter.visitConfigBuilderEnd();
    }

    @Override
    public void setRequiresMethodProcessing(boolean shouldPreProcess) {
        proxyBeanDefinitionWriter.setRequiresMethodProcessing(shouldPreProcess);
    }

    @Override
    public void visitTypeArguments(Map<String, Map<String, ClassElement>> typeArguments) {
        proxyBeanDefinitionWriter.visitTypeArguments(typeArguments);
    }

    @Override
    public boolean requiresMethodProcessing() {
        return proxyBeanDefinitionWriter.requiresMethodProcessing() || (parentWriter != null && parentWriter.requiresMethodProcessing());
    }

    @Override
    public String getProxiedTypeName() {
        return targetClassFullName;
    }

    @Override
    public String getProxiedBeanDefinitionName() {
        return parentWriter != null ? parentWriter.getBeanDefinitionName() : null;
    }

    /**
     * visitInterceptorTypes.
     *
     * @param interceptorBinding the interceptor binding
     */
    public void visitInterceptorBinding(AnnotationValue<?>... interceptorBinding) {
        if (interceptorBinding != null) {
            for (AnnotationValue<?> annotationValue : interceptorBinding) {
                annotationValue.stringValue().ifPresent(annName ->
                    this.interceptorBinding.add(annotationValue)
                );
            }
        }
    }

    private Set<AnnotationValue<?>> toInterceptorBindingMap(AnnotationValue<?>[] interceptorBinding) {
        return new LinkedHashSet<>(Arrays.asList(interceptorBinding));
    }

    private MethodDef writeWithQualifierMethod(FieldDef beanQualifier) {
        return MethodDef.override(WITH_QUALIFIER_METHOD)
            .build((aThis, methodParameters) -> aThis.field(beanQualifier).put(methodParameters.get(0)));
    }

    private MethodDef getSwapMethod(FieldDef targetField, FieldDef writeField) {
        Objects.requireNonNull(targetField);
        Objects.requireNonNull(writeField);
        return MethodDef.override(SWAP_METHOD)
            .build((aThis, methodParameters) -> {
                VariableDef.Field lock = aThis.field(writeField);
                return StatementDef.multi(
                    lock.invoke(LOCK_METHOD),
                    StatementDef.doTry(
                        aThis.field(targetField).newLocal("target", targetVar -> StatementDef.multi(
                            aThis.field(targetField).assign(methodParameters.get(0)),
                            targetVar.returning()
                        ))
                    ).doFinally(lock.invoke(UNLOCK_METHOD))
                );
            });
    }

    private MethodDef getLazyInterceptedTargetMethod(FieldDef beanLocatorField,
                                                     FieldDef beanResolutionContextField,
                                                     FieldDef proxyBeanDefinitionField,
                                                     FieldDef beanQualifierField) {

        return MethodDef.override(METHOD_INTERCEPTED_TARGET)
            .build((aThis, methodParameters) -> pushResolveLazyProxyTargetBean(
                aThis,
                methodParameters,
                beanLocatorField,
                beanResolutionContextField,
                proxyBeanDefinitionField,
                beanQualifierField
            ).returning());
    }

    private MethodDef getCacheLazyTargetInterceptedTargetMethod(FieldDef targetField,
                                                                FieldDef beanLocatorField,
                                                                FieldDef beanResolutionContextField,
                                                                FieldDef proxyBeanDefinitionField,
                                                                FieldDef beanQualifierField) {

        return MethodDef.override(METHOD_INTERCEPTED_TARGET)
            .build((aThis, methodParameters) -> {
//                            B var1 = this.$target;
//                            if (var1 == null) {
//                                synchronized(this) {
//                                    var1 = this.$target;
//                                    if (var1 == null) {
//                                        this.$target = (B)((DefaultBeanContext)this.$beanLocator).getProxyTargetBean(this.$beanResolutionContext, this.$proxyBeanDefinition, Argument.of(B.class, $B$Definition$Intercepted$Definition.$ANNOTATION_METADATA, new Class[0]), this.$beanQualifier);
//                                        this.$beanResolutionContext = null;
//                                    }
//                                }
//                            }
//                            return this.$target;
                VariableDef.Field targetFieldAccess = aThis.field(targetField);
                return StatementDef.multi(
                    targetFieldAccess.newLocal("target", targetVar ->
                        targetVar.ifNull(
                            new StatementDef.Synchronized(
                                aThis,
                                StatementDef.multi(
                                    targetVar.assign(targetFieldAccess),
                                    targetVar.ifNull(
                                        StatementDef.multi(
                                            targetFieldAccess.assign(
                                                pushResolveLazyProxyTargetBean(
                                                    aThis,
                                                    methodParameters,
                                                    beanLocatorField,
                                                    beanResolutionContextField,
                                                    proxyBeanDefinitionField,
                                                    beanQualifierField)
                                            ),
                                            aThis.field(beanResolutionContextField).assign(ExpressionDef.nullValue())
                                        )
                                    )
                                )
                            )
                        )
                    ),
                    targetFieldAccess.returning()
                );
            });
    }

    private MethodDef getHotSwapInterceptedTargetMethod(FieldDef targetField,
                                                        FieldDef readLockField) {

        return MethodDef.override(METHOD_INTERCEPTED_TARGET)
            .build((aThis, methodParameters) -> {
                //       this.$target_rl.lock();
                //
                //        HotswappableProxyingClass var1;
                //        try {
                //            var1 = this.$target;
                //        } finally {
                //            this.$target_rl.unlock();
                //        }
                //
                //        return var1;
                return StatementDef.multi(
                    aThis.field(readLockField).invoke(LOCK_METHOD),
                    aThis.field(targetField).returning()
                        .doTry()
                        .doFinally(aThis.field(readLockField).invoke(UNLOCK_METHOD))
                );
            });
    }

    private MethodDef getSimpleInterceptedTargetMethod(FieldDef targetField) {
        Objects.requireNonNull(targetField);
        return MethodDef.override(METHOD_INTERCEPTED_TARGET)
            .build((aThis, methodParameters) -> aThis.field(targetField).returning());
    }

    private MethodDef getHasCachedInterceptedTargetMethod(FieldDef targetField) {
        Objects.requireNonNull(targetField);
        return MethodDef.builder(METHOD_HAS_CACHED_INTERCEPTED_METHOD.getName())
            .addModifiers(Modifier.PUBLIC)
            .addParameters(METHOD_HAS_CACHED_INTERCEPTED_METHOD.getParameterTypes())
            .build((aThis, methodParameters) -> aThis.field(targetField).isNonNull().returning());
    }

    private void processAlreadyVisitedMethods(BeanDefinitionWriter parent) {
        final List<BeanDefinitionWriter.MethodVisitData> postConstructMethodVisits = parent.getPostConstructMethodVisits();
        for (BeanDefinitionWriter.MethodVisitData methodVisit : postConstructMethodVisits) {
            visitPostConstructMethod(
                methodVisit.getBeanType(),
                methodVisit.getMethodElement(),
                methodVisit.isRequiresReflection(),
                visitorContext
            );
        }
    }

    /**
     * @param p The class element
     * @return The string representation
     */
    private static String toTypeString(ClassElement p) {
        String name = p.getName();
        if (p.isArray()) {
            return name + IntStream.range(0, p.getArrayDimensions()).mapToObj(ignore -> "[]").collect(Collectors.joining());
        }
        return name;
    }

    @Override
    public @NonNull Element[] getOriginatingElements() {
        return originatingElements.getOriginatingElements();
    }

    @Override
    public void addOriginatingElement(Element element) {
        originatingElements.addOriginatingElement(element);
    }

    /**
     * Method Reference class with names and a list of argument types. Used as the targets.
     */
    private static final class MethodRef {
        int methodIndex;
        private final String name;
        private final List<ClassElement> argumentTypes;
        private final List<ClassElement> genericArgumentTypes;
        private final TypeDef returnType;
        private final List<String> rawTypes;

        public MethodRef(String name, List<ParameterElement> parameterElements, TypeDef returnType) {
            this.name = name;
            this.argumentTypes = parameterElements.stream().map(ParameterElement::getType).toList();
            this.genericArgumentTypes = parameterElements.stream().map(ParameterElement::getGenericType).toList();
            this.rawTypes = this.argumentTypes.stream().map(AopProxyWriter::toTypeString).toList();
            this.returnType = returnType;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            MethodRef methodRef = (MethodRef) o;
            return Objects.equals(name, methodRef.name) &&
                Objects.equals(rawTypes, methodRef.rawTypes) &&
                Objects.equals(returnType, methodRef.returnType);
        }

        @Override
        public int hashCode() {
            return Objects.hash(name, rawTypes, returnType);
        }
    }
}