/*
 * Javassist, a Java-bytecode translator toolkit.
 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License.  Alternatively, the contents of this file may be used under
 * the terms of the GNU Lesser General Public License Version 2.1 or later,
 * or the Apache License Version 2.0.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 */

package javassist.bytecode;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.AnnotationMemberValue;
import javassist.bytecode.annotation.AnnotationsWriter;
import javassist.bytecode.annotation.ArrayMemberValue;
import javassist.bytecode.annotation.BooleanMemberValue;
import javassist.bytecode.annotation.ByteMemberValue;
import javassist.bytecode.annotation.CharMemberValue;
import javassist.bytecode.annotation.ClassMemberValue;
import javassist.bytecode.annotation.DoubleMemberValue;
import javassist.bytecode.annotation.EnumMemberValue;
import javassist.bytecode.annotation.FloatMemberValue;
import javassist.bytecode.annotation.IntegerMemberValue;
import javassist.bytecode.annotation.LongMemberValue;
import javassist.bytecode.annotation.MemberValue;
import javassist.bytecode.annotation.ShortMemberValue;
import javassist.bytecode.annotation.StringMemberValue;

/**
 * A class representing
 * <code>RuntimeVisibleAnnotations_attribute</code> and
 * <code>RuntimeInvisibleAnnotations_attribute</code>.
 *
 * <p>To obtain an AnnotationAttribute object, invoke
 * <code>getAttribute(AnnotationsAttribute.visibleTag)</code>
 * in <code>ClassFile</code>, <code>MethodInfo</code>,
 * or <code>FieldInfo</code>.  The obtained attribute is a
 * runtime visible annotations attribute.
 * If the parameter is
 * <code>AnnotationAttribute.invisibleTag</code>, then the obtained
 * attribute is a runtime invisible one.
 *
 * <p>For example,
 *
 * <pre>
 * import javassist.bytecode.annotation.Annotation;
 *    :
 * CtMethod m = ... ;
 * MethodInfo minfo = m.getMethodInfo();
 * AnnotationsAttribute attr = (AnnotationsAttribute)
 *         minfo.getAttribute(AnnotationsAttribute.invisibleTag);
 * Annotation an = attr.getAnnotation("Author");
 * String s = ((StringMemberValue)an.getMemberValue("name")).getValue();
 * System.out.println("@Author(name=" + s + ")");
 * </pre>
 *
 * <p>This code snippet retrieves an annotation of the type <code>Author</code>
 * from the <code>MethodInfo</code> object specified by <code>minfo</code>.
 * Then, it prints the value of <code>name</code> in <code>Author</code>.
 *
 * <p>If the annotation type <code>Author</code> is annotated by a meta annotation:
 *
 * <pre>
 * &#64;Retention(RetentionPolicy.RUNTIME)
 * </pre>
 *
 * <p>Then <code>Author</code> is visible at runtime.  Therefore, the third
 * statement of the code snippet above must be changed into:
 *
 * <pre>
 * AnnotationsAttribute attr = (AnnotationsAttribute)
 *         minfo.getAttribute(AnnotationsAttribute.visibleTag);
 * </pre>
 *
 * <p>The attribute tag must be <code>visibleTag</code> instead of
 * <code>invisibleTag</code>.
 *
 * <p>If the member value of an annotation is not specified, the default value
 * is used as that member value.  If so, <code>getMemberValue()</code> in
 * <code>Annotation</code> returns <code>null</code>
 * since the default value is not included in the
 * <code>AnnotationsAttribute</code>.  It is included in the
 * <code>AnnotationDefaultAttribute</code> of the method declared in the
 * annotation type.
 *
 * <p>If you want to record a new AnnotationAttribute object, execute the
 * following snippet:
 *
 * <pre>
 * ClassFile cf = ... ;
 * ConstPool cp = cf.getConstPool();
 * AnnotationsAttribute attr
 *     = new AnnotationsAttribute(cp, AnnotationsAttribute.visibleTag);
 * Annotation a = new Annotation("Author", cp);
 * a.addMemberValue("name", new StringMemberValue("Chiba", cp));
 * attr.setAnnotation(a);
 * cf.addAttribute(attr);
 * cf.setVersionToJava5();
 * </pre>
 *
 * <p>The last statement is necessary if the class file was produced by
 * <code>javac</code> of JDK 1.4 or earlier.  Otherwise, it is not necessary.
 *
 * @see AnnotationDefaultAttribute
 * @see javassist.bytecode.annotation.Annotation
 */
public class AnnotationsAttribute extends AttributeInfo {
    /**
     * The name of the <code>RuntimeVisibleAnnotations</code> attribute.
     */
    public static final String visibleTag = "RuntimeVisibleAnnotations";

    /**
     * The name of the <code>RuntimeInvisibleAnnotations</code> attribute.
     */
    public static final String invisibleTag = "RuntimeInvisibleAnnotations";

    /**
     * Constructs a <code>Runtime(In)VisibleAnnotations_attribute</code>.
     *
     * @param cp            constant pool
     * @param attrname      attribute name (<code>visibleTag</code> or
     *                      <code>invisibleTag</code>).
     * @param info          the contents of this attribute.  It does not
     *                      include <code>attribute_name_index</code> or
     *                      <code>attribute_length</code>.
     */
    public AnnotationsAttribute(ConstPool cp, String attrname, byte[] info) {
        super(cp, attrname, info);
    }

    /**
     * Constructs an empty
     * <code>Runtime(In)VisibleAnnotations_attribute</code>.
     * A new annotation can be later added to the created attribute
     * by <code>setAnnotations()</code>.
     *
     * @param cp            constant pool
     * @param attrname      attribute name (<code>visibleTag</code> or
     *                      <code>invisibleTag</code>).
     * @see #setAnnotations(Annotation[])
     */
    public AnnotationsAttribute(ConstPool cp, String attrname) {
        this(cp, attrname, new byte[] { 0, 0 });
    }

    /**
     * @param n     the attribute name.
     */
    AnnotationsAttribute(ConstPool cp, int n, DataInputStream in)
        throws IOException
    {
        super(cp, n, in);
    }

    /**
     * Returns <code>num_annotations</code>.
     */
    public int numAnnotations() {
        return ByteArray.readU16bit(info, 0);
    }

    /**
     * Copies this attribute and returns a new copy.
     */
    @Override
    public AttributeInfo copy(ConstPool newCp, Map<String,String> classnames) {
        Copier copier = new Copier(info, constPool, newCp, classnames);
        try {
            copier.annotationArray();
            return new AnnotationsAttribute(newCp, getName(), copier.close());
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Parses the annotations and returns a data structure representing
     * the annotation with the specified type.  See also
     * <code>getAnnotations()</code> as to the returned data structure.
     *
     * @param type      the annotation type.
     * @return null if the specified annotation type is not included.
     * @see #getAnnotations()
     */
    public Annotation getAnnotation(String type) {
        Annotation[] annotations = getAnnotations();
        for (int i = 0; i < annotations.length; i++) {
            if (annotations[i].getTypeName().equals(type))
                return annotations[i];
        }

        return null;
    }

    /**
     * Adds an annotation.  If there is an annotation with the same type,
     * it is removed before the new annotation is added.
     *
     * @param annotation        the added annotation.
     */
    public void addAnnotation(Annotation annotation) {
        String type = annotation.getTypeName();
        Annotation[] annotations = getAnnotations();
        for (int i = 0; i < annotations.length; i++) {
            if (annotations[i].getTypeName().equals(type)) {
                annotations[i] = annotation;
                setAnnotations(annotations);
                return;
            }
        }

        Annotation[] newlist = new Annotation[annotations.length + 1];
        System.arraycopy(annotations, 0, newlist, 0, annotations.length);
        newlist[annotations.length] = annotation;
        setAnnotations(newlist);
    }

    /**
     * Removes an annotation by type.
     * After removing an annotation, if {@link #numAnnotations()} returns 0,
     * this annotations attribute has to be removed.
     *
     * @param type        of annotation to remove
     * @return whether an annotation with the given type has been removed
     * @since 3.21
     */
    public boolean removeAnnotation(String type) {
        Annotation[] annotations = getAnnotations();
        for (int i = 0; i < annotations.length; i++) {
            if (annotations[i].getTypeName().equals(type)) {
                Annotation[] newlist = new Annotation[annotations.length - 1];
                System.arraycopy(annotations, 0, newlist, 0, i);
                if (i < annotations.length - 1) {
                    System.arraycopy(annotations, i + 1, newlist, i,
                                     annotations.length - i - 1);
                }
                setAnnotations(newlist);
                return true;
            }
        }
        return false;
    }

    /**
     * Parses the annotations and returns a data structure representing
     * that parsed annotations.  Note that changes of the node values of the
     * returned tree are not reflected on the annotations represented by
     * this object unless the tree is copied back to this object by
     * <code>setAnnotations()</code>.
     *
     * @see #setAnnotations(Annotation[])
     */
    public Annotation[] getAnnotations() {
        try {
            return new Parser(info, constPool).parseAnnotations();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Changes the annotations represented by this object according to
     * the given array of <code>Annotation</code> objects.
     *
     * @param annotations           the data structure representing the
     *                              new annotations.
     */
    public void setAnnotations(Annotation[] annotations) {
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        AnnotationsWriter writer = new AnnotationsWriter(output, constPool);
        try {
            int n = annotations.length;
            writer.numAnnotations(n);
            for (int i = 0; i < n; ++i)
                annotations[i].write(writer);

            writer.close();
        }
        catch (IOException e) {
            throw new RuntimeException(e);      // should never reach here.
        }

        set(output.toByteArray());
    }

    /**
     * Changes the annotations.  A call to this method is equivalent to:
     * <pre>setAnnotations(new Annotation[] { annotation })</pre>
     *
     * @param annotation    the data structure representing
     *                      the new annotation.
     */
    public void setAnnotation(Annotation annotation) {
        setAnnotations(new Annotation[] { annotation });
    }

    /**
     * @param oldname       a JVM class name.
     * @param newname       a JVM class name.
     */
    @Override
    void renameClass(String oldname, String newname) {
        Map<String,String> map = new HashMap<String,String>();
        map.put(oldname, newname);
        renameClass(map);
    }

    @Override
    void renameClass(Map<String,String> classnames) {
        Renamer renamer = new Renamer(info, getConstPool(), classnames);
        try {
            renamer.annotationArray();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    void getRefClasses(Map<String,String> classnames) { renameClass(classnames); }

    /**
     * Returns a string representation of this object.
     */
    @Override
    public String toString() {
        Annotation[] a = getAnnotations();
        StringBuilder sbuf = new StringBuilder();
        int i = 0;
        while (i < a.length) {
            sbuf.append(a[i++].toString());
            if (i != a.length)
                sbuf.append(", ");
        }

        return sbuf.toString();
    }

    static class Walker {
        byte[] info;

        Walker(byte[] attrInfo) {
            info = attrInfo;
        }

        final void parameters() throws Exception {
            int numParam = info[0] & 0xff;
            parameters(numParam, 1);
        }

        void parameters(int numParam, int pos) throws Exception {
            for (int i = 0; i < numParam; ++i)
                pos = annotationArray(pos);
        }

        final void annotationArray() throws Exception {
            annotationArray(0);
        }

        final int annotationArray(int pos) throws Exception {
            int num = ByteArray.readU16bit(info, pos);
            return annotationArray(pos + 2, num);
        }

        int annotationArray(int pos, int num) throws Exception {
            for (int i = 0; i < num; ++i)
                pos = annotation(pos);

            return pos;
        }

        final int annotation(int pos) throws Exception {
            int type = ByteArray.readU16bit(info, pos);
            int numPairs = ByteArray.readU16bit(info, pos + 2);
            return annotation(pos + 4, type, numPairs);
        }

        int annotation(int pos, int type, int numPairs) throws Exception {
            for (int j = 0; j < numPairs; ++j)
                pos = memberValuePair(pos);

            return pos;
        }

        /**
         * {@code element_value_paris}
         */
        final int memberValuePair(int pos) throws Exception {
            int nameIndex = ByteArray.readU16bit(info, pos);
            return memberValuePair(pos + 2, nameIndex);
        }

        /**
         * {@code element_value_paris[]}
         */
        int memberValuePair(int pos, int nameIndex) throws Exception {
            return memberValue(pos);
        }

        /**
         * {@code element_value}
         */
        final int memberValue(int pos) throws Exception {
            int tag = info[pos] & 0xff;
            if (tag == 'e') {
                int typeNameIndex = ByteArray.readU16bit(info, pos + 1);
                int constNameIndex = ByteArray.readU16bit(info, pos + 3);
                enumMemberValue(pos, typeNameIndex, constNameIndex);
                return pos + 5;
            }
            else if (tag == 'c') {
                int index = ByteArray.readU16bit(info, pos + 1);
                classMemberValue(pos, index);
                return pos + 3;
            }
            else if (tag == '@')
                return annotationMemberValue(pos + 1);
            else if (tag == '[') {
                int num = ByteArray.readU16bit(info, pos + 1);
                return arrayMemberValue(pos + 3, num);
            }
            else { // primitive types or String.
                int index = ByteArray.readU16bit(info, pos + 1);
                constValueMember(tag, index);
                return pos + 3;
            }
        }

        /**
         * {@code const_value_index}
         */
        void constValueMember(int tag, int index) throws Exception {}

        /**
         * {@code enum_const_value}
         */
        void enumMemberValue(int pos, int typeNameIndex, int constNameIndex)
            throws Exception {
        }

        /**
         * {@code class_info_index}
         */
        void classMemberValue(int pos, int index) throws Exception {}

        /**
         * {@code annotation_value}
         */
        int annotationMemberValue(int pos) throws Exception {
            return annotation(pos);
        }

        /**
         * {@code array_value}
         */
        int arrayMemberValue(int pos, int num) throws Exception {
            for (int i = 0; i < num; ++i) {
                pos = memberValue(pos);
            }

            return pos;
        }
    }

    static class Renamer extends Walker {
        ConstPool cpool;
        Map<String,String> classnames;

        /**
         * Constructs a renamer.  It renames some class names
         * into the new names specified by <code>map</code>.
         *
         * @param info      the annotations attribute.
         * @param cp        the constant pool.
         * @param map       pairs of replaced and substituted class names.
         *                  It can be null.
         */
        Renamer(byte[] info, ConstPool cp, Map<String,String> map) {
            super(info);
            cpool = cp;
            classnames = map;
        }

        @Override
        int annotation(int pos, int type, int numPairs) throws Exception {
            renameType(pos - 4, type);
            return super.annotation(pos, type, numPairs);
        }

        @Override
        void enumMemberValue(int pos, int typeNameIndex, int constNameIndex)
            throws Exception
        {
            renameType(pos + 1, typeNameIndex);
            super.enumMemberValue(pos, typeNameIndex, constNameIndex);
        }

        @Override
        void classMemberValue(int pos, int index) throws Exception {
            renameType(pos + 1, index);
            super.classMemberValue(pos, index);
        }

        private void renameType(int pos, int index) {
            String name = cpool.getUtf8Info(index);
            String newName = Descriptor.rename(name, classnames);
            if (!name.equals(newName)) {
                int index2 = cpool.addUtf8Info(newName);
                ByteArray.write16bit(index2, info, pos);
            }
        }
    }

    static class Copier extends Walker {
        ByteArrayOutputStream output;
        AnnotationsWriter writer;
        ConstPool srcPool, destPool;
        Map<String,String> classnames;

        /**
         * Constructs a copier.  This copier renames some class names
         * into the new names specified by <code>map</code> when it copies
         * an annotation attribute.
         *
         * @param info      the source attribute.
         * @param src       the constant pool of the source class.
         * @param dest      the constant pool of the destination class.
         * @param map       pairs of replaced and substituted class names.
         *                  It can be null.
         */
        Copier(byte[] info, ConstPool src, ConstPool dest, Map<String,String> map) {
            this(info, src, dest, map, true); 
        }

        Copier(byte[] info, ConstPool src, ConstPool dest, Map<String,String> map, boolean makeWriter) {
            super(info);
            output = new ByteArrayOutputStream();
            if (makeWriter)
                writer = new AnnotationsWriter(output, dest);

            srcPool = src;
            destPool = dest;
            classnames = map;
        }

        byte[] close() throws IOException {
            writer.close();
            return output.toByteArray();
        }

        @Override
        void parameters(int numParam, int pos) throws Exception {
            writer.numParameters(numParam);
            super.parameters(numParam, pos);
        }

        @Override
        int annotationArray(int pos, int num) throws Exception {
            writer.numAnnotations(num);
            return super.annotationArray(pos, num);
        }

        @Override
        int annotation(int pos, int type, int numPairs) throws Exception {
            writer.annotation(copyType(type), numPairs);
            return super.annotation(pos, type, numPairs);
        }

        @Override
        int memberValuePair(int pos, int nameIndex) throws Exception {
            writer.memberValuePair(copy(nameIndex));
            return super.memberValuePair(pos, nameIndex);
        }

        @Override
        void constValueMember(int tag, int index) throws Exception {
            writer.constValueIndex(tag, copy(index));
            super.constValueMember(tag, index);
        }

        @Override
        void enumMemberValue(int pos, int typeNameIndex, int constNameIndex)
            throws Exception
        {
            writer.enumConstValue(copyType(typeNameIndex), copy(constNameIndex));
            super.enumMemberValue(pos, typeNameIndex, constNameIndex);
        }

        @Override
        void classMemberValue(int pos, int index) throws Exception {
            writer.classInfoIndex(copyType(index));
            super.classMemberValue(pos, index);
        }

        @Override
        int annotationMemberValue(int pos) throws Exception {
            writer.annotationValue();
            return super.annotationMemberValue(pos);
        }

        @Override
        int arrayMemberValue(int pos, int num) throws Exception {
            writer.arrayValue(num);
            return super.arrayMemberValue(pos, num);
        }

        /**
         * Copies a constant pool entry into the destination constant pool
         * and returns the index of the copied entry.
         *
         * @param srcIndex      the index of the copied entry into the source
         *                      constant pool.
         * @return the index of the copied item into the destination
         *         constant pool.
         */
        int copy(int srcIndex) {
            return srcPool.copy(srcIndex, destPool, classnames);
        }

        /**
         * Copies a constant pool entry into the destination constant pool
         * and returns the index of the copied entry.  That entry must be
         * a Utf8Info representing a class name in the L<class name>; form.
         *
         * @param srcIndex  the index of the copied entry into the source
         *                  constant pool.
         * @return          the index of the copied item into the destination
         *                  constant pool.
         */
        int copyType(int srcIndex) {
            String name = srcPool.getUtf8Info(srcIndex);
            String newName = Descriptor.rename(name, classnames);
            return destPool.addUtf8Info(newName);
        }
    }

    static class Parser extends Walker {
        ConstPool pool;
        Annotation[][] allParams;   // all parameters
        Annotation[] allAnno;       // all annotations
        Annotation currentAnno;     // current annotation
        MemberValue currentMember;  // current member

        /**
         * Constructs a parser.  This parser constructs a parse tree of
         * the annotations.
         *
         * @param info      the attribute.
         * @param src       the constant pool.
         */
        Parser(byte[] info, ConstPool cp) {
            super(info);
            pool = cp;
        }

        Annotation[][] parseParameters() throws Exception {
            parameters();
            return allParams;
        }

        Annotation[] parseAnnotations() throws Exception {
            annotationArray();
            return allAnno;
        }

        MemberValue parseMemberValue() throws Exception {
            memberValue(0);
            return currentMember;
        }

        @Override
        void parameters(int numParam, int pos) throws Exception {
            Annotation[][] params = new Annotation[numParam][];
            for (int i = 0; i < numParam; ++i) {
                pos = annotationArray(pos);
                params[i] = allAnno;
            }

            allParams = params;
        }

        @Override
        int annotationArray(int pos, int num) throws Exception {
            Annotation[] array = new Annotation[num];
            for (int i = 0; i < num; ++i) {
                pos = annotation(pos);
                array[i] = currentAnno;
            }

            allAnno = array;
            return pos;
        }

        @Override
        int annotation(int pos, int type, int numPairs) throws Exception {
            currentAnno = new Annotation(type, pool);
            return super.annotation(pos, type, numPairs);
        }

        @Override
        int memberValuePair(int pos, int nameIndex) throws Exception {
            pos = super.memberValuePair(pos, nameIndex);
            currentAnno.addMemberValue(nameIndex, currentMember);
            return pos;
        }

        @Override
        void constValueMember(int tag, int index) throws Exception {
            MemberValue m;
            ConstPool cp = pool;
            switch (tag) {
            case 'B' :
                m = new ByteMemberValue(index, cp);
                break;
            case 'C' :
                m = new CharMemberValue(index, cp);
                break;
            case 'D' :
                m = new DoubleMemberValue(index, cp);
                break;
            case 'F' :
                m = new FloatMemberValue(index, cp);
                break;
            case 'I' :
                m = new IntegerMemberValue(index, cp);
                break;
            case 'J' :
                m = new LongMemberValue(index, cp);
                break;
            case 'S' :
                m = new ShortMemberValue(index, cp);
                break;
            case 'Z' :
                m = new BooleanMemberValue(index, cp);
                break;
            case 's' :
                m = new StringMemberValue(index, cp);
                break;
            default :
                throw new RuntimeException("unknown tag:" + tag);
            }

            currentMember = m;
            super.constValueMember(tag, index);
        }

        @Override
        void enumMemberValue(int pos, int typeNameIndex, int constNameIndex)
            throws Exception
        {
            currentMember = new EnumMemberValue(typeNameIndex,
                                              constNameIndex, pool);
            super.enumMemberValue(pos, typeNameIndex, constNameIndex);
        }

        @Override
        void classMemberValue(int pos, int index) throws Exception {
            currentMember = new ClassMemberValue(index, pool);
            super.classMemberValue(pos, index);
        }

        @Override
        int annotationMemberValue(int pos) throws Exception {
            Annotation anno = currentAnno;
            pos = super.annotationMemberValue(pos);
            currentMember = new AnnotationMemberValue(currentAnno, pool);
            currentAnno = anno;
            return pos;
        }

        @Override
        int arrayMemberValue(int pos, int num) throws Exception {
            ArrayMemberValue amv = new ArrayMemberValue(pool);
            MemberValue[] elements = new MemberValue[num];
            for (int i = 0; i < num; ++i) {
                pos = memberValue(pos);
                elements[i] = currentMember;
            }

            amv.setValue(elements);
            currentMember = amv;
            return pos;
        }
    }
}
