package com.thoughtworks.qdox.model;

import java.io.Serializable;

import com.thoughtworks.qdox.JavaClassContext;
import com.thoughtworks.qdox.parser.structs.TypeDef;
import com.thoughtworks.qdox.parser.structs.WildcardTypeDef;

public class Type implements Comparable, Serializable {

    public static final Type[] EMPTY_ARRAY = new Type[0];
    public static final Type VOID = new Type("void");

    private String name;
    private JavaClassParent context;
    private String fullName;
    private int dimensions;
    private Type[] actualArgumentTypes;

    public Type(String fullName, String name, int dimensions, JavaClassParent context) {
        this.fullName = fullName;
        this.name = name;
        this.dimensions = dimensions;
        this.context = context;
    }
    
    public Type(String fullName, TypeDef typeDef, int dimensions, JavaClassParent context) {
    	this.fullName = fullName;
        this.name = typeDef.name;
        this.dimensions = typeDef.dimensions + dimensions; //in some cases dimensions can be spread. Collect them here
        if(typeDef.actualArgumentTypes != null && !typeDef.actualArgumentTypes.isEmpty()) {
        	actualArgumentTypes = new Type[typeDef.actualArgumentTypes.size()];
        	for(int index = 0; index < typeDef.actualArgumentTypes.size(); index++) {
        		actualArgumentTypes[index] = createUnresolved((TypeDef) typeDef.actualArgumentTypes.get(index), context);
        	}
        }
        this.context = context;
	}


    public Type(String fullName, int dimensions, JavaClassParent context) {
        this(fullName, (String) null, dimensions, context);
    }

    public Type(String fullName, int dimensions) {
        this(fullName, dimensions, null);
    }

    public Type(String fullName) {
        this(fullName, 0);
    }
    
	public static Type createUnresolved(String name, int dimensions, JavaClassParent context) {
        return new Type(null, name, dimensions, context);
    }
    
	public static Type createUnresolved(TypeDef typeDef, int dimensions, JavaClassParent context) {
        return new Type(null, typeDef, dimensions, context);
	}
	
	public static Type createUnresolved(TypeDef typeDef, JavaClassParent context) {
		if(typeDef instanceof WildcardTypeDef) {
			return new WildcardType((WildcardTypeDef) typeDef, context);
		}
        return new Type(null, typeDef, 0, context);
	}

    
    public JavaClassParent getJavaClassParent() {
        return context;
    }

    /**
     * 
     * @deprecated instead use getFullyQualifiedName()
     */
    public String getFullQualifiedName() {
        return getFullyQualifiedName();
    }

    /**
     * Returns the FQN of an Object or the handler of a Type
     * If the name of the can't be resolved based on the imports and the classes on the classpath the name will be returned
     * InnerClasses will use the $ sign
     * 
     * Some examples how names will be translated 
     * <pre>
     * Object > java.lang.Object
     * java.util.List > java.util.List
     * ?  > ?
     * T  > T
     * anypackage.Outer.Inner > anypackage.Outer$Inner
     * </pre>
     * 
     * @return
     */
    public String getFullyQualifiedName() {
        
        return isResolved() ? fullName : name;
    }

    /**
     * The FQN representation of an Object for code usage
     * This implementation ignores generics
     *
     * Some examples how Objects will be translated
     * <pre>
     * Object > java.lang.object
     * java.util.List<T> > java.util.List
     * ? > ?
     * T > T
     * anypackage.Outer.Inner > anypackage.Outer.Inner
     * </pre>
     * 
     * @return type representation for code usage
     */
    public String getValue() {
        String fqn = getFullyQualifiedName();
        return ( fqn == null ? "" : fqn.replaceAll( "\\$", "." ) );
    }
    
    /**
     * The FQN representation of an Object for code usage
     * This implementation ignores generics
     *
     * Some examples how Objects will be translated
     * <pre>
     * Object > java.lang.object
     * java.util.List<T> > java.util.List
     * ? > ?
     * T > T
     * anypackage.Outer.Inner > anypackage.Outer.Inner
     * </pre>

     * @since 1.8
     * @return generic type representation for code usage 
     */
    public String getGenericValue() {
    	StringBuffer result = new StringBuffer(getValue());
    	if(actualArgumentTypes != null && actualArgumentTypes.length > 0) {
    		result.append("<");
    		for(int index = 0;index < actualArgumentTypes.length; index++) {
    			result.append(actualArgumentTypes[index].getGenericValue());
    			if(index + 1 != actualArgumentTypes.length) {
    				result.append(",");
    			}
    		}
    		result.append(">");
    	}
    	for (int i = 0; i < dimensions; i++) result.append("[]");
        return result.toString();
    }
    
    protected String getGenericValue(TypeVariable[] typeVariableList) {
    	StringBuffer result = new StringBuffer(getResolvedValue(typeVariableList));
    	if(actualArgumentTypes != null && actualArgumentTypes.length > 0) {
    		for(int index = 0;index < actualArgumentTypes.length; index++) {
    			result.append(actualArgumentTypes[index].getResolvedGenericValue(typeVariableList));
    			if(index + 1 != actualArgumentTypes.length) {
    				result.append(",");
    			}
    		}
    	}
        return result.toString();
    }
    
    protected String getResolvedValue(TypeVariable[] typeParameters) {
    	String result = getValue();
    	for(int typeIndex=0;typeIndex<typeParameters.length; typeIndex++) {
			if(typeParameters[typeIndex].getName().equals(getValue())) {
				result = typeParameters[typeIndex].getValue();
				break;
			}
		}
    	return result;
    }
    
    protected String getResolvedGenericValue(TypeVariable[] typeParameters) {
    	String result = getGenericValue(typeParameters);
    	for(int typeIndex=0;typeIndex<typeParameters.length; typeIndex++) {
			if(typeParameters[typeIndex].getName().equals(getValue())) {
				result = typeParameters[typeIndex].getGenericValue();
				break;
			}
		}
    	return result;
    }

    /**
     * Checks if the FQN of this Type is resolved 
     * 
     * @return 
     */
    public boolean isResolved() {
        if (fullName == null && context != null) {
            fullName = context.resolveType(name);
        }
        return (fullName != null);
    }

    /**
     * @see java.lang.Comparable#compareTo(Object)
     */
    public int compareTo(Object o) {
        if (!(o instanceof Type))
            return 0;

        return getValue().compareTo(((Type) o).getValue());
    }

    /**
     * Returns true if this Type is an array
     * 
     * @return
     */
    public boolean isArray() {
        return dimensions > 0;
    }

    /**
     * Returns the depth of this array, 0 if it's not an array
     * 
     * @return The depth of this array
     */
    public int getDimensions() {
        return dimensions;
    }

    /**
     * 
     * @return the actualTypeArguments or null
     */
    public Type[] getActualTypeArguments()
    {
        return actualArgumentTypes;
    }
    
    /**
     * Returns getValue() extended with the array information 
     * 
     * @return
     */
    public String toString() {
        if (dimensions == 0) return getValue();
        StringBuffer buff = new StringBuffer(getValue());
        for (int i = 0; i < dimensions; i++) buff.append("[]");
        String result = buff.toString();
        return result;
    }

    /**
     * Returns getGenericValue() extended with the array information
     * 
     * <pre>
     * Object > java.lang.Object
     * Object[] > java.lang.Object[]
     * List<Object> > java.lang.List<java.lang.Object>
     * Outer.Inner > Outer.Inner 
     * Outer.Inner<Object>[][] > Outer.Inner<java.lang.Object>[][] 
     * </pre>
     * @return 
     */
    public String toGenericString() {
        if (dimensions == 0) return getGenericValue();
        StringBuffer buff = new StringBuffer(getGenericValue());
        for (int i = 0; i < dimensions; i++) buff.append("[]");
        String result = buff.toString();
        return result;
    }

    public boolean equals(Object obj) {
        if (obj == null) return false;
        Type t = (Type) obj;
        return getValue().equals(t.getValue()) && t.getDimensions() == getDimensions();
    }

    public int hashCode() {
        return getValue().hashCode();
    }

    public JavaClass getJavaClass() {
    	JavaClass result = null;
    	
        JavaClassParent javaClassParent = getJavaClassParent();
        if (javaClassParent != null) {
        	result = javaClassParent.getNestedClassByName(getFullyQualifiedName());
	        if(result == null) {
	            JavaClassContext context = javaClassParent.getJavaClassContext();
	            if (context.getClassLibrary() != null) {
	            	result = context.getClassByName(getFullyQualifiedName());
	            }
	        }
        }
        return result;
    }

    /**
     * @since 1.3
     */
    public boolean isA(Type type) {
        if (this.equals(type)) {
            return true;
        } else {
            JavaClass javaClass = getJavaClass();
            if (javaClass != null) {
                // ask our interfaces
                Type[] implementz = javaClass.getImplements();
                for (int i = 0; i < implementz.length; i++) {
                    if (implementz[i].isA(type)) {
                        return true;
                    }
                }

                // ask our superclass
                Type supertype = javaClass.getSuperClass();
                if (supertype != null) {
                    if (supertype.isA(type)) {
                        return true;
                    }
                }
            }
        }
        // We'we walked up the hierarchy and found nothing.
        return false;
    }

    /**
     * @since 1.6
     */
    public boolean isPrimitive() {
        String value = getValue();
        if (value == null || value.length() == 0 || value.indexOf('.') > -1) {
            return false;
        } else {
           return "void".equals(value)           
            || "boolean".equals(value)
            || "byte".equals(value)
            || "char".equals(value)
            || "short".equals(value)
            || "int".equals(value)
            || "long".equals(value)
            || "float".equals(value)
            || "double".equals(value);
        }
    }

    /**
     * @since 1.6
     */
    public boolean isVoid() {
        return "void".equals(getValue());
    }

    /**
     * 
     * @param superClass
     * @return
     * @since 1.12
     */
    protected int getTypeVariableIndex( JavaClass superClass ) {
        TypeVariable[] typeVariables = superClass.getTypeParameters();
        for(int typeIndex=0;typeIndex<typeVariables.length; typeIndex++) {
            if(typeVariables[typeIndex].getFullyQualifiedName().equals( getFullyQualifiedName())) {
                return typeIndex;
            }
        }
        return -1;
    }

    /**
     * 
     * @param parentClass
     * @return
     * @since 1.12
     */
    protected Type resolve( JavaClass parentClass )
    {
        return resolve( parentClass, parentClass );
    }

    /**
     * 
     * @param parentClass
     * @param subclass
     * @return
     * @since 1.12
     */
    protected Type resolve( JavaClass parentClass, JavaClass subclass )
    {
        Type result = this;
        int typeIndex = getTypeVariableIndex( parentClass );
        if ( typeIndex >= 0 )
        {
            String fqn = parentClass.getFullyQualifiedName();
            if ( subclass.getSuperClass() != null && fqn.equals( subclass.getSuperClass().getFullyQualifiedName() ) ) {
                result = subclass.getSuperClass().getActualTypeArguments()[typeIndex];    
            }
            else if ( subclass.getImplementedInterfaces() != null )
            {
                for ( int i = 0; i < subclass.getImplementedInterfaces().length; i++ )
                {
                    if ( fqn.equals( subclass.getImplements()[i].getFullyQualifiedName() ) ) 
                    {
                        result = subclass.getImplements()[i].getActualTypeArguments()[typeIndex].resolve( subclass.getImplementedInterfaces()[i] );
                        break;
                    }
                }
                //no direct interface available, try indirect
            }
        }
        
        if ( this.actualArgumentTypes != null ) {
            result = new Type( this.fullName, this.name, this.dimensions, this.context );
            
            result.actualArgumentTypes = new Type[this.actualArgumentTypes.length];
            for (int i = 0; i < this.getActualTypeArguments().length; i++ )
            {
                result.actualArgumentTypes[i] = this.actualArgumentTypes[i].resolve( parentClass, subclass );
            }
        }
        return result;
    }

}
