/*
 * Copyright 2014 - Present Rafael Winterhalter
 *
 * 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 net.bytebuddy.implementation.bind.annotation;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.build.HashCodeAndEqualsPlugin;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.method.ParameterDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.scaffold.InstrumentedType;
import net.bytebuddy.dynamic.scaffold.TypeValidation;
import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy;
import net.bytebuddy.implementation.ExceptionMethod;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.MethodAccessorFactory;
import net.bytebuddy.implementation.auxiliary.AuxiliaryType;
import net.bytebuddy.implementation.bind.MethodDelegationBinder;
import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
import net.bytebuddy.implementation.bytecode.Duplication;
import net.bytebuddy.implementation.bytecode.StackManipulation;
import net.bytebuddy.implementation.bytecode.TypeCreation;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.implementation.bytecode.member.FieldAccess;
import net.bytebuddy.implementation.bytecode.member.MethodInvocation;
import net.bytebuddy.implementation.bytecode.member.MethodReturn;
import net.bytebuddy.implementation.bytecode.member.MethodVariableAccess;
import net.bytebuddy.utility.RandomString;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.Serializable;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;

import static net.bytebuddy.matcher.ElementMatchers.definedMethod;
import static net.bytebuddy.matcher.ElementMatchers.is;
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.isGetter;
import static net.bytebuddy.matcher.ElementMatchers.isSetter;
import static net.bytebuddy.matcher.ElementMatchers.named;

/**
 * Using this annotation it is possible to access fields by getter and setter types. Before this annotation can be
 * used, it needs to be installed with two types. The getter type must be defined in a single-method interface
 * with a single method that returns an {@link java.lang.Object} type and takes no arguments. The setter interface
 * must similarly return {@code void} and take a single {@link java.lang.Object} argument. After installing these
 * interfaces with the {@link FieldProxy.Binder}, this
 * binder needs to be registered with a {@link net.bytebuddy.implementation.MethodDelegation} before it can be used.
 *
 * @see net.bytebuddy.implementation.MethodDelegation
 * @see net.bytebuddy.implementation.bind.annotation.TargetMethodAnnotationDrivenBinder
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface FieldProxy {

    /**
     * Determines if the proxy should be serializable.
     *
     * @return {@code true} if the proxy should be serializable.
     */
    boolean serializableProxy() default false;

    /**
     * Determines the name of the field that is to be accessed. If this property is not set, a field name is inferred
     * by the intercepted method after the Java beans naming conventions.
     *
     * @return The name of the field to be accessed.
     */
    String value() default TargetMethodAnnotationDrivenBinder.ParameterBinder.ForFieldBinding.BEAN_PROPERTY;

    /**
     * Determines which type defines the field that is to be accessed. If this property is not set, the most field
     * that is defined highest in the type hierarchy is accessed.
     *
     * @return The type that defines the accessed field.
     */
    Class<?> declaringType() default void.class;

    /**
     * A binder for the {@link FieldProxy} annotation.
     */
    @HashCodeAndEqualsPlugin.Enhance
    class Binder extends TargetMethodAnnotationDrivenBinder.ParameterBinder.ForFieldBinding<FieldProxy> {

        /**
         * A reference to the method that declares the field annotation's defining type property.
         */
        private static final MethodDescription.InDefinedShape DECLARING_TYPE;

        /**
         * A reference to the method that declares the field annotation's field name property.
         */
        private static final MethodDescription.InDefinedShape FIELD_NAME;

        /**
         * A reference to the method that declares the field annotation's serializable proxy property.
         */
        private static final MethodDescription.InDefinedShape SERIALIZABLE_PROXY;

        /*
         * Fetches a reference to all annotation properties.
         */
        static {
            MethodList<MethodDescription.InDefinedShape> methodList = TypeDescription.ForLoadedType.of(FieldProxy.class).getDeclaredMethods();
            DECLARING_TYPE = methodList.filter(named("declaringType")).getOnly();
            FIELD_NAME = methodList.filter(named("value")).getOnly();
            SERIALIZABLE_PROXY = methodList.filter(named("serializableProxy")).getOnly();
        }

        /**
         * Creates a binder by installing a single proxy type where annotating a parameter with {@link FieldProxy} allows
         * getting and setting values for a given field.
         *
         * @param type A type which declares exactly one abstract getter and an abstract setter for the {@link Object}
         *             type. The type is allowed to be generic.
         * @return A binder for the {@link FieldProxy} annotation.
         */
        public static TargetMethodAnnotationDrivenBinder.ParameterBinder<FieldProxy> install(Class<?> type) {
            return install(TypeDescription.ForLoadedType.of(type));
        }

        /**
         * Creates a binder by installing a single proxy type where annotating a parameter with {@link FieldProxy} allows
         * getting and setting values for a given field.
         *
         * @param typeDescription A type which declares exactly one abstract getter and an abstract setter for the {@link Object}
         *                        type. The type is allowed to be generic.
         * @return A binder for the {@link FieldProxy} annotation.
         */
        public static TargetMethodAnnotationDrivenBinder.ParameterBinder<FieldProxy> install(TypeDescription typeDescription) {
            if (!typeDescription.isInterface()) {
                throw new IllegalArgumentException(typeDescription + " is not an interface");
            } else if (!typeDescription.getInterfaces().isEmpty()) {
                throw new IllegalArgumentException(typeDescription + " must not extend other interfaces");
            } else if (!typeDescription.isPublic()) {
                throw new IllegalArgumentException(typeDescription + " is not public");
            }
            MethodList<MethodDescription.InDefinedShape> methodCandidates = typeDescription.getDeclaredMethods().filter(isAbstract());
            if (methodCandidates.size() != 2) {
                throw new IllegalArgumentException(typeDescription + " does not declare exactly two non-abstract methods");
            }
            MethodList<MethodDescription.InDefinedShape> getterCandidates = methodCandidates.filter(isGetter(Object.class));
            if (getterCandidates.size() != 1) {
                throw new IllegalArgumentException(typeDescription + " does not declare a getter with an Object type");
            }
            MethodList<MethodDescription.InDefinedShape> setterCandidates = methodCandidates.filter(isSetter(Object.class));
            if (setterCandidates.size() != 1) {
                throw new IllegalArgumentException(typeDescription + " does not declare a setter with an Object type");
            }
            return new Binder(typeDescription, getterCandidates.getOnly(), setterCandidates.getOnly());
        }

        /**
         * Creates a binder by installing two proxy types which are implemented by this binder if a field getter
         * or a field setter is requested by using the
         * {@link FieldProxy} annotation.
         *
         * @param getterType The type which should be used for getter proxies. The type must
         *                   represent an interface which defines a single method which returns an
         *                   {@link java.lang.Object} return type and does not take any arguments. The use of generics
         *                   is permitted.
         * @param setterType The type which should be uses for setter proxies. The type must
         *                   represent an interface which defines a single method which returns {@code void}
         *                   and takes a single {@link java.lang.Object}-typed argument. The use of generics
         *                   is permitted.
         * @return A binder for the {@link FieldProxy} annotation.
         */
        public static TargetMethodAnnotationDrivenBinder.ParameterBinder<FieldProxy> install(Class<?> getterType, Class<?> setterType) {
            return install(TypeDescription.ForLoadedType.of(getterType), TypeDescription.ForLoadedType.of(setterType));
        }

        /**
         * Creates a binder by installing two proxy types which are implemented by this binder if a field getter
         * or a field setter is requested by using the
         * {@link FieldProxy} annotation.
         *
         * @param getterType The type which should be used for getter proxies. The type must
         *                   represent an interface which defines a single method which returns an
         *                   {@link java.lang.Object} return type and does not take any arguments. The use of generics
         *                   is permitted.
         * @param setterType The type which should be uses for setter proxies. The type must
         *                   represent an interface which defines a single method which returns {@code void}
         *                   and takes a single {@link java.lang.Object}-typed argument. The use of generics
         *                   is permitted.
         * @return A binder for the {@link FieldProxy} annotation.
         */
        public static TargetMethodAnnotationDrivenBinder.ParameterBinder<FieldProxy> install(TypeDescription getterType, TypeDescription setterType) {
            MethodDescription.InDefinedShape getterMethod = onlyMethod(getterType);
            if (!getterMethod.getReturnType().asErasure().represents(Object.class)) {
                throw new IllegalArgumentException(getterMethod + " must take a single Object-typed parameter");
            } else if (getterMethod.getParameters().size() != 0) {
                throw new IllegalArgumentException(getterMethod + " must not declare parameters");
            }
            MethodDescription.InDefinedShape setterMethod = onlyMethod(setterType);
            if (!setterMethod.getReturnType().asErasure().represents(void.class)) {
                throw new IllegalArgumentException(setterMethod + " must return void");
            } else if (setterMethod.getParameters().size() != 1 || !setterMethod.getParameters().get(0).getType().asErasure().represents(Object.class)) {
                throw new IllegalArgumentException(setterMethod + " must declare a single Object-typed parameters");
            }
            return new Binder(getterMethod, setterMethod);
        }

        /**
         * Extracts the only method from a given type description which is validated for the required properties for
         * using the type as a proxy base type.
         *
         * @param typeDescription The type description to evaluate.
         * @return The only method which was found to be compatible to the proxy requirements.
         */
        private static MethodDescription.InDefinedShape onlyMethod(TypeDescription typeDescription) {
            if (!typeDescription.isInterface()) {
                throw new IllegalArgumentException(typeDescription + " is not an interface");
            } else if (!typeDescription.getInterfaces().isEmpty()) {
                throw new IllegalArgumentException(typeDescription + " must not extend other interfaces");
            } else if (!typeDescription.isPublic()) {
                throw new IllegalArgumentException(typeDescription + " is not public");
            }
            MethodList<MethodDescription.InDefinedShape> methodCandidates = typeDescription.getDeclaredMethods().filter(isAbstract());
            if (methodCandidates.size() != 1) {
                throw new IllegalArgumentException(typeDescription + " must declare exactly one abstract method");
            }
            return methodCandidates.getOnly();
        }

        /**
         * The field resolver factory to apply by this binder.
         */
        private final FieldResolver.Factory fieldResolverFactory;

        /**
         * Creates a new binder for a {@link FieldProxy} in simplex mode.
         *
         * @param getterMethod The getter method.
         * @param setterMethod The setter method.
         */
        protected Binder(MethodDescription.InDefinedShape getterMethod, MethodDescription.InDefinedShape setterMethod) {
            this(new FieldResolver.Factory.Simplex(getterMethod, setterMethod));
        }

        /**
         * Creates a new binder for a {@link FieldProxy} in duplex mode.
         *
         * @param proxyType    The proxy type.
         * @param getterMethod The getter method.
         * @param setterMethod The setter method.
         */
        protected Binder(TypeDescription proxyType, MethodDescription.InDefinedShape getterMethod, MethodDescription.InDefinedShape setterMethod) {
            this(new FieldResolver.Factory.Duplex(proxyType, getterMethod, setterMethod));
        }

        /**
         * Creates a new binder for a {@link FieldProxy}.
         *
         * @param fieldResolverFactory The field resolver factory to apply by this binder.
         */
        protected Binder(FieldResolver.Factory fieldResolverFactory) {
            this.fieldResolverFactory = fieldResolverFactory;
        }

        /**
         * {@inheritDoc}
         */
        public Class<FieldProxy> getHandledType() {
            return FieldProxy.class;
        }

        @Override
        protected String fieldName(AnnotationDescription.Loadable<FieldProxy> annotation) {
            return annotation.getValue(FIELD_NAME).resolve(String.class);
        }

        @Override
        protected TypeDescription declaringType(AnnotationDescription.Loadable<FieldProxy> annotation) {
            return annotation.getValue(DECLARING_TYPE).resolve(TypeDescription.class);
        }

        @Override
        protected MethodDelegationBinder.ParameterBinding<?> bind(FieldDescription fieldDescription,
                                                                  AnnotationDescription.Loadable<FieldProxy> annotation,
                                                                  MethodDescription source,
                                                                  ParameterDescription target,
                                                                  Implementation.Target implementationTarget,
                                                                  Assigner assigner) {
            FieldResolver fieldResolver = fieldResolverFactory.resolve(target.getType().asErasure(), fieldDescription);
            if (fieldResolver.isResolved()) {
                return new MethodDelegationBinder.ParameterBinding.Anonymous(new AccessorProxy(fieldDescription,
                        implementationTarget.getInstrumentedType(),
                        fieldResolver,
                        assigner,
                        annotation.getValue(SERIALIZABLE_PROXY).resolve(Boolean.class)));
            } else {
                return MethodDelegationBinder.ParameterBinding.Illegal.INSTANCE;
            }
        }

        /**
         * A resolver for creating an instrumentation for a field access.
         */
        protected interface FieldResolver {

            /**
             * Returns {@code true} if the field access can be established.
             *
             * @return {@code true} if the field access can be established.
             */
            boolean isResolved();

            /**
             * Returns the type of the field access proxy.
             *
             * @return The type of the field access proxy.
             */
            TypeDescription getProxyType();

            /**
             * Applies this field resolver to a dynamic type.
             *
             * @param builder               The dynamic type builder to use.
             * @param fieldDescription      The accessed field.
             * @param assigner              The assigner to use.
             * @param methodAccessorFactory The method accessor factory to use.
             * @return The builder for creating the field accessor proxy type.
             */
            DynamicType.Builder<?> apply(DynamicType.Builder<?> builder,
                                         FieldDescription fieldDescription,
                                         Assigner assigner,
                                         MethodAccessorFactory methodAccessorFactory);

            /**
             * A factory for creating a field resolver.
             */
            interface Factory {

                /**
                 * Creates a field resolver.
                 *
                 * @param parameterType    The type of the annotated parameter.
                 * @param fieldDescription The field being proxied.
                 * @return An appropriate field resolver.
                 */
                FieldResolver resolve(TypeDescription parameterType, FieldDescription fieldDescription);

                /**
                 * A duplex factory for a type that both sets and gets a field value.
                 */
                @HashCodeAndEqualsPlugin.Enhance
                class Duplex implements Factory {

                    /**
                     * The type of the accessor proxy.
                     */
                    private final TypeDescription proxyType;

                    /**
                     * The getter method.
                     */
                    private final MethodDescription.InDefinedShape getterMethod;

                    /**
                     * The setter method.
                     */
                    private final MethodDescription.InDefinedShape setterMethod;

                    /**
                     * Creates a new duplex factory.
                     *
                     * @param proxyType    The type of the accessor proxy.
                     * @param getterMethod The getter method.
                     * @param setterMethod The setter method.
                     */
                    protected Duplex(TypeDescription proxyType,
                                     MethodDescription.InDefinedShape getterMethod,
                                     MethodDescription.InDefinedShape setterMethod) {
                        this.proxyType = proxyType;
                        this.getterMethod = getterMethod;
                        this.setterMethod = setterMethod;
                    }

                    /**
                     * {@inheritDoc}
                     */
                    public FieldResolver resolve(TypeDescription parameterType, FieldDescription fieldDescription) {
                        if (parameterType.equals(proxyType)) {
                            return new ForGetterSetterPair(proxyType, getterMethod, setterMethod);
                        } else {
                            throw new IllegalStateException("Cannot use @FieldProxy on a non-installed type");
                        }
                    }
                }

                /**
                 * A simplex factory where field getters and setters both have their own type.
                 */
                @HashCodeAndEqualsPlugin.Enhance
                class Simplex implements Factory {

                    /**
                     * The getter method.
                     */
                    private final MethodDescription.InDefinedShape getterMethod;

                    /**
                     * The setter method.
                     */
                    private final MethodDescription.InDefinedShape setterMethod;

                    /**
                     * Creates a simplex factory.
                     *
                     * @param getterMethod The getter method.
                     * @param setterMethod The setter method.
                     */
                    protected Simplex(MethodDescription.InDefinedShape getterMethod, MethodDescription.InDefinedShape setterMethod) {
                        this.getterMethod = getterMethod;
                        this.setterMethod = setterMethod;
                    }

                    /**
                     * {@inheritDoc}
                     */
                    public FieldResolver resolve(TypeDescription parameterType, FieldDescription fieldDescription) {
                        if (parameterType.equals(getterMethod.getDeclaringType())) {
                            return new ForGetter(getterMethod);
                        } else if (parameterType.equals(setterMethod.getDeclaringType())) {
                            return fieldDescription.isFinal()
                                    ? Unresolved.INSTANCE
                                    : new ForSetter(setterMethod);
                        } else {
                            throw new IllegalStateException("Cannot use @FieldProxy on a non-installed type");
                        }
                    }
                }
            }

            /**
             * An unresolved field resolver.
             */
            enum Unresolved implements FieldResolver {

                /**
                 * The singleton instance.
                 */
                INSTANCE;

                /**
                 * {@inheritDoc}
                 */
                public boolean isResolved() {
                    return false;
                }

                /**
                 * {@inheritDoc}
                 */
                public TypeDescription getProxyType() {
                    throw new IllegalStateException("Cannot read type for unresolved field resolver");
                }

                /**
                 * {@inheritDoc}
                 */
                public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder,
                                                    FieldDescription fieldDescription,
                                                    Assigner assigner,
                                                    MethodAccessorFactory methodAccessorFactory) {
                    throw new IllegalStateException("Cannot apply unresolved field resolver");
                }
            }

            /**
             * A field resolver for a getter accessor.
             */
            @HashCodeAndEqualsPlugin.Enhance
            class ForGetter implements FieldResolver {

                /**
                 * The getter method.
                 */
                private final MethodDescription.InDefinedShape getterMethod;

                /**
                 * Creates a new getter field resolver.
                 *
                 * @param getterMethod The getter method.
                 */
                protected ForGetter(MethodDescription.InDefinedShape getterMethod) {
                    this.getterMethod = getterMethod;
                }

                /**
                 * {@inheritDoc}
                 */
                public boolean isResolved() {
                    return true;
                }

                /**
                 * {@inheritDoc}
                 */
                public TypeDescription getProxyType() {
                    return getterMethod.getDeclaringType();
                }

                /**
                 * {@inheritDoc}
                 */
                public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder,
                                                    FieldDescription fieldDescription,
                                                    Assigner assigner,
                                                    MethodAccessorFactory methodAccessorFactory) {
                    return builder.method(definedMethod(is(getterMethod))).intercept(new FieldGetter(fieldDescription, assigner, methodAccessorFactory));
                }
            }

            /**
             * A field resolver for a setter accessor.
             */
            @HashCodeAndEqualsPlugin.Enhance
            class ForSetter implements FieldResolver {

                /**
                 * The setter method.
                 */
                private final MethodDescription.InDefinedShape setterMethod;

                /**
                 * Creates a new field resolver for a setter accessor.
                 *
                 * @param setterMethod The setter method.
                 */
                protected ForSetter(MethodDescription.InDefinedShape setterMethod) {
                    this.setterMethod = setterMethod;
                }

                /**
                 * {@inheritDoc}
                 */
                public boolean isResolved() {
                    return true;
                }

                /**
                 * {@inheritDoc}
                 */
                public TypeDescription getProxyType() {
                    return setterMethod.getDeclaringType();
                }

                /**
                 * {@inheritDoc}
                 */
                public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder,
                                                    FieldDescription fieldDescription,
                                                    Assigner assigner,
                                                    MethodAccessorFactory methodAccessorFactory) {
                    return builder.method(is(setterMethod)).intercept(new FieldSetter(fieldDescription, assigner, methodAccessorFactory));
                }
            }

            /**
             * A field resolver for an accessor that both gets and sets a field value.
             */
            @HashCodeAndEqualsPlugin.Enhance
            class ForGetterSetterPair implements FieldResolver {

                /**
                 * The type of the accessor proxy.
                 */
                private final TypeDescription proxyType;

                /**
                 * The getter method.
                 */
                private final MethodDescription.InDefinedShape getterMethod;

                /**
                 * The setter method.
                 */
                private final MethodDescription.InDefinedShape setterMethod;

                /**
                 * Creates a new field resolver for an accessor that both gets and sets a field value.
                 *
                 * @param proxyType    The type of the accessor proxy.
                 * @param getterMethod The getter method.
                 * @param setterMethod The setter method.
                 */
                protected ForGetterSetterPair(TypeDescription proxyType,
                                              MethodDescription.InDefinedShape getterMethod,
                                              MethodDescription.InDefinedShape setterMethod) {
                    this.proxyType = proxyType;
                    this.getterMethod = getterMethod;
                    this.setterMethod = setterMethod;
                }

                /**
                 * {@inheritDoc}
                 */
                public boolean isResolved() {
                    return true;
                }

                /**
                 * {@inheritDoc}
                 */
                public TypeDescription getProxyType() {
                    return proxyType;
                }

                /**
                 * {@inheritDoc}
                 */
                public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder,
                                                    FieldDescription fieldDescription,
                                                    Assigner assigner,
                                                    MethodAccessorFactory methodAccessorFactory) {
                    return builder
                            .method(is(getterMethod)).intercept(new FieldGetter(fieldDescription, assigner, methodAccessorFactory))
                            .method(is(setterMethod)).intercept(fieldDescription.isFinal()
                                    ? ExceptionMethod.throwing(UnsupportedOperationException.class, "Cannot set final field " + fieldDescription)
                                    : new FieldSetter(fieldDescription, assigner, methodAccessorFactory));
                }
            }
        }

        /**
         * Represents an implementation for implementing a proxy type constructor when a static field is accessed.
         */
        protected enum StaticFieldConstructor implements Implementation {

            /**
             * The singleton instance.
             */
            INSTANCE;

            /**
             * A reference of the {@link Object} type default constructor.
             */
            private final MethodDescription objectTypeDefaultConstructor;

            /**
             * Creates the constructor call singleton.
             */
            StaticFieldConstructor() {
                objectTypeDefaultConstructor = TypeDescription.ForLoadedType.of(Object.class).getDeclaredMethods().filter(isConstructor()).getOnly();
            }

            /**
             * {@inheritDoc}
             */
            public InstrumentedType prepare(InstrumentedType instrumentedType) {
                return instrumentedType;
            }

            /**
             * {@inheritDoc}
             */
            public ByteCodeAppender appender(Target implementationTarget) {
                return new ByteCodeAppender.Simple(MethodVariableAccess.loadThis(), MethodInvocation.invoke(objectTypeDefaultConstructor), MethodReturn.VOID);
            }
        }

        /**
         * Represents an implementation for implementing a proxy type constructor when a non-static field is accessed.
         */
        @HashCodeAndEqualsPlugin.Enhance
        protected static class InstanceFieldConstructor implements Implementation {

            /**
             * The instrumented type from which a field is to be accessed.
             */
            private final TypeDescription instrumentedType;

            /**
             * Creates a new implementation for implementing a field accessor proxy's constructor when accessing
             * a non-static field.
             *
             * @param instrumentedType The instrumented type from which a field is to be accessed.
             */
            protected InstanceFieldConstructor(TypeDescription instrumentedType) {
                this.instrumentedType = instrumentedType;
            }

            /**
             * {@inheritDoc}
             */
            public InstrumentedType prepare(InstrumentedType instrumentedType) {
                return instrumentedType.withField(new FieldDescription.Token(AccessorProxy.FIELD_NAME,
                        Opcodes.ACC_FINAL | Opcodes.ACC_PRIVATE,
                        this.instrumentedType.asGenericType()));
            }

            /**
             * {@inheritDoc}
             */
            public ByteCodeAppender appender(Target implementationTarget) {
                return new Appender(implementationTarget);
            }

            /**
             * An appender for implementing an
             * {@link FieldProxy.Binder.InstanceFieldConstructor}.
             */
            @HashCodeAndEqualsPlugin.Enhance
            protected static class Appender implements ByteCodeAppender {

                /**
                 * The field to be set within the constructor.
                 */
                private final FieldDescription fieldDescription;

                /**
                 * Creates a new appender.
                 *
                 * @param implementationTarget The implementation target of the current implementation.
                 */
                protected Appender(Target implementationTarget) {
                    fieldDescription = implementationTarget.getInstrumentedType()
                            .getDeclaredFields()
                            .filter((named(AccessorProxy.FIELD_NAME)))
                            .getOnly();
                }

                /**
                 * {@inheritDoc}
                 */
                public Size apply(MethodVisitor methodVisitor,
                                  Context implementationContext,
                                  MethodDescription instrumentedMethod) {
                    StackManipulation.Size stackSize = new StackManipulation.Compound(
                            MethodVariableAccess.loadThis(),
                            MethodInvocation.invoke(StaticFieldConstructor.INSTANCE.objectTypeDefaultConstructor),
                            MethodVariableAccess.allArgumentsOf(instrumentedMethod.asDefined()).prependThisReference(),
                            FieldAccess.forField(fieldDescription).write(),
                            MethodReturn.VOID
                    ).apply(methodVisitor, implementationContext);
                    return new Size(stackSize.getMaximalSize(), instrumentedMethod.getStackSize());
                }
            }
        }

        /**
         * Implementation for a getter method.
         */
        @HashCodeAndEqualsPlugin.Enhance
        protected static class FieldGetter implements Implementation {

            /**
             * The field that is being accessed.
             */
            private final FieldDescription fieldDescription;

            /**
             * The assigner to use.
             */
            private final Assigner assigner;

            /**
             * The accessed type's method accessor factory.
             */
            private final MethodAccessorFactory methodAccessorFactory;

            /**
             * Creates a new getter implementation.
             *
             * @param fieldDescription      The field that is being accessed.
             * @param assigner              The assigner to use.
             * @param methodAccessorFactory The accessed type's method accessor factory.
             */
            protected FieldGetter(FieldDescription fieldDescription,
                                  Assigner assigner,
                                  MethodAccessorFactory methodAccessorFactory) {
                this.fieldDescription = fieldDescription;
                this.assigner = assigner;
                this.methodAccessorFactory = methodAccessorFactory;
            }

            /**
             * {@inheritDoc}
             */
            public InstrumentedType prepare(InstrumentedType instrumentedType) {
                return instrumentedType;
            }

            /**
             * {@inheritDoc}
             */
            public ByteCodeAppender appender(Target implementationTarget) {
                return new Appender(implementationTarget);
            }

            /**
             * A byte code appender for a getter method.
             */
            @HashCodeAndEqualsPlugin.Enhance(includeSyntheticFields = true)
            protected class Appender implements ByteCodeAppender {

                /**
                 * The generated accessor type.
                 */
                private final TypeDescription typeDescription;

                /**
                 * Creates a new appender for a setter method.
                 *
                 * @param implementationTarget The implementation target of the current instrumentation.
                 */
                protected Appender(Target implementationTarget) {
                    typeDescription = implementationTarget.getInstrumentedType();
                }

                /**
                 * {@inheritDoc}
                 */
                public Size apply(MethodVisitor methodVisitor,
                                  Context implementationContext,
                                  MethodDescription instrumentedMethod) {
                    MethodDescription getterMethod = methodAccessorFactory.registerGetterFor(fieldDescription, MethodAccessorFactory.AccessType.DEFAULT);
                    StackManipulation.Size stackSize = new StackManipulation.Compound(
                            fieldDescription.isStatic()
                                    ? StackManipulation.Trivial.INSTANCE
                                    : new StackManipulation.Compound(
                                    MethodVariableAccess.loadThis(),
                                    FieldAccess.forField(typeDescription.getDeclaredFields().filter((named(AccessorProxy.FIELD_NAME))).getOnly()).read()),
                            MethodInvocation.invoke(getterMethod),
                            assigner.assign(getterMethod.getReturnType(), instrumentedMethod.getReturnType(), Assigner.Typing.DYNAMIC),
                            MethodReturn.of(instrumentedMethod.getReturnType().asErasure())
                    ).apply(methodVisitor, implementationContext);
                    return new Size(stackSize.getMaximalSize(), instrumentedMethod.getStackSize());
                }
            }
        }

        /**
         * Implementation for a setter method.
         */
        @HashCodeAndEqualsPlugin.Enhance
        protected static class FieldSetter implements Implementation {

            /**
             * The field that is being accessed.
             */
            private final FieldDescription fieldDescription;

            /**
             * The assigner to use.
             */
            private final Assigner assigner;

            /**
             * The accessed type's method accessor factory.
             */
            private final MethodAccessorFactory methodAccessorFactory;

            /**
             * Creates a new setter implementation.
             *
             * @param fieldDescription      The field that is being accessed.
             * @param assigner              The assigner to use.
             * @param methodAccessorFactory The accessed type's method accessor factory.
             */
            protected FieldSetter(FieldDescription fieldDescription,
                                  Assigner assigner,
                                  MethodAccessorFactory methodAccessorFactory) {
                this.fieldDescription = fieldDescription;
                this.assigner = assigner;
                this.methodAccessorFactory = methodAccessorFactory;
            }

            /**
             * {@inheritDoc}
             */
            public InstrumentedType prepare(InstrumentedType instrumentedType) {
                return instrumentedType;
            }

            /**
             * {@inheritDoc}
             */
            public ByteCodeAppender appender(Target implementationTarget) {
                return new Appender(implementationTarget);
            }

            /**
             * A byte code appender for a setter method.
             */
            @HashCodeAndEqualsPlugin.Enhance(includeSyntheticFields = true)
            protected class Appender implements ByteCodeAppender {

                /**
                 * The generated accessor type.
                 */
                private final TypeDescription typeDescription;

                /**
                 * Creates a new appender for a setter method.
                 *
                 * @param implementationTarget The implementation target of the current instrumentation.
                 */
                protected Appender(Target implementationTarget) {
                    typeDescription = implementationTarget.getInstrumentedType();
                }

                /**
                 * {@inheritDoc}
                 */
                public Size apply(MethodVisitor methodVisitor,
                                  Context implementationContext,
                                  MethodDescription instrumentedMethod) {
                    TypeDescription.Generic parameterType = instrumentedMethod.getParameters().get(0).getType();
                    MethodDescription setterMethod = methodAccessorFactory.registerSetterFor(fieldDescription, MethodAccessorFactory.AccessType.DEFAULT);
                    StackManipulation.Size stackSize = new StackManipulation.Compound(
                            fieldDescription.isStatic()
                                    ? StackManipulation.Trivial.INSTANCE
                                    : new StackManipulation.Compound(
                                    MethodVariableAccess.loadThis(),
                                    FieldAccess.forField(typeDescription.getDeclaredFields()
                                            .filter((named(AccessorProxy.FIELD_NAME))).getOnly()).read()),
                            MethodVariableAccess.of(parameterType).loadFrom(1),
                            assigner.assign(parameterType, setterMethod.getParameters().get(0).getType(), Assigner.Typing.DYNAMIC),
                            MethodInvocation.invoke(setterMethod),
                            MethodReturn.VOID
                    ).apply(methodVisitor, implementationContext);
                    return new Size(stackSize.getMaximalSize(), instrumentedMethod.getStackSize());
                }
            }
        }

        /**
         * A proxy type for accessing a field either by a getter or a setter.
         */
        @HashCodeAndEqualsPlugin.Enhance(includeSyntheticFields = true)
        protected static class AccessorProxy extends StackManipulation.AbstractBase implements AuxiliaryType {

            /**
             * The name of the field that stores the accessed instance if any.
             */
            protected static final String FIELD_NAME = "instance";

            /**
             * The field that is being accessed.
             */
            private final FieldDescription fieldDescription;

            /**
             * The type which is accessed.
             */
            private final TypeDescription instrumentedType;

            /**
             * The field resolver to use.
             */
            private final FieldResolver fieldResolver;

            /**
             * The assigner to use.
             */
            private final Assigner assigner;

            /**
             * {@code true} if the generated proxy should be serializable.
             */
            private final boolean serializableProxy;

            /**
             * @param fieldDescription  The field that is being accessed.
             * @param instrumentedType  The type which is accessed.
             * @param fieldResolver     The field resolver to use.
             * @param assigner          The assigner to use.
             * @param serializableProxy {@code true} if the generated proxy should be serializable.
             */
            protected AccessorProxy(FieldDescription fieldDescription,
                                    TypeDescription instrumentedType,
                                    FieldResolver fieldResolver,
                                    Assigner assigner,
                                    boolean serializableProxy) {
                this.fieldDescription = fieldDescription;
                this.instrumentedType = instrumentedType;
                this.fieldResolver = fieldResolver;
                this.assigner = assigner;
                this.serializableProxy = serializableProxy;
            }

            /**
             * {@inheritDoc}
             */
            public String getSuffix() {
                return RandomString.hashOf(fieldDescription.hashCode()) + (serializableProxy ? "S" : "0");
            }

            /**
             * {@inheritDoc}
             */
            public DynamicType make(String auxiliaryTypeName,
                                    ClassFileVersion classFileVersion,
                                    MethodAccessorFactory methodAccessorFactory) {
                return fieldResolver.apply(new ByteBuddy(classFileVersion)
                        .with(TypeValidation.DISABLED)
                        .subclass(fieldResolver.getProxyType(), ConstructorStrategy.Default.NO_CONSTRUCTORS)
                        .name(auxiliaryTypeName)
                        .modifiers(DEFAULT_TYPE_MODIFIER)
                        .implement(serializableProxy ? new Class<?>[]{Serializable.class} : new Class<?>[0])
                        .defineConstructor().withParameters(fieldDescription.isStatic()
                                ? Collections.<TypeDescription>emptyList()
                                : Collections.singletonList(instrumentedType))
                        .intercept(fieldDescription.isStatic()
                                ? StaticFieldConstructor.INSTANCE
                                : new InstanceFieldConstructor(instrumentedType)), fieldDescription, assigner, methodAccessorFactory).make();
            }

            /**
             * {@inheritDoc}
             */
            public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext) {
                TypeDescription auxiliaryType = implementationContext.register(this);
                return new Compound(
                        TypeCreation.of(auxiliaryType),
                        Duplication.SINGLE,
                        fieldDescription.isStatic()
                                ? Trivial.INSTANCE
                                : MethodVariableAccess.loadThis(),
                        MethodInvocation.invoke(auxiliaryType.getDeclaredMethods().filter(isConstructor()).getOnly())
                ).apply(methodVisitor, implementationContext);
            }
        }
    }
}
