/*
 * Copyright (C) 2009 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 dex.reader;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import dex.reader.DexFileReader.ClassDefItem;
import dex.reader.DexFileReader.FieldIdItem;
import dex.reader.DexFileReader.MethodsIdItem;
import dex.reader.DexFileReader.ProtIdItem;
import dex.structure.DexAnnotation;
import dex.structure.DexClass;
import dex.structure.DexField;
import dex.structure.DexMethod;

/* package */final class DexClassImpl implements DexClass {
    // constant
    private final int NO_INDEX = -1;
    // dex bytes
    private final DexBuffer buffer;
    // allready parsed
    private final ClassDefItem classDefItem;
    private final int[] typeIds;
    private final String[] stringPool;
    private ProtIdItem[] protoIdItems;
    private FieldIdItem[] fieldIdItems;
    private MethodsIdItem[] methodIdItems;

    //
    private List<DexField> fields;
    private List<DexMethod> methods;
    private List<String> interfaces;
    private ClassDataItem classDataItem;
    private AnnotationsDirectoryItem annotationDir;
    private Map<Integer, FieldAnnotation> idToFieldAnnotation =
            new HashMap<Integer, FieldAnnotation>();
    private Map<Integer, MethodAnnotation> idToMethodAnnotation =
            new HashMap<Integer, MethodAnnotation>();
    private Map<Integer, ParameterAnnotation> idToParameterAnnotation =
            new HashMap<Integer, ParameterAnnotation>();

    private Set<DexAnnotation> annotations;
    private TypeFormatter formatter = new TypeFormatter();

    private boolean hasClassData;


    public DexClassImpl(DexBuffer buffer, ClassDefItem classDefItem,
            String[] stringPool, int[] typeIds, ProtIdItem[] protoIdItems,
            FieldIdItem[] fieldIdItems, MethodsIdItem[] methodIdItems) {
        this.buffer = buffer;
        this.classDefItem = classDefItem;
        this.stringPool = stringPool;
        this.typeIds = typeIds;
        this.protoIdItems = protoIdItems;
        this.fieldIdItems = fieldIdItems;
        this.methodIdItems = methodIdItems;
        hasClassData = classDefItem.class_data_off != 0;
        parseClassData();
        parseAnnotationDirectory();
        parseClassAnnotations();
    }

    static class AnnotationsDirectoryItem {
        int class_annotations_off; // uint
        int fields_size; // uint
        int methods_size; // uint
        int annotated_params_size; // uint
        FieldAnnotation[] fieldAnnotations;
        MethodAnnotation[] methodAnnotations;
        ParameterAnnotation[] parameterAnnotations;
    }

    static class AnnotationSetItem {
        int size;// uint
        int[] annotationOffItem;
    }

    static class FieldAnnotation {
        int fieldIdx;// uint
        int annotationsOff;// uint
        AnnotationSetItem[] annotationSetItems;
    }

    static class MethodAnnotation {
        int methodIdx;// uint
        int annotationsOff;// uint
        AnnotationSetItem[] annotationSetItems;
    }

    static class ParameterAnnotation {
        int methodIdx;// uint
        int annotationsOff;// uint
        // AnnotationSetRefListItem[] annotationSetRefListItems;
    }

    private void parseAnnotationDirectory() {
        if (classDefItem.annotations_off != 0) {
            buffer.setPosition(classDefItem.annotations_off);
            annotationDir = new AnnotationsDirectoryItem();
            annotationDir.class_annotations_off = buffer.readUInt();
            annotationDir.fields_size = buffer.readUInt();
            annotationDir.methods_size = buffer.readUInt();
            annotationDir.annotated_params_size = buffer.readUInt();

            if (annotationDir.fields_size != 0) {
                annotationDir.fieldAnnotations =
                        new FieldAnnotation[annotationDir.fields_size];
                for (int i = 0; i < annotationDir.fields_size; i++) {
                    annotationDir.fieldAnnotations[i] = new FieldAnnotation();
                    annotationDir.fieldAnnotations[i].fieldIdx = buffer
                            .readUInt();
                    annotationDir.fieldAnnotations[i].annotationsOff = buffer
                            .readUInt();
                    idToFieldAnnotation.put(
                            annotationDir.fieldAnnotations[i].fieldIdx,
                            annotationDir.fieldAnnotations[i]);
                }
            }
            if (annotationDir.methods_size != 0) {
                annotationDir.methodAnnotations =
                        new MethodAnnotation[annotationDir.methods_size];
                for (int i = 0; i < annotationDir.methods_size; i++) {
                    annotationDir.methodAnnotations[i] = new MethodAnnotation();
                    annotationDir.methodAnnotations[i].methodIdx = buffer
                            .readUInt();
                    annotationDir.methodAnnotations[i].annotationsOff = buffer
                            .readUInt();
                    idToMethodAnnotation.put(
                            annotationDir.methodAnnotations[i].methodIdx,
                            annotationDir.methodAnnotations[i]);
                }
            }
            if (annotationDir.annotated_params_size != 0) {
                annotationDir.parameterAnnotations =
                        new ParameterAnnotation[annotationDir
                                .annotated_params_size];
                for (int i = 0; i < annotationDir.annotated_params_size; i++) {
                    annotationDir.parameterAnnotations[i] =
                            new ParameterAnnotation();
                    annotationDir.parameterAnnotations[i].methodIdx = buffer
                            .readUInt();
                    annotationDir.parameterAnnotations[i].annotationsOff =
                            buffer.readUInt();
                    idToParameterAnnotation.put(
                            annotationDir.parameterAnnotations[i].methodIdx,
                            annotationDir.parameterAnnotations[i]);
                }
            }
        }
    }

    static class ClassDataItem {
        int static_fields_size;// uleb128
        int instance_fields_size;// uleb128
        int direct_methods_size;// uleb128
        int virtual_methods_size;// uleb128
        EncodedField[] staticFields;
        EncodedField[] instanceFields;
        EncodedMethod[] directMethods;
        EncodedMethod[] virtualMethods;
    }

    static class EncodedField {
        int field_idx_diff; // uleb128
        int access_flags; // uleb128
    }

    static class EncodedMethod {
        int method_idx_diff;// uleb128
        int access_flags;// uleb128
        int code_off; // uleb128
    }

    private void parseClassData() {
        if (hasClassData) {
            buffer.setPosition(classDefItem.class_data_off);
            classDataItem = new ClassDataItem();
            classDataItem.static_fields_size = buffer.readUleb128();
            classDataItem.instance_fields_size = buffer.readUleb128();
            classDataItem.direct_methods_size = buffer.readUleb128();
            classDataItem.virtual_methods_size = buffer.readUleb128();
            classDataItem.staticFields = parseFields(
                    classDataItem.static_fields_size);
            classDataItem.instanceFields = parseFields(
                    classDataItem.instance_fields_size);
            classDataItem.directMethods = parseMethods(
                    classDataItem.direct_methods_size);
            classDataItem.virtualMethods = parseMethods(
                    classDataItem.virtual_methods_size);
        }
    }

    private EncodedField[] parseFields(int size) {
        EncodedField[] fields = new EncodedField[size];
        for (int i = 0; i < fields.length; i++) {
            fields[i] = new EncodedField();
            fields[i].field_idx_diff = buffer.readUleb128();
            fields[i].access_flags = buffer.readUleb128();
        }
        return fields;
    }

    private EncodedMethod[] parseMethods(int size) {
        EncodedMethod[] methods = new EncodedMethod[size];
        for (int i = 0; i < methods.length; i++) {
            methods[i] = new EncodedMethod();
            methods[i].method_idx_diff = buffer.readUleb128();
            methods[i].access_flags = buffer.readUleb128();
            methods[i].code_off = buffer.readUleb128();
        }
        return methods;
    }

    private void parseClassAnnotations() {
        annotations = new HashSet<DexAnnotation>();
        if (annotationDir != null && annotationDir.class_annotations_off != 0) {
            buffer.setPosition(annotationDir.class_annotations_off);
            final int size = buffer.readUInt();
            for (int i = 0; i < size; i++) {
                annotations.add(new DexAnnotationImpl(buffer.createCopy(),
                        buffer.readUInt(), typeIds, stringPool, fieldIdItems));
            }
        }
    }

    public synchronized List<DexField> getFields() {
        if (fields == null) {
            fields = new ArrayList<DexField>();
            if (hasClassData) {
                fields.addAll(getDexFields(classDataItem.staticFields));
                fields.addAll(getDexFields(classDataItem.instanceFields));
            }
        }
        return fields;
    }

    private List<DexField> getDexFields(EncodedField[] fields) {
        List<DexField> dexFields = new ArrayList<DexField>(fields.length);
        if (fields.length != 0) {
            int fieldIdIdx = 0;
            for (int i = 0; i < fields.length; i++) {
                int accessFlags = fields[i].access_flags;
                fieldIdIdx = (i == 0) ? fields[i].field_idx_diff : fieldIdIdx
                        + fields[i].field_idx_diff;
                dexFields.add(new DexFieldImpl(buffer.createCopy(), this,
                        fieldIdItems[fieldIdIdx], accessFlags,
                        idToFieldAnnotation.get(fieldIdIdx), stringPool,
                        typeIds, fieldIdItems));
            }
        }
        return dexFields;
    }

    public synchronized List<DexMethod> getMethods() {
        if (methods == null) {
            methods = new ArrayList<DexMethod>();
            if (hasClassData) {
                methods.addAll(getDexMethods(classDataItem.directMethods));
                methods.addAll(getDexMethods(classDataItem.virtualMethods));
            }
        }
        return methods;
    }

    private List<DexMethod> getDexMethods(EncodedMethod[] methods) {
        List<DexMethod> dexMethods = new ArrayList<DexMethod>(methods.length);
        if (methods.length != 0) {
            int methodIdIdx = 0;
            EncodedMethod method = null;
            for (int i = 0; i < methods.length; i++) {
                method = methods[i];
                methodIdIdx = (i == 0) ? method.method_idx_diff : methodIdIdx
                        + method.method_idx_diff;
                dexMethods.add(new DexMethodImpl(buffer, this,
                        methodIdItems[methodIdIdx],
                        protoIdItems[methodIdItems[methodIdIdx].proto_idx],
                        method.access_flags, idToMethodAnnotation
                                .get(methodIdIdx), idToParameterAnnotation
                                .get(methodIdIdx), stringPool, typeIds,
                        fieldIdItems));
            }
        }
        return dexMethods;
    }



    public synchronized List<String> getInterfaces() {
        if (interfaces == null) {
            interfaces = new LinkedList<String>();
            if (classDefItem.interfaces_off != 0) {
                buffer.setPosition(classDefItem.interfaces_off);
                int size = buffer.readUInt();
                for (int i = 0; i < size; i++) {
                    interfaces.add(stringPool[typeIds[buffer.readUShort()]]);
                }
            }
        }
        return interfaces;
    }

    // returns null if no super class is present
    public String getSuperClass() {
        return classDefItem.superclass_idx == NO_INDEX ? null
                : stringPool[typeIds[classDefItem.superclass_idx]];
    }

    public Set<DexAnnotation> getAnnotations() {
        return annotations;
    }

    public String getName() {
        return stringPool[typeIds[classDefItem.class_idx]];
    }

    public int getModifiers() {
        return classDefItem.access_flags;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append(formatter.formatAnnotations(getAnnotations()));
        builder.append(Modifier.toString(getModifiers()));
        builder.append(" class ");
        builder.append(formatter.format(getName()));
        if (getSuperClass() != null) {
            builder.append(" extends ");
            builder.append(formatter.format(getSuperClass()));
        }
        if (!getInterfaces().isEmpty()) {
            builder.append(" implements ");
            builder.append(formatter.format(getInterfaces()));
        }
        return builder.toString();
    }

}