/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.myfaces.extensions.scripting.core.common.util;

import org.apache.myfaces.extensions.scripting.core.api.WeavingContext;

import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @author Werner Punz (latest modification by $Author$)
 * @version $Revision$ $Date$
 */

public class ReflectUtil
{

    static final Logger _logger = Logger.getLogger(ReflectUtil.class.getName());

    public static Object instantiate(String clazz, Object... varargs)
    {
        return instantiate(ClassUtils.forName(clazz), varargs);
    }

    /**
     * A simplified instantiation over reflection
     *
     * @param clazz   the class to be instantiated
     * @param varargs the instantiation parameters
     * @return the instantiated object
     */
    public static Object instantiate(Class clazz, Object... varargs)
    {
        Class[] classes = new Class[varargs.length];
        for (int cnt = 0; cnt < varargs.length; cnt++)
        {

            if (varargs[cnt] instanceof Cast)
            {
                classes[cnt] = ((Cast) varargs[cnt]).getClazz();
                varargs[cnt] = ((Cast) varargs[cnt]).getValue();
            } else
            {
                classes[cnt] = varargs[cnt].getClass();
            }
        }

        try
        {
            Constructor constr = clazz.getConstructor(classes);
            return constr.newInstance(varargs);
        }
        catch (NoSuchMethodException e)
        {
            throw new RuntimeException(e);
        }
        catch (InvocationTargetException e)
        {
            throw new RuntimeException(e);
        }
        catch (IllegalAccessException e)
        {
            throw new RuntimeException(e);
        }
        catch (InstantiationException e)
        {
            throw new RuntimeException(e);
        }
    }/*this is mostly just a helper to bypass a groovy bug in a more
   * complex delegation environment. Groovy throws a classcast
   * exception wrongly, delegating the instantiation code to java
   * fixes that
   * */

    public static Object newObject(Class clazz) throws IllegalAccessException, InstantiationException
    {
        return clazz.newInstance();
    }

    /**
     * Generic execute method which simplifies the reflection api
     * down to a usable system
     *
     * @param obj        the target object the method has to be executed upon
     * @param methodName the method name
     * @param varargs    the arguments which have to be passed to the method
     * @return the return value of the method
     */
    public static Object executeStaticMethod(Class obj, String methodName, Object... varargs)
    {

        Collection<Method> methods = getMethods(obj, methodName, varargs.length);

        Object retVal = handleStaticMethod(obj, methodName, methods, varargs);
        if (!methodNotFound(retVal))
        {
            return retVal;
        }

        methods = getAllMethods(obj, methodName, varargs.length);
        retVal = handleStaticMethod(obj, methodName, methods, varargs);
        if (!methodNotFound(retVal))
        {
            return retVal;
        }

        throw new RuntimeException("Static Method of :" + methodName + " from class " + obj.getClass().getName() + " not found");

    }

    public static void setField(Object obj, String fieldName, Object value, boolean protectedField)
    {
        try
        {
            Field f = null;
            try
            {
                f = obj.getClass().getDeclaredField(fieldName);
            }
            catch (NoSuchFieldException e)
            {
                f = obj.getClass().getField(fieldName);
            }

            f.setAccessible(protectedField);
            f.set(obj, value);

        }
        catch (NoSuchFieldException e)
        {
            throw new RuntimeException(e);
        }
        catch (IllegalAccessException e)
        {
            throw new RuntimeException(e);
        }
    }

    public static Object getField(Object obj, String fieldName, boolean protectedField)
    {
        try
        {
            Field f = null;
            try
            {
                f = obj.getClass().getDeclaredField(fieldName);
            }
            catch (NoSuchFieldException e)
            {
                f = obj.getClass().getField(fieldName);
            }
            f.setAccessible(protectedField);
            return f.get(obj);
        }
        catch (NoSuchFieldException e)
        {
            throw new RuntimeException(e);
        }
        catch (IllegalAccessException e)
        {
            throw new RuntimeException(e);
        }
    }

    public static Collection<Method> getAllMethods(Class clazz, String methodName, int varargLength)
    {
        ArrayList<Method> retVal = new ArrayList<Method>(30);
        while (clazz != null)
        {
            for (Method m : clazz.getDeclaredMethods())
            {
                if (m.getParameterTypes().length == varargLength && m.getName().equals(methodName))
                {
                    retVal.add(m);
                }
            }
            clazz = clazz.getSuperclass();
        }

        return retVal;
    }

    public static Collection<Method> getMethods(Class clazz, String methodName, int varargLength)
    {
        ArrayList<Method> retVal = new ArrayList<Method>(30);
        for (Method m : clazz.getDeclaredMethods())
        {
            if (m.getParameterTypes().length == varargLength && m.getName().equals(methodName))
            {
                retVal.add(m);
            }
        }

        return retVal;
    }

    /**
     * Generic execute method which simplifies the reflection api
     * down to a usable system
     *
     * @param obj        the target object the method has to be executed upon
     * @param methodName the method name
     * @param varargs    the arguments which have to be passed to the method
     * @return the return value of the method
     * @throws RuntimeException a generic runtime exception in case of a failure
     *                          we use unmanaged exceptions here to get a behavior similar to scripting
     *                          language execution where failures can happen but method executions
     *                          should not enforce exception handling
     */
    public static Object executeMethod(Object obj, String methodName, Object... varargs)
    {

        Collection<Method> methods;
        //if we have an invocationHandler here we
        //can work over the generic invoke interface
        //That way we can cover more dynamic stuff
        //our reload invocation handler is treated differently here

        if (obj instanceof InvocationHandler)
        {
            InvocationHandler objToInvoke = (InvocationHandler) obj;

            Object realTarget = WeavingContext.getInstance().getDelegateFromProxy(objToInvoke);

            //first we try only the public because they are the most likely ones
            //to be accessed
            methods = getMethods(realTarget.getClass(), methodName, varargs.length);
            Object retVal = handleInvHandlerMethod(objToInvoke, methodName, methods, varargs);
            if (!methodNotFound(retVal))
            {
                return retVal;
            }
            //if not we try all of them until we have a match
            methods = getAllMethods(realTarget.getClass(), methodName, varargs.length);
            retVal = handleInvHandlerMethod(objToInvoke, methodName, methods, varargs);
            if (!(methodNotFound(retVal)))
            {
                return retVal;
            }

            throw new RuntimeException("Method of :" + methodName + " from class " + obj.getClass().getName() + " not found");
        }

        Class clazz = obj.getClass();

        //first we try only the public because they are the most likely ones
        //to be accessed
        methods = getMethods(clazz, methodName, varargs.length);
        Object retVal = handleObjMethod(obj, methodName, methods, varargs);
        if (!methodNotFound(retVal))
        {
            return retVal;
        }

        //if not we try all of them until we have a match
        methods = getAllMethods(clazz, methodName, varargs.length);
        retVal = handleObjMethod(obj, methodName, methods, varargs);
        if (!methodNotFound(retVal))
        {
            return retVal;
        }

        throw new RuntimeException("Method of :" + methodName + " from class " + obj.getClass().getName() + " not found");
    }

    /**
     * special marker class which is a special return value indicating
     * that not method has been found which can be executed
     */
    static class _MethodNotFound
    {
    }

    /**
     * check if the return value is a method not found return val which
     * indicates we have to follow the next workflow step
     *
     * @param retVal the retVal which has to be investigated
     * @return true if the retVal is instance of _MethodNotFound false otherwise
     */
    private static boolean methodNotFound(Object retVal)
    {
        return retVal instanceof _MethodNotFound;
    }

    /**
     * executes a method in an invocation handler with a set of
     * methods which are canidates for execution
     *
     * @param objToInvoke the invokee object
     * @param methodName  the method name
     * @param methods     the methods which are under investigation for invoking
     * @param varargs     the list of varargs to be passed to the method
     * @return the result of the invocation, or an object of type _MethodNotFound otherwise
     */
    static private Object handleInvHandlerMethod(InvocationHandler objToInvoke, String methodName, Collection<Method> methods, Object... varargs)
    {
        for (Method m : methods)
        {
            if (!m.getName().equals(methodName) || m.getParameterTypes().length != varargs.length)
            {
                continue;
            }
            try
            {
                return objToInvoke.invoke(objToInvoke, m, varargs);
            }
            catch (Throwable e)
            {
                handleException(e);
            }
        }
        return new _MethodNotFound();
    }

    /**
     * executes a method on an object
     *
     * @param objToInvoke the invokee object
     * @param methodName  the method name
     * @param methods     the methods which are under investigation for invoking
     * @param varargs     the list of varargs to be passed to the method
     * @return the result of the invocation, or an object of type _MethodNotFound otherwise
     */
    static private Object handleObjMethod(Object objToInvoke, String methodName, Collection<Method> methods, Object... varargs)
    {
        for (Method m : methods)
        {
            if (!m.getName().equals(methodName) || m.getParameterTypes().length != varargs.length)
            {
                continue;
            }
            try
            {
                return m.invoke(objToInvoke, varargs);
            }
            catch (Throwable e)
            {
                handleException(e);
            }
        }
        return new _MethodNotFound();
    }

    /**
     * executes a static method on a class
     *
     * @param objToInvoke the invokee object
     * @param methodName  the method name
     * @param methods     the methods which are under investigation for invoking
     * @param varargs     the list of varargs to be passed to the method
     * @return the result of the invocation, or an object of type _MethodNotFound otherwise
     */
    static private Object handleStaticMethod(Class objToInvoke, String methodName, Collection<Method> methods, Object... varargs)
    {
        for (Method m : methods)
        {
            if (!m.getName().equals(methodName) || m.getParameterTypes().length != varargs.length)
            {
                continue;
            }
            try
            {
                return m.invoke(objToInvoke, varargs);
            }
            catch (Throwable e)
            {
                handleException(e);
            }
        }
        return new _MethodNotFound();
    }

    private static void handleException(Throwable e)
    {
        if (e instanceof IllegalAccessException)
        {
            if (_logger.isLoggable(Level.FINEST))
            {
                _logger.log(Level.FINEST, "", e);
            }
        } else if (e instanceof IllegalArgumentException)
        {
            if (_logger.isLoggable(Level.FINEST))
            {
                _logger.log(Level.FINEST, "", e);
            }
        } else
        {
            throw new RuntimeException(e);
        }
    }

    /**
     * executes a function method on a target object
     *
     * @param obj        the target object
     * @param methodName the method name
     * @param varargs    a list of objects casts or nulls defining the parameter classes and its values
     *                   if something occurs on introspection level an unmanaged exception is throw, just like
     *                   it would happen in a scripting class
     * @return the result object for the Method(method) call
     * @throws RuntimeException an unmanaged runtime exception in case of an introspection error
     */
    public static Object fastExecuteMethod(Object obj, String methodName, Object... varargs)
    {
        Class[] classes = new Class[varargs.length];
        for (int cnt = 0; cnt < varargs.length; cnt++)
        {

            if (varargs[cnt] instanceof Cast)
            {
                classes[cnt] = ((Cast) varargs[cnt]).getClazz();
                varargs[cnt] = ((Cast) varargs[cnt]).getValue();
            } else
            {
                classes[cnt] = varargs[cnt].getClass();
            }
        }

        try
        {
            Method m = fastGetMethod(obj, methodName, classes);
            return m.invoke(obj, varargs);
        }
        catch (NoSuchMethodException e)
        {
            throw new RuntimeException(e);
        }
        catch (InvocationTargetException e)
        {
            throw new RuntimeException(e);
        }
        catch (IllegalAccessException e)
        {
            throw new RuntimeException(e);
        }

    }

    /**
     * faster reflection call
     * if we know the data types exactly we can
     * trigger a direct call instead of walking through all methods
     * note this method only allows to trigger against directly declared methods
     * it ignores the inheritance hierarchy for faster access
     *
     * @param obj        the invokee object
     * @param methodName the metod name
     * @param classes    the parameter type classes
     * @return the method if found
     * @throws NoSuchMethodException in case it could not be found
     */
    public static Method fastGetMethod(Object obj, String methodName, Class[] classes) throws NoSuchMethodException
    {
        Method m;
        try
        {
            m = obj.getClass().getDeclaredMethod(methodName, classes);
        }
        catch (NoSuchMethodException e)
        {
            m = obj.getClass().getMethod(methodName, classes);
        }
        return m;
    }

    /**
     * executes a function method on a target object
     *
     * @param obj        the target object
     * @param methodName the method name
     * @param varargs    a list of objects casts or nulls defining the parameter classes and its values
     *                   if something occurs on introspection level an unmanaged exception is throw, just like
     *                   it would happen in a scripting class
     * @return the result object for the Method(method) call
     * @throws RuntimeException an unmanaged runtime exception in case of an introspection error
     */
    public static Object fastExecuteStaticMethod(Class obj, String methodName, Object... varargs)
    {
        Class[] classes = new Class[varargs.length];
        for (int cnt = 0; cnt < varargs.length; cnt++)
        {

            if (varargs[cnt] instanceof Cast)
            {
                classes[cnt] = ((Cast) varargs[cnt]).getClazz();
                varargs[cnt] = ((Cast) varargs[cnt]).getValue();
            } else
            {
                classes[cnt] = varargs[cnt].getClass();
            }
        }

        try
        {
            Method m = fastGetStaticMethod(obj, methodName, classes);
            return m.invoke(obj, varargs);
        }
        catch (NoSuchMethodException e)
        {
            throw new RuntimeException(e);
        }
        catch (InvocationTargetException e)
        {
            throw new RuntimeException(e);
        }
        catch (IllegalAccessException e)
        {
            throw new RuntimeException(e);
        }

    }

    public static Method fastGetStaticMethod(Class obj, String methodName, Class[] classes) throws NoSuchMethodException
    {
        Method m;
        try
        {
            m = obj.getDeclaredMethod(methodName, classes);
        }
        catch (NoSuchMethodException e)
        {
            m = obj.getMethod(methodName, classes);
        }
        return m;
    }

    /**
     * convenience method which makes the code a little bit more readable
     * use it in conjunction with static imports
     *
     * @param clazz the cast target for the method call
     * @param value the value object to be used as param
     * @return a Cast object of the parameters
     */
    public static Cast cast(Class clazz, Object value)
    {
        return new Cast(clazz, value);
    }

    /**
     * convenience method which makes the code a little bit more readable
     * use it in conjunction with static imports
     *
     * @param clazz the cast target for the method call
     * @return a null value Cast object of the parameters
     */
    public static Null nullCast(Class clazz)
    {
        return new Null(clazz);
    }
}
