/*
 * Copyright (C) 2004 TiongHiang Lee
 *
 * This library 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 library 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 library; if not,  write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Email: thlee@onemindsoft.org
 */

package org.onemind.commons.java.lang.reflect;

import java.lang.reflect.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.onemind.commons.java.datastructure.Scoreable;
import org.onemind.commons.java.util.StringUtils;
/**
 * Reflection related utilities
 * @author TiongHiang Lee (thlee@onemindsoft.org)
 * @version $Id: ReflectUtils.java,v 1.12 2006/08/01 23:57:03 thlee Exp $ $Name:  $
 */
public final class ReflectUtils
{

    /** the logger * */
    private static final Logger _logger = Logger.getLogger(ReflectUtils.class.getName());

    /** the lookup cache * */
    private static final Map _classCache = new HashMap();

    /** the method cache **/
    private static final Map _methodCache = new HashMap();

    /** class caching setting **/
    private static boolean _classCaching = true;

    /** method caching setting **/
    private static boolean _methodCaching = true;

    /** keep a primitive class and their compatible types **/
    private static final Map WIDENABLES = new HashMap();
    static
    {
        Object[][] primitiveWideningMap = new Object[][]{
                {Boolean.TYPE, new Object[]{Boolean.TYPE, Boolean.class}},
                {Boolean.class, new Object[]{Boolean.TYPE, Boolean.class}},
                {Byte.TYPE, new Object[]{Byte.TYPE, Byte.class, Short.class, Short.TYPE, Integer.class, Integer.TYPE, Long.class, Long.TYPE, Float.class, Float.TYPE, Double.class, Double.TYPE}},
                {Byte.class, new Object[]{Byte.TYPE, Byte.class, Short.class, Short.TYPE, Integer.class, Integer.TYPE, Long.class, Long.TYPE, Float.class, Float.TYPE, Double.class, Double.TYPE}},
                {Short.TYPE, new Object[]{Short.TYPE, Short.class, Integer.class, Integer.TYPE, Long.class, Long.TYPE, Float.class, Float.TYPE, Double.class, Double.TYPE}},
                {Short.class, new Object[]{Short.TYPE, Short.class, Integer.class, Integer.TYPE, Long.class, Long.TYPE, Float.class, Float.TYPE, Double.class, Double.TYPE}},
                {Character.TYPE, new Object[]{Character.TYPE, Character.class, Integer.class, Integer.TYPE, Long.class, Long.TYPE, Float.class, Float.TYPE, Double.class, Double.TYPE}},
                {Character.class, new Object[]{Character.TYPE, Character.class, Integer.class, Integer.TYPE, Long.class, Long.TYPE, Float.class, Float.TYPE, Double.class, Double.TYPE}},
                {Integer.TYPE, new Object[]{Integer.TYPE, Integer.class, Long.class, Long.TYPE, Float.class, Float.TYPE, Double.class, Double.TYPE}},
                {Integer.class, new Object[]{Integer.TYPE, Integer.class, Long.class, Long.TYPE, Float.class, Float.TYPE, Double.class, Double.TYPE}},
                {Long.TYPE, new Object[]{Long.TYPE, Long.class, Float.class, Float.TYPE, Double.class, Double.TYPE}},
                {Long.class, new Object[]{Long.TYPE, Long.class, Float.class, Float.TYPE, Double.class, Double.TYPE}},
                {Float.TYPE, new Object[]{Float.TYPE, Float.class, Double.class, Double.TYPE}},
                {Float.class, new Object[]{Float.TYPE, Float.class, Double.class, Double.TYPE}},
                {Double.TYPE, new Object[]{Double.TYPE, Double.class}},
                {Double.class, new Object[]{Double.TYPE, Double.class}}};
        
        for (int i = 0; i < primitiveWideningMap.length; i++)
        {
            WIDENABLES.put(primitiveWideningMap[i][0], Arrays.asList((Object[]) primitiveWideningMap[i][1]));
        }
    }

    /** 
     * The method key 
     */
    private static class MethodKey
    {

        /** the name **/
        private String _name;

        /** the class **/
        private Class _clazz;

        /** the arguments **/
        private Class[] _args;

        /** the hash code **/
        private int _hashCode;

        /**
         * Constructor
         * @param clazz the class
         * @param name the name
         * @param args the arguments
         */
        public MethodKey(Class clazz, String name, Class[] args)
        {
            _clazz = clazz;
            _name = name;
            _args = args;
            _hashCode = _clazz.hashCode() + _name.hashCode();
        }

        /** 
         * {@inheritDoc}
         */
        public int hashCode()
        {
            return _hashCode;
        }

        /** 
         * {@inheritDoc}
         */
        public boolean equals(Object obj)
        {
            if (obj instanceof MethodKey)
            {
                MethodKey key = (MethodKey) obj;
                return _clazz.equals(key._clazz) && _name.equals(key._name) && Arrays.equals(_args, key._args);
            } else
            {
                throw new IllegalArgumentException("Cannot compare " + this + " to " + obj);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    private ReflectUtils()
    {
    };

    /**
     * Construct the argument type class array from a list of arg objects
     * @param args the arguments
     * @return the class array
     * @todo decide what to do with null value arguments and what to do with isCompatibleCheck
     * TODO decide what to do with null value arguments and what to do with isCompatibleCheck
     */
    public static final Class[] toArgTypes(Object[] args)
    {
        if (args == null)
        {
            args = new Object[0];
        }
        Class[] argTypes = new Class[args.length];
        for (int i = 0; i < args.length; i++)
        {
            if (args[i] != null)
            {
                argTypes[i] = args[i].getClass();
            }
        }
        return argTypes;
    }

    /**
     * Get the class
     * @param name the name of the class
     * @return the class
     * @throws ClassNotFoundException if the class cannot be found
     */
    public static final Class getClass(String name) throws ClassNotFoundException
    {
        if (_classCaching && _classCache.containsKey(name))
        {
            Class c = (Class) _classCache.get(name);
            if (c == null)
            {
                throw new ClassNotFoundException("Class " + name + " not found");
            } else
            {
                return c;
            }
        } else
        {
            Class c = null;
            try
            {
                c = Class.forName(name);
                return c;
            } finally
            {
                if (_classCaching)
                {
                    _classCache.put(name, c);
                }
            }
        }
    }

    /**
     * Get the constructor of the type given the arguments to the constructor
     * @param type the type
     * @param args the arguments
     * @return the constructor
     * @throws NoSuchMethodException if the constructor cannot be found
     */
    public static final Constructor getConstructor(Class type, Object[] args) throws NoSuchMethodException
    {
        if (args == null)
        {
            args = new Object[0];
        }
        Class[] argTypes = toArgTypes(args);
        Constructor c = null;
        if (_methodCaching) //look in cache
        {
            c = (Constructor) _methodCache.get(new MethodKey(type, "$Constructor", argTypes));
            if (c != null)
            {
                return c;
            }
        }
        try
        {
            //first trial
            if (_logger.isLoggable(Level.FINEST))
            {
                _logger.finest("Looking for constructor for " + type.getName() + "(" + StringUtils.concat(argTypes, ",") + ")");
            }
            c = type.getConstructor(argTypes);
        } catch (NoSuchMethodException e)
        {
            c = searchConstructor(type, argTypes);
        }
        if (c == null)
        {
            throw new NoSuchMethodException("Constructor not found for class " + toMethodString(type.getName(), args));
        } else if (_methodCaching)
        {
            _methodCache.put(new MethodKey(type, "$Constructor", argTypes), c);
        }
        return c;
    }

    /**
     * To the method representation string e.g. toString()
     * @param methodName the method
     * @param args the arguments
     * @return the method representation string
     */
    public static final String toMethodString(String methodName, Object[] args)
    {
        StringBuffer sb = new StringBuffer(methodName);
        sb.append("(");
        if (args != null)
        {
            sb.append(StringUtils.concat(args, ","));
        }
        sb.append(")");
        return sb.toString();
    }

    /**
     * Search for a particular constructor based on arg types classes
     * @param type the type
     * @param argTypes the argument types
     * @return the constructor
     */
    public static final Constructor searchConstructor(Class type, Class[] argTypes)
    {
        if (_logger.isLoggable(Level.FINEST))
        {
            _logger.finest("Searching for constructor for " + type.getName());
        }
        Constructor[] constructors = type.getConstructors();
        TreeSet scoreboard = new TreeSet();
        for (int i = 0; i < constructors.length; i++)
        {
            Class[] types = constructors[i].getParameterTypes();
            if (_logger.isLoggable(Level.FINEST))
            {
                _logger.finest("trying arg types " + StringUtils.concat(types, ","));
            }
            int score = computeCompatibalityScore(types, argTypes);
            if (score > 0)
            {
                scoreboard.add(new Scoreable(score, constructors[i]));
            }
        }
        if (scoreboard.size() > 0)
        {
            return (Constructor) ((Scoreable) scoreboard.last()).getObject();
        } else
        {
            return null;
        }
    }

    /**
     * Return whether the argument objects is compatible with the argument types specification
     * @param types the argument types
     * @param args the arguments
     * @return true if compatible
     */
    public static final boolean isCompatible(Class[] types, Object[] args)
    {
        return computeCompatibalityScore(types, toArgTypes(args)) > 0;
    }

    public static final boolean isCompatible(Class[] types, Class[] argTypes)
    {
        return computeCompatibalityScore(types, argTypes) > 0;
    }

    /**
     * Return whether the types of arguments is compatible with the argument type spec of a method
     * @param methodTypes the argument type spec of a method
     * @param argTypes the argument type
     * @return true if compatible
     */
    public static final int computeCompatibalityScore(Class[] methodTypes, Class[] argTypes)
    {
        int score = 0;
        if ((methodTypes == null) || (methodTypes.length == 0))
        {
            if ((argTypes == null) || (argTypes.length == 0))
            {
                score = 1;
            }
        } else if (argTypes != null && methodTypes.length == argTypes.length)
        {
            for (int i = 0; i < methodTypes.length; i++)
            {
                if (_logger.isLoggable(Level.FINEST))
                {
                    _logger.finest("Comparing " + methodTypes[i] + " to " + argTypes[i]);
                }
                if (methodTypes[i] == argTypes[i])
                {
                    score += 2;
                }else if (argTypes[i] == null)
                {
                    if (methodTypes[i].isPrimitive()){
                        score = 0;
                        break;
                    } else {
                        score += 1; //assume underlying args is null which is allowable    
                    }
                } else if (WIDENABLES.containsKey(argTypes[i]))
                {//maybe it can be widen
                    
                    int thisScore = computeWideningScore(methodTypes[i], argTypes[i]);
                    if (thisScore == 0)
                    {
                        score = 0;
                        break;
                    } else
                    {
                        score += thisScore;
                    }
                }  else if (methodTypes[i].isAssignableFrom(argTypes[i]))
                {
                    score += 1;
                } else {
                    score = 0;
                    break;
                }
            }
        }
        return score;
    }

    /**
     * Create a new instance of the class type with the arguments to constructor
     * @param type the type
     * @param args the argument
     * @return the new instance
     * @throws IllegalAccessException if there's access problem
     * @throws InstantiationException if there's instantiation problem
     * @throws InvocationTargetException if there's target exception
     * @throws NoSuchMethodException if there's no such constructor
     */
    public static final Object newInstance(Class type, Object[] args) throws IllegalAccessException, InstantiationException,
            InvocationTargetException, NoSuchMethodException
    {
        if (args == null)
        {
            args = new Object[0];
        }
        Constructor c = getConstructor(type, args);
        if (c != null)
        {
            return c.newInstance(args);
        } else
        {
            throw new NoSuchMethodException("Constructor not found for " + type);
        }
    }

    /**
     * Invoke a named method on the object using the arguments
     * @param o the object
     * @param methodName the name of the method
     * @param args the arguments
     * @return the object return by the invocation
     * @throws NoSuchMethodException if there's no such method
     * @throws IllegalAccessException if there's access problem
     * @throws InvocationTargetException if there's target problem
     * @todo decide if is necessary to check for declaring class before invoke
     */
    public static final Object invoke(Object o, String methodName, Object[] args) throws NoSuchMethodException,
            IllegalAccessException, InvocationTargetException
    {
        if (args == null)
        {
            args = new Object[0];
        }
        Method m = null;
        if (o instanceof Class)
        {
            try
            {
                //try to get static method
                m = getMethod((Class) o, methodName, args);
            } catch (NoSuchMethodException e)
            {
                //when user trying to get the "class instance" method
                m = getMethod(o.getClass(), methodName, args);
            }
        } else
        {
            m = getMethod(o.getClass(), methodName, args);
        }
        if (m != null)
        {
            if (_logger.isLoggable(Level.FINEST))
            {
                _logger.finest("Invoking " + m + " on " + o);
            }
            return m.invoke(o, args);
        } else
        {
            throw new NoSuchMethodException("There's no method " + toMethodString(methodName, args) + " for " + m);
        }
    }

    /**
     * Resolve the method from the interfaces
     * @param c the class
     * @param methodName the method
     * @param argTypes the arg types
     * @return the method or null
     * @todo decide if this method is needed
     */
    public static final Method getInterfaceMethod(Class[] c, String methodName, Class[] argTypes)
    {
        //TODO: decide if needed
        return null;
    }

    /**
     * Get a named method of class type with the argument type compatible with the argument passed in. 
     * 
     * @param type the class
     * @param methodName the method name
     * @param args the arguments
     * @return the method
     * @throws NoSuchMethodException if the method cannot be found
     */
    public static final Method getMethod(Class type, String methodName, Object[] args) throws NoSuchMethodException
    {
        Method m = null;
        if (_logger.isLoggable(Level.FINEST))
        {
            _logger.finest("Finding method " + toMethodString(methodName, args) + " of " + type);
        }
        if (args == null)
        {
            args = new Object[0];
        }
        Class[] argTypes = toArgTypes(args);
        return getMethod(type, methodName, argTypes);
    }

    /**
     * Get a named method of class type with the argument type compatible with the argument passed in. 
     * 
     * @param type the class
     * @param methodName the method name
     * @param args the arguments
     * @return the method
     * @throws NoSuchMethodException if the method cannot be found
     */
    public static final Method getMethod(Class type, String methodName, Class[] argTypes) throws NoSuchMethodException
    {
        Method m;
        if (_methodCaching) //look in cache
        {
            m = (Method) _methodCache.get(new MethodKey(type, methodName, argTypes));
            if (m != null)
            {
                return m;
            }
        }
        try
        {
            //first trial
            m = type.getMethod(methodName, argTypes);
            if (_logger.isLoggable(Level.FINEST))
            {
                _logger.finest("Found using reflection");
            }
        } catch (NoSuchMethodException nme)
        {
            if (_logger.isLoggable(Level.FINEST))
            {
                _logger.finest("Failed using reflection: " + nme.getMessage() + ". Search for method.");
            }
            m = searchMethod(type, methodName, argTypes);
        }
        if (m != null)
        {
            if (_methodCaching)
            {
                _methodCache.put(new MethodKey(type, methodName, argTypes), m);
            }
            if (!m.isAccessible())
            {
                m.setAccessible(true);
            }
        } else
        {
            throw new NoSuchMethodException("Method " + type.getName() + "." + toMethodString(methodName, argTypes) + " not found.");
        }
        return m;
    }

    /**
     * Search a named method of class type through the class's hierachy
     * @param type the class
     * @param methodName the method name
     * @param argTypes the argument types
     * @return the method
     */
    private static final Method searchMethod(Class type, String methodName, Class[] argTypes)
    {
        TreeSet scoreboard = new TreeSet();
        Method[] methods = type.getMethods();
        for (int i = 0; i < methods.length; i++)
        {
            Method m = methods[i];
            if (_logger.isLoggable(Level.FINEST))
            {
                _logger.finest("Checking compatibility with " + m);
            }
            if (m.getName().equals(methodName))
            {
                int score = computeCompatibalityScore(m.getParameterTypes(), argTypes);
                if (score > 0)
                {
                    scoreboard.add(new Scoreable(score, methods[i]));
                }
            }
        }
        if (scoreboard.size() > 0)
        {
            return (Method) ((Scoreable) scoreboard.last()).getObject();
        }
        return null;
    }

    /**
     * Set the classCaching
     * @param caching true to turn on class caching
     */
    protected static final void setClassCaching(boolean caching)
    {
        _classCaching = caching;
    }

    /**
     * Set the _methodCaching
     * @param caching true to turn on method caching
     */
    protected static final void setMethodCaching(boolean caching)
    {
        _methodCaching = caching;
    }

    /**
     * Return whether a given object is a primitive or compatible (through unwrapping and widening) instance of primitiveClass
     * @param primitiveClass the primitive class 
     * @param obj the object
     * @return true if is instance
     */
    public static final boolean isPrimitiveInstance(Class primitiveClass, Object obj)
    {
        if (!primitiveClass.isPrimitive())
        {
            throw new IllegalArgumentException(primitiveClass + " is not primitive type ");
        }
        if (obj == null)
        {
            return false;
        } else
        {
            return isPrimitiveCompatible(primitiveClass, obj.getClass());
        }
    }
    
    /**
     * Check if class c can be widen to targetClass and return the score.
     * Return 2 if c==primitiveClass, 1 if c can be widened, or 0 if c cannot be widened.
     * @param primitiveClass
     * @param c
     * @return
     */

    private static final int computeWideningScore(Class primitiveClass, Class c)
    {
        //check if c can be widen to primitiveClass
        List set = (List) WIDENABLES.get(c);
        int i = set.indexOf(primitiveClass);
        if (i==-1){
            return 0;
        } else if (i<2){
            return 2; //exact match
        } else {
            return 1;
        }
    }

    /**
     * Return true if primitiveClass and clazz is both primitive and clazz is primitive compatible with primitiveClass
     * using java rules (unwrapping or widening)
     * @param primitiveClass 
     * @param clazz
     * @return
     */
    public static final boolean isPrimitiveCompatible(Class primitiveClass, Class clazz)
    {
        return computeWideningScore(primitiveClass, clazz) > 0;
    }
}