/*
 * 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.dexgen.dex.file;

import com.android.dexgen.rop.annotation.Annotations;
import com.android.dexgen.rop.annotation.AnnotationsList;
import com.android.dexgen.rop.code.AccessFlags;
import com.android.dexgen.rop.cst.Constant;
import com.android.dexgen.rop.cst.CstArray;
import com.android.dexgen.rop.cst.CstFieldRef;
import com.android.dexgen.rop.cst.CstMethodRef;
import com.android.dexgen.rop.cst.CstType;
import com.android.dexgen.rop.cst.CstUtf8;
import com.android.dexgen.rop.type.StdTypeList;
import com.android.dexgen.rop.type.TypeList;
import com.android.dexgen.util.AnnotatedOutput;
import com.android.dexgen.util.Hex;
import com.android.dexgen.util.Writers;

import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.TreeSet;

/**
 * Representation of a Dalvik class, which is basically a set of
 * members (fields or methods) along with a few more pieces of
 * information.
 */
public final class ClassDefItem extends IndexedItem {
    /** size of instances when written out to a file, in bytes */
    public static final int WRITE_SIZE = 32;

    /** {@code non-null;} type constant for this class */
    private final CstType thisClass;

    /** access flags */
    private final int accessFlags;

    /**
     * {@code null-ok;} superclass or {@code null} if this class is a/the
     * root class
     */
    private final CstType superclass;

    /** {@code null-ok;} list of implemented interfaces */
    private TypeListItem interfaces;

    /** {@code null-ok;} source file name or {@code null} if unknown */
    private final CstUtf8 sourceFile;

    /** {@code non-null;} associated class data object */
    private final ClassDataItem classData;

    /**
     * {@code null-ok;} item wrapper for the static values, initialized
     * in {@link #addContents}
     */
    private EncodedArrayItem staticValuesItem;

    /** {@code non-null;} annotations directory */
    private AnnotationsDirectoryItem annotationsDirectory;

    /**
     * Constructs an instance. Its sets of members and annotations are
     * initially empty.
     *
     * @param thisClass {@code non-null;} type constant for this class
     * @param accessFlags access flags
     * @param superclass {@code null-ok;} superclass or {@code null} if
     * this class is a/the root class
     * @param interfaces {@code non-null;} list of implemented interfaces
     * @param sourceFile {@code null-ok;} source file name or
     * {@code null} if unknown
     */
    public ClassDefItem(CstType thisClass, int accessFlags,
            CstType superclass, TypeList interfaces, CstUtf8 sourceFile) {
        if (thisClass == null) {
            throw new NullPointerException("thisClass == null");
        }

        /*
         * TODO: Maybe check accessFlags and superclass, at
         * least for easily-checked stuff?
         */

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

        this.thisClass = thisClass;
        this.accessFlags = accessFlags;
        this.superclass = superclass;
        this.interfaces =
            (interfaces.size() == 0) ? null :  new TypeListItem(interfaces);
        this.sourceFile = sourceFile;
        this.classData = new ClassDataItem(thisClass);
        this.staticValuesItem = null;
        this.annotationsDirectory = new AnnotationsDirectoryItem();
    }

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

    /** {@inheritDoc} */
    @Override
    public int writeSize() {
        return WRITE_SIZE;
    }

    /** {@inheritDoc} */
    @Override
    public void addContents(DexFile file) {
        TypeIdsSection typeIds = file.getTypeIds();
        MixedItemSection byteData = file.getByteData();
        MixedItemSection wordData = file.getWordData();
        MixedItemSection typeLists = file.getTypeLists();
        StringIdsSection stringIds = file.getStringIds();

        typeIds.intern(thisClass);

        if (!classData.isEmpty()) {
            MixedItemSection classDataSection = file.getClassData();
            classDataSection.add(classData);

            CstArray staticValues = classData.getStaticValuesConstant();
            if (staticValues != null) {
                staticValuesItem =
                    byteData.intern(new EncodedArrayItem(staticValues));
            }
        }

        if (superclass != null) {
            typeIds.intern(superclass);
        }

        if (interfaces != null) {
            interfaces = typeLists.intern(interfaces);
        }

        if (sourceFile != null) {
            stringIds.intern(sourceFile);
        }

        if (! annotationsDirectory.isEmpty()) {
            if (annotationsDirectory.isInternable()) {
                annotationsDirectory = wordData.intern(annotationsDirectory);
            } else {
                wordData.add(annotationsDirectory);
            }
        }
    }

    /** {@inheritDoc} */
    @Override
    public void writeTo(DexFile file, AnnotatedOutput out) {
        boolean annotates = out.annotates();
        TypeIdsSection typeIds = file.getTypeIds();
        int classIdx = typeIds.indexOf(thisClass);
        int superIdx = (superclass == null) ? -1 :
            typeIds.indexOf(superclass);
        int interOff = OffsettedItem.getAbsoluteOffsetOr0(interfaces);
        int annoOff = annotationsDirectory.isEmpty() ? 0 :
            annotationsDirectory.getAbsoluteOffset();
        int sourceFileIdx = (sourceFile == null) ? -1 :
            file.getStringIds().indexOf(sourceFile);
        int dataOff = classData.isEmpty()? 0 : classData.getAbsoluteOffset();
        int staticValuesOff =
            OffsettedItem.getAbsoluteOffsetOr0(staticValuesItem);

        if (annotates) {
            out.annotate(0, indexString() + ' ' + thisClass.toHuman());
            out.annotate(4, "  class_idx:           " + Hex.u4(classIdx));
            out.annotate(4, "  access_flags:        " +
                         AccessFlags.classString(accessFlags));
            out.annotate(4, "  superclass_idx:      " + Hex.u4(superIdx) +
                         " // " + ((superclass == null) ? "<none>" :
                          superclass.toHuman()));
            out.annotate(4, "  interfaces_off:      " + Hex.u4(interOff));
            if (interOff != 0) {
                TypeList list = interfaces.getList();
                int sz = list.size();
                for (int i = 0; i < sz; i++) {
                    out.annotate(0, "    " + list.getType(i).toHuman());
                }
            }
            out.annotate(4, "  source_file_idx:     " + Hex.u4(sourceFileIdx) +
                         " // " + ((sourceFile == null) ? "<none>" :
                          sourceFile.toHuman()));
            out.annotate(4, "  annotations_off:     " + Hex.u4(annoOff));
            out.annotate(4, "  class_data_off:      " + Hex.u4(dataOff));
            out.annotate(4, "  static_values_off:   " +
                    Hex.u4(staticValuesOff));
        }

        out.writeInt(classIdx);
        out.writeInt(accessFlags);
        out.writeInt(superIdx);
        out.writeInt(interOff);
        out.writeInt(sourceFileIdx);
        out.writeInt(annoOff);
        out.writeInt(dataOff);
        out.writeInt(staticValuesOff);
    }

    /**
     * Gets the constant corresponding to this class.
     *
     * @return {@code non-null;} the constant
     */
    public CstType getThisClass() {
        return thisClass;
    }

    /**
     * Gets the access flags.
     *
     * @return the access flags
     */
    public int getAccessFlags() {
        return accessFlags;
    }

    /**
     * Gets the superclass.
     *
     * @return {@code null-ok;} the superclass or {@code null} if
     * this class is a/the root class
     */
    public CstType getSuperclass() {
        return superclass;
    }

    /**
     * Gets the list of interfaces implemented.
     *
     * @return {@code non-null;} the interfaces list
     */
    public TypeList getInterfaces() {
        if (interfaces == null) {
            return StdTypeList.EMPTY;
        }

        return interfaces.getList();
    }

    /**
     * Gets the source file name.
     *
     * @return {@code null-ok;} the source file name or {@code null} if unknown
     */
    public CstUtf8 getSourceFile() {
        return sourceFile;
    }

    /**
     * Adds a static field.
     *
     * @param field {@code non-null;} the field to add
     * @param value {@code null-ok;} initial value for the field, if any
     */
    public void addStaticField(EncodedField field, Constant value) {
        classData.addStaticField(field, value);
    }

    /**
     * Adds an instance field.
     *
     * @param field {@code non-null;} the field to add
     */
    public void addInstanceField(EncodedField field) {
        classData.addInstanceField(field);
    }

    /**
     * Adds a direct ({@code static} and/or {@code private}) method.
     *
     * @param method {@code non-null;} the method to add
     */
    public void addDirectMethod(EncodedMethod method) {
        classData.addDirectMethod(method);
    }

    /**
     * Adds a virtual method.
     *
     * @param method {@code non-null;} the method to add
     */
    public void addVirtualMethod(EncodedMethod method) {
        classData.addVirtualMethod(method);
    }

    /**
     * Gets all the methods in this class. The returned list is not linked
     * in any way to the underlying lists contained in this instance, but
     * the objects contained in the list are shared.
     *
     * @return {@code non-null;} list of all methods
     */
    public ArrayList<EncodedMethod> getMethods() {
        return classData.getMethods();
    }

    /**
     * Sets the direct annotations on this class. These are annotations
     * made on the class, per se, as opposed to on one of its members.
     * It is only valid to call this method at most once per instance.
     *
     * @param annotations {@code non-null;} annotations to set for this class
     */
    public void setClassAnnotations(Annotations annotations) {
        annotationsDirectory.setClassAnnotations(annotations);
    }

    /**
     * Adds a field annotations item to this class.
     *
     * @param field {@code non-null;} field in question
     * @param annotations {@code non-null;} associated annotations to add
     */
    public void addFieldAnnotations(CstFieldRef field,
            Annotations annotations) {
        annotationsDirectory.addFieldAnnotations(field, annotations);
    }

    /**
     * Adds a method annotations item to this class.
     *
     * @param method {@code non-null;} method in question
     * @param annotations {@code non-null;} associated annotations to add
     */
    public void addMethodAnnotations(CstMethodRef method,
            Annotations annotations) {
        annotationsDirectory.addMethodAnnotations(method, annotations);
    }

    /**
     * Adds a parameter annotations item to this class.
     *
     * @param method {@code non-null;} method in question
     * @param list {@code non-null;} associated list of annotation sets to add
     */
    public void addParameterAnnotations(CstMethodRef method,
            AnnotationsList list) {
        annotationsDirectory.addParameterAnnotations(method, list);
    }

    /**
     * Gets the method annotations for a given method, if any. This is
     * meant for use by debugging / dumping code.
     *
     * @param method {@code non-null;} the method
     * @return {@code null-ok;} the method annotations, if any
     */
    public Annotations getMethodAnnotations(CstMethodRef method) {
        return annotationsDirectory.getMethodAnnotations(method);
    }

    /**
     * Gets the parameter annotations for a given method, if any. This is
     * meant for use by debugging / dumping code.
     *
     * @param method {@code non-null;} the method
     * @return {@code null-ok;} the parameter annotations, if any
     */
    public AnnotationsList getParameterAnnotations(CstMethodRef method) {
        return annotationsDirectory.getParameterAnnotations(method);
    }

    /**
     * Prints out the contents of this instance, in a debugging-friendly
     * way.
     *
     * @param out {@code non-null;} where to output to
     * @param verbose whether to be verbose with the output
     */
    public void debugPrint(Writer out, boolean verbose) {
        PrintWriter pw = Writers.printWriterFor(out);

        pw.println(getClass().getName() + " {");
        pw.println("  accessFlags: " + Hex.u2(accessFlags));
        pw.println("  superclass: " + superclass);
        pw.println("  interfaces: " +
                ((interfaces == null) ? "<none>" : interfaces));
        pw.println("  sourceFile: " +
                ((sourceFile == null) ? "<none>" : sourceFile.toQuoted()));

        classData.debugPrint(out, verbose);
        annotationsDirectory.debugPrint(pw);

        pw.println("}");
    }
}
