/*
 * Copyright 2013, Google Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.jf.dexlib2.analysis;

import com.google.common.base.Joiner;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.*;
import com.google.common.primitives.Ints;
import org.jf.dexlib2.AccessFlags;
import org.jf.dexlib2.analysis.util.TypeProtoUtils;
import org.jf.dexlib2.base.reference.BaseMethodReference;
import org.jf.dexlib2.iface.*;
import org.jf.dexlib2.iface.reference.FieldReference;
import org.jf.dexlib2.iface.reference.MethodReference;
import org.jf.dexlib2.util.MethodUtil;
import org.jf.util.AlignmentUtils;
import org.jf.util.ExceptionWithContext;
import org.jf.util.SparseArray;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.Map.Entry;

/**
 * A class "prototype". This contains things like the interfaces, the superclass, the vtable and the instance fields
 * and their offsets.
 */
public class ClassProto implements TypeProto {
    private static final byte REFERENCE = 0;
    private static final byte WIDE = 1;
    private static final byte OTHER = 2;

    @Nonnull protected final ClassPath classPath;
    @Nonnull protected final String type;

    protected boolean vtableFullyResolved = true;
    protected boolean interfacesFullyResolved = true;

    protected Set<String> unresolvedInterfaces = null;

    public ClassProto(@Nonnull ClassPath classPath, @Nonnull String type) {
        if (type.charAt(0) != 'L') {
            throw new ExceptionWithContext("Cannot construct ClassProto for non reference type: %s", type);
        }
        this.classPath = classPath;
        this.type = type;
    }

    @Override public String toString() { return type; }
    @Nonnull @Override public ClassPath getClassPath() { return classPath; }
    @Nonnull @Override public String getType() { return type; }

    @Nonnull
    public ClassDef getClassDef() {
        return classDefSupplier.get();
    }


    @Nonnull private final Supplier<ClassDef> classDefSupplier = Suppliers.memoize(new Supplier<ClassDef>() {
        @Override public ClassDef get() {
            return classPath.getClassDef(type);
        }
    });

    /**
     * Returns true if this class is an interface.
     *
     * If this class is not defined, then this will throw an UnresolvedClassException
     *
     * @return True if this class is an interface
     */
    public boolean isInterface() {
        ClassDef classDef = getClassDef();
        return (classDef.getAccessFlags() & AccessFlags.INTERFACE.getValue()) != 0;
    }

    /**
     * Returns the set of interfaces that this class implements as a Map<String, ClassDef>.
     *
     * The ClassDef value will be present only for the interfaces that this class directly implements (including any
     * interfaces transitively implemented), but not for any interfaces that are only implemented by a superclass of
     * this class
     *
     * For any interfaces that are only implemented by a superclass (or the class itself, if the class is an interface),
     * the value will be null.
     *
     * If any interface couldn't be resolved, then the interfacesFullyResolved field will be set to false upon return.
     *
     * @return the set of interfaces that this class implements as a Map<String, ClassDef>.
     */
    @Nonnull
    protected LinkedHashMap<String, ClassDef> getInterfaces() {
        if (!classPath.isArt() || classPath.oatVersion < 72) {
            return preDefaultMethodInterfaceSupplier.get();
        } else {
            return postDefaultMethodInterfaceSupplier.get();
        }
    }

    /**
     * This calculates the interfaces in the order required for vtable generation for dalvik and pre-default method ART
     */
    @Nonnull
    private final Supplier<LinkedHashMap<String, ClassDef>> preDefaultMethodInterfaceSupplier =
            Suppliers.memoize(new Supplier<LinkedHashMap<String, ClassDef>>() {
                @Override public LinkedHashMap<String, ClassDef> get() {
                    Set<String> unresolvedInterfaces = new HashSet<String>(0);
                    LinkedHashMap<String, ClassDef> interfaces = Maps.newLinkedHashMap();

                    try {
                        for (String interfaceType: getClassDef().getInterfaces()) {
                            if (!interfaces.containsKey(interfaceType)) {
                                ClassDef interfaceDef;
                                try {
                                    interfaceDef = classPath.getClassDef(interfaceType);
                                    interfaces.put(interfaceType, interfaceDef);
                                } catch (UnresolvedClassException ex) {
                                    interfaces.put(interfaceType, null);
                                    unresolvedInterfaces.add(interfaceType);
                                    interfacesFullyResolved = false;
                                }

                                ClassProto interfaceProto = (ClassProto) classPath.getClass(interfaceType);
                                for (String superInterface: interfaceProto.getInterfaces().keySet()) {
                                    if (!interfaces.containsKey(superInterface)) {
                                        interfaces.put(superInterface,
                                                interfaceProto.getInterfaces().get(superInterface));
                                    }
                                }
                                if (!interfaceProto.interfacesFullyResolved) {
                                    unresolvedInterfaces.addAll(interfaceProto.getUnresolvedInterfaces());
                                    interfacesFullyResolved = false;
                                }
                            }
                        }
                    } catch (UnresolvedClassException ex) {
                        interfaces.put(type, null);
                        unresolvedInterfaces.add(type);
                        interfacesFullyResolved = false;
                    }

                    // now add self and super class interfaces, required for common super class lookup
                    // we don't really need ClassDef's for that, so let's just use null

                    if (isInterface() && !interfaces.containsKey(getType())) {
                        interfaces.put(getType(), null);
                    }

                    String superclass = getSuperclass();
                    try {
                        if (superclass != null) {
                            ClassProto superclassProto = (ClassProto) classPath.getClass(superclass);
                            for (String superclassInterface: superclassProto.getInterfaces().keySet()) {
                                if (!interfaces.containsKey(superclassInterface)) {
                                    interfaces.put(superclassInterface, null);
                                }
                            }
                            if (!superclassProto.interfacesFullyResolved) {
                                unresolvedInterfaces.addAll(superclassProto.getUnresolvedInterfaces());
                                interfacesFullyResolved = false;
                            }
                        }
                    } catch (UnresolvedClassException ex) {
                        unresolvedInterfaces.add(superclass);
                        interfacesFullyResolved = false;
                    }

                    if (unresolvedInterfaces.size() > 0) {
                        ClassProto.this.unresolvedInterfaces = unresolvedInterfaces;
                    }

                    return interfaces;
                }
            });

    /**
     * This calculates the interfaces in the order required for vtable generation for post-default method ART
     */
    @Nonnull
    private final Supplier<LinkedHashMap<String, ClassDef>> postDefaultMethodInterfaceSupplier =
            Suppliers.memoize(new Supplier<LinkedHashMap<String, ClassDef>>() {
                @Override public LinkedHashMap<String, ClassDef> get() {
                    Set<String> unresolvedInterfaces = new HashSet<String>(0);
                    LinkedHashMap<String, ClassDef> interfaces = Maps.newLinkedHashMap();

                    String superclass = getSuperclass();
                    if (superclass != null) {
                        ClassProto superclassProto = (ClassProto) classPath.getClass(superclass);
                        for (String superclassInterface: superclassProto.getInterfaces().keySet()) {
                            interfaces.put(superclassInterface, null);
                        }
                        if (!superclassProto.interfacesFullyResolved) {
                            unresolvedInterfaces.addAll(superclassProto.getUnresolvedInterfaces());
                            interfacesFullyResolved = false;
                        }
                    }

                    try {
                        for (String interfaceType: getClassDef().getInterfaces()) {
                            if (!interfaces.containsKey(interfaceType)) {
                                ClassProto interfaceProto = (ClassProto)classPath.getClass(interfaceType);
                                try {
                                    for (Entry<String, ClassDef> entry: interfaceProto.getInterfaces().entrySet()) {
                                        if (!interfaces.containsKey(entry.getKey())) {
                                            interfaces.put(entry.getKey(), entry.getValue());
                                        }
                                    }
                                } catch (UnresolvedClassException ex) {
                                    interfaces.put(interfaceType, null);
                                    unresolvedInterfaces.add(interfaceType);
                                    interfacesFullyResolved = false;
                                }
                                if (!interfaceProto.interfacesFullyResolved) {
                                    unresolvedInterfaces.addAll(interfaceProto.getUnresolvedInterfaces());
                                    interfacesFullyResolved = false;
                                }
                                try {
                                    ClassDef interfaceDef = classPath.getClassDef(interfaceType);
                                    interfaces.put(interfaceType, interfaceDef);
                                } catch (UnresolvedClassException ex) {
                                    interfaces.put(interfaceType, null);
                                    unresolvedInterfaces.add(interfaceType);
                                    interfacesFullyResolved = false;
                                }
                            }
                        }
                    } catch (UnresolvedClassException ex) {
                        interfaces.put(type, null);
                        unresolvedInterfaces.add(type);
                        interfacesFullyResolved = false;
                    }

                    if (unresolvedInterfaces.size() > 0) {
                        ClassProto.this.unresolvedInterfaces = unresolvedInterfaces;
                    }

                    return interfaces;
                }
            });

    @Nonnull
    protected Set<String> getUnresolvedInterfaces() {
        if (unresolvedInterfaces == null) {
            return ImmutableSet.of();
        }
        return unresolvedInterfaces;
    }

    /**
     * Gets the interfaces directly implemented by this class, or the interfaces they transitively implement.
     *
     * This does not include any interfaces that are only implemented by a superclass
     *
     * @return An iterables of ClassDefs representing the directly or transitively implemented interfaces
     * @throws UnresolvedClassException if interfaces could not be fully resolved
     */
    @Nonnull
    protected Iterable<ClassDef> getDirectInterfaces() {
        Iterable<ClassDef> directInterfaces =
                FluentIterable.from(getInterfaces().values()).filter(Predicates.notNull());

        if (!interfacesFullyResolved) {
            throw new UnresolvedClassException("Interfaces for class %s not fully resolved: %s", getType(),
                    Joiner.on(',').join(getUnresolvedInterfaces()));
        }

        return directInterfaces;
    }

    /**
     * Checks if this class implements the given interface.
     *
     * If the interfaces of this class cannot be fully resolved then this
     * method will either return true or throw an UnresolvedClassException
     *
     * @param iface The interface to check for
     * @return true if this class implements the given interface, otherwise false
     * @throws UnresolvedClassException if the interfaces for this class could not be fully resolved, and the interface
     * is not one of the interfaces that were successfully resolved
     */
    @Override
    public boolean implementsInterface(@Nonnull String iface) {
        if (getInterfaces().containsKey(iface)) {
            return true;
        }
        if (!interfacesFullyResolved) {
            throw new UnresolvedClassException("Interfaces for class %s not fully resolved", getType());
        }
        return false;
    }

    @Nullable @Override
    public String getSuperclass() {
        return getClassDef().getSuperclass();
    }

    /**
     * This is a helper method for getCommonSuperclass
     *
     * It checks if this class is an interface, and if so, if other implements it.
     *
     * If this class is undefined, we go ahead and check if it is listed in other's interfaces. If not, we throw an
     * UndefinedClassException
     *
     * If the interfaces of other cannot be fully resolved, we check the interfaces that can be resolved. If not found,
     * we throw an UndefinedClassException
     *
     * @param other The class to check the interfaces of
     * @return true if this class is an interface (or is undefined) other implements this class
     *
     */
    private boolean checkInterface(@Nonnull ClassProto other) {
        boolean isResolved = true;
        boolean isInterface = true;
        try {
            isInterface = isInterface();
        } catch (UnresolvedClassException ex) {
            isResolved = false;
            // if we don't know if this class is an interface or not,
            // we can still try to call other.implementsInterface(this)
        }
        if (isInterface) {
            try {
                if (other.implementsInterface(getType())) {
                    return true;
                }
            } catch (UnresolvedClassException ex) {
                // There are 2 possibilities here, depending on whether we were able to resolve this class.
                // 1. If this class is resolved, then we know it is an interface class. The other class either
                //    isn't defined, or its interfaces couldn't be fully resolved.
                //    In this case, we throw an UnresolvedClassException
                // 2. If this class is not resolved, we had tried to call implementsInterface anyway. We don't
                //    know for sure if this class is an interface or not. We return false, and let processing
                //    continue in getCommonSuperclass
                if (isResolved) {
                    throw ex;
                }
            }
        }
        return false;
    }

    @Override @Nonnull
    public TypeProto getCommonSuperclass(@Nonnull TypeProto other) {
        // use the other type's more specific implementation
        if (!(other instanceof ClassProto)) {
            return other.getCommonSuperclass(this);
        }

        if (this == other || getType().equals(other.getType())) {
            return this;
        }

        if (this.getType().equals("Ljava/lang/Object;")) {
            return this;
        }

        if (other.getType().equals("Ljava/lang/Object;")) {
            return other;
        }

        boolean gotException = false;
        try {
            if (checkInterface((ClassProto)other)) {
                return this;
            }
        } catch (UnresolvedClassException ex) {
            gotException = true;
        }

        try {
            if (((ClassProto)other).checkInterface(this)) {
                return other;
            }
        } catch (UnresolvedClassException ex) {
            gotException = true;
        }
        if (gotException) {
            return classPath.getUnknownClass();
        }

        List<TypeProto> thisChain = Lists.<TypeProto>newArrayList(this);
        Iterables.addAll(thisChain, TypeProtoUtils.getSuperclassChain(this));

        List<TypeProto> otherChain = Lists.newArrayList(other);
        Iterables.addAll(otherChain, TypeProtoUtils.getSuperclassChain(other));

        // reverse them, so that the first entry is either Ljava/lang/Object; or Ujava/lang/Object;
        thisChain = Lists.reverse(thisChain);
        otherChain = Lists.reverse(otherChain);

        for (int i=Math.min(thisChain.size(), otherChain.size())-1; i>=0; i--) {
            TypeProto typeProto = thisChain.get(i);
            if (typeProto.getType().equals(otherChain.get(i).getType())) {
                return typeProto;
            }
        }

        return classPath.getUnknownClass();
    }

    @Override
    @Nullable
    public FieldReference getFieldByOffset(int fieldOffset) {
        if (getInstanceFields().size() == 0) {
            return null;
        }
        return getInstanceFields().get(fieldOffset);
    }

    @Override
    @Nullable
    public Method getMethodByVtableIndex(int vtableIndex) {
        List<Method> vtable = getVtable();
        if (vtableIndex < 0 || vtableIndex >= vtable.size()) {
            return null;
        }

        return vtable.get(vtableIndex);
    }

    public int findMethodIndexInVtable(@Nonnull MethodReference method) {
        return findMethodIndexInVtable(getVtable(), method);
    }

    private int findMethodIndexInVtable(@Nonnull List<Method> vtable, MethodReference method) {
        for (int i=0; i<vtable.size(); i++) {
            Method candidate = vtable.get(i);
            if (MethodUtil.methodSignaturesMatch(candidate, method)) {
                if (!classPath.shouldCheckPackagePrivateAccess() ||
                        AnalyzedMethodUtil.canAccess(this, candidate, true, false, false)) {
                    return i;
                }
            }
        }
        return -1;
    }

    private int findMethodIndexInVtableReverse(@Nonnull List<Method> vtable, MethodReference method) {
        for (int i=vtable.size() - 1; i>=0; i--) {
            Method candidate = vtable.get(i);
            if (MethodUtil.methodSignaturesMatch(candidate, method)) {
                if (!classPath.shouldCheckPackagePrivateAccess() ||
                        AnalyzedMethodUtil.canAccess(this, candidate, true, false, false)) {
                    return i;
                }
            }
        }
        return -1;
    }

    @Nonnull public SparseArray<FieldReference> getInstanceFields() {
        if (classPath.isArt()) {
            return artInstanceFieldsSupplier.get();
        } else {
            return dalvikInstanceFieldsSupplier.get();
        }
    }

    @Nonnull private final Supplier<SparseArray<FieldReference>> dalvikInstanceFieldsSupplier =
            Suppliers.memoize(new Supplier<SparseArray<FieldReference>>() {
                @Override public SparseArray<FieldReference> get() {
                    //This is a bit of an "involved" operation. We need to follow the same algorithm that dalvik uses to
                    //arrange fields, so that we end up with the same field offsets (which is needed for deodexing).
                    //See mydroid/dalvik/vm/oo/Class.c - computeFieldOffsets()

                    ArrayList<Field> fields = getSortedInstanceFields(getClassDef());
                    final int fieldCount = fields.size();
                    //the "type" for each field in fields. 0=reference,1=wide,2=other
                    byte[] fieldTypes = new byte[fields.size()];
                    for (int i=0; i<fieldCount; i++) {
                        fieldTypes[i] = getFieldType(fields.get(i));
                    }

                    //The first operation is to move all of the reference fields to the front. To do this, find the first
                    //non-reference field, then find the last reference field, swap them and repeat
                    int back = fields.size() - 1;
                    int front;
                    for (front = 0; front<fieldCount; front++) {
                        if (fieldTypes[front] != REFERENCE) {
                            while (back > front) {
                                if (fieldTypes[back] == REFERENCE) {
                                    swap(fieldTypes, fields, front, back--);
                                    break;
                                }
                                back--;
                            }
                        }

                        if (fieldTypes[front] != REFERENCE) {
                            break;
                        }
                    }

                    int startFieldOffset = 8;
                    String superclassType = getSuperclass();
                    ClassProto superclass = null;
                    if (superclassType != null) {
                        superclass = (ClassProto) classPath.getClass(superclassType);
                        startFieldOffset = superclass.getNextFieldOffset();
                    }

                    int fieldIndexMod;
                    if ((startFieldOffset % 8) == 0) {
                        fieldIndexMod = 0;
                    } else {
                        fieldIndexMod = 1;
                    }

                    //next, we need to group all the wide fields after the reference fields. But the wide fields have to be
                    //8-byte aligned. If we're on an odd field index, we need to insert a 32-bit field. If the next field
                    //is already a 32-bit field, use that. Otherwise, find the first 32-bit field from the end and swap it in.
                    //If there are no 32-bit fields, do nothing for now. We'll add padding when calculating the field offsets
                    if (front < fieldCount && (front % 2) != fieldIndexMod) {
                        if (fieldTypes[front] == WIDE) {
                            //we need to swap in a 32-bit field, so the wide fields will be correctly aligned
                            back = fieldCount - 1;
                            while (back > front) {
                                if (fieldTypes[back] == OTHER) {
                                    swap(fieldTypes, fields, front++, back);
                                    break;
                                }
                                back--;
                            }
                        } else {
                            //there's already a 32-bit field here that we can use
                            front++;
                        }
                    }

                    //do the swap thing for wide fields
                    back = fieldCount - 1;
                    for (; front<fieldCount; front++) {
                        if (fieldTypes[front] != WIDE) {
                            while (back > front) {
                                if (fieldTypes[back] == WIDE) {
                                    swap(fieldTypes, fields, front, back--);
                                    break;
                                }
                                back--;
                            }
                        }

                        if (fieldTypes[front] != WIDE) {
                            break;
                        }
                    }

                    SparseArray<FieldReference> superFields;
                    if (superclass != null) {
                        superFields = superclass.getInstanceFields();
                    } else {
                        superFields = new SparseArray<FieldReference>();
                    }
                    int superFieldCount = superFields.size();

                    //now the fields are in the correct order. Add them to the SparseArray and lookup, and calculate the offsets
                    int totalFieldCount = superFieldCount + fieldCount;
                    SparseArray<FieldReference> instanceFields = new SparseArray<FieldReference>(totalFieldCount);

                    int fieldOffset;

                    if (superclass != null && superFieldCount > 0) {
                        for (int i=0; i<superFieldCount; i++) {
                            instanceFields.append(superFields.keyAt(i), superFields.valueAt(i));
                        }

                        fieldOffset = instanceFields.keyAt(superFieldCount-1);

                        FieldReference lastSuperField = superFields.valueAt(superFieldCount-1);
                        char fieldType = lastSuperField.getType().charAt(0);
                        if (fieldType == 'J' || fieldType == 'D') {
                            fieldOffset += 8;
                        } else {
                            fieldOffset += 4;
                        }
                    } else {
                        //the field values start at 8 bytes into the DataObject dalvik structure
                        fieldOffset = 8;
                    }

                    boolean gotDouble = false;
                    for (int i=0; i<fieldCount; i++) {
                        FieldReference field = fields.get(i);

                        //add padding to align the wide fields, if needed
                        if (fieldTypes[i] == WIDE && !gotDouble) {
                            if (fieldOffset % 8 != 0) {
                                assert fieldOffset % 8 == 4;
                                fieldOffset += 4;
                            }
                            gotDouble = true;
                        }

                        instanceFields.append(fieldOffset, field);
                        if (fieldTypes[i] == WIDE) {
                            fieldOffset += 8;
                        } else {
                            fieldOffset += 4;
                        }
                    }

                    return instanceFields;
                }

                @Nonnull
                private ArrayList<Field> getSortedInstanceFields(@Nonnull ClassDef classDef) {
                    ArrayList<Field> fields = Lists.newArrayList(classDef.getInstanceFields());
                    Collections.sort(fields);
                    return fields;
                }

                private void swap(byte[] fieldTypes, List<Field> fields, int position1, int position2) {
                    byte tempType = fieldTypes[position1];
                    fieldTypes[position1] = fieldTypes[position2];
                    fieldTypes[position2] = tempType;

                    Field tempField = fields.set(position1, fields.get(position2));
                    fields.set(position2, tempField);
                }
            });

    private static abstract class FieldGap implements Comparable<FieldGap> {
        public final int offset;
        public final int size;

        public static FieldGap newFieldGap(int offset, int size, int oatVersion) {
            if (oatVersion >= 67) {
                return new FieldGap(offset, size) {
                    @Override public int compareTo(@Nonnull FieldGap o) {
                        int result = Ints.compare(o.size, size);
                        if (result != 0) {
                            return result;
                        }
                        return Ints.compare(offset, o.offset);
                    }
                };
            } else {
                return new FieldGap(offset, size) {
                    @Override public int compareTo(@Nonnull FieldGap o) {
                        int result = Ints.compare(size, o.size);
                        if (result != 0) {
                            return result;
                        }
                        return Ints.compare(o.offset, offset);
                    }
                };
            }
        }

        private FieldGap(int offset, int size) {
            this.offset = offset;
            this.size = size;
        }
    }

    @Nonnull private final Supplier<SparseArray<FieldReference>> artInstanceFieldsSupplier =
            Suppliers.memoize(new Supplier<SparseArray<FieldReference>>() {

                @Override public SparseArray<FieldReference> get() {
                    // We need to follow the same algorithm that art uses to arrange fields, so that we end up with the
                    // same field offsets, which is needed for deodexing.
                    // See LinkFields() in art/runtime/class_linker.cc

                    PriorityQueue<FieldGap> gaps = new PriorityQueue<FieldGap>();

                    SparseArray<FieldReference> linkedFields = new SparseArray<FieldReference>();
                    ArrayList<Field> fields = getSortedInstanceFields(getClassDef());

                    int fieldOffset = 0;
                    String superclassType = getSuperclass();
                    if (superclassType != null) {
                        // TODO: what to do if superclass doesn't exist?
                        ClassProto superclass = (ClassProto) classPath.getClass(superclassType);
                        SparseArray<FieldReference> superFields = superclass.getInstanceFields();
                        FieldReference field = null;
                        int lastOffset = 0;
                        for (int i=0; i<superFields.size(); i++) {
                            int offset = superFields.keyAt(i);
                            field = superFields.valueAt(i);
                            linkedFields.put(offset, field);
                            lastOffset = offset;
                        }
                        if (field != null) {
                            fieldOffset = lastOffset + getFieldSize(field);
                        }
                    }

                    for (Field field: fields) {
                        int fieldSize = getFieldSize(field);

                        if (!AlignmentUtils.isAligned(fieldOffset, fieldSize)) {
                            int oldOffset = fieldOffset;
                            fieldOffset = AlignmentUtils.alignOffset(fieldOffset, fieldSize);
                            addFieldGap(oldOffset, fieldOffset, gaps);
                        }

                        FieldGap gap = gaps.peek();
                        if (gap != null && gap.size >= fieldSize) {
                            gaps.poll();
                            linkedFields.put(gap.offset, field);
                            if (gap.size > fieldSize) {
                                addFieldGap(gap.offset + fieldSize, gap.offset + gap.size, gaps);
                            }
                        } else {
                            linkedFields.append(fieldOffset, field);
                            fieldOffset += fieldSize;
                        }
                    }

                    return linkedFields;
                }

                private void addFieldGap(int gapStart, int gapEnd, @Nonnull PriorityQueue<FieldGap> gaps) {
                    int offset = gapStart;

                    while (offset < gapEnd) {
                        int remaining = gapEnd - offset;

                        if ((remaining >= 4) && (offset % 4 == 0)) {
                            gaps.add(FieldGap.newFieldGap(offset, 4, classPath.oatVersion));
                            offset += 4;
                        } else if (remaining >= 2 && (offset % 2 == 0)) {
                            gaps.add(FieldGap.newFieldGap(offset, 2, classPath.oatVersion));
                            offset += 2;
                        } else {
                            gaps.add(FieldGap.newFieldGap(offset, 1, classPath.oatVersion));
                            offset += 1;
                        }
                    }
                }

                @Nonnull
                private ArrayList<Field> getSortedInstanceFields(@Nonnull ClassDef classDef) {
                    ArrayList<Field> fields = Lists.newArrayList(classDef.getInstanceFields());
                    Collections.sort(fields, new Comparator<Field>() {
                        @Override public int compare(Field field1, Field field2) {
                            int result = Ints.compare(getFieldSortOrder(field1), getFieldSortOrder(field2));
                            if (result != 0) {
                                return result;
                            }

                            result = field1.getName().compareTo(field2.getName());
                            if (result != 0) {
                                return result;
                            }
                            return field1.getType().compareTo(field2.getType());
                        }
                    });
                    return fields;
                }

                private int getFieldSortOrder(@Nonnull FieldReference field) {
                    // The sort order is based on type size (except references are first), and then based on the
                    // enum value of the primitive type for types of equal size. See: Primitive::Type enum
                    // in art/runtime/primitive.h
                    switch (field.getType().charAt(0)) {
                        /* reference */
                        case '[':
                        case 'L':
                            return 0;
                        /* 64 bit */
                        case 'J':
                            return 1;
                        case 'D':
                            return 2;
                        /* 32 bit */
                        case 'I':
                            return 3;
                        case 'F':
                            return 4;
                        /* 16 bit */
                        case 'C':
                            return 5;
                        case 'S':
                            return 6;
                        /* 8 bit */
                        case 'Z':
                            return 7;
                        case 'B':
                            return 8;
                    }
                    throw new ExceptionWithContext("Invalid field type: %s", field.getType());
                }

                private int getFieldSize(@Nonnull FieldReference field) {
                    return getTypeSize(field.getType().charAt(0));
                }
            });

    private int getNextFieldOffset() {
        SparseArray<FieldReference> instanceFields = getInstanceFields();
        if (instanceFields.size() == 0) {
            return classPath.isArt() ? 0 : 8;
        }

        int lastItemIndex = instanceFields.size()-1;
        int fieldOffset = instanceFields.keyAt(lastItemIndex);
        FieldReference lastField = instanceFields.valueAt(lastItemIndex);

        if (classPath.isArt()) {
            return fieldOffset + getTypeSize(lastField.getType().charAt(0));
        } else {
            switch (lastField.getType().charAt(0)) {
                case 'J':
                case 'D':
                    return fieldOffset + 8;
                default:
                    return fieldOffset + 4;
            }
        }
    }

    private static int getTypeSize(char type) {
        switch (type) {
            case 'J':
            case 'D':
                return 8;
            case '[':
            case 'L':
            case 'I':
            case 'F':
                return 4;
            case 'C':
            case 'S':
                return 2;
            case 'B':
            case 'Z':
                return 1;
        }
        throw new ExceptionWithContext("Invalid type: %s", type);
    }

    @Nonnull public List<Method> getVtable() {
        if (!classPath.isArt() || classPath.oatVersion < 72) {
            return preDefaultMethodVtableSupplier.get();
        } else if (classPath.oatVersion < 87) {
            return buggyPostDefaultMethodVtableSupplier.get();
        } else {
            return postDefaultMethodVtableSupplier.get();
        }
    }

    //TODO: check the case when we have a package private method that overrides an interface method
    @Nonnull private final Supplier<List<Method>> preDefaultMethodVtableSupplier = Suppliers.memoize(new Supplier<List<Method>>() {
        @Override public List<Method> get() {
            List<Method> vtable = Lists.newArrayList();

            //copy the virtual methods from the superclass
            String superclassType;
            try {
                superclassType = getSuperclass();
            } catch (UnresolvedClassException ex) {
                vtable.addAll(((ClassProto)classPath.getClass("Ljava/lang/Object;")).getVtable());
                vtableFullyResolved = false;
                return vtable;
            }

            if (superclassType != null) {
                ClassProto superclass = (ClassProto) classPath.getClass(superclassType);
                vtable.addAll(superclass.getVtable());

                // if the superclass's vtable wasn't fully resolved, then we can't know where the new methods added by this
                // class should start, so we just propagate what we can from the parent and hope for the best.
                if (!superclass.vtableFullyResolved) {
                    vtableFullyResolved = false;
                    return vtable;
                }
            }

            //iterate over the virtual methods in the current class, and only add them when we don't already have the
            //method (i.e. if it was implemented by the superclass)
            if (!isInterface()) {
                addToVtable(getClassDef().getVirtualMethods(), vtable, true, true);

                // We use the current class for any vtable method references that we add, rather than the interface, so
                // we don't end up trying to call invoke-virtual using an interface, which will fail verification
                Iterable<ClassDef> interfaces = getDirectInterfaces();
                for (ClassDef interfaceDef: interfaces) {
                    List<Method> interfaceMethods = Lists.newArrayList();
                    for (Method interfaceMethod: interfaceDef.getVirtualMethods()) {
                        interfaceMethods.add(new ReparentedMethod(interfaceMethod, type));
                    }
                    addToVtable(interfaceMethods, vtable, false, true);
                }
            }
            return vtable;
        }
    });

    /**
     * This is the vtable supplier for a version of art that had buggy vtable calculation logic. In some cases it can
     * produce multiple vtable entries for a given virtual method. This supplier duplicates this buggy logic in order to
     * generate an identical vtable
     */
    @Nonnull private final Supplier<List<Method>> buggyPostDefaultMethodVtableSupplier = Suppliers.memoize(new Supplier<List<Method>>() {
        @Override public List<Method> get() {
            List<Method> vtable = Lists.newArrayList();

            //copy the virtual methods from the superclass
            String superclassType;
            try {
                superclassType = getSuperclass();
            } catch (UnresolvedClassException ex) {
                vtable.addAll(((ClassProto)classPath.getClass("Ljava/lang/Object;")).getVtable());
                vtableFullyResolved = false;
                return vtable;
            }

            if (superclassType != null) {
                ClassProto superclass = (ClassProto) classPath.getClass(superclassType);
                vtable.addAll(superclass.getVtable());

                // if the superclass's vtable wasn't fully resolved, then we can't know where the new methods added by
                // this class should start, so we just propagate what we can from the parent and hope for the best.
                if (!superclass.vtableFullyResolved) {
                    vtableFullyResolved = false;
                    return vtable;
                }
            }

            //iterate over the virtual methods in the current class, and only add them when we don't already have the
            //method (i.e. if it was implemented by the superclass)
            if (!isInterface()) {
                addToVtable(getClassDef().getVirtualMethods(), vtable, true, true);

                List<String> interfaces = Lists.newArrayList(getInterfaces().keySet());

                List<Method> defaultMethods = Lists.newArrayList();
                List<Method> defaultConflictMethods = Lists.newArrayList();
                List<Method> mirandaMethods = Lists.newArrayList();

                final HashMap<MethodReference, Integer> methodOrder = Maps.newHashMap();

                for (int i=interfaces.size()-1; i>=0; i--) {
                    String interfaceType = interfaces.get(i);
                    ClassDef interfaceDef = classPath.getClassDef(interfaceType);

                    for (Method interfaceMethod : interfaceDef.getVirtualMethods()) {

                        int vtableIndex = findMethodIndexInVtableReverse(vtable, interfaceMethod);
                        Method oldVtableMethod = null;
                        if (vtableIndex >= 0) {
                            oldVtableMethod = vtable.get(vtableIndex);
                        }

                        for (int j=0; j<vtable.size(); j++) {
                            Method candidate = vtable.get(j);
                            if (MethodUtil.methodSignaturesMatch(candidate, interfaceMethod)) {
                                if (!classPath.shouldCheckPackagePrivateAccess() ||
                                        AnalyzedMethodUtil.canAccess(ClassProto.this, candidate, true, false, false)) {
                                    if (interfaceMethodOverrides(interfaceMethod, candidate)) {
                                        vtable.set(j, interfaceMethod);
                                    }
                                }
                            }
                        }

                        if (vtableIndex >= 0) {
                            if (!isOverridableByDefaultMethod(vtable.get(vtableIndex))) {
                                continue;
                            }
                        }

                        int defaultMethodIndex = findMethodIndexInVtable(defaultMethods, interfaceMethod);

                        if (defaultMethodIndex >= 0) {
                            if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) {
                                ClassProto existingInterface = (ClassProto)classPath.getClass(
                                        defaultMethods.get(defaultMethodIndex).getDefiningClass());
                                if (!existingInterface.implementsInterface(interfaceMethod.getDefiningClass())) {
                                    Method removedMethod = defaultMethods.remove(defaultMethodIndex);
                                    defaultConflictMethods.add(removedMethod);
                                }
                            }
                            continue;
                        }

                        int defaultConflictMethodIndex = findMethodIndexInVtable(
                                defaultConflictMethods, interfaceMethod);
                        if (defaultConflictMethodIndex >= 0) {
                            // There's already a matching method in the conflict list, we don't need to do
                            // anything else
                            continue;
                        }

                        int mirandaMethodIndex = findMethodIndexInVtable(mirandaMethods, interfaceMethod);

                        if (mirandaMethodIndex >= 0) {
                            if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) {

                                ClassProto existingInterface = (ClassProto)classPath.getClass(
                                        mirandaMethods.get(mirandaMethodIndex).getDefiningClass());
                                if (!existingInterface.implementsInterface(interfaceMethod.getDefiningClass())) {
                                    Method oldMethod = mirandaMethods.remove(mirandaMethodIndex);
                                    int methodOrderValue = methodOrder.get(oldMethod);
                                    methodOrder.put(interfaceMethod, methodOrderValue);
                                    defaultMethods.add(interfaceMethod);
                                }
                            }
                            continue;
                        }

                        if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) {
                            if (oldVtableMethod != null) {
                                if (!interfaceMethodOverrides(interfaceMethod, oldVtableMethod)) {
                                    continue;
                                }
                            }
                            defaultMethods.add(interfaceMethod);
                            methodOrder.put(interfaceMethod, methodOrder.size());
                        } else {
                            // TODO: do we need to check interfaceMethodOverrides here?
                            if (oldVtableMethod == null) {
                                mirandaMethods.add(interfaceMethod);
                                methodOrder.put(interfaceMethod, methodOrder.size());
                            }
                        }
                    }
                }

                Comparator<MethodReference> comparator = new Comparator<MethodReference>() {
                    @Override public int compare(MethodReference o1, MethodReference o2) {
                        return Ints.compare(methodOrder.get(o1), methodOrder.get(o2));
                    }
                };

                // The methods should be in the same order within each list as they were iterated over.
                // They can be misordered if, e.g. a method was originally added to the default list, but then moved
                // to the conflict list.
                Collections.sort(mirandaMethods, comparator);
                Collections.sort(defaultMethods, comparator);
                Collections.sort(defaultConflictMethods, comparator);

                vtable.addAll(mirandaMethods);
                vtable.addAll(defaultMethods);
                vtable.addAll(defaultConflictMethods);
            }
            return vtable;
        }
    });

    @Nonnull private final Supplier<List<Method>> postDefaultMethodVtableSupplier = Suppliers.memoize(new Supplier<List<Method>>() {
        @Override public List<Method> get() {
            List<Method> vtable = Lists.newArrayList();

            //copy the virtual methods from the superclass
            String superclassType;
            try {
                superclassType = getSuperclass();
            } catch (UnresolvedClassException ex) {
                vtable.addAll(((ClassProto)classPath.getClass("Ljava/lang/Object;")).getVtable());
                vtableFullyResolved = false;
                return vtable;
            }

            if (superclassType != null) {
                ClassProto superclass = (ClassProto) classPath.getClass(superclassType);
                vtable.addAll(superclass.getVtable());

                // if the superclass's vtable wasn't fully resolved, then we can't know where the new methods added by
                // this class should start, so we just propagate what we can from the parent and hope for the best.
                if (!superclass.vtableFullyResolved) {
                    vtableFullyResolved = false;
                    return vtable;
                }
            }

            //iterate over the virtual methods in the current class, and only add them when we don't already have the
            //method (i.e. if it was implemented by the superclass)
            if (!isInterface()) {
                addToVtable(getClassDef().getVirtualMethods(), vtable, true, true);

                Iterable<ClassDef> interfaces = Lists.reverse(Lists.newArrayList(getDirectInterfaces()));

                List<Method> defaultMethods = Lists.newArrayList();
                List<Method> defaultConflictMethods = Lists.newArrayList();
                List<Method> mirandaMethods = Lists.newArrayList();

                final HashMap<MethodReference, Integer> methodOrder = Maps.newHashMap();

                for (ClassDef interfaceDef: interfaces) {
                    for (Method interfaceMethod : interfaceDef.getVirtualMethods()) {

                        int vtableIndex = findMethodIndexInVtable(vtable, interfaceMethod);

                        if (vtableIndex >= 0) {
                            if (interfaceMethodOverrides(interfaceMethod, vtable.get(vtableIndex))) {
                                vtable.set(vtableIndex, interfaceMethod);
                            }
                        } else {
                            int defaultMethodIndex = findMethodIndexInVtable(defaultMethods, interfaceMethod);

                            if (defaultMethodIndex >= 0) {
                                if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) {
                                    ClassProto existingInterface = (ClassProto)classPath.getClass(
                                            defaultMethods.get(defaultMethodIndex).getDefiningClass());
                                    if (!existingInterface.implementsInterface(interfaceMethod.getDefiningClass())) {
                                        Method removedMethod = defaultMethods.remove(defaultMethodIndex);
                                        defaultConflictMethods.add(removedMethod);
                                    }
                                }
                                continue;
                            }

                            int defaultConflictMethodIndex = findMethodIndexInVtable(
                                    defaultConflictMethods, interfaceMethod);
                            if (defaultConflictMethodIndex >= 0) {
                                // There's already a matching method in the conflict list, we don't need to do
                                // anything else
                                continue;
                            }

                            int mirandaMethodIndex = findMethodIndexInVtable(mirandaMethods, interfaceMethod);

                            if (mirandaMethodIndex >= 0) {
                                if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) {

                                    ClassProto existingInterface = (ClassProto)classPath.getClass(
                                            mirandaMethods.get(mirandaMethodIndex).getDefiningClass());
                                    if (!existingInterface.implementsInterface(interfaceMethod.getDefiningClass())) {
                                        Method oldMethod = mirandaMethods.remove(mirandaMethodIndex);
                                        int methodOrderValue = methodOrder.get(oldMethod);
                                        methodOrder.put(interfaceMethod, methodOrderValue);
                                        defaultMethods.add(interfaceMethod);
                                    }
                                }
                                continue;
                            }

                            if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) {
                                defaultMethods.add(interfaceMethod);
                                methodOrder.put(interfaceMethod, methodOrder.size());
                            } else {
                                mirandaMethods.add(interfaceMethod);
                                methodOrder.put(interfaceMethod, methodOrder.size());
                            }
                        }
                    }
                }

                Comparator<MethodReference> comparator = new Comparator<MethodReference>() {
                    @Override public int compare(MethodReference o1, MethodReference o2) {
                        return Ints.compare(methodOrder.get(o1), methodOrder.get(o2));
                    }
                };

                // The methods should be in the same order within each list as they were iterated over.
                // They can be misordered if, e.g. a method was originally added to the default list, but then moved
                // to the conflict list.
                Collections.sort(defaultMethods, comparator);
                Collections.sort(defaultConflictMethods, comparator);
                Collections.sort(mirandaMethods, comparator);
                addToVtable(defaultMethods, vtable, false, false);
                addToVtable(defaultConflictMethods, vtable, false, false);
                addToVtable(mirandaMethods, vtable, false, false);
            }
            return vtable;
        }
    });

    private void addToVtable(@Nonnull Iterable<? extends Method> localMethods, @Nonnull List<Method> vtable,
                             boolean replaceExisting, boolean sort) {
        if (sort) {
            ArrayList<Method> methods = Lists.newArrayList(localMethods);
            Collections.sort(methods);
            localMethods = methods;
        }

        for (Method virtualMethod: localMethods) {
            int vtableIndex = findMethodIndexInVtable(vtable, virtualMethod);

            if (vtableIndex >= 0) {
                if (replaceExisting) {
                    vtable.set(vtableIndex, virtualMethod);
                }
            } else {
                // we didn't find an equivalent method, so add it as a new entry
                vtable.add(virtualMethod);
            }
        }
    }

    private static byte getFieldType(@Nonnull FieldReference field) {
        switch (field.getType().charAt(0)) {
            case '[':
            case 'L':
                return 0; //REFERENCE
            case 'J':
            case 'D':
                return 1; //WIDE
            default:
                return 2; //OTHER
        }
    }

    private boolean isOverridableByDefaultMethod(@Nonnull Method method) {
        ClassProto classProto = (ClassProto)classPath.getClass(method.getDefiningClass());
        return classProto.isInterface();
    }

    /**
     * Checks if the interface method overrides the virtual or interface method2
     * @param method A Method from an interface
     * @param method2 A Method from an interface or a class
     * @return true if the interface method overrides the virtual or interface method2
     */
    private boolean interfaceMethodOverrides(@Nonnull Method method, @Nonnull Method method2) {
        ClassProto classProto = (ClassProto)classPath.getClass(method2.getDefiningClass());

        if (classProto.isInterface()) {
            ClassProto targetClassProto = (ClassProto)classPath.getClass(method.getDefiningClass());
            return targetClassProto.implementsInterface(method2.getDefiningClass());
        } else {
            return false;
        }
    }

    static class ReparentedMethod extends BaseMethodReference implements Method {
        private final Method method;
        private final String definingClass;

        public ReparentedMethod(Method method, String definingClass) {
            this.method = method;
            this.definingClass = definingClass;
        }

        @Nonnull @Override public String getDefiningClass() {
            return definingClass;
        }

        @Nonnull @Override public String getName() {
            return method.getName();
        }

        @Nonnull @Override public List<? extends CharSequence> getParameterTypes() {
            return method.getParameterTypes();
        }

        @Nonnull @Override public String getReturnType() {
            return method.getReturnType();
        }

        @Nonnull @Override public List<? extends MethodParameter> getParameters() {
            return method.getParameters();
        }

        @Override public int getAccessFlags() {
            return method.getAccessFlags();
        }

        @Nonnull @Override public Set<? extends Annotation> getAnnotations() {
            return method.getAnnotations();
        }

        @Nullable @Override public MethodImplementation getImplementation() {
            return method.getImplementation();
        }
    }
}
