package com.thoughtworks.qdox.model;

import com.thoughtworks.qdox.JavaClassContext;
import com.thoughtworks.qdox.model.util.OrderedMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author <a href="mailto:joew@thoughtworks.com">Joe Walnes</a>
 * @author Aslak Helles&oslash;y
 */
public class JavaClass extends AbstractInheritableJavaEntity implements JavaClassParent {

    private static Type OBJECT = new Type("java.lang.Object");
    private static Type ENUM = new Type("java.lang.Enum");
    private static Type ANNOTATION = new Type("java.lang.annotation.Annotation");

    private List methods = new LinkedList();
    private JavaMethod[] methodsArray;
    private List fields = new LinkedList();
    private JavaField[] fieldsArray;
    private List classes = new LinkedList();
    private JavaClass[] classesArray;
    private boolean interfce;
    private boolean isEnum;
    private boolean isAnnotation;

    // Don't access this directly. Use asType() to get my Type
    private Type type;
    private Type superClass;
    private Type[] implementz = new Type[0];
    private TypeVariable[] typeParameters = TypeVariable.EMPTY_ARRAY; 
    private JavaClassContext context;
    
    //sourceless class can use this property
	private JavaPackage javaPackage;
	
	private JavaSource source;

    public JavaClass() {
    }
    
    public JavaClass(String name) {
        setName(name);
    }

    public void setJavaClassContext(JavaClassContext context) {
		this.context = context;

        // reassign OBJECT. This will make it have a "source" too,
        // causing Type.getJavaClass() to return a JavaClass, instead
        // of null.
        OBJECT = context.getClassByName("java.lang.Object").asType();
    }

    /**
     * is interface?  (otherwise enum or class)
     */
    public boolean isInterface() {
        return interfce;
    }

    /**
     * is enum?  (otherwise class or interface)
     */
    public boolean isEnum() {
        return isEnum;
    }

    public Type getSuperClass() {
        boolean iAmJavaLangObject = OBJECT.equals(asType());

        if (isEnum) {
            return ENUM;
        } else if (!interfce && !isAnnotation && (superClass == null) && !iAmJavaLangObject) {
            return OBJECT;
        }

        return superClass;
    }

    /**
     * Shorthand for getSuperClass().getJavaClass() with null checking.
     */
    public JavaClass getSuperJavaClass() {
        if (getSuperClass() != null) {
            return getSuperClass().getJavaClass();
        } else {
            return null;
        }
    }

    public Type[] getImplements() {
        return implementz;
    }

    /**
     * @since 1.3
     */
    public JavaClass[] getImplementedInterfaces() {
        Type[] type = getImplements();
        JavaClass[] result = new JavaClass[type.length];

        for (int i = 0; i < result.length; i++) {
            result[i] = type[i].getJavaClass();
        }

        return result;
    }

    protected void writeBody(IndentBuffer result) {
        writeAccessibilityModifier(result);
        writeNonAccessibilityModifiers(result);

        result.write(isEnum ? "enum " : interfce ? "interface " : isAnnotation ? "@interface " : "class ");
        result.write(name);

        // subclass
        if (superClass != null) {
            result.write(" extends ");
            result.write(superClass.getValue());
        }

        // implements
        if (implementz.length > 0) {
            result.write(interfce ? " extends " : " implements ");

            for (int i = 0; i < implementz.length; i++) {
                if (i > 0) {
                    result.write(", ");
                }

                result.write(implementz[i].getValue());
            }
        }

        result.write(" {");
        result.newline();
        result.indent();

        // fields
        for (Iterator iterator = fields.iterator(); iterator.hasNext();) {
            JavaField javaField = (JavaField) iterator.next();

            result.newline();
            javaField.write(result);
        }

        // methods
        for (Iterator iterator = methods.iterator(); iterator.hasNext();) {
            JavaMethod javaMethod = (JavaMethod) iterator.next();

            result.newline();
            javaMethod.write(result);
        }

        // inner-classes
        for (Iterator iterator = classes.iterator(); iterator.hasNext();) {
            JavaClass javaClass = (JavaClass) iterator.next();

            result.newline();
            javaClass.write(result);
        }

        result.deindent();
        result.newline();
        result.write('}');
        result.newline();
    }

    public void setInterface(boolean interfce) {
        this.interfce = interfce;
    }

    public void setEnum(boolean isEnum) {
        this.isEnum = isEnum;
    }

    public void setAnnotation(boolean isAnnotation) {
        this.isAnnotation = isAnnotation;
    }

    public void addMethod(JavaMethod meth) {
        meth.setParentClass( this );
        methods.add(meth);
        methodsArray = null;
    }

    public void setSuperClass(Type type) {
        if (isEnum) throw new IllegalArgumentException("enums cannot extend other classes");
        superClass = type;
    }

    public void setImplementz(Type[] implementz) {
        this.implementz = implementz;
    }
    
    public TypeVariable[] getTypeParameters()
    {
        return typeParameters;
    }
    
    public void setTypeParameters( TypeVariable[] typeParameters )
    {
        this.typeParameters = typeParameters;
    }

    public void addField(JavaField javaField) {
        javaField.setParentClass( this );
        fields.add(javaField);
        fieldsArray = null;
    }
    
    /**
     * Only used when constructing the model by hand / without source 
     * 
     * @param javaPackage
     */
    public void setJavaPackage(JavaPackage javaPackage) {
    	this.javaPackage = javaPackage;
    }

    public void setSource( JavaSource source )
    {
        this.source = source;
    }
    
    public JavaSource getParentSource() {
        return (getParentClass() != null ? getParentClass().getParentSource() : source);
    }
    
    public JavaSource getSource()
    {
        return getParentSource();
    }

    public JavaPackage getPackage() {
        return getParentSource() != null ? getParentSource().getPackage() : javaPackage;
    }
    
    public JavaClassParent getParent()
    {
        JavaClassParent result = getParentClass();
        if (result == null) {
            result = getParentSource();
        }
        return result;
    }

    /**
     * If this class has a package, the packagename will be returned.
     * Otherwise an empty String.
     * 
     * @return
     */
    public String getPackageName() {
        JavaPackage javaPackage = getPackage();
        return (javaPackage != null && javaPackage.getName() != null) ? javaPackage.getName() : "";
    }

    public String getFullyQualifiedName() {
        return (getParentClass() != null ? (getParentClass().getClassNamePrefix()) : getPackage() != null ? (getPackage().getName()+".") : "") + getName();
    }

    /**
     * @since 1.3
     */
    public boolean isInner() {
        return getParentClass() != null;
    }

    public String resolveType(String typeName) {
        // Maybe it's an inner class?
        JavaClass[] innerClasses = getNestedClasses();
        for (int i = 0; i < innerClasses.length; i++) {
            if (innerClasses[i].getName().equals(typeName)) {
                return innerClasses[i].getFullyQualifiedName();
            }
        }

        return getParent().resolveType(typeName);
    }

    public JavaClassContext getJavaClassContext() {
        return getParent().getJavaClassContext();
    }

    public String getClassNamePrefix() {
        return getFullyQualifiedName() + "$";
    }

    public Type asType() {
        if (type == null) {
            type = new Type(getFullyQualifiedName(), 0, this);
        }

        return type;
    }

    public JavaMethod[] getMethods() {
        if (methodsArray == null) {
            methodsArray = new JavaMethod[methods.size()];
            methods.toArray(methodsArray);
        }

        return methodsArray;
    }

    /**
     * @since 1.3
     */
    public JavaMethod[] getMethods(boolean superclasses) {
        if (superclasses) {
            Set signatures = new HashSet();
            List methods = new ArrayList();

            addMethodsFromSuperclassAndInterfaces(signatures, methods, this);

            return (JavaMethod[]) methods.toArray(new JavaMethod[methods.size()]);
        } else {
            return getMethods();
        }
    }

    private void addMethodsFromSuperclassAndInterfaces(Set signatures,
                                                       List methodList, JavaClass callingClazz) {
        JavaMethod[] methods = callingClazz.getMethods();

        addNewMethods(signatures, methodList, methods);

        JavaClass superclass = callingClazz.getSuperJavaClass();

        // TODO workaround for a bug in getSuperJavaClass
        if ((superclass != null) && (superclass != callingClazz)) {
            callingClazz.addMethodsFromSuperclassAndInterfaces(signatures, methodList,
                    superclass);
        }

        JavaClass[] implementz = callingClazz.getImplementedInterfaces();

        for (int i = 0; i < implementz.length; i++) {
            if (implementz[i] != null) {
                callingClazz.addMethodsFromSuperclassAndInterfaces(signatures, methodList,
                        implementz[i]);
            }
        }
    }

    private void addNewMethods(Set signatures, List methodList,
                               JavaMethod[] methods) {
        for (int i = 0; i < methods.length; i++) {
            JavaMethod method = methods[i];

            if (!method.isPrivate()) {
                String signature = method.getDeclarationSignature(false);

                if (!signatures.contains(signature)) {
                    methodList.add( new JavaMethodDelegate( this, method ) );
                    signatures.add(signature);
                }
            }
        }
    }

    /**
     * 
     * @param name           method name
     * @param parameterTypes parameter types or null if there are no parameters.
     * @return the matching method or null if no match is found.
     */
    public JavaMethod getMethodBySignature(String name, Type[] parameterTypes) {
        return getMethod( name, parameterTypes, false );
    }

    /**
     * This should be the signature for getMethodBySignature
     * 
     * @param name
     * @param parameterTypes
     * @param varArgs
     * @return
     */
    public JavaMethod getMethod(String name, Type[] parameterTypes, boolean varArgs) {
        JavaMethod[] methods = getMethods();

        for (int i = 0; i < methods.length; i++) {
            if (methods[i].signatureMatches(name, parameterTypes, varArgs)) {
                return methods[i];
            }
        }

        return null;
    }
    
    /**
     * 
     * @param name
     * @param parameterTypes
     * @param superclasses
     * @return
     */
    public JavaMethod getMethodBySignature(String name, Type[] parameterTypes,
                                           boolean superclasses) {
        return getMethodBySignature( name, parameterTypes, superclasses, false );
    }
    
    /**
     * 
     * @param name
     * @param parameterTypes
     * @param superclasses
     * @param varArg
     * @return
     */
    public JavaMethod getMethodBySignature(String name, Type[] parameterTypes,
                                           boolean superclasses, boolean varArg) {
        
        JavaMethod[] result = getMethodsBySignature(name, parameterTypes,
                superclasses, varArg);

        return (result.length > 0) ? result[0] : null;
    }
    
    /**
     * 
     * @param name
     * @param parameterTypes
     * @param superclasses
     * @return
     */
    public JavaMethod[] getMethodsBySignature(String name,
                                              Type[] parameterTypes, boolean superclasses) {
        return getMethodsBySignature( name, parameterTypes, superclasses, false );
    }

    /**
     * 
     * @param name
     * @param parameterTypes
     * @param superclasses
     * @param varArg
     * @return
     */
    public JavaMethod[] getMethodsBySignature(String name,
                                              Type[] parameterTypes, boolean superclasses, boolean varArg) {
        List result = new ArrayList();

        JavaMethod methodInThisClass = getMethod(name, parameterTypes, varArg);

        if (methodInThisClass != null) {
            result.add(methodInThisClass);
        }

        if (superclasses) {
            JavaClass superclass = getSuperJavaClass();

            if (superclass != null) {
                JavaMethod method = superclass.getMethodBySignature(name,
                        parameterTypes, true, varArg );

                // todo: ideally we should check on package privacy too. oh well.
                if ((method != null) && !method.isPrivate()) {
                    result.add( new JavaMethodDelegate( this, method ) );
                }
            }

            JavaClass[] implementz = getImplementedInterfaces();

            for (int i = 0; i < implementz.length; i++) {
                JavaMethod method = implementz[i].getMethodBySignature(name,
                        parameterTypes, true, varArg );

                if (method != null) {
                    result.add( new JavaMethodDelegate( this, method ) );
                }
            }
        }

        return (JavaMethod[]) result.toArray(new JavaMethod[result.size()]);
    }

    public JavaField[] getFields() {
        if (fieldsArray == null) {
            fieldsArray = new JavaField[fields.size()];
            fields.toArray(fieldsArray);
        }

        return fieldsArray;
    }

    public JavaField getFieldByName(String name) {
        JavaField[] fields = getFields();

        for (int i = 0; i < fields.length; i++) {
            if (fields[i].getName().equals(name)) {
                return fields[i];
            }
        }

        return null;
    }

    public void addClass(JavaClass cls) {
        cls.setParentClass( this );
        classes.add(cls);
        classesArray = null;
    }

    /**
     * @deprecated Use {@link #getNestedClasses()} instead.
     */
    public JavaClass[] getClasses() {
        return getNestedClasses();
    }

    /**
     * @since 1.3
     */
    public JavaClass[] getNestedClasses() {
        if (classesArray == null) {
            classesArray = new JavaClass[classes.size()];
            classes.toArray(classesArray);
        }

        return classesArray;
    }

    public JavaClass getNestedClassByName(String name) {
        JavaClass[] classes = getNestedClasses();
        
        int separatorIndex = name.indexOf('.');
        String directInnerClassName = (separatorIndex > 0 ? name.substring(0, separatorIndex) : name); 
        for (int i = 0; i < classes.length; i++) {
        	JavaClass jClass = classes[i];
            if (jClass.getName().equals(directInnerClassName)) {
            	if(separatorIndex > 0) {
                    return jClass.getNestedClassByName(name.substring(separatorIndex+1));
            	}
            	else {
                    return jClass;
            	}
            }
        }
        return null;
    }

    /**
     * @deprecated old name for {@link #getNestedClasses()} 
     */
    public JavaClass[] getInnerClasses() {
        return getNestedClasses();
    }

    /**
     * @deprecated old name for {@link #getNestedClassByName(String)} 
     */
    public JavaClass getInnerClassByName(String name) {
       return getNestedClassByName(name);
    }
    
    /**
     * @since 1.3
     */
    public boolean isA(String fullClassName) {
        Type type = new Type(fullClassName, 0, this);
        return asType().isA(type);
    }

    /**
     * @since 1.3
     */
    public boolean isA(JavaClass javaClass) {
        return asType().isA(javaClass.asType());
    }

    /**
     * Gets bean properties without looking in superclasses or interfaces.
     *
     * @since 1.3
     */
    public BeanProperty[] getBeanProperties() {
        return getBeanProperties(false);
    }

    /**
     * @since 1.3
     */
    public BeanProperty[] getBeanProperties(boolean superclasses) {
        Map beanPropertyMap = getBeanPropertyMap(superclasses);
        Collection beanPropertyCollection = beanPropertyMap.values();

        return (BeanProperty[]) beanPropertyCollection.toArray(new BeanProperty[beanPropertyCollection
                .size()]);
    }

    private Map getBeanPropertyMap(boolean superclasses) {
        JavaMethod[] methods = getMethods(superclasses);
        Map beanPropertyMap = new OrderedMap();

        // loop over the methods.
        for (int i = 0; i < methods.length; i++) {
            JavaMethod method = methods[i];

            if (method.isPropertyAccessor()) {
                String propertyName = method.getPropertyName();
                BeanProperty beanProperty = getOrCreateProperty(beanPropertyMap,
                        propertyName);

                beanProperty.setAccessor(method);
                beanProperty.setType(method.getPropertyType());
            } else if (method.isPropertyMutator()) {
                String propertyName = method.getPropertyName();
                BeanProperty beanProperty = getOrCreateProperty(beanPropertyMap,
                        propertyName);

                beanProperty.setMutator(method);
                beanProperty.setType(method.getPropertyType());
            }
        }

        return beanPropertyMap;
    }

    private BeanProperty getOrCreateProperty(Map beanPropertyMap,
                                             String propertyName) {
        BeanProperty result = (BeanProperty) beanPropertyMap.get(propertyName);

        if (result == null) {
            result = new BeanProperty(propertyName);
            beanPropertyMap.put(propertyName, result);
        }

        return result;
    }

    /**
     * Gets bean property without looking in superclasses or interfaces.
     *
     * @since 1.3
     */
    public BeanProperty getBeanProperty(String propertyName) {
        return getBeanProperty(propertyName, false);
    }

    /**
     * @since 1.3
     */
    public BeanProperty getBeanProperty(String propertyName,
                                        boolean superclasses) {
        return (BeanProperty) getBeanPropertyMap(superclasses).get(propertyName);
    }

    /**
     * Gets the known derived classes. That is, subclasses or implementing classes.
     */
    public JavaClass[] getDerivedClasses() {
        List result = new ArrayList();
        JavaClass[] classes = context.getClasses();

        for (int i = 0; i < classes.length; i++) {
            JavaClass clazz = classes[i];

            if (clazz.isA(this) && !(clazz == this)) {
                result.add(clazz);
            }
        }

        return (JavaClass[]) result.toArray(new JavaClass[result.size()]);
    }

    public DocletTag[] getTagsByName(String name, boolean superclasses) {
        List result = new ArrayList();

        addTagsRecursive(result, this, name, superclasses);

        return (DocletTag[]) result.toArray(new DocletTag[result.size()]);
    }

    private void addTagsRecursive(List result, JavaClass javaClass,
                                  String name, boolean superclasses) {
        DocletTag[] tags = javaClass.getTagsByName(name);

        addNewTags(result, tags);

        if (superclasses) {
            JavaClass superclass = javaClass.getSuperJavaClass();

            // THIS IS A HACK AROUND A BUG THAT MUST BE SOLVED!!!
            // SOMETIMES A CLASS RETURNS ITSELF AS SUPER ?!?!?!?!?!
            if ((superclass != null) && (superclass != javaClass)) {
                addTagsRecursive(result, superclass, name, superclasses);
            }

            JavaClass[] implementz = javaClass.getImplementedInterfaces();

            for (int h = 0; h < implementz.length; h++) {
                if (implementz[h] != null) {
                    addTagsRecursive(result, implementz[h], name, superclasses);
                }
            }
        }
    }

    private void addNewTags(List list, DocletTag[] tags) {
        for (int i = 0; i < tags.length; i++) {
            DocletTag superTag = tags[i];

            if (!list.contains(superTag)) {
                list.add(superTag);
            }
        }
    }

    public int compareTo(Object o) {
        return getFullyQualifiedName().compareTo(((JavaClass) o).getFullyQualifiedName());
    }

    /**
     * @see http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Class.html#toString()
     */
    public String toString() {
    	StringBuffer sb = new StringBuffer();
    	if(asType().isPrimitive() || (Type.VOID.equals(asType()))) {
    		sb.append(asType().getValue());
    	}
    	else {
        	sb.append(isInterface() ? "interface" : "class");
        	sb.append(" ");
        	sb.append(getFullyQualifiedName());
    	}
    	return sb.toString();
    }

}
