/**************************************************************************************
 * Copyright (c) Jonas BonŽr, Alexandre Vasseur. All rights reserved.                 *
 * http://aspectwerkz.codehaus.org                                                    *
 * ---------------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the LGPL license      *
 * a copy of which has been included with this distribution in the license.txt file.  *
 **************************************************************************************/
package org.codehaus.aspectwerkz.reflect.impl.java;

import gnu.trove.TIntObjectHashMap;
import org.codehaus.aspectwerkz.annotation.Annotations;
import org.codehaus.aspectwerkz.reflect.ClassInfo;
import org.codehaus.aspectwerkz.reflect.ConstructorInfo;
import org.codehaus.aspectwerkz.reflect.FieldInfo;
import org.codehaus.aspectwerkz.reflect.MethodInfo;
import org.codehaus.aspectwerkz.reflect.ReflectHelper;
import org.codehaus.aspectwerkz.reflect.ReflectHelper;
import org.codehaus.aspectwerkz.reflect.StaticInitializationInfo;
import org.codehaus.aspectwerkz.reflect.StaticInitializationInfoImpl;
import org.codehaus.aspectwerkz.reflect.impl.asm.AsmClassInfo;
import org.codehaus.aspectwerkz.transform.TransformationConstants;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;

/**
 * Implementation of the ClassInfo interface for java.lang.reflect.*.
 *
 * @author <a href="mailto:jboner@codehaus.org">Jonas BonŽr </a>
 */
public class JavaClassInfo implements ClassInfo {
    /**
     * The class.
     */
    // TODO might be safer to wrap this member in a weak ref
    private final Class m_class;

    /**
     * The name of the class.
     */
    private String m_name;

    /**
     * The signature of the class.
     */
    private String m_signature;

    /**
     * Is the class an interface.
     */
    private boolean m_isInterface = false;

    /**
     * Is the class a primitive type.
     */
    private boolean m_isPrimitive = false;

    /**
     * Is the class of type array.
     */
    private boolean m_isArray = false;

    /**
     * A list with the <code>ConstructorInfo</code> instances.
     */
    private final TIntObjectHashMap m_constructors = new TIntObjectHashMap();

    /**
     * A list with the <code>MethodInfo</code> instances.
     */
    private final TIntObjectHashMap m_methods = new TIntObjectHashMap();

    /**
     * A list with the <code>FieldInfo</code> instances.
     */
    private final TIntObjectHashMap m_fields = new TIntObjectHashMap();

    /**
     * A list with the interfaces.
     */
    private ClassInfo[] m_interfaces = null;

    /**
     * The super class.
     */
    private ClassInfo m_superClass = null;

    /**
     * The annotations.
     */
    private List m_annotations = null;

    /**
     * The component type if array type.
     */
    private ClassInfo m_componentType = null;

    /**
     * The class info repository.
     */
    private final JavaClassInfoRepository m_classInfoRepository;

    /**
     * Lazy, the static initializer info or null if not present
     */
    private StaticInitializationInfo m_staticInitializer = null;

    /**
     * Creates a new class meta data instance.
     *
     * @param klass
     */
    JavaClassInfo(final Class klass) {
        if (klass == null) {
            throw new IllegalArgumentException("class can not be null");
        }
        m_class = klass;

        m_signature = ReflectHelper.getClassSignature(klass);

        m_classInfoRepository = JavaClassInfoRepository.getRepository(klass.getClassLoader());
        m_isInterface = klass.isInterface();
        if (klass.isPrimitive()) {
            m_name = klass.getName();
            m_isPrimitive = true;
        } else if (klass.getComponentType() != null) {
            m_name = convertJavaArrayTypeNameToHumanTypeName(klass.getName());
            m_isArray = true;
            m_interfaces = new ClassInfo[0];
        } else {
            m_name = klass.getName();
            Method[] methods = m_class.getDeclaredMethods();
            for (int i = 0; i < methods.length; i++) {
                Method method = methods[i];
                m_methods.put(ReflectHelper.calculateHash(method), new JavaMethodInfo(method, this));
            }
            Constructor[] constructors = m_class.getDeclaredConstructors();
            for (int i = 0; i < constructors.length; i++) {
                Constructor constructor = constructors[i];
                m_constructors.put(
                        ReflectHelper.calculateHash(constructor), new JavaConstructorInfo(
                                constructor,
                                this
                        )
                );
            }
            Field[] fields = m_class.getDeclaredFields();
            for (int i = 0; i < fields.length; i++) {
                if (fields[i].getName().startsWith(TransformationConstants.ASPECTWERKZ_PREFIX)) {
                    continue;
                }
                Field field = fields[i];
                m_fields.put(ReflectHelper.calculateHash(field), new JavaFieldInfo(field, this));
            }
        }
        m_classInfoRepository.addClassInfo(this);
    }

    /**
     * Returns the class info for a specific class.
     *
     * @return the class info
     */
    public static ClassInfo getClassInfo(final Class clazz) {
        JavaClassInfoRepository repository = JavaClassInfoRepository.getRepository(clazz.getClassLoader());
        ClassInfo classInfo = repository.getClassInfo(clazz.getName());
        if (classInfo == null) {
            classInfo = new JavaClassInfo(clazz);
        }
        return classInfo;
    }

    /**
     * Returns the annotations infos.
     *
     * @return the annotations infos
     */
    public List getAnnotations() {
        if (m_annotations == null) {
            // TODO this means that JavaClassInfo is always using AsmClassInfo to get that annotations
            // TODO should optimize for Java5
            m_annotations = Annotations.getAnnotationInfos(m_class);
        }
        return m_annotations;
    }

    /**
     * Returns the name of the class.
     *
     * @return the name of the class
     */
    public String getName() {
        return m_name.replace('/', '.');
    }

    /**
     * Checks if the class has a static initalizer.
     *
     * @return
     */
    public boolean hasStaticInitializer() {
        ClassInfo classInfo = AsmClassInfo.getClassInfo(getName(), getClassLoader());
        return classInfo.hasStaticInitializer();
    }

	/**
	 * Returns the static initializer info of the current underlying class if any.
	 * 
	 * @see org.codehaus.aspectwerkz.reflect.ClassInfo#staticInitializer()
	 */
	public StaticInitializationInfo staticInitializer() {
		if(hasStaticInitializer() && m_staticInitializer == null) {
			m_staticInitializer = new StaticInitializationInfoImpl(this);
		}
		return m_staticInitializer;
	}
	
    /**
     * Returns the signature for the element.
     *
     * @return the signature for the element
     */
    public String getSignature() {
        return m_signature;
    }

    /**
     * Returns the class modifiers.
     *
     * @return the class modifiers
     */
    public int getModifiers() {
        return m_class.getModifiers();
    }

    /**
     * Returns the class loader that loaded this class.
     *
     * @return the class loader
     */
    public ClassLoader getClassLoader() {
        return m_class.getClassLoader();
    }

    /**
     * Returns a constructor info by its hash.
     *
     * @param hash
     * @return
     */
    public ConstructorInfo getConstructor(final int hash) {
        ConstructorInfo constructor = (ConstructorInfo) m_constructors.get(hash);
        if (constructor == null && getSuperclass() != null) {
            constructor = getSuperclass().getConstructor(hash);
        }
        return constructor;
    }

    /**
     * Returns a list with all the constructors info.
     *
     * @return the constructors info
     */
    public ConstructorInfo[] getConstructors() {
        Object[] values = m_constructors.getValues();
        ConstructorInfo[] methodInfos = new ConstructorInfo[values.length];
        for (int i = 0; i < values.length; i++) {
            methodInfos[i] = (ConstructorInfo) values[i];
        }
        return methodInfos;
    }

    /**
     * Returns a method info by its hash.
     *
     * @param hash
     * @return
     */
    public MethodInfo getMethod(final int hash) {
        MethodInfo method = (MethodInfo) m_methods.get(hash);
        if (method == null) {
            for (int i = 0; i < getInterfaces().length; i++) {
                method = getInterfaces()[i].getMethod(hash);
                if (method != null) {
                    break;
                }
            }
        }
        if (method == null && getSuperclass() != null) {
            method = getSuperclass().getMethod(hash);
        }
        return method;
    }

    /**
     * Returns a list with all the methods info.
     *
     * @return the methods info
     */
    public MethodInfo[] getMethods() {
        Object[] values = m_methods.getValues();
        MethodInfo[] methodInfos = new MethodInfo[values.length];
        for (int i = 0; i < values.length; i++) {
            methodInfos[i] = (MethodInfo) values[i];
        }
        return methodInfos;
    }

    /**
     * Returns a field info by its hash.
     *
     * @param hash
     * @return
     */
    public FieldInfo getField(final int hash) {
        FieldInfo field = (FieldInfo) m_fields.get(hash);
        if (field == null && getSuperclass() != null) {
            field = getSuperclass().getField(hash);
        }
        return field;
    }

    /**
     * Returns a list with all the field info.
     *
     * @return the field info
     */
    public FieldInfo[] getFields() {
        Object[] values = m_fields.getValues();
        FieldInfo[] fieldInfos = new FieldInfo[values.length];
        for (int i = 0; i < values.length; i++) {
            fieldInfos[i] = (FieldInfo) values[i];
        }
        return fieldInfos;
    }

    /**
     * Returns the interfaces.
     *
     * @return the interfaces
     */
    public ClassInfo[] getInterfaces() {
        if (m_interfaces == null) {
            Class[] interfaces = m_class.getInterfaces();
            m_interfaces = new ClassInfo[interfaces.length];
            for (int i = 0; i < interfaces.length; i++) {
                Class anInterface = interfaces[i];
                ClassInfo classInfo = JavaClassInfo.getClassInfo(anInterface);
                m_interfaces[i] = classInfo;
                if (!m_classInfoRepository.hasClassInfo(anInterface.getName())) {
                    m_classInfoRepository.addClassInfo(classInfo);
                }
            }
        }
        return m_interfaces;
    }

    /**
     * Returns the super class.
     *
     * @return the super class
     */
    public ClassInfo getSuperclass() {
        if (m_superClass == null) {
            Class superclass = m_class.getSuperclass();
            if (superclass != null) {
                if (m_classInfoRepository.hasClassInfo(superclass.getName())) {
                    m_superClass = m_classInfoRepository.getClassInfo(superclass.getName());
                } else {
                    m_superClass = JavaClassInfo.getClassInfo(superclass);
                    m_classInfoRepository.addClassInfo(m_superClass);
                }
            }
        }
        return m_superClass;
    }

    /**
     * Returns the component type if array type else null.
     *
     * @return the component type
     */
    public ClassInfo getComponentType() {
        if (isArray() && (m_componentType == null)) {
            Class componentType = m_class.getComponentType();
            if (m_classInfoRepository.hasClassInfo(componentType.getName())) {
                m_componentType = m_classInfoRepository.getClassInfo(componentType.getName());
            } else {
                m_componentType = JavaClassInfo.getClassInfo(componentType);
                m_classInfoRepository.addClassInfo(m_componentType);
            }
        }
        return m_componentType;
    }

    /**
     * Is the class an interface.
     *
     * @return
     */
    public boolean isInterface() {
        return m_isInterface;
    }

    /**
     * Is the class a primitive type.
     *
     * @return
     */
    public boolean isPrimitive() {
        return m_isPrimitive;
    }

    /**
     * Is the class an array type.
     *
     * @return
     */
    public boolean isArray() {
        return m_isArray;
    }

    /**
     * Converts an internal Java array type name ([Lblabla) to the a the format used by the expression matcher
     * (blabla[])
     *
     * @param typeName is type name
     * @return
     */
    public static String convertJavaArrayTypeNameToHumanTypeName(final String typeName) {
        int index = typeName.lastIndexOf('[');
        if (index != -1) {
            StringBuffer arrayType = new StringBuffer();
            if (typeName.endsWith("I")) {
                arrayType.append("int");
            } else if (typeName.endsWith("J")) {
                arrayType.append("long");
            } else if (typeName.endsWith("S")) {
                arrayType.append("short");
            } else if (typeName.endsWith("F")) {
                arrayType.append("float");
            } else if (typeName.endsWith("D")) {
                arrayType.append("double");
            } else if (typeName.endsWith("Z")) {
                arrayType.append("boolean");
            } else if (typeName.endsWith("C")) {
                arrayType.append("char");
            } else if (typeName.endsWith("B")) {
                arrayType.append("byte");
            } else {
                arrayType.append(typeName.substring(index + 2, typeName.length() - 1));
            }
            for (int i = 0; i < (index + 1); i++) {
                arrayType.append("[]");
            }
            return arrayType.toString();
        } else {
            return typeName;
        }
    }

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof ClassInfo)) {
            return false;
        }
        ClassInfo classInfo = (ClassInfo) o;
        return m_class.getName().toString().equals(classInfo.getName().toString());
    }

    public int hashCode() {
        return m_class.getName().toString().hashCode();
    }

    public String toString() {
        return getName();
    }
}