/*
 * Copyright 2014 - 2020 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.build;

import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.modifier.FieldPersistence;
import net.bytebuddy.description.modifier.Ownership;
import net.bytebuddy.description.modifier.SyntheticState;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.utility.RandomString;

import java.lang.annotation.*;
import java.util.HashMap;
import java.util.Map;

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

/**
 * A plugin that caches the return value of a method in a synthetic field. The caching mechanism is not thread-safe but can be used in a
 * concurrent setup if the cached value is frozen, i.e. only defines {@code final} fields. In this context, it is possible that
 * the method is executed multiple times by different threads but at the same time, this approach avoids a {@code volatile} field
 * declaration. For methods with a primitive return type, the type's default value is used to indicate that a method was not yet invoked.
 * For methods that return a reference type, {@code null} is used as an indicator. If a method returns such a value, this mechanism will
 * not work. This plugin does not need to be closed.
 */
@HashCodeAndEqualsPlugin.Enhance
public class CachedReturnPlugin extends Plugin.ForElementMatcher implements Plugin.Factory {

    /**
     * An infix between a field and the random suffix if no field name is chosen.
     */
    private static final String NAME_INFIX = "_";

    /**
     * The infix symbol for advice classes.
     */
    private static final String ADVICE_INFIX = "$";

    /**
     * A random string to use for avoid field name collisions.
     */
    @HashCodeAndEqualsPlugin.ValueHandling(HashCodeAndEqualsPlugin.ValueHandling.Sort.IGNORE)
    private final RandomString randomString;

    /**
     * The class file locator to use.
     */
    private final ClassFileLocator classFileLocator;

    /**
     * A map of advice types mapped by their argument type. All advice types are precompiled using Java 6 to allow
     * for releasing Byte Buddy with a Java 5 byte code level where compiled classes do not contain stack map frames.
     * Byte Buddy filters stack map frames when applying advice in newer version but it cannot add stack map frames
     * without explicit frame computation which is expensive which is why precompilation was used. To avoid loading
     * Java classes in incompatible versions, all advice types are resolved using a type pool.
     */
    @HashCodeAndEqualsPlugin.ValueHandling(HashCodeAndEqualsPlugin.ValueHandling.Sort.IGNORE)
    private final Map<TypeDescription, TypeDescription> adviceByType;

    /**
     * Creates a plugin for caching method return values.
     */
    public CachedReturnPlugin() {
        super(declaresMethod(isAnnotatedWith(Enhance.class)));
        randomString = new RandomString();
        classFileLocator = ClassFileLocator.ForClassLoader.of(CachedReturnPlugin.class.getClassLoader());
        TypePool typePool = TypePool.Default.of(classFileLocator);
        adviceByType = new HashMap<TypeDescription, TypeDescription>();
        for (Class<?> type : new Class<?>[]{
                boolean.class,
                byte.class,
                short.class,
                char.class,
                int.class,
                long.class,
                float.class,
                double.class,
                Object.class
        }) {
            adviceByType.put(TypeDescription.ForLoadedType.ForLoadedType.of(type), typePool.describe(CachedReturnPlugin.class.getName()
                    + ADVICE_INFIX
                    + type.getSimpleName()).resolve());
        }
    }

    /**
     * {@inheritDoc}
     */
    public Plugin make() {
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassFileLocator classFileLocator) {
        for (MethodDescription.InDefinedShape methodDescription : typeDescription.getDeclaredMethods()
                .filter(not(isBridge()).<MethodDescription>and(isAnnotatedWith(Enhance.class)))) {
            if (methodDescription.isAbstract()) {
                throw new IllegalStateException("Cannot cache the value of an abstract method: " + methodDescription);
            } else if (!methodDescription.getParameters().isEmpty()) {
                throw new IllegalStateException("Cannot cache the value of a method with parameters: " + methodDescription);
            } else if (methodDescription.getReturnType().represents(void.class)) {
                throw new IllegalStateException("Cannot cache void result for " + methodDescription);
            }
            String name = methodDescription.getDeclaredAnnotations().ofType(Enhance.class).load().value();
            if (name.length() == 0) {
                name = methodDescription.getName() + NAME_INFIX + randomString.nextString();
            }
            builder = builder
                    .defineField(name, methodDescription.getReturnType().asErasure(), methodDescription.isStatic()
                            ? Ownership.STATIC
                            : Ownership.MEMBER, Visibility.PRIVATE, SyntheticState.SYNTHETIC, FieldPersistence.TRANSIENT)
                    .visit(Advice.withCustomMapping()
                            .bind(CacheField.class, new CacheFieldOffsetMapping(name))
                            .to(adviceByType.get(methodDescription.getReturnType().isPrimitive()
                                    ? methodDescription.getReturnType().asErasure()
                                    : TypeDescription.OBJECT), this.classFileLocator)
                            .on(is(methodDescription)));
        }
        return builder;
    }

    /**
     * {@inheritDoc}
     */
    public void close() {
        /* do nothing */
    }

    /**
     * Indicates methods that should be cached, i.e. where the return value is stored in a synthetic field. For this to be
     * possible, the returned value should not be altered and the instance must be thread-safe if the value might be used from
     * multiple threads.
     */
    @Documented
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Enhance {

        /**
         * The fields name or an empty string if the name should be generated randomly.
         *
         * @return The fields name or an empty string if the name should be generated randomly.
         */
        String value() default "";
    }

    /**
     * Indicates the field that stores the cached value.
     */
    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    protected @interface CacheField {
        /* empty */
    }

    /**
     * An offset mapping for the cached field.
     */
    @HashCodeAndEqualsPlugin.Enhance
    protected static class CacheFieldOffsetMapping implements Advice.OffsetMapping {

        /**
         * The field's name.
         */
        private final String name;

        /**
         * Creates an offset mapping for the cached field.
         *
         * @param name The field's name.
         */
        protected CacheFieldOffsetMapping(String name) {
            this.name = name;
        }

        /**
         * {@inheritDoc}
         */
        public Target resolve(TypeDescription instrumentedType,
                              MethodDescription instrumentedMethod,
                              Assigner assigner,
                              Advice.ArgumentHandler argumentHandler,
                              Sort sort) {
            return new Target.ForField.ReadWrite(instrumentedType.getDeclaredFields().filter(named(name)).getOnly());
        }
    }
}