/*
 * Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.oracle.svm.hosted.annotation;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.graalvm.compiler.api.replacements.SnippetReflectionProvider;

import com.oracle.svm.core.util.VMError;
import com.oracle.svm.util.ReflectionUtil;

import jdk.vm.ci.meta.JavaConstant;
import jdk.vm.ci.meta.JavaType;
import jdk.vm.ci.meta.MetaAccessProvider;
import jdk.vm.ci.meta.ResolvedJavaMethod;
import sun.reflect.annotation.TypeNotPresentExceptionProxy;

public class AnnotationSubstitutionField extends CustomSubstitutionField {

    private final ResolvedJavaMethod accessorMethod;
    private final Map<JavaConstant, JavaConstant> valueCache;
    private final SnippetReflectionProvider snippetReflection;
    private final MetaAccessProvider metaAccess;

    public AnnotationSubstitutionField(AnnotationSubstitutionType declaringClass, ResolvedJavaMethod accessorMethod,
                    SnippetReflectionProvider snippetReflection,
                    MetaAccessProvider metaAccess) {
        super(declaringClass);
        this.accessorMethod = accessorMethod;
        this.snippetReflection = snippetReflection;
        this.valueCache = Collections.synchronizedMap(new HashMap<>());
        this.metaAccess = metaAccess;
    }

    @Override
    public String getName() {
        return accessorMethod.getName();
    }

    @Override
    public JavaType getType() {
        /*
         * The type of an annotation element can be one of: primitive, String, Class, an enum type,
         * an annotation type, or an array type whose component type is one of the preceding types,
         * according to https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.6.1.
         */
        JavaType actualType = accessorMethod.getSignature().getReturnType(accessorMethod.getDeclaringClass());
        if (AnnotationSupport.isClassType(actualType, metaAccess)) {
            /*
             * Annotation elements that have a Class type can reference classes that are missing at
             * runtime. We declare the corresponding fields with the Object type to be able to store
             * a TypeNotPresentExceptionProxy which we then use to generate the
             * TypeNotPresentException at runtime (see below).
             */
            return metaAccess.lookupJavaType(Object.class);
        }
        return actualType;
    }

    @Override
    public JavaConstant readValue(JavaConstant receiver) {
        JavaConstant result = valueCache.get(receiver);
        if (result == null) {
            Object annotationFieldValue;
            /*
             * Invoke the accessor method of the annotation object. Since array attributes return a
             * different, newly allocated, array at every invocation, we cache the result value.
             */
            try {
                /*
                 * The code below assumes that the annotations have already been parsed and the
                 * result cached in the AnnotationInvocationHandler.memberValues field. The parsing
                 * is triggered, at the least, during object graph checking in
                 * Inflation.checkType(), or earlier when the type annotations are accessed for the
                 * first time, e.g., ImageClassLoader.includedInPlatform() due to the call to
                 * Class.getAnnotation(Platforms.class).
                 */
                Proxy proxy = snippetReflection.asObject(Proxy.class, receiver);

                /*
                 * Reflect on the proxy interface, i.e., the annotation class, instead of the
                 * generated proxy class to avoid module access issues with dynamically generated
                 * modules. The dynamically generated module that the generated proxies belong to,
                 * i.e., `jdk.proxy1`, cannot be open to all-unnamed-modules like we do with other
                 * modules.
                 */
                Class<?> annotationInterface = AnnotationSupport.findAnnotationInterfaceTypeForMarkedAnnotationType(proxy.getClass());
                annotationFieldValue = ReflectionUtil.lookupMethod(annotationInterface, accessorMethod.getName()).invoke(proxy);
            } catch (IllegalAccessException | IllegalArgumentException ex) {
                throw VMError.shouldNotReachHere(ex);
            } catch (InvocationTargetException ex) {
                Throwable cause = ex.getCause();
                if (cause instanceof TypeNotPresentException) {
                    /*
                     * When an annotation has a Class<?> parameter but is referencing a missing
                     * class a TypeNotPresentException is thrown. The TypeNotPresentException is
                     * usually created when the annotation is first parsed, i.e., one some other
                     * parameter is queried, and cached as an TypeNotPresentExceptionProxy. We catch
                     * and repackage it here, then rely on the runtime mechanism to unpack and
                     * rethrow it.
                     */
                    TypeNotPresentException tnpe = (TypeNotPresentException) cause;
                    annotationFieldValue = new TypeNotPresentExceptionProxy(tnpe.typeName(), new NoClassDefFoundError(tnpe.typeName()));
                } else {
                    throw VMError.shouldNotReachHere(ex);
                }
            }

            result = snippetReflection.forBoxed(getJavaKind(), annotationFieldValue);

            valueCache.put(receiver, result);
        }
        return result;
    }

    @Override
    public boolean allowConstantFolding() {
        return true;
    }

    @Override
    public boolean injectFinalForRuntimeCompilation() {
        /*
         * Value of annotations never change at run time, so we can treat the field as final for
         * runtime compilations.
         */
        return true;
    }

    @Override
    public String toString() {
        return "AnnotationField<" + format("%h.%n") + ">";
    }
}