/*
 * 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.dx.rop.type;

import com.android.dx.util.Hex;

import java.util.HashMap;

/**
 * Representation of a value type, such as may appear in a field, in a
 * local, on a stack, or in a method descriptor. Instances of this
 * class are generally interned and may be usefully compared with each
 * other using {@code ==}.
 */
public final class Type implements TypeBearer, Comparable<Type> {
    /** {@code non-null;} intern table mapping string descriptors to instances */
    private static final HashMap<String, Type> internTable =
        new HashMap<String, Type>(500);

    /** basic type constant for {@code void} */
    public static final int BT_VOID = 0;

    /** basic type constant for {@code boolean} */
    public static final int BT_BOOLEAN = 1;

    /** basic type constant for {@code byte} */
    public static final int BT_BYTE = 2;

    /** basic type constant for {@code char} */
    public static final int BT_CHAR = 3;

    /** basic type constant for {@code double} */
    public static final int BT_DOUBLE = 4;

    /** basic type constant for {@code float} */
    public static final int BT_FLOAT = 5;

    /** basic type constant for {@code int} */
    public static final int BT_INT = 6;

    /** basic type constant for {@code long} */
    public static final int BT_LONG = 7;

    /** basic type constant for {@code short} */
    public static final int BT_SHORT = 8;

    /** basic type constant for {@code Object} */
    public static final int BT_OBJECT = 9;

    /** basic type constant for a return address */
    public static final int BT_ADDR = 10;

    /** count of basic type constants */
    public static final int BT_COUNT = 11;

    /** {@code non-null;} instance representing {@code boolean} */
    public static final Type BOOLEAN = new Type("Z", BT_BOOLEAN);

    /** {@code non-null;} instance representing {@code byte} */
    public static final Type BYTE = new Type("B", BT_BYTE);

    /** {@code non-null;} instance representing {@code char} */
    public static final Type CHAR = new Type("C", BT_CHAR);

    /** {@code non-null;} instance representing {@code double} */
    public static final Type DOUBLE = new Type("D", BT_DOUBLE);

    /** {@code non-null;} instance representing {@code float} */
    public static final Type FLOAT = new Type("F", BT_FLOAT);

    /** {@code non-null;} instance representing {@code int} */
    public static final Type INT = new Type("I", BT_INT);

    /** {@code non-null;} instance representing {@code long} */
    public static final Type LONG = new Type("J", BT_LONG);

    /** {@code non-null;} instance representing {@code short} */
    public static final Type SHORT = new Type("S", BT_SHORT);

    /** {@code non-null;} instance representing {@code void} */
    public static final Type VOID = new Type("V", BT_VOID);

    /** {@code non-null;} instance representing a known-{@code null} */
    public static final Type KNOWN_NULL = new Type("<null>", BT_OBJECT);

    /** {@code non-null;} instance representing a subroutine return address */
    public static final Type RETURN_ADDRESS = new Type("<addr>", BT_ADDR);

    static {
        /*
         * Put all the primitive types into the intern table. This needs
         * to happen before the array types below get interned.
         */
        putIntern(BOOLEAN);
        putIntern(BYTE);
        putIntern(CHAR);
        putIntern(DOUBLE);
        putIntern(FLOAT);
        putIntern(INT);
        putIntern(LONG);
        putIntern(SHORT);
        /*
         * Note: VOID isn't put in the intern table, since it's special and
         * shouldn't be found by a normal call to intern().
         */
    }

    /**
     * {@code non-null;} instance representing
     * {@code java.lang.annotation.Annotation}
     */
    public static final Type ANNOTATION =
        intern("Ljava/lang/annotation/Annotation;");

    /** {@code non-null;} instance representing {@code java.lang.Class} */
    public static final Type CLASS = intern("Ljava/lang/Class;");

    /** {@code non-null;} instance representing {@code java.lang.Cloneable} */
    public static final Type CLONEABLE = intern("Ljava/lang/Cloneable;");

    /** {@code non-null;} instance representing {@code java.lang.Object} */
    public static final Type OBJECT = intern("Ljava/lang/Object;");

    /** {@code non-null;} instance representing {@code java.io.Serializable} */
    public static final Type SERIALIZABLE = intern("Ljava/io/Serializable;");

    /** {@code non-null;} instance representing {@code java.lang.String} */
    public static final Type STRING = intern("Ljava/lang/String;");

    /** {@code non-null;} instance representing {@code java.lang.Throwable} */
    public static final Type THROWABLE = intern("Ljava/lang/Throwable;");

    /**
     * {@code non-null;} instance representing {@code java.lang.Boolean}; the
     * suffix on the name helps disambiguate this from the instance
     * representing a primitive type
     */
    public static final Type BOOLEAN_CLASS = intern("Ljava/lang/Boolean;");

    /**
     * {@code non-null;} instance representing {@code java.lang.Byte}; the
     * suffix on the name helps disambiguate this from the instance
     * representing a primitive type
     */
    public static final Type BYTE_CLASS = intern("Ljava/lang/Byte;");

    /**
     * {@code non-null;} instance representing {@code java.lang.Character}; the
     * suffix on the name helps disambiguate this from the instance
     * representing a primitive type
     */
    public static final Type CHARACTER_CLASS = intern("Ljava/lang/Character;");

    /**
     * {@code non-null;} instance representing {@code java.lang.Double}; the
     * suffix on the name helps disambiguate this from the instance
     * representing a primitive type
     */
    public static final Type DOUBLE_CLASS = intern("Ljava/lang/Double;");

    /**
     * {@code non-null;} instance representing {@code java.lang.Float}; the
     * suffix on the name helps disambiguate this from the instance
     * representing a primitive type
     */
    public static final Type FLOAT_CLASS = intern("Ljava/lang/Float;");

    /**
     * {@code non-null;} instance representing {@code java.lang.Integer}; the
     * suffix on the name helps disambiguate this from the instance
     * representing a primitive type
     */
    public static final Type INTEGER_CLASS = intern("Ljava/lang/Integer;");

    /**
     * {@code non-null;} instance representing {@code java.lang.Long}; the
     * suffix on the name helps disambiguate this from the instance
     * representing a primitive type
     */
    public static final Type LONG_CLASS = intern("Ljava/lang/Long;");

    /**
     * {@code non-null;} instance representing {@code java.lang.Short}; the
     * suffix on the name helps disambiguate this from the instance
     * representing a primitive type
     */
    public static final Type SHORT_CLASS = intern("Ljava/lang/Short;");

    /**
     * {@code non-null;} instance representing {@code java.lang.Void}; the
     * suffix on the name helps disambiguate this from the instance
     * representing a primitive type
     */
    public static final Type VOID_CLASS = intern("Ljava/lang/Void;");

    /** {@code non-null;} instance representing {@code boolean[]} */
    public static final Type BOOLEAN_ARRAY = BOOLEAN.getArrayType();

    /** {@code non-null;} instance representing {@code byte[]} */
    public static final Type BYTE_ARRAY = BYTE.getArrayType();

    /** {@code non-null;} instance representing {@code char[]} */
    public static final Type CHAR_ARRAY = CHAR.getArrayType();

    /** {@code non-null;} instance representing {@code double[]} */
    public static final Type DOUBLE_ARRAY = DOUBLE.getArrayType();

    /** {@code non-null;} instance representing {@code float[]} */
    public static final Type FLOAT_ARRAY = FLOAT.getArrayType();

    /** {@code non-null;} instance representing {@code int[]} */
    public static final Type INT_ARRAY = INT.getArrayType();

    /** {@code non-null;} instance representing {@code long[]} */
    public static final Type LONG_ARRAY = LONG.getArrayType();

    /** {@code non-null;} instance representing {@code Object[]} */
    public static final Type OBJECT_ARRAY = OBJECT.getArrayType();

    /** {@code non-null;} instance representing {@code short[]} */
    public static final Type SHORT_ARRAY = SHORT.getArrayType();

    /** {@code non-null;} field descriptor for the type */
    private final String descriptor;

    /**
     * basic type corresponding to this type; one of the
     * {@code BT_*} constants
     */
    private final int basicType;

    /**
     * {@code >= -1;} for an uninitialized type, bytecode index that this
     * instance was allocated at; {@code Integer.MAX_VALUE} if it
     * was an incoming uninitialized instance; {@code -1} if this
     * is an <i>inititialized</i> instance
     */
    private final int newAt;

    /**
     * {@code null-ok;} the internal-form class name corresponding to this type, if
     * calculated; only valid if {@code this} is a reference type and
     * additionally not a return address
     */
    private String className;

    /**
     * {@code null-ok;} the type corresponding to an array of this type, if
     * calculated
     */
    private Type arrayType;

    /**
     * {@code null-ok;} the type corresponding to elements of this type, if
     * calculated; only valid if {@code this} is an array type
     */
    private Type componentType;

    /**
     * {@code null-ok;} the type corresponding to the initialized version of
     * this type, if this instance is in fact an uninitialized type
     */
    private Type initializedType;

    /**
     * Returns the unique instance corresponding to the type with the
     * given descriptor. See vmspec-2 sec4.3.2 for details on the
     * field descriptor syntax. This method does <i>not</i> allow
     * {@code "V"} (that is, type {@code void}) as a valid
     * descriptor.
     *
     * @param descriptor {@code non-null;} the descriptor
     * @return {@code non-null;} the corresponding instance
     * @throws IllegalArgumentException thrown if the descriptor has
     * invalid syntax
     */
    public static Type intern(String descriptor) {
        Type result = internTable.get(descriptor);
        if (result != null) {
            return result;
        }

        char firstChar;
        try {
            firstChar = descriptor.charAt(0);
        } catch (IndexOutOfBoundsException ex) {
            // Translate the exception.
            throw new IllegalArgumentException("descriptor is empty");
        } catch (NullPointerException ex) {
            // Elucidate the exception.
            throw new NullPointerException("descriptor == null");
        }

        if (firstChar == '[') {
            /*
             * Recursively strip away array markers to get at the underlying
             * type, and build back on to form the result.
             */
            result = intern(descriptor.substring(1));
            return result.getArrayType();
        }

        /*
         * If the first character isn't '[' and it wasn't found in the
         * intern cache, then it had better be the descriptor for a class.
         */

        int length = descriptor.length();
        if ((firstChar != 'L') ||
            (descriptor.charAt(length - 1) != ';')) {
            throw new IllegalArgumentException("bad descriptor");
        }

        /*
         * Validate the characters of the class name itself. Note that
         * vmspec-2 does not have a coherent definition for valid
         * internal-form class names, and the definition here is fairly
         * liberal: A name is considered valid as long as it doesn't
         * contain any of '[' ';' '.' '(' ')', and it has no more than one
         * '/' in a row, and no '/' at either end.
         */

        int limit = (length - 1); // Skip the final ';'.
        for (int i = 1; i < limit; i++) {
            char c = descriptor.charAt(i);
            switch (c) {
                case '[':
                case ';':
                case '.':
                case '(':
                case ')': {
                    throw new IllegalArgumentException("bad descriptor");
                }
                case '/': {
                    if ((i == 1) ||
                        (i == (length - 1)) ||
                        (descriptor.charAt(i - 1) == '/')) {
                        throw new IllegalArgumentException("bad descriptor");
                    }
                    break;
                }
            }
        }

        result = new Type(descriptor, BT_OBJECT);
        return putIntern(result);
    }

    /**
     * Returns the unique instance corresponding to the type with the
     * given descriptor, allowing {@code "V"} to return the type
     * for {@code void}. Other than that one caveat, this method
     * is identical to {@link #intern}.
     *
     * @param descriptor {@code non-null;} the descriptor
     * @return {@code non-null;} the corresponding instance
     * @throws IllegalArgumentException thrown if the descriptor has
     * invalid syntax
     */
    public static Type internReturnType(String descriptor) {
        try {
            if (descriptor.equals("V")) {
                // This is the one special case where void may be returned.
                return VOID;
            }
        } catch (NullPointerException ex) {
            // Elucidate the exception.
            throw new NullPointerException("descriptor == null");
        }

        return intern(descriptor);
    }

    /**
     * Returns the unique instance corresponding to the type of the
     * class with the given name. Calling this method is equivalent to
     * calling {@code intern(name)} if {@code name} begins
     * with {@code "["} and calling {@code intern("L" + name + ";")}
     * in all other cases.
     *
     * @param name {@code non-null;} the name of the class whose type is desired
     * @return {@code non-null;} the corresponding type
     * @throws IllegalArgumentException thrown if the name has
     * invalid syntax
     */
    public static Type internClassName(String name) {
        if (name == null) {
            throw new NullPointerException("name == null");
        }

        if (name.startsWith("[")) {
            return intern(name);
        }

        return intern('L' + name + ';');
    }

    /**
     * Constructs an instance corresponding to an "uninitialized type."
     * This is a private constructor; use one of the public static
     * methods to get instances.
     *
     * @param descriptor {@code non-null;} the field descriptor for the type
     * @param basicType basic type corresponding to this type; one of the
     * {@code BT_*} constants
     * @param newAt {@code >= -1;} allocation bytecode index
     */
    private Type(String descriptor, int basicType, int newAt) {
        if (descriptor == null) {
            throw new NullPointerException("descriptor == null");
        }

        if ((basicType < 0) || (basicType >= BT_COUNT)) {
            throw new IllegalArgumentException("bad basicType");
        }

        if (newAt < -1) {
            throw new IllegalArgumentException("newAt < -1");
        }

        this.descriptor = descriptor;
        this.basicType = basicType;
        this.newAt = newAt;
        this.arrayType = null;
        this.componentType = null;
        this.initializedType = null;
    }

    /**
     * Constructs an instance corresponding to an "initialized type."
     * This is a private constructor; use one of the public static
     * methods to get instances.
     *
     * @param descriptor {@code non-null;} the field descriptor for the type
     * @param basicType basic type corresponding to this type; one of the
     * {@code BT_*} constants
     */
    private Type(String descriptor, int basicType) {
        this(descriptor, basicType, -1);
    }

    /** {@inheritDoc} */
    @Override
    public boolean equals(Object other) {
        if (this == other) {
            /*
             * Since externally-visible types are interned, this check
             * helps weed out some easy cases.
             */
            return true;
        }

        if (!(other instanceof Type)) {
            return false;
        }

        return descriptor.equals(((Type) other).descriptor);
    }

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

    /** {@inheritDoc} */
    public int compareTo(Type other) {
        return descriptor.compareTo(other.descriptor);
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return descriptor;
    }

    /** {@inheritDoc} */
    public String toHuman() {
        switch (basicType) {
            case BT_VOID:    return "void";
            case BT_BOOLEAN: return "boolean";
            case BT_BYTE:    return "byte";
            case BT_CHAR:    return "char";
            case BT_DOUBLE:  return "double";
            case BT_FLOAT:   return "float";
            case BT_INT:     return "int";
            case BT_LONG:    return "long";
            case BT_SHORT:   return "short";
            case BT_OBJECT:  break;
            default:         return descriptor;
        }

        if (isArray()) {
            return getComponentType().toHuman() + "[]";
        }

        // Remove the "L...;" around the type and convert "/" to ".".
        return getClassName().replace("/", ".");
    }

    /** {@inheritDoc} */
    public Type getType() {
        return this;
    }

    /** {@inheritDoc} */
    public Type getFrameType() {
        switch (basicType) {
            case BT_BOOLEAN:
            case BT_BYTE:
            case BT_CHAR:
            case BT_INT:
            case BT_SHORT: {
                return INT;
            }
        }

        return this;
    }

    /** {@inheritDoc} */
    public int getBasicType() {
        return basicType;
    }

    /** {@inheritDoc} */
    public int getBasicFrameType() {
        switch (basicType) {
            case BT_BOOLEAN:
            case BT_BYTE:
            case BT_CHAR:
            case BT_INT:
            case BT_SHORT: {
                return BT_INT;
            }
        }

        return basicType;
    }

    /** {@inheritDoc} */
    public boolean isConstant() {
        return false;
    }

    /**
     * Gets the descriptor.
     *
     * @return {@code non-null;} the descriptor
     */
    public String getDescriptor() {
        return descriptor;
    }

    /**
     * Gets the name of the class this type corresponds to, in internal
     * form. This method is only valid if this instance is for a
     * normal reference type (that is, a reference type and
     * additionally not a return address).
     *
     * @return {@code non-null;} the internal-form class name
     */
    public String getClassName() {
        if (className == null) {
            if (!isReference()) {
                throw new IllegalArgumentException("not an object type: " +
                                                   descriptor);
            }

            if (descriptor.charAt(0) == '[') {
                className = descriptor;
            } else {
                className = descriptor.substring(1, descriptor.length() - 1);
            }
        }

        return className;
    }

    /**
     * Gets the category. Most instances are category 1. {@code long}
     * and {@code double} are the only category 2 types.
     *
     * @see #isCategory1
     * @see #isCategory2
     * @return the category
     */
    public int getCategory() {
        switch (basicType) {
            case BT_LONG:
            case BT_DOUBLE: {
                return 2;
            }
        }

        return 1;
    }

    /**
     * Returns whether or not this is a category 1 type.
     *
     * @see #getCategory
     * @see #isCategory2
     * @return whether or not this is a category 1 type
     */
    public boolean isCategory1() {
        switch (basicType) {
            case BT_LONG:
            case BT_DOUBLE: {
                return false;
            }
        }

        return true;
    }

    /**
     * Returns whether or not this is a category 2 type.
     *
     * @see #getCategory
     * @see #isCategory1
     * @return whether or not this is a category 2 type
     */
    public boolean isCategory2() {
        switch (basicType) {
            case BT_LONG:
            case BT_DOUBLE: {
                return true;
            }
        }

        return false;
    }

    /**
     * Gets whether this type is "intlike." An intlike type is one which, when
     * placed on a stack or in a local, is automatically converted to an
     * {@code int}.
     *
     * @return whether this type is "intlike"
     */
    public boolean isIntlike() {
        switch (basicType) {
            case BT_BOOLEAN:
            case BT_BYTE:
            case BT_CHAR:
            case BT_INT:
            case BT_SHORT: {
                return true;
            }
        }

        return false;
    }

    /**
     * Gets whether this type is a primitive type. All types are either
     * primitive or reference types.
     *
     * @return whether this type is primitive
     */
    public boolean isPrimitive() {
        switch (basicType) {
            case BT_BOOLEAN:
            case BT_BYTE:
            case BT_CHAR:
            case BT_DOUBLE:
            case BT_FLOAT:
            case BT_INT:
            case BT_LONG:
            case BT_SHORT:
            case BT_VOID: {
                return true;
            }
        }

        return false;
    }

    /**
     * Gets whether this type is a normal reference type. A normal
     * reference type is a reference type that is not a return
     * address. This method is just convenient shorthand for
     * {@code getBasicType() == Type.BT_OBJECT}.
     *
     * @return whether this type is a normal reference type
     */
    public boolean isReference() {
        return (basicType == BT_OBJECT);
    }

    /**
     * Gets whether this type is an array type. If this method returns
     * {@code true}, then it is safe to use {@link #getComponentType}
     * to determine the component type.
     *
     * @return whether this type is an array type
     */
    public boolean isArray() {
        return (descriptor.charAt(0) == '[');
    }

    /**
     * Gets whether this type is an array type or is a known-null, and
     * hence is compatible with array types.
     *
     * @return whether this type is an array type
     */
    public boolean isArrayOrKnownNull() {
        return isArray() || equals(KNOWN_NULL);
    }

    /**
     * Gets whether this type represents an uninitialized instance. An
     * uninitialized instance is what one gets back from the {@code new}
     * opcode, and remains uninitialized until a valid constructor is
     * invoked on it.
     *
     * @return whether this type is "uninitialized"
     */
    public boolean isUninitialized() {
        return (newAt >= 0);
    }

    /**
     * Gets the bytecode index at which this uninitialized type was
     * allocated.  This returns {@code Integer.MAX_VALUE} if this
     * type is an uninitialized incoming parameter (i.e., the
     * {@code this} of an {@code <init>} method) or
     * {@code -1} if this type is in fact <i>initialized</i>.
     *
     * @return {@code >= -1;} the allocation bytecode index
     */
    public int getNewAt() {
        return newAt;
    }

    /**
     * Gets the initialized type corresponding to this instance, but only
     * if this instance is in fact an uninitialized object type.
     *
     * @return {@code non-null;} the initialized type
     */
    public Type getInitializedType() {
        if (initializedType == null) {
            throw new IllegalArgumentException("initialized type: " +
                                               descriptor);
        }

        return initializedType;
    }

    /**
     * Gets the type corresponding to an array of this type.
     *
     * @return {@code non-null;} the array type
     */
    public Type getArrayType() {
        if (arrayType == null) {
            arrayType = putIntern(new Type('[' + descriptor, BT_OBJECT));
        }

        return arrayType;
    }

    /**
     * Gets the component type of this type. This method is only valid on
     * array types.
     *
     * @return {@code non-null;} the component type
     */
    public Type getComponentType() {
        if (componentType == null) {
            if (descriptor.charAt(0) != '[') {
                throw new IllegalArgumentException("not an array type: " +
                                                   descriptor);
            }
            componentType = intern(descriptor.substring(1));
        }

        return componentType;
    }

    /**
     * Returns a new interned instance which is identical to this one, except
     * it is indicated as uninitialized and allocated at the given bytecode
     * index. This instance must be an initialized object type.
     *
     * @param newAt {@code >= 0;} the allocation bytecode index
     * @return {@code non-null;} an appropriately-constructed instance
     */
    public Type asUninitialized(int newAt) {
        if (newAt < 0) {
            throw new IllegalArgumentException("newAt < 0");
        }

        if (!isReference()) {
            throw new IllegalArgumentException("not a reference type: " +
                                               descriptor);
        }

        if (isUninitialized()) {
            /*
             * Dealing with uninitialized types as a starting point is
             * a pain, and it's not clear that it'd ever be used, so
             * just disallow it.
             */
            throw new IllegalArgumentException("already uninitialized: " +
                                               descriptor);
        }

        /*
         * Create a new descriptor that is unique and shouldn't conflict
         * with "normal" type descriptors
         */
        String newDesc = 'N' + Hex.u2(newAt) + descriptor;
        Type result = new Type(newDesc, BT_OBJECT, newAt);
        result.initializedType = this;
        return putIntern(result);
    }

    /**
     * Puts the given instance in the intern table if it's not already
     * there. If a conflicting value is already in the table, then leave it.
     * Return the interned value.
     *
     * @param type {@code non-null;} instance to make interned
     * @return {@code non-null;} the actual interned object
     */
    private static Type putIntern(Type type) {
        synchronized (internTable) {
            String descriptor = type.getDescriptor();
            Type already = internTable.get(descriptor);
            if (already != null) {
                return already;
            }
            internTable.put(descriptor, type);
            return type;
        }
    }
}
