/* JFox, the OpenSource J2EE Application Server
 *
 * Distributable under GNU LGPL license by gun.org
 * more details please visit http://www.huihoo.org/jfox
 */

package org.jfox.framework.util;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

/**
 * @author <a href="mailto:young_yy@hotmail.com">Young Yang</a>
 */

public class Classes {

    public static final char INNER_CLASS_SEPARATOR_CHAR = '$';
    public final static Class[] EMPTY_CLASS_ARRAY = new Class[0];

    /**
     * <p>The inner class separator String: <code>"$"</code>.</p>
     */
    public static final String INNER_CLASS_SEPARATOR = String.valueOf(INNER_CLASS_SEPARATOR_CHAR);

    /**
     * Maps primitive <code>Class</code>es to their corresponding wrapper <code>Class</code>.
     */
    private static Map primitiveClassToWrapper = new HashMap();

    static {
        primitiveClassToWrapper.put(Boolean.TYPE, Boolean.class);
        primitiveClassToWrapper.put(Byte.TYPE, Byte.class);
        primitiveClassToWrapper.put(Character.TYPE, Character.class);
        primitiveClassToWrapper.put(Short.TYPE, Short.class);
        primitiveClassToWrapper.put(Integer.TYPE, Integer.class);
        primitiveClassToWrapper.put(Long.TYPE, Long.class);
        primitiveClassToWrapper.put(Double.TYPE, Double.class);
        primitiveClassToWrapper.put(Float.TYPE, Float.class);
    }

    private static Map primitiveStringToClass = new HashMap();

    static {
        primitiveStringToClass.put(Boolean.TYPE.getName(), Boolean.TYPE);
        primitiveStringToClass.put(Byte.TYPE.getName(), Byte.TYPE);
        primitiveStringToClass.put(Character.TYPE.getName(), Character.TYPE);
        primitiveStringToClass.put(Short.TYPE.getName(), Short.TYPE);
        primitiveStringToClass.put(Integer.TYPE.getName(), Integer.TYPE);
        primitiveStringToClass.put(Long.TYPE.getName(), Long.TYPE);
        primitiveStringToClass.put(Double.TYPE.getName(), Double.TYPE);
        primitiveStringToClass.put(Float.TYPE.getName(), Float.TYPE);
    }

    /**
     * <p>ClassUtils instances should NOT be constructed in standard programming.
     * Instead, the class should be used as
     * <code>ClassUtils.getShortClassName(cls)</code>.</p>
     * <p/>
     * <p>This constructor is public to permit tools that require a JavaBean
     * instance to operate.</p>
     */
    private Classes() {
    }

    /**
     * <p>Gets a <code>List</code> of superclasses for the given class.</p>
     *
     * @param cls the class to look up, may be <code>null</code>
     * @return the <code>List</code> of superclasses in order going up from this one
     *         <code>null</code> if null input
     */
    public static List getAllSuperclasses(Class cls) {
        if(cls == null) {
            return null;
        }
        List classes = new ArrayList();
        Class superclass = cls.getSuperclass();
        while(superclass != null) {
            classes.add(superclass);
            superclass = superclass.getSuperclass();
        }
        return classes;
    }

    /**
     * <p>Gets a <code>List</code> of all interfaces implemented by the given
     * class and its superclasses.</p>
     * <p/>
     * <p>The order is determined by looking through each interface in turn as
     * declared in the source file and following its hierarchy up. Then each
     * superclass is considered in the same way. Later duplicates are ignored,
     * so the order is maintained.</p>
     *
     * @param cls the class to look up, may be <code>null</code>
     * @return the <code>List</code> of interfaces in order,
     *         <code>null</code> if null input
     */
    public static List getAllInterfaces(Class cls) {
        if(cls == null) {
            return null;
        }
        List list = new ArrayList();
        while(cls != null) {
            Class[] interfaces = cls.getInterfaces();
            for(int i = 0; i < interfaces.length; i++) {
                if(list.contains(interfaces[i]) == false) {
                    list.add(interfaces[i]);
                }
                List superInterfaces = getAllInterfaces(interfaces[i]);
                for(Iterator it = superInterfaces.iterator(); it.hasNext();) {
                    Class intface = (Class) it.next();
                    if(list.contains(intface) == false) {
                        list.add(intface);
                    }
                }
            }
            cls = cls.getSuperclass();
        }
        return list;
    }


    /**
     * <p>Checks if an array of Classes can be assigned to another array of Classes.</p>
     * <p/>
     * <p>This method calls {@link #isAssignable(Class, Class) isAssignable} for each
     * Class pair in the input arrays. It can be used to check if a set of arguments
     * (the first parameter) are suitably compatible with a set of method parameter types
     * (the second parameter).</p>
     * <p/>
     * <p>Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method, this
     * method takes into account widenings of primitive classes and
     * <code>null</code>s.</p>
     * <p/>
     * <p>Primitive widenings allow an int to be assigned to a <code>long</code>,
     * <code>float</code> or <code>double</code>. This method returns the correct
     * result for these cases.</p>
     * <p/>
     * <p><code>Null</code> may be assigned to any reference type. This method will
     * return <code>true</code> if <code>null</code> is passed in and the toClass is
     * non-primitive.</p>
     * <p/>
     * <p>Specifically, this method tests whether the type represented by the
     * specified <code>Class</code> parameter can be converted to the type
     * represented by this <code>Class</code> object via an identity conversion
     * widening primitive or widening reference conversion. See
     * <em><a href="http://java.sun.com/docs/books/jls/">The Java Language Specification</a></em>,
     * sections 5.1.1, 5.1.2 and 5.1.4 for details.</p>
     *
     * @param classArray   the array of Classes to check, may be <code>null</code>
     * @param toClassArray the array of Classes to try to assign into, may be <code>null</code>
     * @return <code>true</code> if assignment possible
     */
    public static boolean isAssignable(Class[] classArray, Class[] toClassArray) {
        if(classArray.length != toClassArray.length) {
            return false;
        }
        if(classArray == null) {
            classArray = EMPTY_CLASS_ARRAY;
        }
        if(toClassArray == null) {
            toClassArray = EMPTY_CLASS_ARRAY;
        }
        for(int i = 0; i < classArray.length; i++) {
            if(isAssignable(classArray[i], toClassArray[i]) == false) {
                return false;
            }
        }
        return true;
    }

    /**
     * <p>Checks if one <code>Class</code> can be assigned to a variable of
     * another <code>Class</code>.</p>
     * <p/>
     * <p>Unlike the {@link Class#isAssignableFrom(java.lang.Class)} method,
     * this method takes into account widenings of primitive classes and
     * <code>null</code>s.</p>
     * <p/>
     * <p>Primitive widenings allow an int to be assigned to a long, float or
     * double. This method returns the correct result for these cases.</p>
     * <p/>
     * <p><code>Null</code> may be assigned to any reference type. This method
     * will return <code>true</code> if <code>null</code> is passed in and the
     * toClass is non-primitive.</p>
     * <p/>
     * <p>Specifically, this method tests whether the type represented by the
     * specified <code>Class</code> parameter can be converted to the type
     * represented by this <code>Class</code> object via an identity conversion
     * widening primitive or widening reference conversion. See
     * <em><a href="http://java.sun.com/docs/books/jls/">The Java Language Specification</a></em>,
     * sections 5.1.1, 5.1.2 and 5.1.4 for details.</p>
     *
     * @param cls     the Class to check, may be null
     * @param toClass the Class to try to assign into, returns false if null
     * @return <code>true</code> if assignment possible
     */
    public static boolean isAssignable(Class cls, Class toClass) {
        if(toClass == null) {
            return false;
        }
        // have to check for null, as isAssignableFrom doesn't
        if(cls == null) {
            return !(toClass.isPrimitive());
        }
        if(cls.equals(toClass)) {
            return true;
        }
        if(cls.isPrimitive()) {
            if(toClass.isPrimitive() == false) {
                return false;
            }
            if(Integer.TYPE.equals(cls)) {
                return Long.TYPE.equals(toClass)
                        || Float.TYPE.equals(toClass)
                        || Double.TYPE.equals(toClass);
            }
            if(Long.TYPE.equals(cls)) {
                return Float.TYPE.equals(toClass)
                        || Double.TYPE.equals(toClass);
            }
            if(Boolean.TYPE.equals(cls)) {
                return false;
            }
            if(Double.TYPE.equals(cls)) {
                return false;
            }
            if(Float.TYPE.equals(cls)) {
                return Double.TYPE.equals(toClass);
            }
            if(Character.TYPE.equals(cls)) {
                return Integer.TYPE.equals(toClass)
                        || Long.TYPE.equals(toClass)
                        || Float.TYPE.equals(toClass)
                        || Double.TYPE.equals(toClass);
            }
            if(Short.TYPE.equals(cls)) {
                return Integer.TYPE.equals(toClass)
                        || Long.TYPE.equals(toClass)
                        || Float.TYPE.equals(toClass)
                        || Double.TYPE.equals(toClass);
            }
            if(Byte.TYPE.equals(cls)) {
                return Short.TYPE.equals(toClass)
                        || Integer.TYPE.equals(toClass)
                        || Long.TYPE.equals(toClass)
                        || Float.TYPE.equals(toClass)
                        || Double.TYPE.equals(toClass);
            }
            // should never get here
            return false;
        }
        return toClass.isAssignableFrom(cls);
    }

    /**
     * <p>Converts the specified primitive Class object to its corresponding
     * wrapper Class object.</p>
     *
     * @param cls the class to convert, may be null
     * @return the wrapper class for <code>cls</code> or <code>cls</code> if
     *         <code>cls</code> is not a primitive. <code>null</code> if null input.
     */
    public static Class primitiveToWrapper(Class cls) {
        Class convertedClass = cls;
        if(cls != null && cls.isPrimitive()) {
            convertedClass = (Class) primitiveClassToWrapper.get(cls);
        }
        return convertedClass;
    }

    /**
     * if is primitive class, get it's concrete class object
     * else use reflect with a string paramter constructor
     *
     * @param clz
     * @param value
     * @return
     */
    public static Object newObject(Class clz, String value) {
        Object obj = value;
        if(clz.isPrimitive()) {
            if(clz.equals(boolean.class) || clz.equals(Boolean.class)) {
                obj = Boolean.valueOf(value);
            }
            else if(clz.equals(char.class) || clz.equals(Character.class)) {
                obj = new Character(value.charAt(0));
            }
            else if(clz.equals(byte.class) || clz.equals(Byte.class)) {
                obj = Byte.valueOf(value);
            }
            else if(clz.equals(short.class) || clz.equals(Short.class)) {
                obj = Short.valueOf(value);
            }
            else if(clz.equals(int.class) || clz.equals(Integer.class)) {
                obj = Integer.valueOf(value);
            }
            else if(clz.equals(long.class) || clz.equals(Long.class)) {
                obj = Long.valueOf(value);
            }
            else if(clz.equals(float.class) || clz.equals(Float.class)) {
                obj = Float.valueOf(value);
            }
            else if(clz.equals(double.class) || clz.equals(Double.class)) {
                obj = Double.valueOf(value);
            }

        }
        else if(clz.isArray()) {
            Class originClass = getClassFromArrayClass(clz);
            StringTokenizer st = new StringTokenizer(value);
            int length = st.countTokens();
            Object array = Array.newInstance(originClass, length);
            for(int i = 0; st.hasMoreTokens(); i++) {
                Object param = newObject(originClass, st.nextToken());
                Array.set(array, i, param);
            }
            obj = array;
        }
        else {
            try {
                obj = clz.getConstructor(new Class[]{String.class}).newInstance(new String[]{value});
            }
            catch(NoSuchMethodException e) {
                e.printStackTrace();
            }
            catch(Exception e) {
                e.printStackTrace();
            }
        }
        return obj;
    }

    /**
     * <p>Is the specified class an inner class or static nested class.</p>
     *
     * @param cls the class to check, may be null
     * @return <code>true</code> if the class is an inner or static nested class,
     *         false if not or <code>null</code>
     */
    public static boolean isInnerClass(Class cls) {
        if(cls == null) {
            return false;
        }
        return (cls.getName().indexOf(INNER_CLASS_SEPARATOR_CHAR) >= 0);
    }

    public static Method getMethod(Class clz, String methodName, Class[] expectedTypes) throws NoSuchMethodException {
        Method method = null;
        try {
            method = clz.getMethod(methodName, expectedTypes);
        }
        catch(NoSuchMethodException e) {
            Method[] methods = clz.getMethods();
            for(int i = 0; i < methods.length; i++) {
                Method _method = methods[i];
                if(_method.getName().equals(methodName)) {
                    if(isAssignable(expectedTypes, _method.getParameterTypes())) {
                        if(method == null) {
                            method = _method;
                        }
                        else {
                            if(Classes.isAssignable(_method.getParameterTypes(), method.getParameterTypes())) {
                                method = _method;
                            }
                        }

                    }
                }
            }
            if(method == null) throw e;
        }
        return method;
    }

    public static Constructor getConstructor(Class clz, Class[] expectedTypes) throws NoSuchMethodException {
        Constructor constructor = null;
        try {
            constructor = clz.getConstructor(expectedTypes);
        }
        catch(NoSuchMethodException e) {
            Constructor[] constructors = clz.getConstructors();
            for(int i = 0; i < constructors.length; i++) {
                Constructor constr = constructors[i];
                if(Classes.isAssignable(expectedTypes, constr.getParameterTypes())) {
                    if(constructor == null) {
                        constructor = constr;
                    }
                    else {
                        if(Classes.isAssignable(constr.getParameterTypes(), constructor.getParameterTypes())) {
                            constructor = constr;
                        }
                    }
                }
            }
            if(constructor == null) throw e;
        }
        return constructor;
    }

    /**
     * get the narrowest class from the types
     *
     * @param types
     * @return
     */
    public static Class getNarrowestClass(Class[] types) {
        if(types == null || types.length == 0) {
            throw new IllegalArgumentException("types is null or empty.");
        }
        Class clz = types[0];
        for(int i = 1; i < types.length; i++) {
            if(clz.isAssignableFrom(types[i])) {
                clz = types[i];
            }
            else if(!types[i].isAssignableFrom(clz)) {
                throw new IllegalArgumentException(types[i].getName() + " not has assignable relation with " + clz.getName());
            }
        }
        return clz;
    }

    public static boolean isPrimitiveClass(String primitiveClassName) {
        return primitiveStringToClass.containsKey(primitiveClassName);
    }

    public static Class loadPrimitiveClass(String primitiveClassName) {
        if(isPrimitiveClass(primitiveClassName)) {
            return (Class) primitiveStringToClass.get(primitiveClassName);
        }
        return null;
    }

    public static Class getClassFromArrayClass(Class clz) {
        if(!clz.isArray()) return clz;
        // [Ljava.lang.String;
        String name = clz.getName();
        String className = name.substring(2, name.length() - 1);
        Class _clz = null;
        try {
            System.out.println(clz.getClassLoader());
            _clz = Thread.currentThread().getContextClassLoader().loadClass(className);
        }
        catch(Exception e) {
            e.printStackTrace();
        }
        return _clz;
    }

    public static String classNameWithoutPackage(Class objectClass){
        return classNameWithoutPackage(objectClass.getName());

    }

    public static String classNameWithoutPackage(String fullClassName){
        return fullClassName.substring(fullClassName.lastIndexOf(".")+1, fullClassName.length());
    }

    public static void main(String[] args) {
        Object array = newObject(new String[]{}.getClass(), "a b  cc ");
        System.out.println(array);
    }

}

