package org.checkerframework.framework.type;

import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Symbol;
import java.util.List;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import org.checkerframework.framework.source.Result;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable;
import org.checkerframework.framework.type.visitor.AnnotatedTypeScanner;
import org.checkerframework.framework.util.element.ClassTypeParamApplier;
import org.checkerframework.framework.util.element.ElementAnnotationUtil.UnexpectedAnnotationLocationException;
import org.checkerframework.framework.util.element.MethodApplier;
import org.checkerframework.framework.util.element.MethodTypeParamApplier;
import org.checkerframework.framework.util.element.ParamApplier;
import org.checkerframework.framework.util.element.SuperTypeApplier;
import org.checkerframework.framework.util.element.TypeDeclarationApplier;
import org.checkerframework.framework.util.element.TypeVarUseApplier;
import org.checkerframework.framework.util.element.VariableApplier;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.Pair;

/**
 * Utility methods for adding the annotations that are stored in an Element to the type that
 * represents that element (or a use of that Element).
 *
 * <p>In a way, this class is a hack: the Type representation for the Elements should contain all
 * annotations that we want. However, due to javac bugs
 * http://mail.openjdk.java.net/pipermail/type-annotations-dev/2013-December/001449.html decoding
 * the type annotations from the Element is necessary.
 *
 * <p>Even once these bugs are fixed, this class might be useful: in TypesIntoElements it is easy to
 * add additional annotations to the element and have them stored in the bytecode by the compiler.
 * It would be more work (and might not work in the end) to instead modify the Type directly. The
 * interaction between TypeFromElement and TypesIntoElements allows us to write the defaulted
 * annotations into the Element and have them read later by other parts.
 */
public class ElementAnnotationApplier {

    /**
     * Add all of the relevant annotations stored in Element to type. This includes both top-level
     * primary annotations and nested annotations. For the most part the TypeAnnotationPosition of
     * the element annotations are used to locate the annotation in the right AnnotatedTypeMirror
     * location though the individual applier classes may have special rules (such as those for
     * upper and lower bounds and intersections).
     *
     * <p>Note: Element annotations come from two sources.
     *
     * <ol>
     *   <li>Annotations found on elements may represent those in source code or bytecode; these are
     *       added to the element by the compiler.
     *   <li>The annotations may also represent those that were inferred or defaulted by the Checker
     *       Framework after a previous call to this method. The Checker Framework will store any
     *       annotations on declarations back into the elements that represent them (see {@link
     *       org.checkerframework.framework.type.TypesIntoElements}). Subsequent, calls to apply
     *       will encounter these annotations on the provided element.
     * </ol>
     *
     * Note: This is not the ONLY place that annotations are explicitly added to types. See {@link
     * org.checkerframework.framework.type.TypeFromTree}.
     *
     * @param type the type to which we wish to apply the element's annotations
     * @param element an element that possibly contains annotations
     * @param typeFactory the typeFactory used to create the given type
     */
    public static void apply(
            final AnnotatedTypeMirror type,
            final Element element,
            final AnnotatedTypeFactory typeFactory) {
        try {
            applyInternal(type, element, typeFactory);
        } catch (UnexpectedAnnotationLocationException e) {
            reportInvalidLocation(element, typeFactory);
        }
        // Also copy annotations from type parameters to their uses.
        new TypeVarAnnotator().visit(type, typeFactory);
    }

    /** Issues an "invalid.annotation.location.bytecode warning. */
    private static void reportInvalidLocation(Element element, AnnotatedTypeFactory typeFactory) {
        Element report = element;
        if (element.getEnclosingElement().getKind() == ElementKind.METHOD) {
            report = element.getEnclosingElement();
        }
        // There's a bug in Java 8 compiler that creates bad bytecode such that an
        // annotation on a lambda parameter is applied to a method parameter. (This bug has
        // been fixed in Java 9.) If this happens, then the location could refer to a
        // location, such as a type argument, that doesn't exist. Since Java 8 bytecode
        // might be on the classpath, catch this exception and ignore the type.
        // TODO: Issue an error if this annotation is from Java 9+ bytecode.
        if (!typeFactory.checker.hasOption("ignoreInvalidAnnotationLocations")) {
            typeFactory.checker.report(
                    Result.warning(
                            "invalid.annotation.location.bytecode",
                            ElementUtils.getVerboseName(report)),
                    element);
        }
    }

    /** Same as apply except that annotations aren't copied from type parameter declarations. */
    private static void applyInternal(
            final AnnotatedTypeMirror type,
            final Element element,
            final AnnotatedTypeFactory typeFactory)
            throws UnexpectedAnnotationLocationException {

        if (element == null) {
            throw new BugInCF("ElementAnnotationUtil.apply: element cannot be null");

        } else if (TypeVarUseApplier.accepts(type, element)) {
            TypeVarUseApplier.apply(type, element, typeFactory);

        } else if (VariableApplier.accepts(type, element)) {
            if (element.getKind() != ElementKind.LOCAL_VARIABLE) {
                // For local variables we have the source code,
                // so there is no need to look at the Element.
                // This is needed to avoid a bug in the JDK:
                // https://github.com/eisop/checker-framework/issues/14
                VariableApplier.apply(type, element);
            }

        } else if (MethodApplier.accepts(type, element)) {
            MethodApplier.apply(type, element, typeFactory);

        } else if (TypeDeclarationApplier.accepts(type, element)) {
            TypeDeclarationApplier.apply(type, element, typeFactory);

        } else if (ClassTypeParamApplier.accepts(type, element)) {
            ClassTypeParamApplier.apply((AnnotatedTypeVariable) type, element, typeFactory);

        } else if (MethodTypeParamApplier.accepts(type, element)) {
            MethodTypeParamApplier.apply((AnnotatedTypeVariable) type, element, typeFactory);

        } else if (ParamApplier.accepts(type, element)) {
            ParamApplier.apply(type, element, typeFactory);

        } else if (isCaptureConvertedTypeVar(element)) {
            // Types resulting from capture conversion cannot have explicit annotations

        } else {
            throw new BugInCF(
                    "ElementAnnotationUtil.apply: illegal argument: "
                            + element
                            + " ["
                            + element.getKind()
                            + "]"
                            + " with type "
                            + type);
        }
    }

    /**
     * Annotate the list of supertypes using the annotations on the TypeElement representing a class
     * or interface.
     *
     * @param supertypes types representing supertype declarations of TypeElement
     * @param subtypeElement an element representing the declaration of the class which is a subtype
     *     of supertypes
     */
    public static void annotateSupers(
            List<AnnotatedDeclaredType> supertypes, TypeElement subtypeElement) {
        try {
            SuperTypeApplier.annotateSupers(supertypes, subtypeElement);
        } catch (UnexpectedAnnotationLocationException e) {
            reportInvalidLocation(subtypeElement, supertypes.get(0).atypeFactory);
        }
    }

    /**
     * Helper method to get the lambda tree for ParamApplier. Ideally, this method would be located
     * in ElementAnnotationUtil but since AnnotatedTypeFactory.declarationFromElement is protected,
     * it has been placed here.
     *
     * @param varEle the element that may represent a lambda's parameter
     * @return a LambdaExpressionTree if the varEle represents a parameter in a lambda expression,
     *     otherwise null
     */
    public static Pair<VariableTree, LambdaExpressionTree> getParamAndLambdaTree(
            VariableElement varEle, AnnotatedTypeFactory typeFactory) {
        VariableTree paramDecl = (VariableTree) typeFactory.declarationFromElement(varEle);

        if (paramDecl != null) {
            final Tree parentTree = typeFactory.getPath(paramDecl).getParentPath().getLeaf();
            if (parentTree != null && parentTree.getKind() == Kind.LAMBDA_EXPRESSION) {
                return Pair.of(paramDecl, (LambdaExpressionTree) parentTree);
            }
        }

        return null;
    }

    /**
     * Was the type passed in generated by capture conversion.
     *
     * @param element the element which type represents
     * @return true if type was generated via capture conversion false otherwise
     */
    private static boolean isCaptureConvertedTypeVar(final Element element) {
        final Element enclosure = element.getEnclosingElement();
        return (((Symbol) enclosure).kind == com.sun.tools.javac.code.Kinds.Kind.NIL);
    }

    /**
     * Annotates uses of type variables with annotation written explicitly on the type parameter
     * declaration and/or its upper bound.
     */
    private static class TypeVarAnnotator extends AnnotatedTypeScanner<Void, AnnotatedTypeFactory> {
        @Override
        public Void visitTypeVariable(AnnotatedTypeVariable type, AnnotatedTypeFactory factory) {
            TypeParameterElement tpelt =
                    (TypeParameterElement) type.getUnderlyingType().asElement();

            if (type.getAnnotations().isEmpty()
                    && type.getUpperBound().getAnnotations().isEmpty()
                    && tpelt.getEnclosingElement().getKind() != ElementKind.TYPE_PARAMETER) {
                try {
                    ElementAnnotationApplier.applyInternal(type, tpelt, factory);
                } catch (UnexpectedAnnotationLocationException e) {
                    // The above is the second call to applyInternal on this type and element, so
                    // any errors were already reported by the first call. (See the only use of this
                    // class.)
                }
            }
            return super.visitTypeVariable(type, factory);
        }
    }
}
