/*
* JBoss, Home of Professional Open Source
* Copyright 2008-10 Red Hat and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
* @authors Andrew Dinn
*/
package org.jboss.byteman.rule.type;

import org.jboss.byteman.rule.exception.TypeException;
import org.objectweb.asm.Opcodes;

import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;

/**
 * models the type of a rule binding or expression
 */
public class Type {
    /**
     * create a type with a given name and optionally an associated class
     *
     * @param typeName the name of the type which may or may not be fully qualified
     * @param clazz the class associated with this name if it is know otherwise null
     */
    public Type(String typeName, Class clazz)
    {
        this(typeName, clazz, F_OBJECT, 4, null);
    }

    /**
     * create a type with a given name and no associated class
     *
     * @param typeName the name of the type which may or may not be fully qualified
     */
    public Type(String typeName)
    {
        this(typeName, null);
    }

    /**
     * create an array type from this base type
     * @return an array type
     */
    public Type arrayType()
    {
        return arrayType(null);
    }
    
    /**
     * create an array type from this base type
     * @param clazz the class for the array type derived from the class of base type or
     * null if the base type is undefined
     * @return an array type
     */
    public Type arrayType(Class clazz)
    {
        if (this.arrayType ==  null) {
            arrayType = new Type(typeName + "[]", clazz, F_ARRAY, 4, this);
        }
        return arrayType;
    }

    /**
     * retrieve the base type for an array type or null if this is not an array type
     * @return an array type
     */                                    
    public Type getBaseType()
    {
        if (isArray()) {
            return baseType;
        } else {
            return null;
        }
    }

    /**
     * get the possibly unqualified name with which this type was created
     * @return the type name
     */
    public String getName()
    {
        return typeName;
    }

    /**
     * get the internal name for this type used by the class loader. this is only valid for
     * defined types, defined array types or primitive types
     * @return the type name
     */
    public String getInternalName()
    {
        return getInternalName(false, true);
    }

    /**
     * get the internal name for this type used by the class
     * loader. this is only valid for defined types, defined array
     * types or primitive types
     * @param forDescriptor true if we need the name to appear in a
     * decriptor false if not
     * @param slashSeparate true if the package separator should be
     * slash false if it should be dot
     * @return the type name
     */
    public String getInternalName(boolean forDescriptor, boolean slashSeparate)
    {
        if (isArray()) {
            // the base name has to be L...; bracketed
            return "[" + baseType.getInternalName(true, slashSeparate);
        } else if (isPrimitive()) {
            return internalNames.get(typeName);
        } else if (isVoid()) {
            return internalNames.get(typeName);
        } else {
            Class targetClass = aliasFor.getTargetClass();
            Class enclosingClass = targetClass.getEnclosingClass();
            String name;
            if (enclosingClass != null) {
                // retain the $ separator for inner classes and local/anon classes
                name = targetClass.getName();
            } else {
                name = targetClass.getCanonicalName();
                if (name == null) {
                    // local or anonymous class
                    name = targetClass.getName();
                }
            }
            if (slashSeparate) {
                name = name.replace('.', '/');
            }
            if (forDescriptor) {
                name = "L" + name + ";";
            }
            return name;
        }
    }

    /**
     * get the class associated with this type if it has one or a special undefined class if
     * the type is not defined or null if there is no associated class
     * @return the associated class
     */
    public Class getTargetClass()
    {
        return clazz;
    }

    /**
     * get the package component of the name associated with this type or the empty String
     * if it has no package or is was defiend with an unqualified name or is a builtin type
     * @return the package component or an empty string
     */

    public String getPackageName()
    {
            return packageName;
    }

    /**
     * dereference an object type to the fully qualified named type to which it is aliased where
     * such an alias has been found to exist or return the supplied type if no alias exists or the
     * type is a non-objecttype or was originally specified using a fully qualified type name.
     *
     * @param target the type to be dereferenced
     * @return the alias where it exists or the supplied type where there is no alias or null
     * if a null value is supplied
     */
    public static Type dereference(Type target)
    {
        if (target == null) {
            return null;
        }

        while (target.aliasFor != target) {
            target = target.aliasFor;
        }

        return target;
    }

    public void resolve(ClassLoader loader)
    {
        if (this.isDefined() || this == Type.UNDEFINED || this == Type.N) {
            return;
        }
        if (aliasFor != this) {
            aliasFor.resolve(loader);
            clazz = aliasFor.clazz;
            if (clazz != null) {
                flags &= ~F_UNKNOWN;
            }
        } else {
            try {
                clazz = loader.loadClass(getName());
                flags &= ~F_UNKNOWN;
            } catch (ClassNotFoundException e) {
                // ok give up here -- we should get a type error later
            }
        }
        // ensure any classes which might have been created via this one are also resolved
        if (baseType != null) {
            baseType.resolve(loader);
        }
        if (arrayType != null) {
            arrayType.resolve(loader);
        }
    }

    /**
     * attempt to establish an alias from an package unqualified named object type to a package
     * qualified named object type whose unqualified name equals this type's name
     * @param target the package qualified named type for which this type should become an alias
     * @return true if the alias link can be established or already exsits or false if an alias
     * to a different target already exists or this type or the target are not object types
     */
    public boolean aliasTo(Type target)
    {
        // we can only legitimately alias an object type without a package to a type with a package
        // ???caller should already have checked this???
        if (!isObject() || !target.isObject()) {
            return false;
        }
        // ???caller should already have checked this???
        if (packageName.length() != 0 || target.packageName.length() == 0) {
            return false;
        }
        // if there is already an alias for this type then it has to be the same as the supplied type

        if (aliasFor != this) {
            return (aliasFor == target);
        } else {
            // we assume the caller has already checked that the names match so . . .
            // update the alias
            aliasFor = target;
            // propagate type-class binding up or down alias link if available
            if (clazz != null) {
                if (target.clazz != null) {
                    // oops class mismatch!
                    return false;
                } else {
                    target.clazz = clazz;
                    target.flags &= ~F_UNKNOWN;
                }
            } else if (target.clazz != null) {
                clazz = target.clazz;
                flags &= ~F_UNKNOWN;
            }
            if (arrayType != null) {
                // ensure array types are also aliased
                if (target.arrayType == null) {
                    target.arrayType(target.clazz);
                }
                arrayType.aliasTo(target.arrayType);
            } else if (target.arrayType != null) {
                // point the array type at the target array type
                arrayType = arrayType(this.clazz);
                arrayType.aliasTo(target.arrayType);
            }
            return true;
        }
    }

    /**
     * check whether this type can be assigned with values of the supplied type including
     * the case where numeric conversion from known or unknown numeric types but excluding
     * any other cases where this type is undefined
     *
     * n.b. the caller must dereference the recipient and argument types before calling
     * this method
     * @param type the type poviding RHS values
     * @return true if it is known that the assignment is valid, false if it is not known to be valid or
     * is known not to be valid
     */

    public boolean isAssignableFrom(Type type)
    {
        if (this.aliasFor != this) {
            return Type.dereference(this).isAssignableFrom(type);
        }
        type = Type.dereference(type);
        
        // check for unknown cases first
        if (isNumeric()) {
            // can always coerce numerics even if it involves boxing
            return type.isNumeric();
        } else if (isUndefined() || type.isUndefined()) {
            // cannot answer this question yet
            return false;
        } else if (this == type) {
            return true;
        } else if (isString()) {
            // can always convert anything to a string via boxing and Object.toString();
            return true;
        } else if (isVoid()) {
            // can always assign to void
            return true;
        } else if (isPrimitive()) {
           if (!type.isPrimitive()) {
               // see if we can arrive at the correct type by unboxing
               Type unboxedType = boxedTypes.get(type);
               return (unboxedType == this);
           } else {
               return false;
           }
        } else if (type.isPrimitive()) {
            // see if we can arrive at the correct type by boxing
            Type boxedType = boxedTypes.get(type);
            if (boxedType == this) {
                return true;
            } else {
                // we only get here if we have a known type i.e. both clazz values are non-null
                // see if the supplied type is assignable from this type's class
                return (this.clazz.isAssignableFrom(boxedType.clazz));
            }
        } else if (isObject() || isArray()) {
            // we only get here if we have a known type i.e. both clazz values are non-null
            // see if the supplied type is assignable from this type's class
            return (this.clazz.isAssignableFrom(type.clazz));
        } else {
            return false;
        }
    }

    /**
     * test if this type is an unknown type. a type may be unknown either because it is one
     * of the pseudo types used as type variables or because it represents an object type
     * mentioned in a rule but not yet bound to a specific class
     * @return true if the type is unknown otherwise false
     */
    public boolean isUndefined()
    {
        return ((flags & F_UNKNOWN) != 0);
    }

    /**
     * check if this type is a known type. this is just teh oppositeof isUndefined
     * @return false if the type is unknown otherwise true
     */
    public boolean isDefined()
    {
        return !isUndefined();
    }

    /**
     * return true if this is a type mentioned in a rule but not yet bound to a specific class
     * @return true if the type is not yet bound to a specific class
     */
    public boolean isUnbound()
    {
        // this only happens where we have an object type marked with the UNKNOWN marker

        return (flags & (F_OBJECT | F_UNKNOWN)) == (F_OBJECT | F_UNKNOWN);
    }

    /**
     * return true if this is a primitive value type
     * @return true if this is a primitive value type
     */
    public boolean isPrimitive()
    {
        return (flags & F_PRIMITIVE) != 0;
    }

    /**
     * return true if this is a value type, which includes the boxed versions of primitive types
     * @return true if this is a value type
     */
    public boolean isValue()
    {
        return (flags & F_VALUE) != 0;
    }

    /**
     * return true if this is the void type
     * @return true if this is void type
     */
    public boolean isVoid()
    {
        return (flags & F_VOID) != 0;
    }

    /**
     * return true if this is the string type
     * @return true if this is string type
     */
    public boolean isString()
    {
        return (flags & F_STRING) != 0;
    }

    /**
     * return true if this is a numeric type, including the unknown primitive numeric type
     * @return true if this is a numeric type
     */
    public boolean isNumeric()
    {
        return (flags & F_NUMERIC) != 0;
    }

    /**
     * return true if this is an integral type of whatever size, including the unknown
     * primitive numeric type
     * @return true if this is an integral type
     */
    public boolean isIntegral()
    {
        return (flags & F_INTEGRAL) == F_INTEGRAL;
    }

    /**
     * return true if this is a floating type of whatever size, including the unknown
     * primitive numeric type
     * @return true if this is a floating type
     */
    public boolean isFloating()
    {
        return (flags & F_FLOATING) == F_FLOATING;
    }

    /**
     * return true if this is a boolean type
     * @return true if this is a boolean type
     */
    public boolean isBoolean()
    {
        return (flags & F_BOOLEAN) != 0;
    }

    /**
     * return true if this is an object type, including unbound types mentioned in rules
     * @return true if this is an object type
     */
    public boolean isObject()
    {
        return (flags & F_OBJECT) != 0;
    }

    /**
     * return true if this is an array type
     * @return true if this is an array type
     */
    public boolean isArray()
    {
        return (flags & F_ARRAY) != 0;
    }

    /**
     * return the number of stack words occupied by instances of this type
     * @return true if this is an array type
     */
    public int getNBytes()
    {
        return nBytes;
    }

    /**
     * return the builtin type associated with a given class
     * @param clazz the class for the builtin type
     * @return the corresponding builtin type
     */
    public static Type builtinType(Class clazz)
    {
        return builtinTypes.get(clazz.getName());
    }

    /**
     * return the primitive type whose boxed equivalent is associated with a given class
     * @param clazz the class for the primitivebuiltin type
     * @return the corresponding primitive type
     */
    public static Type boxType(Class clazz)
    {
        Type type = builtinType(clazz);

        return boxedTypes.get(type);
    }

    /**
     * return the primitive type for a boxed type or vice versa
     * @param type the boxed type
     * @return the corresponding primitive type
     */
    public static Type boxType(Type type)
    {
        return boxedTypes.get(type);
    }

    private String typeName;
    private Class clazz;
    private String packageName;
    private int flags;
    private int nBytes;
    private Type aliasFor;
    private Type baseType;
    private Type arrayType;

    protected Type(String typeName, Class clazz, int flags, int nBytes)
    {
        this(typeName, clazz, flags, nBytes, null);
    }

    protected Type(String typeName, Class clazz, int flags, int nBytes, Type baseType)
    {
        this.typeName = typeName;

        if (clazz == null) {
            flags |= F_UNKNOWN;
        }

        this.clazz = clazz;

        this.flags = flags;

        this.nBytes = nBytes;

        if ((flags & F_ARRAY) != 0) {
            this.baseType = baseType;
            baseType.arrayType = this;
        } else {
            this.baseType = null;
        }
        this.arrayType = null;

        // types dereference to themselves until they are aliased

        aliasFor = this;

        packageName = packagePart(typeName);
    }

    /**
     * compute the type to which a binary arithmetic operator should promote its operands
     * before combination based on the two operand types which is also the type to be
     * used for the result of the operation
     * @param type1 the type of the left operand  which must be numeric but may be undefined
     * @param type2 the type of the right operand which must be numeric but may be undefined
     * @return the corresponding promotion/result type which may be undefined numeric
     * @throws TypeException if types are undefined or promotion is invalid
     */
    public static Type promote(Type type1, Type type2) throws TypeException {
        if (type1.isUndefined() || type2.isUndefined()) {
                // don't know for sure which is which so return undefined numeric
                return N;
        } else if (!type1.isNumeric() || !type2.isNumeric()) {
                // should not happen!
            throw new TypeException("Type.promote : unexpected non-numeric type argument");
        } else if (type1.isFloating() || type2.isFloating()) {
            if (type1 == DOUBLE || type2 == DOUBLE || type1 == D || type2 == D) {
                return D;
            } else {
                return F;
            }
        } else {
            // integral types -- ok lets invent^H^H^H declare some rules here :-)
            // either arg long forces a long result
            // either arg integer forces an int result
            // a matched pair of short, char or byte arguments retains the same type in the result
            // otherwise the result is an int and args will be coerced to int
            if (type1 == LONG || type2 == LONG || type1 == J || type2 == J) {
                return J;
            } else if (type1 == INTEGER || type2 == INTEGER || type1 == I || type2 == I) {
                return I;
            } else if ((type1 == SHORT || type1 == S) && (type2 == SHORT || type2 == S)) {
                return S;
            } else if ((type1 == CHARACTER || type1 == C) && (type2 == CHARACTER || type2 == C)) {
                return C;
            } else if ((type1 == BYTE || type1 == B) && (type2 == BYTE || type2 == B)) {
                return B;
            } else {
                return I;
            }
        }
    }
    /* TODO we don't seem to need this?
    private static String classPart(String className)
    {
        int dotIdx = className.lastIndexOf('.');

        if (dotIdx < 0) {
            return className;
        } else {
            return className.substring(dotIdx);
        }
    }
    */
    private static String packagePart(String className)
    {
        int dotIdx = className.lastIndexOf('.');

        if (dotIdx < 0) {
            return "";
        } else {
            return className.substring(0, dotIdx);
        }
    }

    public static List<String> parseMethodDescriptor(String descriptor, boolean includeReturnType)
    {
        List<String> argTypes = new ArrayList<String>();
        int length = descriptor.length();
        int idx = descriptor.indexOf("(");
        int arrayDepth = 0;
        if (idx < 0) {
            return null;
        }
        idx = idx + 1;
        while (idx < length) {
            char c = descriptor.charAt(idx);
            switch(descriptor.charAt(idx))
            {
                case 'Z':
                {
                    String baseType = "boolean";
                    argTypes.add(fixArrayType(baseType, arrayDepth));
                    arrayDepth = 0;
                    idx++;
                }
                break;
                case 'B':
                {
                    String baseType = "byte";
                    argTypes.add(fixArrayType(baseType, arrayDepth));
                    arrayDepth = 0;
                    idx++;
                }
                break;
                case 'S':
                {
                    String baseType = "short";
                    argTypes.add(fixArrayType(baseType, arrayDepth));
                    arrayDepth = 0;
                    idx++;
                }
                break;
                case 'C':
                {
                    String baseType = "char";
                    argTypes.add(fixArrayType(baseType, arrayDepth));
                    arrayDepth = 0;
                    idx++;
                }
                break;
                case 'I':
                {
                    String baseType = "int";
                    argTypes.add(fixArrayType(baseType, arrayDepth));
                    arrayDepth = 0;
                    idx++;
                }
                break;
                case 'J':
                {
                    String baseType = "long";
                    argTypes.add(fixArrayType(baseType, arrayDepth));
                    arrayDepth = 0;
                    idx++;
                }
                break;
                case 'F':
                {
                    String baseType = "float";
                    argTypes.add(fixArrayType(baseType, arrayDepth));
                    arrayDepth = 0;
                    idx++;
                }
                break;
                case 'D':
                {
                    String baseType = "double";
                    argTypes.add(fixArrayType(baseType, arrayDepth));
                    arrayDepth = 0;
                    idx++;
                }
                break;
                case 'V':
                {
                    if (arrayDepth != 0) {
                        // hmm void arrays are definitely not kosher
                        return null;
                    } else if (!includeReturnType) {
                        // hmm should not have got here
                        return null;
                    }
                    argTypes.add("void");
                    idx++;
                }
                break;
                case 'L':
                {
                    int endIdx = descriptor.indexOf(';', idx);
                    if (endIdx < 0) {
                        return null;
                    }
                    String baseType = descriptor.substring(idx+1, endIdx).replace('/', '.');
                    argTypes.add(fixArrayType(baseType, arrayDepth));
                    arrayDepth = 0;
                    idx = endIdx + 1;
                }
                break;
                case '[':
                {
                    arrayDepth++;
                    idx++;
                }
                break;
                case ')':
                {
                    if (arrayDepth != 0) {
                        return null;
                    } else if (!includeReturnType) {
                        // stop here
                        return argTypes;
                    } else {
                        // skip any trailing spaces before the return type
                        idx++;
                        while (idx < length && descriptor.charAt(idx) == ' ')
                        {
                            idx++;
                        }
                        if (idx == length) {
                            // ok, we need to add a void return type
                            argTypes.add("void");
                        }
                    }
                }
                break;
                default:
                    return null;
            }
        }

        return (arrayDepth == 0 ? argTypes : null);
    }

    public static String parseFieldDescriptor(String descriptor)
    {
        int length = descriptor.length();
        int idx = 0;
        int arrayDepth = 0;
        while (idx < length) {
            char c = descriptor.charAt(idx);
            switch(descriptor.charAt(idx))
            {
                case 'Z':
                {
                    String baseType = "boolean";
                    return(fixArrayType(baseType, arrayDepth));
                }
                case 'B':
                {
                    String baseType = "byte";
                    return(fixArrayType(baseType, arrayDepth));
                }
                case 'S':
                {
                    String baseType = "short";
                    return(fixArrayType(baseType, arrayDepth));
                }
                case 'C':
                {
                    String baseType = "char";
                    return(fixArrayType(baseType, arrayDepth));
                }
                case 'I':
                {
                    String baseType = "int";
                    return(fixArrayType(baseType, arrayDepth));
                }
                case 'J':
                {
                    String baseType = "long";
                    return(fixArrayType(baseType, arrayDepth));
                }
                case 'F':
                {
                    String baseType = "float";
                    return(fixArrayType(baseType, arrayDepth));
                }
                case 'D':
                {
                    String baseType = "double";
                    return(fixArrayType(baseType, arrayDepth));
                }
                case 'V':
                {
                    String baseType = "void";
                    return(fixArrayType(baseType, arrayDepth));
                }
                case 'L':
                {
                    int endIdx = descriptor.indexOf(';', idx);
                    if (endIdx < 0) {
                        return null;
                    }
                    String baseType = descriptor.substring(idx+1, endIdx).replace('/', '.');
                    return(fixArrayType(baseType, arrayDepth));
                }
                case '[':
                {
                    arrayDepth++;
                    idx++;
                }
                break;
                default:
                    return null;
            }
        }
        return null;
    }

    public static String parseMethodReturnType(String descriptor)
    {
        int length = descriptor.length();
        int idx = descriptor.indexOf(")");
        int arrayDepth = 0;
        
        if (idx < 0) {
            return "void";
        }
        idx = idx + 1;
        while (idx < length) {
            char c = descriptor.charAt(idx);
            switch(c)
            {
                case 'Z':
                {
                    String baseType = "boolean";
                    return fixArrayType(baseType, arrayDepth);
                }
                case 'B':
                {
                    String baseType = "byte";
                    return fixArrayType(baseType, arrayDepth);
                }
                case 'S':
                {
                    String baseType = "short";
                    return fixArrayType(baseType, arrayDepth);
                }
                case 'C':
                {
                    String baseType = "char";
                    return fixArrayType(baseType, arrayDepth);
                }
                case 'I':
                {
                    String baseType = "int";
                    return fixArrayType(baseType, arrayDepth);
                }
                case 'J':
                {
                    String baseType = "long";
                    return fixArrayType(baseType, arrayDepth);
                }
                case 'F':
                {
                    String baseType = "float";
                    return fixArrayType(baseType, arrayDepth);
                }
                case 'D':
                {
                    String baseType = "double";
                    return fixArrayType(baseType, arrayDepth);
                }
                case 'V':
                {
                    return "void";
                }
                case 'L':
                {
                    int endIdx = descriptor.indexOf(';', idx);
                    if (endIdx < 0) {
                        return "void";
                    }
                    String baseType = descriptor.substring(idx+1, endIdx).replace('/', '.');
                    return fixArrayType(baseType, arrayDepth);
                }
                case '[':
                {
                    arrayDepth++;
                    idx++;
                }
                break;
                default:
                    return "void";
            }
        }
        return "void";
    }

    /**
     * identify the local var slot used to store a method parameter identified by parameter index
     * @param access the access flags for the method including whether or not it is static
     * @param desc the intrenal form descriptor for the maethod
     * @param paramIdx the index of the parameter in the parameter lost starting with 0 for this or 1 for
     * actual parameters
     * @return the corresponding local var slot or -1 if there is no such parameter
     */
    public static int paramSlotIdx(int access, String desc, int paramIdx)
    {
        boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;
        if (paramIdx ==  0) {
            if (isStatic) {
                return -1;
            } else {
                return 0;
            }
        } else {
            int slotIdx =  (isStatic ? 0 : 1);
            int descIdx = 0;
            char[] chars = desc.toCharArray();
            char next = chars[descIdx++];
            // skip leading '('
            if (next == '(') {
                next = chars[descIdx++];
            }
            //  skip  all slots preceding the desired one
            for (int i = 1;  i < paramIdx;  i++) {
                switch (next) {
                    case 'Z':
                    case 'B':
                    case 'S':
                    case 'C':
                    case 'I':
                    case 'F':
                    {
                        slotIdx++;
                    }
                    break;
                    case 'J':
                    case 'D':
                    {
                        slotIdx += 2;
                    }
                    break;
                    case 'L':
                    {
                        slotIdx++;
                        // skip forward in descriptor up to ';'

                        while (next != ';') {
                            next = chars[descIdx++];
                        }
                    }
                    break;
                    case '[':
                    {
                        slotIdx++;
                        // skip any extra '[' chars to get to the base array type
                        while (next == '[') {
                            next = chars[descIdx++];
                        }
                        if (next == 'L') {
                            // skip forward in descriptor up to ';'

                            while (next != ';') {
                                next = chars[descIdx++];
                            }
                        }
                    }
                    break;
                    case ')':
                    default:
                    {
                        // invalid param index or invalid descriptor --either way this is a problem
                        return -1;
                    }
                }
                next = chars[descIdx++];
            }
            return slotIdx;
        }
    }

    public static String fixArrayType(String baseType, int dimension)
    {
        String result = baseType;

        for (int i = 0; i < dimension; i++) {
            result  += "[]";
        }
        
        return result;
    }

    public static String internalName(Class<?> clazz)
    {
        return internalName(clazz, false);
    }

    public static String internalName(Class<?> clazz, boolean forField)
    {
        if (clazz.isPrimitive()) {
            if (forField) {
                return internalNames.get(clazz.getName());
            } else {
                return clazz.getName();
            }
        } else if (clazz.isArray()) {
            Class base = clazz.getComponentType();
            return "[" + internalName(base, true);
        } else if (forField) {
            return "L" + clazz.getName().replace('.', '/') + ";";
        } else {
            return clazz.getName().replace('.', '/');
        }
    }

    // private class used to type unknown types
    private static class Undefined {
    }
    // tags divide types into exclusive categories
    // unknown types may be associated with a tag group such as numeric, or object
    // markers are used to identify type properties such as unknown, primitive

    // value type tags
    final public static int F_BOOLEAN       = 0x0001;
    final public static int F_INTEGRAL      = 0x0002;
    final public static int F_FLOATING      = 0x0004;
    // object type tag
    final public static int F_OBJECT        = 0x0008;
    // void type tag
    final public static int F_VOID          = 0x0010;
    // array type tag
    final public static int F_ARRAY         = 0x0020;

    // value type tag groups
    final public static int F_NUMERIC       = F_INTEGRAL | F_FLOATING;
    final public static int F_VALUE         = F_BOOLEAN | F_NUMERIC;

    // unknown type marker
    final public static int F_UNKNOWN       = 0x1000;
    // primitive type marker
    final public static int F_PRIMITIVE     = 0x2000;                
    // string type marker
    final public static int F_STRING        = 0x4000;

    // we need to cope with array types

    final public static Type Z = new Type("boolean", boolean.class, F_BOOLEAN|F_PRIMITIVE, 4);
    final public static Type B = new Type("byte", byte.class, F_INTEGRAL|F_PRIMITIVE, 1);
    final public static Type S = new Type("short", short.class, F_INTEGRAL|F_PRIMITIVE, 2);
    final public static Type C = new Type("char", char.class, F_INTEGRAL|F_PRIMITIVE, 2);
    final public static Type I = new Type("int", int.class, F_INTEGRAL|F_PRIMITIVE, 4);
    final public static Type J = new Type("long", long.class, F_INTEGRAL|F_PRIMITIVE, 8);
    final public static Type F = new Type("float", float.class, F_FLOATING|F_PRIMITIVE, 4);
    final public static Type D = new Type("double", double.class, F_FLOATING|F_PRIMITIVE, 8);
    // pseudo type representing an undefined numeric primitive type
    final public static Type N = new Type("", null, F_UNKNOWN|F_NUMERIC|F_PRIMITIVE, 0);

    final public static Type BOOLEAN = new Type("java.lang.Boolean", Boolean.class, F_OBJECT|F_BOOLEAN, 4);
    final public static Type BYTE = new Type("java.lang.Byte", Byte.class, F_OBJECT|F_INTEGRAL, 4);
    final public static Type SHORT = new Type("java.lang.Short", Short.class, F_OBJECT|F_INTEGRAL, 4);
    final public static Type CHARACTER = new Type("java.lang.Character", Character.class, F_OBJECT|F_INTEGRAL, 4);
    final public static Type INTEGER = new Type("java.lang.Integer", Integer.class, F_OBJECT|F_INTEGRAL, 4);
    final public static Type LONG = new Type("java.lang.Long", Long.class, F_OBJECT|F_INTEGRAL, 4);
    final public static Type FLOAT = new Type("java.lang.Float", Float.class, F_OBJECT|F_FLOATING, 4);
    final public static Type DOUBLE = new Type("java.lang.Double", Double.class, F_OBJECT|F_FLOATING, 4);
    final public static Type STRING = new Type("java.lang.String", String.class, F_OBJECT|F_STRING, 4);
    final public static Type VOID = new Type("void", void.class, F_VOID, 0);
    final public static Type NUMBER = new Type("java.lang.Number", Number.class, F_OBJECT|F_NUMERIC, 0);
    final public static Type OBJECT = new Type("java.lang.Object", Object.class, F_OBJECT, 0);
    // pseudo type representing an undefined primitive or object type
    final public static Type UNDEFINED = new Type("", Undefined.class, F_UNKNOWN, 0);

    final private static HashMap<String, Type> builtinTypes;
    final private static HashMap<String, Type> primitiveTypes;
    final private static HashMap<Type, Type> boxedTypes;
    final private static HashMap<String, String> internalNames;

    static {
        builtinTypes = new HashMap<String, Type>();
        // primitive type names
        builtinTypes.put(Z.getName(), Z);
        builtinTypes.put(B.getName(), B);
        builtinTypes.put(S.getName(), S);
        builtinTypes.put(C.getName(), C);
        builtinTypes.put(I.getName(), I);
        builtinTypes.put(J.getName(), J);
        builtinTypes.put(F.getName(), F);
        builtinTypes.put(D.getName(), D);
        builtinTypes.put("$number$", N);
        // canonical names
        builtinTypes.put(BOOLEAN.getTargetClass().getName(), BOOLEAN);
        builtinTypes.put(BYTE.getTargetClass().getName(), BYTE);
        builtinTypes.put(SHORT.getTargetClass().getName(), SHORT);
        builtinTypes.put(CHARACTER.getTargetClass().getName(), CHARACTER);
        builtinTypes.put(INTEGER.getTargetClass().getName(), INTEGER);
        builtinTypes.put(LONG.getTargetClass().getName(), LONG);
        builtinTypes.put(FLOAT.getTargetClass().getName(), FLOAT);
        builtinTypes.put(DOUBLE.getTargetClass().getName(), DOUBLE);
        builtinTypes.put(STRING.getTargetClass().getName(), STRING);
        builtinTypes.put(VOID.getTargetClass().getName(), VOID);
        builtinTypes.put(NUMBER.getTargetClass().getName(), NUMBER);
        builtinTypes.put(UNDEFINED.getTargetClass().getName(), UNDEFINED);
        builtinTypes.put(OBJECT.getTargetClass().getName(), OBJECT);
        // nicknames
        builtinTypes.put("Boolean", BOOLEAN);
        builtinTypes.put("Byte", BYTE);
        builtinTypes.put("Short", SHORT);
        builtinTypes.put("Character", CHARACTER);
        builtinTypes.put("Integer", INTEGER);
        builtinTypes.put("Long", LONG);
        builtinTypes.put("Float", FLOAT);
        builtinTypes.put("Double", DOUBLE);
        builtinTypes.put("String", STRING);
        builtinTypes.put("Number", NUMBER);
        builtinTypes.put("Object", OBJECT);
        builtinTypes.put("", UNDEFINED);
        // allow undefined to be spelled out
        builtinTypes.put("Undefined", UNDEFINED);

        primitiveTypes = new HashMap<String, Type>();
        primitiveTypes.put(Z.getName(), Z);
        primitiveTypes.put(B.getName(), B);
        primitiveTypes.put(S.getName(), S);
        primitiveTypes.put(C.getName(), C);
        primitiveTypes.put(I.getName(), I);
        primitiveTypes.put(J.getName(), J);
        primitiveTypes.put(F.getName(), F);
        primitiveTypes.put(D.getName(), D);
        primitiveTypes.put("$number$", N);

        // allow for boxing
        boxedTypes = new HashMap<Type, Type>();
        boxedTypes.put(Z, BOOLEAN);
        boxedTypes.put(B, BYTE);
        boxedTypes.put(S, SHORT);
        boxedTypes.put(C, CHARACTER);
        boxedTypes.put(I, INTEGER);
        boxedTypes.put(J, LONG);
        boxedTypes.put(F, FLOAT);
        boxedTypes.put(D, DOUBLE);
        // also allow for unboxing
        boxedTypes.put(BOOLEAN, Z);
        boxedTypes.put(BYTE, B);
        boxedTypes.put(SHORT, S);
        boxedTypes.put(CHARACTER, C);
        boxedTypes.put(INTEGER, I);
        boxedTypes.put(LONG, J);
        boxedTypes.put(FLOAT, F);
        boxedTypes.put(DOUBLE, D);

        internalNames = new HashMap<String, String>();
        // add translations from primitive names to internal tag
        internalNames.put("Z", "boolean");
        internalNames.put("B", "byte");
        internalNames.put("S", "short");
        internalNames.put("C", "char");
        internalNames.put("I", "int");
        internalNames.put("J", "long");
        internalNames.put("F", "float");
        internalNames.put("D", "double");
        internalNames.put("V", "void");
        // also add reverse translations
        internalNames.put("boolean", "Z");
        internalNames.put("byte", "B");
        internalNames.put("short", "S");
        internalNames.put("char", "C");
        internalNames.put("int", "I");
        internalNames.put("long", "J");
        internalNames.put("float", "F");
        internalNames.put("double", "D");
        internalNames.put("void", "V");
    }
    public String toString()
    {
        return getName();
    }
}
