/*
 * Copyright (C) 2008 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.dexgen.dex.file;

import com.android.dexgen.rop.annotation.Annotation;
import com.android.dexgen.rop.annotation.AnnotationVisibility;
import com.android.dexgen.rop.annotation.NameValuePair;
import com.android.dexgen.rop.cst.Constant;
import com.android.dexgen.rop.cst.CstAnnotation;
import com.android.dexgen.rop.cst.CstArray;
import com.android.dexgen.rop.cst.CstUtf8;
import com.android.dexgen.util.AnnotatedOutput;
import com.android.dexgen.util.ByteArrayAnnotatedOutput;

import java.util.Arrays;
import java.util.Comparator;

/**
 * Single annotation, which consists of a type and a set of name-value
 * element pairs.
 */
public final class AnnotationItem extends OffsettedItem {
    /** annotation visibility constant: visible at build time only */
    private static final int VISIBILITY_BUILD = 0;

    /** annotation visibility constant: visible at runtime */
    private static final int VISIBILITY_RUNTIME = 1;

    /** annotation visibility constant: visible at runtime only to system */
    private static final int VISIBILITY_SYSTEM = 2;

    /** the required alignment for instances of this class */
    private static final int ALIGNMENT = 1;

    /** {@code non-null;} unique instance of {@link #TypeIdSorter} */
    private static final TypeIdSorter TYPE_ID_SORTER = new TypeIdSorter();

    /** {@code non-null;} the annotation to represent */
    private final Annotation annotation;

    /**
     * {@code null-ok;} type reference for the annotation type; set during
     * {@link #addContents}
     */
    private TypeIdItem type;

    /**
     * {@code null-ok;} encoded form, ready for writing to a file; set during
     * {@link #place0}
     */
    private byte[] encodedForm;

    /**
     * Comparator that sorts (outer) instances by type id index.
     */
    private static class TypeIdSorter implements Comparator<AnnotationItem> {
        /** {@inheritDoc} */
        public int compare(AnnotationItem item1, AnnotationItem item2) {
            int index1 = item1.type.getIndex();
            int index2 = item2.type.getIndex();

            if (index1 < index2) {
                return -1;
            } else if (index1 > index2) {
                return 1;
            }

            return 0;
        }
    }

    /**
     * Sorts an array of instances, in place, by type id index,
     * ignoring all other aspects of the elements. This is only valid
     * to use after type id indices are known.
     *
     * @param array {@code non-null;} array to sort
     */
    public static void sortByTypeIdIndex(AnnotationItem[] array) {
        Arrays.sort(array, TYPE_ID_SORTER);
    }

    /**
     * Constructs an instance.
     *
     * @param annotation {@code non-null;} annotation to represent
     */
    public AnnotationItem(Annotation annotation) {
        /*
         * The write size isn't known up-front because (the variable-lengthed)
         * leb128 type is used to represent some things.
         */
        super(ALIGNMENT, -1);

        if (annotation == null) {
            throw new NullPointerException("annotation == null");
        }

        this.annotation = annotation;
        this.type = null;
        this.encodedForm = null;
    }

    /** {@inheritDoc} */
    @Override
    public ItemType itemType() {
        return ItemType.TYPE_ANNOTATION_ITEM;
    }

    /** {@inheritDoc} */
    @Override
    public int hashCode() {
        return annotation.hashCode();
    }

    /** {@inheritDoc} */
    @Override
    protected int compareTo0(OffsettedItem other) {
        AnnotationItem otherAnnotation = (AnnotationItem) other;

        return annotation.compareTo(otherAnnotation.annotation);
    }

    /** {@inheritDoc} */
    @Override
    public String toHuman() {
        return annotation.toHuman();
    }

    /** {@inheritDoc} */
    public void addContents(DexFile file) {
        type = file.getTypeIds().intern(annotation.getType());
        ValueEncoder.addContents(file, annotation);
    }

    /** {@inheritDoc} */
    @Override
    protected void place0(Section addedTo, int offset) {
        // Encode the data and note the size.

        ByteArrayAnnotatedOutput out = new ByteArrayAnnotatedOutput();
        ValueEncoder encoder = new ValueEncoder(addedTo.getFile(), out);

        encoder.writeAnnotation(annotation, false);
        encodedForm = out.toByteArray();

        // Add one for the visibility byte in front of the encoded annotation.
        setWriteSize(encodedForm.length + 1);
    }

    /**
     * Write a (listing file) annotation for this instance to the given
     * output, that consumes no bytes of output. This is for annotating
     * a reference to this instance at the point of the reference.
     *
     * @param out {@code non-null;} where to output to
     * @param prefix {@code non-null;} prefix for each line of output
     */
    public void annotateTo(AnnotatedOutput out, String prefix) {
        out.annotate(0, prefix + "visibility: " +
                annotation.getVisibility().toHuman());
        out.annotate(0, prefix + "type: " + annotation.getType().toHuman());

        for (NameValuePair pair : annotation.getNameValuePairs()) {
            CstUtf8 name = pair.getName();
            Constant value = pair.getValue();

            out.annotate(0, prefix + name.toHuman() + ": " +
                    ValueEncoder.constantToHuman(value));
        }
    }

    /** {@inheritDoc} */
    @Override
    protected void writeTo0(DexFile file, AnnotatedOutput out) {
        boolean annotates = out.annotates();
        AnnotationVisibility visibility = annotation.getVisibility();

        if (annotates) {
            out.annotate(0, offsetString() + " annotation");
            out.annotate(1, "  visibility: VISBILITY_" + visibility);
        }

        switch (visibility) {
            case BUILD:   out.writeByte(VISIBILITY_BUILD); break;
            case RUNTIME: out.writeByte(VISIBILITY_RUNTIME); break;
            case SYSTEM:  out.writeByte(VISIBILITY_SYSTEM); break;
            default: {
                // EMBEDDED shouldn't appear at the top level.
                throw new RuntimeException("shouldn't happen");
            }
        }

        if (annotates) {
            /*
             * The output is to be annotated, so redo the work previously
             * done by place0(), except this time annotations will actually
             * get emitted.
             */
            ValueEncoder encoder = new ValueEncoder(file, out);
            encoder.writeAnnotation(annotation, true);
        } else {
            out.write(encodedForm);
        }
    }
}
