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.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.constant.*;
import net.bytebuddy.implementation.bytecode.member.FieldAccess;
import net.bytebuddy.implementation.bytecode.member.MethodReturn;
import net.bytebuddy.implementation.bytecode.member.MethodVariableAccess;
import net.bytebuddy.utility.JavaConstant;
import net.bytebuddy.utility.JavaType;
import net.bytebuddy.utility.RandomString;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import static net.bytebuddy.matcher.ElementMatchers.named;

/**
 * This implementation returns a fixed value for a method. Other than the {@link net.bytebuddy.implementation.StubMethod}
 * implementation, this implementation allows to determine a specific value which must be assignable to the returning value
 * of any instrumented method. Otherwise, an exception will be thrown.
 *
 * @see FieldAccessor
 */
@EqualsAndHashCode
public abstract class FixedValue implements Implementation {

    /**
     * The assigner that is used for assigning the fixed value to a method's return type.
     */
    protected final Assigner assigner;

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

    /**
     * Creates a new fixed value implementation.
     *
     * @param assigner The assigner to use for assigning the fixed value to the return type of the instrumented value.
     * @param typing   Indicates if dynamic type castings should be attempted for incompatible assignments.
     */
    protected FixedValue(Assigner assigner, Assigner.Typing typing) {
        this.assigner = assigner;
        this.typing = typing;
    }

    /**
     * <p>
     * Returns a fixed value from any intercepted method. The fixed value is stored in the constant pool if this is possible.
     * Java is capable of storing any primitive value, {@link String} values and {@link Class} references in the constant pool.
     * Since Java 7, {@code MethodHandle} as well as {@code MethodType} references are also supported. Alternatively, the fixed
     * value is stored in a static field.
     * </p>
     * <p>
     * When a value is stored in the class's constant pool, its identity is lost. If an object's identity is important, the
     * {@link FixedValue#reference(Object)} method should be used instead.
     * </p>
     * <p>
     * <b>Important</b>: When supplying a method handle or a method type, all types that are implied must be visible to the instrumented
     * type or an {@link IllegalAccessException} will be thrown at runtime.
     * </p>
     *
     * @param fixedValue The fixed value to return from the method.
     * @return An implementation for the given {@code value}.
     */
    public static AssignerConfigurable value(Object fixedValue) {
        Class<?> type = fixedValue.getClass();
        if (type == String.class) {
            return new ForPoolValue(new TextConstant((String) fixedValue), TypeDescription.STRING);
        } else if (type == Class.class) {
            return new ForPoolValue(ClassConstant.of(new TypeDescription.ForLoadedType((Class<?>) fixedValue)), TypeDescription.CLASS);
        } else if (type == Boolean.class) {
            return new ForPoolValue(IntegerConstant.forValue((Boolean) fixedValue), boolean.class);
        } else if (type == Byte.class) {
            return new ForPoolValue(IntegerConstant.forValue((Byte) fixedValue), byte.class);
        } else if (type == Short.class) {
            return new ForPoolValue(IntegerConstant.forValue((Short) fixedValue), short.class);
        } else if (type == Character.class) {
            return new ForPoolValue(IntegerConstant.forValue((Character) fixedValue), char.class);
        } else if (type == Integer.class) {
            return new ForPoolValue(IntegerConstant.forValue((Integer) fixedValue), int.class);
        } else if (type == Long.class) {
            return new ForPoolValue(LongConstant.forValue((Long) fixedValue), long.class);
        } else if (type == Float.class) {
            return new ForPoolValue(FloatConstant.forValue((Float) fixedValue), float.class);
        } else if (type == Double.class) {
            return new ForPoolValue(DoubleConstant.forValue((Double) fixedValue), double.class);
        } else if (JavaType.METHOD_HANDLE.getTypeStub().isAssignableFrom(type)) {
            return new ForPoolValue(new JavaConstantValue(JavaConstant.MethodHandle.ofLoaded(fixedValue)), type);
        } else if (JavaType.METHOD_TYPE.getTypeStub().represents(type)) {
            return new ForPoolValue(new JavaConstantValue(JavaConstant.MethodType.ofLoaded(fixedValue)), type);
        } else {
            return reference(fixedValue);
        }
    }

    /**
     * Other than {@link net.bytebuddy.implementation.FixedValue#value(Object)}, this function
     * will create a fixed value implementation that will always defined a field in the instrumented class. As a result,
     * object identity will be preserved between the given {@code value} and the value that is returned by
     * instrumented methods. The field name can be explicitly determined. The field name is generated from the fixed value's
     * hash code.
     *
     * @param fixedValue The fixed value to be returned by methods that are instrumented by this implementation.
     * @return An implementation for the given {@code value}.
     */
    public static AssignerConfigurable reference(Object fixedValue) {
        return new ForValue(fixedValue);
    }

    /**
     * Other than {@link net.bytebuddy.implementation.FixedValue#value(Object)}, this function
     * will create a fixed value implementation that will always defined a field in the instrumented class. As a result,
     * object identity will be preserved between the given {@code value} and the value that is returned by
     * instrumented methods. The field name can be explicitly determined.
     *
     * @param fixedValue The fixed value to be returned by methods that are instrumented by this implementation.
     * @param fieldName  The name of the field for storing the fixed value.
     * @return An implementation for the given {@code value}.
     */
    public static AssignerConfigurable reference(Object fixedValue, String fieldName) {
        return new ForValue(fieldName, fixedValue);
    }

    /**
     * Returns the given type in form of a loaded type. The value is loaded from the written class's constant pool.
     *
     * @param fixedValue The type to return from the method.
     * @return An implementation for the given {@code value}.
     */
    public static AssignerConfigurable value(TypeDescription fixedValue) {
        return new ForPoolValue(ClassConstant.of(fixedValue), TypeDescription.CLASS);
    }

    /**
     * Returns the loaded version of the given {@link JavaConstant}. The value is loaded from the written class's constant pool.
     *
     * @param fixedValue The type to return from the method.
     * @return An implementation for the given {@code value}.
     */
    public static AssignerConfigurable value(JavaConstant fixedValue) {
        return new ForPoolValue(fixedValue.asStackManipulation(), fixedValue.getType());
    }

    /**
     * Returns the argument at the specified index.
     *
     * @param index The index of the argument to return.
     * @return An implementation of a method that returns the argument at the specified index.
     */
    public static AssignerConfigurable argument(int index) {
        if (index < 0) {
            throw new IllegalArgumentException("Argument index cannot be negative: " + index);
        }
        return new ForArgument(index);
    }

    /**
     * Returns {@code this} from an instrumented method.
     *
     * @return An implementation that returns {@code this} from a method.
     */
    public static AssignerConfigurable self() {
        return new ForThisValue();
    }

    /**
     * Returns a {@code null} value from an instrumented method.
     *
     * @return An implementation that returns {@code null} from a method.
     */
    public static Implementation nullValue() {
        return ForNullValue.INSTANCE;
    }

    /**
     * Returns the origin type from an instrumented method.
     *
     * @return An implementation that returns the origin type of the current instrumented type.
     */
    public static AssignerConfigurable originType() {
        return new ForOriginType();
    }

    /**
     * Blueprint method that for applying the actual implementation.
     *
     * @param methodVisitor           The method visitor to which the implementation is applied to.
     * @param implementationContext   The implementation context for the given implementation.
     * @param instrumentedMethod      The instrumented method that is target of the implementation.
     * @param fixedValueType          A description of the type of the fixed value that is loaded by the
     *                                {@code valueLoadingInstruction}.
     * @param valueLoadingInstruction A stack manipulation that represents the loading of the fixed value onto the
     *                                operand stack.
     * @return A representation of the stack and variable array sized that are required for this implementation.
     */
    protected ByteCodeAppender.Size apply(MethodVisitor methodVisitor,
                                          Context implementationContext,
                                          MethodDescription instrumentedMethod,
                                          TypeDescription.Generic fixedValueType,
                                          StackManipulation valueLoadingInstruction) {
        StackManipulation assignment = assigner.assign(fixedValueType, instrumentedMethod.getReturnType(), typing);
        if (!assignment.isValid()) {
            throw new IllegalArgumentException("Cannot return value of type " + fixedValueType + " for " + instrumentedMethod);
        }
        StackManipulation.Size stackSize = new StackManipulation.Compound(
                valueLoadingInstruction,
                assignment,
                MethodReturn.of(instrumentedMethod.getReturnType())
        ).apply(methodVisitor, implementationContext);
        return new ByteCodeAppender.Size(stackSize.getMaximalSize(), instrumentedMethod.getStackSize());
    }

    /**
     * Represents a fixed value implementation that is using a default assigner for attempting to assign
     * the fixed value to the return type of the instrumented method.
     */
    public interface AssignerConfigurable extends Implementation {

        /**
         * Defines an explicit assigner to this fixed value implementation.
         *
         * @param assigner The assigner to use for assigning the fixed value to the return type of the
         *                 instrumented value.
         * @param typing   Indicates if dynamic type castings should be attempted for incompatible assignments.
         * @return A fixed value implementation that makes use of the given assigner.
         */
        Implementation withAssigner(Assigner assigner, Assigner.Typing typing);
    }

    /**
     * A fixed value of {@code null}.
     */
    protected enum ForNullValue implements Implementation, ByteCodeAppender {

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

        @Override
        public ByteCodeAppender appender(Target implementationTarget) {
            return this;
        }

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

        @Override
        public Size apply(MethodVisitor methodVisitor, Context implementationContext, MethodDescription instrumentedMethod) {
            if (instrumentedMethod.getReturnType().isPrimitive()) {
                throw new IllegalStateException("Cannot return null from " + instrumentedMethod);
            }
            return new ByteCodeAppender.Simple(
                    NullConstant.INSTANCE,
                    MethodReturn.REFERENCE
            ).apply(methodVisitor, implementationContext, instrumentedMethod);
        }
    }

    /**
     * A fixed value that appends the origin type of the instrumented type.
     */
    protected static class ForOriginType extends FixedValue implements AssignerConfigurable {

        /**
         * Creates a new fixed value appender for the origin type.
         */
        protected ForOriginType() {
            this(Assigner.DEFAULT, Assigner.Typing.STATIC);
        }

        /**
         * Creates a new fixed value appender for the origin type.
         *
         * @param assigner The assigner to use for assigning the fixed value to the return type of the instrumented value.
         * @param typing   Indicates if dynamic type castings should be attempted for incompatible assignments.
         */
        private ForOriginType(Assigner assigner, Assigner.Typing typing) {
            super(assigner, typing);
        }

        @Override
        public Implementation withAssigner(Assigner assigner, Assigner.Typing typing) {
            return new ForOriginType(assigner, typing);
        }

        @Override
        public ByteCodeAppender appender(Target implementationTarget) {
            return new Appender(implementationTarget.getOriginType().asErasure());
        }

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

        /**
         * An appender for writing the origin type.
         */
        protected class Appender implements ByteCodeAppender {

            /**
             * The instrumented type.
             */
            private final TypeDescription originType;

            /**
             * Creates a new appender.
             *
             * @param originType The instrumented type.
             */
            protected Appender(TypeDescription originType) {
                this.originType = originType;
            }

            @Override
            public Size apply(MethodVisitor methodVisitor, Context implementationContext, MethodDescription instrumentedMethod) {
                return ForOriginType.this.apply(methodVisitor,
                        implementationContext,
                        instrumentedMethod,
                        TypeDescription.CLASS.asGenericType(),
                        ClassConstant.of(originType));
            }

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

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

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

    /**
     * A fixed value of {@code this}.
     */
    protected static class ForThisValue extends FixedValue implements AssignerConfigurable {

        /**
         * Creates an implementation that returns the instance of the instrumented type.
         */
        protected ForThisValue() {
            super(Assigner.DEFAULT, Assigner.Typing.STATIC);
        }

        /**
         * Creates an implementation that returns the instance of the instrumented type.
         *
         * @param assigner The assigner to use for assigning the fixed value to the return type of the instrumented value.
         * @param typing   Indicates if dynamic type castings should be attempted for incompatible assignments.
         */
        private ForThisValue(Assigner assigner, Assigner.Typing typing) {
            super(assigner, typing);
        }

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

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

        @Override
        public Implementation withAssigner(Assigner assigner, Assigner.Typing typing) {
            return new ForThisValue(assigner, typing);
        }

        /**
         * A byte code appender for returning {@code this}.
         */
        @EqualsAndHashCode
        protected static class Appender implements ByteCodeAppender {

            /**
             * The instrumented type.
             */
            private final TypeDescription instrumentedType;

            /**
             * Creates a new byte code appender for returning {@code this}.
             *
             * @param instrumentedType The instrumented type.
             */
            protected Appender(TypeDescription instrumentedType) {
                this.instrumentedType = instrumentedType;
            }

            @Override
            public Size apply(MethodVisitor methodVisitor, Context implementationContext, MethodDescription instrumentedMethod) {
                if (instrumentedMethod.isStatic() || !instrumentedType.isAssignableTo(instrumentedMethod.getReturnType().asErasure())) {
                    throw new IllegalStateException("Cannot return 'this' from " + instrumentedMethod);
                }
                return new ByteCodeAppender.Simple(
                        MethodVariableAccess.loadThis(),
                        MethodReturn.REFERENCE
                ).apply(methodVisitor, implementationContext, instrumentedMethod);
            }
        }
    }

    /**
     * A fixed value implementation that returns a method's argument.
     */
    @EqualsAndHashCode(callSuper = true)
    protected static class ForArgument extends FixedValue implements AssignerConfigurable, ByteCodeAppender {

        /**
         * The argument index.
         */
        private final int index;

        /**
         * Creates a new fixed value implementation that returns a method's argument.
         *
         * @param index The argument's index.
         */
        protected ForArgument(int index) {
            this(Assigner.DEFAULT, Assigner.Typing.STATIC, index);
        }

        /**
         * Creates a new fixed value implementation that returns a method's argument.
         *
         * @param assigner The assigner to use for assigning the fixed value to the return type of the
         *                 instrumented value.
         * @param typing   Indicates if dynamic type castings should be attempted for incompatible assignments.
         * @param index    The argument's index.
         */
        private ForArgument(Assigner assigner, Assigner.Typing typing, int index) {
            super(assigner, typing);
            this.index = index;
        }

        @Override
        public Size apply(MethodVisitor methodVisitor, Context implementationContext, MethodDescription instrumentedMethod) {
            if (instrumentedMethod.getParameters().size() <= index) {
                throw new IllegalStateException(instrumentedMethod + " does not define a parameter with index " + index);
            }
            ParameterDescription parameterDescription = instrumentedMethod.getParameters().get(index);
            StackManipulation stackManipulation = new StackManipulation.Compound(
                    MethodVariableAccess.load(parameterDescription),
                    assigner.assign(parameterDescription.getType(), instrumentedMethod.getReturnType(), typing),
                    MethodReturn.of(instrumentedMethod.getReturnType())
            );
            if (!stackManipulation.isValid()) {
                throw new IllegalStateException("Cannot assign " + instrumentedMethod.getReturnType() + " to " + parameterDescription);
            }
            return new Size(stackManipulation.apply(methodVisitor, implementationContext).getMaximalSize(), instrumentedMethod.getStackSize());
        }

        @Override
        public ByteCodeAppender appender(Target implementationTarget) {
            return this;
        }

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

        @Override
        public Implementation withAssigner(Assigner assigner, Assigner.Typing typing) {
            return new ForArgument(assigner, typing, index);
        }
    }

    /**
     * A fixed value implementation that represents its fixed value as a value that is written to the instrumented
     * class's constant pool.
     */
    @EqualsAndHashCode(callSuper = true)
    protected static class ForPoolValue extends FixedValue implements AssignerConfigurable, ByteCodeAppender {

        /**
         * The stack manipulation which is responsible for loading the fixed value onto the operand stack.
         */
        private final StackManipulation valueLoadInstruction;

        /**
         * The type of the fixed value.
         */
        private final TypeDescription loadedType;

        /**
         * Creates a new constant pool fixed value implementation.
         *
         * @param valueLoadInstruction The instruction that is responsible for loading the constant pool value onto the
         *                             operand stack.
         * @param loadedType           A type description representing the loaded type.
         */
        protected ForPoolValue(StackManipulation valueLoadInstruction, Class<?> loadedType) {
            this(valueLoadInstruction, new TypeDescription.ForLoadedType(loadedType));
        }

        /**
         * Creates a new constant pool fixed value implementation.
         *
         * @param valueLoadInstruction The instruction that is responsible for loading the constant pool value onto the
         *                             operand stack.
         * @param loadedType           A type description representing the loaded type.
         */
        protected ForPoolValue(StackManipulation valueLoadInstruction, TypeDescription loadedType) {
            this(Assigner.DEFAULT, Assigner.Typing.STATIC, valueLoadInstruction, loadedType);
        }

        /**
         * Creates a new constant pool fixed value implementation.
         *
         * @param valueLoadInstruction The instruction that is responsible for loading the constant pool value onto the
         *                             operand stack.
         * @param loadedType           A type description representing the loaded type.
         * @param assigner             The assigner to use for assigning the fixed value to the return type of the
         *                             instrumented value.
         * @param typing               Indicates if dynamic type castings should be attempted for incompatible assignments.
         */
        private ForPoolValue(Assigner assigner, Assigner.Typing typing, StackManipulation valueLoadInstruction, TypeDescription loadedType) {
            super(assigner, typing);
            this.valueLoadInstruction = valueLoadInstruction;
            this.loadedType = loadedType;
        }

        @Override
        public Implementation withAssigner(Assigner assigner, Assigner.Typing typing) {
            return new ForPoolValue(assigner, typing, valueLoadInstruction, loadedType);
        }

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

        @Override
        public ByteCodeAppender appender(Target implementationTarget) {
            return this;
        }

        @Override
        public Size apply(MethodVisitor methodVisitor, Context implementationContext, MethodDescription instrumentedMethod) {
            return apply(methodVisitor, implementationContext, instrumentedMethod, loadedType.asGenericType(), valueLoadInstruction);
        }
    }

    /**
     * A fixed value implementation that represents its fixed value as a static field of the instrumented class.
     */
    @EqualsAndHashCode(callSuper = true, exclude = "fieldType")
    protected static class ForValue extends FixedValue implements AssignerConfigurable {

        /**
         * The prefix of the static field that is created for storing the fixed value.
         */
        private static final String PREFIX = "value";

        /**
         * The name of the field in which the fixed value is stored.
         */
        private final String fieldName;

        /**
         * The value that is to be stored in the static field.
         */
        private final Object value;

        /**
         * The type if the field for storing the fixed value.
         */
        private final TypeDescription.Generic fieldType;

        /**
         * Creates a new static field fixed value implementation with a random name for the field containing the fixed
         * value.
         *
         * @param value The fixed value to be returned.
         */
        protected ForValue(Object value) {
            this(String.format("%s$%s", PREFIX, RandomString.hashOf(value.hashCode())), value);
        }

        /**
         * Creates a new static field fixed value implementation.
         *
         * @param fieldName  The name of the field for storing the fixed value.
         * @param value The fixed value to be returned.
         */
        protected ForValue(String fieldName, Object value) {
            this(Assigner.DEFAULT, Assigner.Typing.STATIC, fieldName, value);
        }

        /**
         * Creates a new static field fixed value implementation.
         *
         * @param fieldName  The name of the field for storing the fixed value.
         * @param value The fixed value to be returned.
         * @param assigner   The assigner to use for assigning the fixed value to the return type of the
         *                   instrumented value.
         * @param typing     Indicates if dynamic type castings should be attempted for incompatible assignments.
         */
        private ForValue(Assigner assigner, Assigner.Typing typing, String fieldName, Object value) {
            super(assigner, typing);
            this.fieldName = fieldName;
            this.value = value;
            fieldType = new TypeDescription.Generic.OfNonGenericType.ForLoadedType(value.getClass());
        }

        @Override
        public Implementation withAssigner(Assigner assigner, Assigner.Typing typing) {
            return new ForValue(assigner, typing, fieldName, value);
        }

        @Override
        public InstrumentedType prepare(InstrumentedType instrumentedType) {
            return instrumentedType
                    .withField(new FieldDescription.Token(fieldName, Opcodes.ACC_SYNTHETIC | Opcodes.ACC_STATIC | Opcodes.ACC_PUBLIC, fieldType))
                    .withInitializer(new LoadedTypeInitializer.ForStaticField(fieldName, value));
        }

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

        /**
         * A byte code appender for returning the fixed value that was stored in a static field.
         */
        @EqualsAndHashCode
        private class StaticFieldByteCodeAppender implements ByteCodeAppender {

            /**
             * The stack manipulation that loads the fixed value onto the operand stack.
             */
            private final StackManipulation fieldGetAccess;

            /**
             * Creates a new byte code appender for returning a value of a static field from an instrumented method.
             *
             * @param instrumentedType The instrumented type that is subject of the instrumentation.
             */
            private StaticFieldByteCodeAppender(TypeDescription instrumentedType) {
                fieldGetAccess = FieldAccess.forField(instrumentedType.getDeclaredFields().filter((named(fieldName))).getOnly()).read();
            }

            @Override
            public Size apply(MethodVisitor methodVisitor, Context implementationContext, MethodDescription instrumentedMethod) {
                return ForValue.this.apply(methodVisitor, implementationContext, instrumentedMethod, fieldType, fieldGetAccess);
            }
        }
    }
}
