/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * 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 com.android.dx.dex.cf;

import com.android.dx.cf.attrib.AttAnnotationDefault;
import com.android.dx.cf.attrib.AttEnclosingMethod;
import com.android.dx.cf.attrib.AttExceptions;
import com.android.dx.cf.attrib.AttInnerClasses;
import com.android.dx.cf.attrib.AttRuntimeInvisibleAnnotations;
import com.android.dx.cf.attrib.AttRuntimeInvisibleParameterAnnotations;
import com.android.dx.cf.attrib.AttRuntimeVisibleAnnotations;
import com.android.dx.cf.attrib.AttRuntimeVisibleParameterAnnotations;
import com.android.dx.cf.attrib.AttSignature;
import com.android.dx.cf.attrib.InnerClassList;
import com.android.dx.cf.direct.DirectClassFile;
import com.android.dx.cf.iface.AttributeList;
import com.android.dx.cf.iface.Method;
import com.android.dx.cf.iface.MethodList;
import com.android.dx.dex.file.AnnotationUtils;
import com.android.dx.rop.annotation.Annotation;
import com.android.dx.rop.annotation.AnnotationVisibility;
import com.android.dx.rop.annotation.Annotations;
import com.android.dx.rop.annotation.AnnotationsList;
import com.android.dx.rop.annotation.NameValuePair;
import com.android.dx.rop.code.AccessFlags;
import com.android.dx.rop.cst.CstMethodRef;
import com.android.dx.rop.cst.CstNat;
import com.android.dx.rop.cst.CstType;
import com.android.dx.rop.type.StdTypeList;
import com.android.dx.rop.type.Type;
import com.android.dx.rop.type.TypeList;
import com.android.dx.util.Warning;

import java.util.ArrayList;

/**
 * Utility methods that translate various classfile attributes
 * into forms suitable for use in creating {@code dex} files.
 */
/*package*/ class AttributeTranslator {
    /**
     * This class is uninstantiable.
     */
    private AttributeTranslator() {
        // This space intentionally left blank.
    }

    /**
     * Gets the list of thrown exceptions for a given method.
     *
     * @param method {@code non-null;} the method in question
     * @return {@code non-null;} the list of thrown exceptions
     */
    public static TypeList getExceptions(Method method) {
        AttributeList attribs = method.getAttributes();
        AttExceptions exceptions = (AttExceptions)
            attribs.findFirst(AttExceptions.ATTRIBUTE_NAME);

        if (exceptions == null) {
            return StdTypeList.EMPTY;
        }

        return exceptions.getExceptions();
    }

    /**
     * Gets the annotations out of a given {@link AttributeList}. This
     * combines both visible and invisible annotations into a single
     * result set and also adds in a system annotation for the
     * {@code Signature} attribute if present.
     *
     * @param attribs {@code non-null;} the attributes list to search in
     * @return {@code non-null;} the set of annotations, which may be empty
     */
    public static Annotations getAnnotations(AttributeList attribs) {
        Annotations result = getAnnotations0(attribs);
        Annotation signature = getSignature(attribs);

        if (signature != null) {
            result = Annotations.combine(result, signature);
        }

        return result;
    }

    /**
     * Gets the annotations out of a given class, similar to {@link
     * #getAnnotations}, also including annotations for translations
     * of class-level attributes {@code EnclosingMethod} and
     * {@code InnerClasses}, if present. Additionally, if the
     * class is an annotation class, then this also includes a
     * representation of all the {@code AnnotationDefault}
     * values.
     *
     * @param cf {@code non-null;} the class in question
     * @param args {@code non-null;} the high-level options
     * @return {@code non-null;} the set of annotations, which may be empty
     */
    public static Annotations getClassAnnotations(DirectClassFile cf,
            CfOptions args) {
        CstType thisClass = cf.getThisClass();
        AttributeList attribs = cf.getAttributes();
        Annotations result = getAnnotations(attribs);
        Annotation enclosingMethod = translateEnclosingMethod(attribs);

        try {
            Annotations innerClassAnnotations =
                translateInnerClasses(thisClass, attribs,
                        enclosingMethod == null);
            if (innerClassAnnotations != null) {
                result = Annotations.combine(result, innerClassAnnotations);
            }
        } catch (Warning warn) {
            args.warn.println("warning: " + warn.getMessage());
        }

        if (enclosingMethod != null) {
            result = Annotations.combine(result, enclosingMethod);
        }

        if (AccessFlags.isAnnotation(cf.getAccessFlags())) {
            Annotation annotationDefault =
                translateAnnotationDefaults(cf);
            if (annotationDefault != null) {
                result = Annotations.combine(result, annotationDefault);
            }
        }

        return result;
    }

    /**
     * Gets the annotations out of a given method, similar to {@link
     * #getAnnotations}, also including an annotation for the translation
     * of the method-specific attribute {@code Exceptions}.
     *
     * @param method {@code non-null;} the method in question
     * @return {@code non-null;} the set of annotations, which may be empty
     */
    public static Annotations getMethodAnnotations(Method method) {
        Annotations result = getAnnotations(method.getAttributes());
        TypeList exceptions = getExceptions(method);

        if (exceptions.size() != 0) {
            Annotation throwsAnnotation =
                AnnotationUtils.makeThrows(exceptions);
            result = Annotations.combine(result, throwsAnnotation);
        }

        return result;
    }

    /**
     * Helper method for {@link #getAnnotations} which just gets the
     * existing annotations, per se.
     *
     * @param attribs {@code non-null;} the attributes list to search in
     * @return {@code non-null;} the set of annotations, which may be empty
     */
    private static Annotations getAnnotations0(AttributeList attribs) {
        AttRuntimeVisibleAnnotations visible =
            (AttRuntimeVisibleAnnotations)
            attribs.findFirst(AttRuntimeVisibleAnnotations.ATTRIBUTE_NAME);
        AttRuntimeInvisibleAnnotations invisible =
            (AttRuntimeInvisibleAnnotations)
            attribs.findFirst(AttRuntimeInvisibleAnnotations.ATTRIBUTE_NAME);

        if (visible == null) {
            if (invisible == null) {
                return Annotations.EMPTY;
            }
            return invisible.getAnnotations();
        }

        if (invisible == null) {
            return visible.getAnnotations();
        }

        // Both are non-null, so combine them.

        return Annotations.combine(visible.getAnnotations(),
                invisible.getAnnotations());
    }

    /**
     * Gets the {@code Signature} attribute out of a given
     * {@link AttributeList}, if any, translating it to an annotation.
     *
     * @param attribs {@code non-null;} the attributes list to search in
     * @return {@code null-ok;} the converted {@code Signature} annotation,
     * if there was an attribute to translate
     */
    private static Annotation getSignature(AttributeList attribs) {
        AttSignature signature = (AttSignature)
            attribs.findFirst(AttSignature.ATTRIBUTE_NAME);

        if (signature == null) {
            return null;
        }

        return AnnotationUtils.makeSignature(signature.getSignature());
    }

    /**
     * Gets the {@code EnclosingMethod} attribute out of a given
     * {@link AttributeList}, if any, translating it to an annotation.
     * If the class really has an enclosing method, this returns an
     * {@code EnclosingMethod} annotation; if not, this returns
     * an {@code EnclosingClass} annotation.
     *
     * @param attribs {@code non-null;} the attributes list to search in
     * @return {@code null-ok;} the converted {@code EnclosingMethod} or
     * {@code EnclosingClass} annotation, if there was an
     * attribute to translate
     */
    private static Annotation translateEnclosingMethod(AttributeList attribs) {
        AttEnclosingMethod enclosingMethod = (AttEnclosingMethod)
            attribs.findFirst(AttEnclosingMethod.ATTRIBUTE_NAME);

        if (enclosingMethod == null) {
            return null;
        }

        CstType enclosingClass = enclosingMethod.getEnclosingClass();
        CstNat nat = enclosingMethod.getMethod();

        if (nat == null) {
            /*
             * Dalvik doesn't use EnclosingMethod annotations unless
             * there really is an enclosing method. Anonymous classes
             * are unambiguously identified by having an InnerClass
             * annotation with an empty name along with an appropriate
             * EnclosingClass.
             */
            return AnnotationUtils.makeEnclosingClass(enclosingClass);
        }

        return AnnotationUtils.makeEnclosingMethod(
                new CstMethodRef(enclosingClass, nat));
    }

    /**
     * Gets the {@code InnerClasses} attribute out of a given
     * {@link AttributeList}, if any, translating it to one or more of an
     * {@code InnerClass}, {@code EnclosingClass}, or
     * {@code MemberClasses} annotation.
     *
     * @param thisClass {@code non-null;} type representing the class being
     * processed
     * @param attribs {@code non-null;} the attributes list to search in
     * @param needEnclosingClass whether to include an
     * {@code EnclosingClass} annotation
     * @return {@code null-ok;} the converted list of annotations, if there
     * was an attribute to translate
     */
    private static Annotations translateInnerClasses(CstType thisClass,
            AttributeList attribs, boolean needEnclosingClass) {
        AttInnerClasses innerClasses = (AttInnerClasses)
            attribs.findFirst(AttInnerClasses.ATTRIBUTE_NAME);

        if (innerClasses == null) {
            return null;
        }

        /*
         * Search the list for the element representing the current class
         * as well as for any named member classes.
         */

        InnerClassList list = innerClasses.getInnerClasses();
        int size = list.size();
        InnerClassList.Item foundThisClass = null;
        ArrayList<Type> membersList = new ArrayList<Type>();

        for (int i = 0; i < size; i++) {
            InnerClassList.Item item = list.get(i);
            CstType innerClass = item.getInnerClass();
            if (innerClass.equals(thisClass)) {
                foundThisClass = item;
            } else if (thisClass.equals(item.getOuterClass())) {
                membersList.add(innerClass.getClassType());
            }
        }

        int membersSize = membersList.size();

        if ((foundThisClass == null) && (membersSize == 0)) {
            return null;
        }

        Annotations result = new Annotations();

        if (foundThisClass != null) {
            result.add(AnnotationUtils.makeInnerClass(
                               foundThisClass.getInnerName(),
                               foundThisClass.getAccessFlags()));
            if (needEnclosingClass) {
                CstType outer = foundThisClass.getOuterClass();
                if (outer == null) {
                    throw new Warning(
                            "Ignoring InnerClasses attribute for an " +
                            "anonymous inner class\n" +
                            "(" + thisClass.toHuman() +
                            ") that doesn't come with an\n" +
                            "associated EnclosingMethod attribute. " +
                            "This class was probably produced by a\n" +
                            "compiler that did not target the modern " +
                            ".class file format. The recommended\n" +
                            "solution is to recompile the class from " +
                            "source, using an up-to-date compiler\n" +
                            "and without specifying any \"-target\" type " +
                            "options. The consequence of ignoring\n" +
                            "this warning is that reflective operations " +
                            "on this class will incorrectly\n" +
                            "indicate that it is *not* an inner class.");
                }
                result.add(AnnotationUtils.makeEnclosingClass(
                                   foundThisClass.getOuterClass()));
            }
        }

        if (membersSize != 0) {
            StdTypeList typeList = new StdTypeList(membersSize);
            for (int i = 0; i < membersSize; i++) {
                typeList.set(i, membersList.get(i));
            }
            typeList.setImmutable();
            result.add(AnnotationUtils.makeMemberClasses(typeList));
        }

        result.setImmutable();
        return result;
    }

    /**
     * Gets the parameter annotations out of a given method. This
     * combines both visible and invisible annotations into a single
     * result set.
     *
     * @param method {@code non-null;} the method in question
     * @return {@code non-null;} the list of annotation sets, which may be
     * empty
     */
    public static AnnotationsList getParameterAnnotations(Method method) {
        AttributeList attribs = method.getAttributes();
        AttRuntimeVisibleParameterAnnotations visible =
            (AttRuntimeVisibleParameterAnnotations)
            attribs.findFirst(
                    AttRuntimeVisibleParameterAnnotations.ATTRIBUTE_NAME);
        AttRuntimeInvisibleParameterAnnotations invisible =
            (AttRuntimeInvisibleParameterAnnotations)
            attribs.findFirst(
                    AttRuntimeInvisibleParameterAnnotations.ATTRIBUTE_NAME);

        if (visible == null) {
            if (invisible == null) {
                return AnnotationsList.EMPTY;
            }
            return invisible.getParameterAnnotations();
        }

        if (invisible == null) {
            return visible.getParameterAnnotations();
        }

        // Both are non-null, so combine them.

        return AnnotationsList.combine(visible.getParameterAnnotations(),
                invisible.getParameterAnnotations());
    }

    /**
     * Gets the {@code AnnotationDefault} attributes out of a
     * given class, if any, reforming them as an
     * {@code AnnotationDefault} annotation.
     *
     * @param cf {@code non-null;} the class in question
     * @return {@code null-ok;} an appropriately-constructed
     * {@code AnnotationDefault} annotation, if there were any
     * annotation defaults in the class, or {@code null} if not
     */
    private static Annotation translateAnnotationDefaults(DirectClassFile cf) {
        CstType thisClass = cf.getThisClass();
        MethodList methods = cf.getMethods();
        int sz = methods.size();
        Annotation result =
            new Annotation(thisClass, AnnotationVisibility.EMBEDDED);
        boolean any = false;

        for (int i = 0; i < sz; i++) {
            Method one = methods.get(i);
            AttributeList attribs = one.getAttributes();
            AttAnnotationDefault oneDefault = (AttAnnotationDefault)
                attribs.findFirst(AttAnnotationDefault.ATTRIBUTE_NAME);

            if (oneDefault != null) {
                NameValuePair pair = new NameValuePair(
                        one.getNat().getName(),
                        oneDefault.getValue());
                result.add(pair);
                any = true;
            }
        }

        if (! any) {
            return null;
        }

        result.setImmutable();
        return AnnotationUtils.makeAnnotationDefault(result);
    }
}
