/**
 * Copyright (c) 2006 - 2009 Smaxe Ltd (www.smaxe.com).
 * All rights reserved.
 */
package com.smaxe.uv.invoker.support;

import com.smaxe.uv.invoker.IMethodInvoker;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;

/**
 * <code>MethodInvoker</code> - very simple, general and, of course,
 * inefficient {@link IMethodInvoker} implementation.
 * 
 * @author Andrei Sochirca
 */
public final class MethodInvoker extends Object implements IMethodInvoker
{
    // fields
    private final Executor executor;
    private final Map<Class, Class> primitives;
    
    private final String method;
    
    /**
     * Constructor.
     */
    public MethodInvoker()
    {
        this(null /*executor*/, ".client()");
    }
    
    /**
     * Constructor.
     * 
     * @param method
     */
    public MethodInvoker(final String method)
    {
        this(null /*executor*/, method);
    }
    
    /**
     * Constructor.
     * 
     * @param executor executor to use for method invocation
     * @param method class method
     */
    public MethodInvoker(final Executor executor, final String method)
    {
        this.method = method;
        this.executor = executor;
        this.primitives = createPrimitives();
    }
    
    public void invoke(final Object o, final String method, final ICallback callback, final Object... args)
    {
        final String methodToInvoke = method.indexOf('|') < 0 ? method : method.replace('|', '_');
        
        final Runnable task = new Runnable()
        {
            @SuppressWarnings("unchecked")
            public void run()
            {
                try
                {
                    final List<Method> methods = findMethods(o.getClass().getMethods(), methodToInvoke);
                    
                    switch (methods.size())
                    {
                        case 0:
                        {
                            final String clazz = o.getClass().getSimpleName();
                            
                            throw new NoSuchMethodException("Method '" + method +
                                    "' is not found in the " + clazz + MethodInvoker.this.method + " class. " +
                                    "Please define method '" + methodToInvoke + "' in the " +
                                    clazz + MethodInvoker.this.method + " class that accepts parameters " + Arrays.toString(args));
                        }
                        case 1:
                        {
                            final Method method = methods.get(0);
                            final Class[] parameterTypes = method.getParameterTypes();
                            
                            Object result = null;
                            
                            if (parameterTypes.length == 1 && (parameterTypes[0] == Object[].class))
                            {
                                result = method.invoke(o, new Object[] {args});
                            }
                            else
                            {
                                result = method.invoke(o, args);
                            }
                            
                            if (callback != null) callback.onResult(result);
                        } break;
                        default:
                        {
                            Method method = null;
                            
                            for (Method m : methods)
                            {
                                final Class[] parameterTypes = m.getParameterTypes();
                                if (parameterTypes.length != args.length) continue;
                                
                                boolean success = true;
                                
                                for (int i = 0; i < args.length; i++)
                                {
                                    final Object value = args[i];
                                    if (value == null) continue;
                                    
                                    final Class parameterType = getParameterClass(parameterTypes[i]);
                                    
                                    if (!parameterType.isAssignableFrom(value.getClass()))
                                    {
                                        success = false;
                                        break;
                                    }
                                }
                                
                                if (success)
                                {
                                    method = m;
                                    break;
                                }
                            }
                            
                            if (method == null) throw new NoSuchMethodException("Method '" + methodToInvoke + "' has different signature in the " + o.getClass());
                            
                            final Object result = method.invoke(o, args);
                            
                            if (callback != null) callback.onResult(result);
                        }
                    }
                }
                catch (Exception e)
                {
                    if (callback != null) callback.onException(e);
                }
            }
        };
        
        if (executor == null)
        {
            task.run();
        }
        else
        {
            executor.execute(task);
        }
    }
    
    // inner use methods
    /**
     * Returns parameter class.
     * 
     * @param clazz
     * @return parameter class
     */
    private Class getParameterClass(final Class clazz)
    {
        final Class c = primitives.get(clazz);
        
        return c == null ? clazz : c;
    }
    
    /**
     * Creates and returns primitive -> class mapping.
     * 
     * @return primitive -> class mapping
     */
    private Map<Class, Class> createPrimitives()
    {
        Map<Class, Class> map = new HashMap<Class, Class>(32);
        
        map.put(Boolean.TYPE, Boolean.class);
        map.put(Character.TYPE, Number.class);
        map.put(Byte.TYPE, Number.class);
        map.put(Short.TYPE, Number.class);
        map.put(Integer.TYPE, Number.class);
        map.put(Long.TYPE, Number.class);
        map.put(Float.TYPE, Number.class);
        map.put(Double.TYPE, Number.class);
        
        return map;
    }
    
    /**
     * Finds methods defined by <code>name</code>.
     * 
     * @param methods
     * @param name
     * @return methods
     */
    private static List<Method> findMethods(final Method[] methods, final String name)
    {
        List<Method> methodsList = new ArrayList<Method>(methods.length);
        
        for (Method method : methods)
        {
            if (name.equals(method.getName())) methodsList.add(method);
        }
        
        return methodsList;
    }
}