package net.bytebuddy.implementation;

import lombok.EqualsAndHashCode;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.ParameterDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.scaffold.FieldLocator;
import net.bytebuddy.dynamic.scaffold.InstrumentedType;
import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
import net.bytebuddy.implementation.bytecode.StackManipulation;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.implementation.bytecode.member.FieldAccess;
import net.bytebuddy.implementation.bytecode.member.MethodReturn;
import net.bytebuddy.implementation.bytecode.member.MethodVariableAccess;
import org.objectweb.asm.MethodVisitor;

import java.lang.reflect.Field;

/**
 * <p>
 * Defines a method to access a given field by following the Java bean conventions for getters and setters:
 * </p>
 * <ul>
 * <li>Getter: A method named {@code getFoo()} will be instrumented to read and return the value of a field {@code foo}
 * or another field if one was specified explicitly. If a property is of type {@link java.lang.Boolean} or
 * {@code boolean}, the name {@code isFoo()} is also permitted.</li>
 * <li>Setter: A method named {@code setFoo(value)} will be instrumented to write the given argument {@code value}
 * to a field {@code foo} or to another field if one was specified explicitly.</li>
 * </ul>
 * <p>
 * Field accessors always implement a getter if a non-{@code void} value is returned from a method and attempt to define a setter
 * otherwise. If a field accessor is not explicitly defined as a setter via {@link PropertyConfigurable}, an instrumented
 * method must define exactly one parameter. Using the latter API, an explicit parameter index can be defined and a return
 * value can be specified explicitly when {@code void} is not returned.
 * </p>
 */
@EqualsAndHashCode
public abstract class FieldAccessor implements Implementation {

    /**
     * The field's location.
     */
    protected final FieldLocation fieldLocation;

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

    /**
     * Indicates if dynamic type castings should be attempted for incompatible assignments.
     */
    protected final Assigner.Typing typing;

    /**
     * Creates a new field accessor.
     *
     * @param fieldLocation The field's location.
     * @param assigner      The assigner to use.
     * @param typing        Indicates if dynamic type castings should be attempted for incompatible assignments.
     */
    protected FieldAccessor(FieldLocation fieldLocation, Assigner assigner, Assigner.Typing typing) {
        this.fieldLocation = fieldLocation;
        this.assigner = assigner;
        this.typing = typing;
    }

    /**
     * Defines a field accessor where any access is targeted to a field named {@code name}.
     *
     * @param name The name of the field to be accessed.
     * @return A field accessor for a field of a given name.
     */
    public static OwnerTypeLocatable ofField(String name) {
        return of(new FieldNameExtractor.ForFixedValue(name));
    }

    /**
     * Defines a field accessor where any access is targeted to a field that matches the methods
     * name with the Java specification for bean properties, i.e. a method {@code getFoo} or {@code setFoo(value)}
     * will either read or write a field named {@code foo}.
     *
     * @return A field accessor that follows the Java naming conventions for bean properties.
     */
    public static OwnerTypeLocatable ofBeanProperty() {
        return of(FieldNameExtractor.ForBeanProperty.INSTANCE);
    }

    /**
     * Defines a custom strategy for determining the field that is accessed by this field accessor.
     *
     * @param fieldNameExtractor The field name extractor to use.
     * @return A field accessor using the given field name extractor.
     */
    public static OwnerTypeLocatable of(FieldNameExtractor fieldNameExtractor) {
        return new ForImplicitProperty(new FieldLocation.Relative(fieldNameExtractor));
    }

    /**
     * Defines a field accessor where the specified field is accessed. The field must be within the hierarchy of the instrumented type.
     *
     * @param field The field being accessed.
     * @return A field accessor for the given field.
     */
    public static AssignerConfigurable of(Field field) {
        return of(new FieldDescription.ForLoadedField(field));
    }

    /**
     * Defines a field accessor where the specified field is accessed. The field must be within the hierarchy of the instrumented type.
     *
     * @param fieldDescription The field being accessed.
     * @return A field accessor for the given field.
     */
    public static AssignerConfigurable of(FieldDescription fieldDescription) {
        return new ForImplicitProperty(new FieldLocation.Absolute(fieldDescription));
    }

    /**
     * Creates a getter getter.
     *
     * @param fieldDescription   The field to read the value from.
     * @param instrumentedMethod The getter method.
     * @return A stack manipulation that gets the field's value.
     */
    protected StackManipulation getter(FieldDescription fieldDescription, MethodDescription instrumentedMethod) {
        return access(fieldDescription, instrumentedMethod, new StackManipulation.Compound(FieldAccess.forField(fieldDescription).read(),
                assigner.assign(fieldDescription.getType(), instrumentedMethod.getReturnType(), typing)));
    }

    /**
     * Creates a setter instruction.
     *
     * @param fieldDescription     The field to set a value for.
     * @param parameterDescription The parameter for what value is to be set.
     * @return A stack manipulation that sets the field's value.
     */
    protected StackManipulation setter(FieldDescription fieldDescription, ParameterDescription parameterDescription) {
        if (fieldDescription.isFinal() && parameterDescription.getDeclaringMethod().isMethod()) {
            throw new IllegalArgumentException("Cannot set final field " + fieldDescription + " from " + parameterDescription.getDeclaringMethod());
        }
        return access(fieldDescription,
                parameterDescription.getDeclaringMethod(),
                new StackManipulation.Compound(MethodVariableAccess.load(parameterDescription),
                        assigner.assign(parameterDescription.getType(), fieldDescription.getType(), typing),
                        FieldAccess.forField(fieldDescription).write()));
    }

    /**
     * Checks a field access and loads the {@code this} instance if necessary.
     *
     * @param fieldDescription   The field to get a value
     * @param instrumentedMethod The instrumented method.
     * @param fieldAccess        A stack manipulation describing the field access.
     * @return An appropriate stack manipulation.
     */
    private StackManipulation access(FieldDescription fieldDescription, MethodDescription instrumentedMethod, StackManipulation fieldAccess) {
        if (!fieldAccess.isValid()) {
            throw new IllegalStateException("Incompatible type of " + fieldDescription + " and " + instrumentedMethod);
        } else if (instrumentedMethod.isStatic() && !fieldDescription.isStatic()) {
            throw new IllegalArgumentException("Cannot call instance field " + fieldDescription + " from static method " + instrumentedMethod);
        }
        return new StackManipulation.Compound(fieldDescription.isStatic()
                ? StackManipulation.Trivial.INSTANCE
                : MethodVariableAccess.loadThis(), fieldAccess);
    }

    @Override
    public InstrumentedType prepare(InstrumentedType instrumentedType) {
        return instrumentedType;
    }

    /**
     * A field location represents an identified field description which depends on the instrumented type and method.
     */
    protected interface FieldLocation {

        /**
         * Specifies a field locator factory to use.
         *
         * @param fieldLocatorFactory The field locator factory to use.
         * @return An appropriate field location.
         */
        FieldLocation with(FieldLocator.Factory fieldLocatorFactory);

        /**
         * A prepared field location.
         *
         * @param instrumentedType The instrumented type.
         * @return A prepared field location.
         */
        Prepared prepare(TypeDescription instrumentedType);

        /**
         * A prepared field location.
         */
        interface Prepared {

            /**
             * Resolves the field description to use.
             *
             * @param instrumentedMethod The instrumented method.
             * @return The resolved field description.
             */
            FieldDescription resolve(MethodDescription instrumentedMethod);
        }

        /**
         * An absolute field description representing a previously resolved field.
         */
        @EqualsAndHashCode
        class Absolute implements FieldLocation, Prepared {

            /**
             * The field description.
             */
            private final FieldDescription fieldDescription;

            /**
             * Creates an absolute field location.
             *
             * @param fieldDescription The field description.
             */
            protected Absolute(FieldDescription fieldDescription) {
                this.fieldDescription = fieldDescription;
            }

            @Override
            public FieldLocation with(FieldLocator.Factory fieldLocatorFactory) {
                throw new IllegalStateException("Cannot specify a field locator factory for an absolute field location");
            }

            @Override
            public Prepared prepare(TypeDescription instrumentedType) {
                if (!instrumentedType.isAssignableTo(fieldDescription.getDeclaringType().asErasure())) {
                    throw new IllegalStateException(fieldDescription + " is not declared by " + instrumentedType);
                } else if (!fieldDescription.isVisibleTo(instrumentedType)) {
                    throw new IllegalStateException("Cannot access " + fieldDescription + " from " + instrumentedType);
                }
                return this;
            }

            @Override
            public FieldDescription resolve(MethodDescription instrumentedMethod) {
                return fieldDescription;
            }
        }

        /**
         * A relative field location where a field is located dynamically.
         */
        @EqualsAndHashCode
        class Relative implements FieldLocation {

            /**
             * The field name extractor to use.
             */
            private final FieldNameExtractor fieldNameExtractor;

            /**
             * The field locator factory to use.
             */
            private final FieldLocator.Factory fieldLocatorFactory;

            /**
             * Creates a new relative field location.
             *
             * @param fieldNameExtractor The field name extractor to use.
             */
            protected Relative(FieldNameExtractor fieldNameExtractor) {
                this(fieldNameExtractor, FieldLocator.ForClassHierarchy.Factory.INSTANCE);
            }

            /**
             * Creates a new relative field location.
             *
             * @param fieldNameExtractor  The field name extractor to use.
             * @param fieldLocatorFactory The field locator factory to use.
             */
            private Relative(FieldNameExtractor fieldNameExtractor, FieldLocator.Factory fieldLocatorFactory) {
                this.fieldNameExtractor = fieldNameExtractor;
                this.fieldLocatorFactory = fieldLocatorFactory;
            }

            @Override
            public FieldLocation with(FieldLocator.Factory fieldLocatorFactory) {
                return new Relative(fieldNameExtractor, fieldLocatorFactory);
            }

            @Override
            public FieldLocation.Prepared prepare(TypeDescription instrumentedType) {
                return new Prepared(fieldNameExtractor, fieldLocatorFactory.make(instrumentedType));
            }

            /**
             * A prepared version of a field location.
             */
            @EqualsAndHashCode
            protected static class Prepared implements FieldLocation.Prepared {

                /**
                 * The field name extractor to use.
                 */
                private final FieldNameExtractor fieldNameExtractor;

                /**
                 * The field locator factory to use.
                 */
                private final FieldLocator fieldLocator;

                /**
                 * Creates a new relative field location.
                 *
                 * @param fieldNameExtractor The field name extractor to use.
                 * @param fieldLocator       The field locator to use.
                 */
                protected Prepared(FieldNameExtractor fieldNameExtractor, FieldLocator fieldLocator) {
                    this.fieldNameExtractor = fieldNameExtractor;
                    this.fieldLocator = fieldLocator;
                }

                @Override
                public FieldDescription resolve(MethodDescription instrumentedMethod) {
                    FieldLocator.Resolution resolution = fieldLocator.locate(fieldNameExtractor.resolve(instrumentedMethod));
                    if (!resolution.isResolved()) {
                        throw new IllegalStateException("Cannot resolve field for " + instrumentedMethod + " using " + fieldLocator);
                    }
                    return resolution.getField();
                }
            }
        }
    }

    /**
     * A field name extractor is responsible for determining a field name to a method that is implemented
     * to access this method.
     */
    public interface FieldNameExtractor {

        /**
         * Extracts a field name to be accessed by a getter or setter method.
         *
         * @param methodDescription The method for which a field name is to be determined.
         * @return The name of the field to be accessed by this method.
         */
        String resolve(MethodDescription methodDescription);

        /**
         * A {@link net.bytebuddy.implementation.FieldAccessor.FieldNameExtractor} that determines a field name
         * according to the rules of Java bean naming conventions.
         */
        enum ForBeanProperty implements FieldNameExtractor {

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

            @Override
            public String resolve(MethodDescription methodDescription) {
                String name = methodDescription.getInternalName();
                int crop;
                if (name.startsWith("get") || name.startsWith("set")) {
                    crop = 3;
                } else if (name.startsWith("is")) {
                    crop = 2;
                } else {
                    throw new IllegalArgumentException(methodDescription + " does not follow Java bean naming conventions");
                }
                name = name.substring(crop);
                if (name.length() == 0) {
                    throw new IllegalArgumentException(methodDescription + " does not specify a bean name");
                }
                return Character.toLowerCase(name.charAt(0)) + name.substring(1);
            }
        }

        /**
         * A field name extractor that returns a fixed value.
         */
        @EqualsAndHashCode
        class ForFixedValue implements FieldNameExtractor {

            /**
             * The name to return.
             */
            private final String name;

            /**
             * Creates a new field name extractor for a fixed value.
             *
             * @param name The name to return.
             */
            protected ForFixedValue(String name) {
                this.name = name;
            }

            @Override
            public String resolve(MethodDescription methodDescription) {
                return name;
            }
        }
    }

    /**
     * A field accessor that allows to define the access to be a field write of a given argument.
     */
    public interface PropertyConfigurable extends Implementation {

        /**
         * Creates a field accessor for the described field that serves as a setter for the supplied parameter index. The instrumented
         * method must return {@code void} or a chained instrumentation must be supplied.
         *
         * @param index The index of the parameter for which to set the field's value.
         * @return An instrumentation that sets the parameter's value to the described field.
         */
        Implementation.Composable setsArgumentAt(int index);
    }

    /**
     * A field accessor that can be configured to use a given assigner and runtime type use configuration.
     */
    public interface AssignerConfigurable extends PropertyConfigurable {

        /**
         * Returns a field accessor that is identical to this field accessor but uses the given assigner
         * and runtime type use configuration.
         *
         * @param assigner The assigner to use.
         * @param typing   Indicates if dynamic type castings should be attempted for incompatible assignments.
         * @return This field accessor with the given assigner and runtime type use configuration.
         */
        PropertyConfigurable withAssigner(Assigner assigner, Assigner.Typing typing);
    }

    /**
     * A field accessor that can be configured to locate a field in a specific manner.
     */
    public interface OwnerTypeLocatable extends AssignerConfigurable {

        /**
         * Determines that a field should only be considered when it was defined in a given type.
         *
         * @param type The type to be considered.
         * @return This field accessor which will only considered fields that are defined in the given type.
         */
        AssignerConfigurable in(Class<?> type);

        /**
         * Determines that a field should only be considered when it was defined in a given type.
         *
         * @param typeDescription A description of the type to be considered.
         * @return This field accessor which will only considered fields that are defined in the given type.
         */
        AssignerConfigurable in(TypeDescription typeDescription);

        /**
         * Determines that a field should only be considered when it was identified by a field locator that is
         * produced by the given factory.
         *
         * @param fieldLocatorFactory A factory that will produce a field locator that will be used to find locate
         *                            a field to be accessed.
         * @return This field accessor which will only considered fields that are defined in the given type.
         */
        AssignerConfigurable in(FieldLocator.Factory fieldLocatorFactory);
    }

    /**
     * A field accessor for an implicit property where a getter or setter property is inferred from the signature.
     */
    protected static class ForImplicitProperty extends FieldAccessor implements OwnerTypeLocatable {

        /**
         * Creates a field accessor for an implicit property.
         *
         * @param fieldLocation The field's location.
         */
        protected ForImplicitProperty(FieldLocation fieldLocation) {
            this(fieldLocation, Assigner.DEFAULT, Assigner.Typing.STATIC);
        }

        /**
         * Creates a field accessor for an implicit property.
         *
         * @param fieldLocation The field's location.
         * @param assigner      The assigner to use.
         * @param typing        The typing to use.
         */
        private ForImplicitProperty(FieldLocation fieldLocation, Assigner assigner, Assigner.Typing typing) {
            super(fieldLocation, assigner, typing);
        }

        @Override
        public ByteCodeAppender appender(Target implementationTarget) {
            return new Appender(fieldLocation.prepare(implementationTarget.getInstrumentedType()));
        }

        @Override
        public Composable setsArgumentAt(int index) {
            if (index < 0) {
                throw new IllegalArgumentException("A parameter index cannot be negative: " + index);
            }
            return new ForParameterSetter(fieldLocation, assigner, typing, index);
        }

        @Override
        public PropertyConfigurable withAssigner(Assigner assigner, Assigner.Typing typing) {
            return new ForImplicitProperty(fieldLocation, assigner, typing);
        }

        @Override
        public AssignerConfigurable in(Class<?> type) {
            return in(new TypeDescription.ForLoadedType(type));
        }

        @Override
        public AssignerConfigurable in(TypeDescription typeDescription) {
            return in(new FieldLocator.ForExactType.Factory(typeDescription));
        }

        @Override
        public AssignerConfigurable in(FieldLocator.Factory fieldLocatorFactory) {
            return new ForImplicitProperty(fieldLocation.with(fieldLocatorFactory), assigner, typing);
        }

        /**
         * An byte code appender for an field accessor implementation.
         */
        protected class Appender implements ByteCodeAppender {

            /**
             * The field's location.
             */
            private final FieldLocation.Prepared fieldLocation;

            /**
             * Creates a new byte code appender for a field accessor implementation.
             *
             * @param fieldLocation The field's location.
             */
            protected Appender(FieldLocation.Prepared fieldLocation) {
                this.fieldLocation = fieldLocation;
            }

            @Override
            public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext, MethodDescription instrumentedMethod) {
                if (!instrumentedMethod.isMethod()) {
                    throw new IllegalArgumentException(instrumentedMethod + " does not describe a field getter or setter");
                }
                FieldDescription fieldDescription = fieldLocation.resolve(instrumentedMethod);
                StackManipulation implementation;
                if (!instrumentedMethod.getReturnType().represents(void.class)) {
                    implementation = new StackManipulation.Compound(getter(fieldDescription, instrumentedMethod), MethodReturn.of(instrumentedMethod.getReturnType()));
                } else if (instrumentedMethod.getReturnType().represents(void.class) && instrumentedMethod.getParameters().size() == 1) {
                    implementation = new StackManipulation.Compound(setter(fieldDescription, instrumentedMethod.getParameters().get(0)), MethodReturn.VOID);
                } else {
                    throw new IllegalArgumentException("Method " + implementationContext + " is no bean property");
                }
                return new Size(implementation.apply(methodVisitor, implementationContext).getMaximalSize(), instrumentedMethod.getStackSize());
            }

            /**
             * Returns the outer instance.
             *
             * @return The outer instance.
             */
            private ForImplicitProperty getOuter() {
                return ForImplicitProperty.this;
            }

            @Override // HE: Remove when Lombok support for getOuter is added.
            public boolean equals(Object object) {
                if (this == object) return true;
                if (object == null || getClass() != object.getClass()) return false;
                Appender appender = (Appender) object;
                return fieldLocation.equals(appender.fieldLocation) && ForImplicitProperty.this.equals(appender.getOuter());
            }

            @Override // HE: Remove when Lombok support for getOuter is added.
            public int hashCode() {
                return fieldLocation.hashCode() + 31 * ForImplicitProperty.this.hashCode();
            }
        }
    }

    /**
     * A field accessor that sets a parameters value of a given index.
     */
    @EqualsAndHashCode(callSuper = true)
    protected static class ForParameterSetter extends FieldAccessor implements Implementation.Composable {

        /**
         * The targeted parameter index.
         */
        private final int index;

        /**
         * The termination handler to apply.
         */
        private final TerminationHandler terminationHandler;

        /**
         * Creates a new field accessor.
         *
         * @param fieldLocation The field's location.
         * @param assigner      The assigner to use.
         * @param typing        Indicates if dynamic type castings should be attempted for incompatible assignments.
         * @param index         The targeted parameter index.
         */
        protected ForParameterSetter(FieldLocation fieldLocation, Assigner assigner, Assigner.Typing typing, int index) {
            this(fieldLocation, assigner, typing, index, TerminationHandler.RETURNING);
        }

        /**
         * Creates a new field accessor.
         *
         * @param fieldLocation      The field's location.
         * @param assigner           The assigner to use.
         * @param typing             Indicates if dynamic type castings should be attempted for incompatible assignments.
         * @param index              The targeted parameter index.
         * @param terminationHandler The termination handler to apply.
         */
        private ForParameterSetter(FieldLocation fieldLocation, Assigner assigner, Assigner.Typing typing, int index, TerminationHandler terminationHandler) {
            super(fieldLocation, assigner, typing);
            this.index = index;
            this.terminationHandler = terminationHandler;
        }

        @Override
        public ByteCodeAppender appender(Target implementationTarget) {
            return new Appender(fieldLocation.prepare(implementationTarget.getInstrumentedType()));
        }

        @Override
        public Implementation andThen(Implementation implementation) {
            return new Compound(new ForParameterSetter(fieldLocation,
                    assigner,
                    typing,
                    index, TerminationHandler.NON_OPERATIONAL), implementation);
        }

        /**
         * A termination handler is responsible for handling a field accessor's return.
         */
        protected enum TerminationHandler {

            /**
             * Returns {@code void} or throws an exception if this is not the return type of the instrumented method.
             */
            RETURNING {
                @Override
                protected StackManipulation resolve(MethodDescription instrumentedMethod) {
                    if (!instrumentedMethod.getReturnType().represents(void.class)) {
                        throw new IllegalStateException("Cannot implement setter with return value for " + instrumentedMethod);
                    }
                    return MethodReturn.VOID;
                }
            },

            /**
             * Does not return from the method at all.
             */
            NON_OPERATIONAL {
                @Override
                protected StackManipulation resolve(MethodDescription instrumentedMethod) {
                    return StackManipulation.Trivial.INSTANCE;
                }
            };

            /**
             * Resolves the return instruction.
             *
             * @param instrumentedMethod The instrumented method.
             * @return An appropriate stack manipulation.
             */
            protected abstract StackManipulation resolve(MethodDescription instrumentedMethod);
        }

        /**
         * An appender for a field accessor that sets a parameter of a given index.
         */
        protected class Appender implements ByteCodeAppender {

            /**
             * The field's location.
             */
            private final FieldLocation.Prepared fieldLocation;

            /**
             * Creates a new byte code appender for a field accessor implementation.
             *
             * @param fieldLocation The field's location.
             */
            protected Appender(FieldLocation.Prepared fieldLocation) {
                this.fieldLocation = fieldLocation;
            }

            @Override
            public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext, MethodDescription instrumentedMethod) {
                if (instrumentedMethod.getParameters().size() <= index) {
                    throw new IllegalStateException(instrumentedMethod + " does not define a parameter with index " + index);
                } else {
                    return new Size(new StackManipulation.Compound(
                            setter(fieldLocation.resolve(instrumentedMethod), instrumentedMethod.getParameters().get(index)),
                            terminationHandler.resolve(instrumentedMethod)
                    ).apply(methodVisitor, implementationContext).getMaximalSize(), instrumentedMethod.getStackSize());
                }
            }

            /**
             * Returns the outer instance.
             *
             * @return The outer instance.
             */
            private ForParameterSetter getOuter() {
                return ForParameterSetter.this;
            }

            @Override // HE: Remove when Lombok support for getOuter is added.
            public boolean equals(Object object) {
                if (this == object) return true;
                if (object == null || getClass() != object.getClass()) return false;
                ForParameterSetter.Appender appender = (ForParameterSetter.Appender) object;
                return fieldLocation.equals(appender.fieldLocation) && ForParameterSetter.this.equals(appender.getOuter());
            }

            @Override // HE: Remove when Lombok support for getOuter is added.
            public int hashCode() {
                return fieldLocation.hashCode() + 31 * ForParameterSetter.this.hashCode();
            }
        }
    }
}
