/*
 * 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.jorphan.reflect;

import java.lang.reflect.Method;
import java.util.Arrays;

import org.apache.jorphan.logging.LoggingManager;
import org.apache.jorphan.util.JMeterError;
import org.apache.log.Logger;

/**
 * Implements function call-backs.
 * 
 * Functors may be defined for instance objects or classes.
 * 
 * The method is created on first use, which allows the invokee (class or instance)
 * to be omitted from the constructor.
 * 
 * The class name takes precedence over the instance.
 * 
 * If a functor is created with a particular instance, then that is used for all future calls;
 * if an object is provided, it is ignored. 
 * This allows easy override of the table model behaviour.
 * 
 * If an argument list is provided in the constructor, then that is ignored in subsequent invoke() calls.
 * 
 * Usage:
 * f = new Functor("methodName")
 * o = f.invoke(object) - OR -
 * o = f.invoke(object,params)
 * 
 * f2 = new Functor(object,"methodName");
 * o = f2.invoke() - OR -
 * o = f2.invoke(params)
 * 
 * f3 = new Functor(class,"methodName");
 * o = f3.invoke(object) - will be ignored
 * o = f3.invoke() - OR -
 * o = f3.invoke(params)
 * o = f3.invoke(object,params) - object will be ignored
 * 
 */
public class Functor {
	private static final Logger log = LoggingManager.getLoggerForClass();

	/*
	 * If non-null, then any object provided to invoke() is ignored.
	 */
	private final Object invokee;

	/*
	 * Class to be used to create the Method.
	 * Will be non-null if either Class or Object was provided during construction.
	 * 
	 *  Can be used instead of invokee, e.g. when using interfaces.
	*/
	private final Class clazz; 

	// Methondname must always be provided.
	private final String methodName;

	/*
	 * If non-null, then any argument list passed to invoke() will be ignored.
	 */
	private Object[] args;

	/*
	 * Argument types used to create the method.
	 * May be provided explicitly, or derived from the constructor argument list.
	 */
	private final Class[] types;

	/*
	 * This depends on the class or invokee and either args or types;
	 * it is set once by doCreateMethod(), which must be the only method to access it.
	*/
	private Method methodToInvoke;
	
	Functor(){
		throw new IllegalArgumentException("Must provide at least one argument");
	}

	/**
	 * Create a functor with the invokee and a method name.
	 * 
	 * The invokee will be used in all future invoke calls.
	 * 
	 * @param _invokee object on which to invoke the method
	 * @param _methodName method name
	 */
	public Functor(Object _invokee, String _methodName) {
		this(null, _invokee, _methodName, null, null);
	}

	/**
	 * Create a functor from class and method name.
	 * This is useful for methods defined in interfaces.
	 * 
	 * The actual invokee must be provided in all invoke() calls,
	 * and must be an instance of the class.
	 * 
	 * @param _clazz class to be used
	 * @param _methodName method name
	 */
	public Functor(Class _clazz, String _methodName) {
		this(_clazz, null, _methodName, null, null);
	}

	/**
	 * Create a functor with the invokee, method name, and argument class types.
	 * 
	 * The invokee will be ignored in any invoke() calls.
	 * 
	 * @param _invokee object on which to invoke the method
	 * @param _methodName method name
	 * @param _types
	 */
	public Functor(Object _invokee, String _methodName, Class[] _types) {
		this(null, _invokee, _methodName, null, _types);
	}

	/**
	 * Create a functor with the class, method name, and argument class types.
	 *
     * Subsequent invoke() calls must provide the appropriate ivokee object.
     *
	 * @param _clazz the class in which to find the method
	 * @param _methodName method name
	 * @param _types
	 */
	public Functor(Class _clazz, String _methodName, Class[] _types) {
		this(_clazz, null, _methodName, null, _types);
	}

	/**
	 * Create a functor with just the method name.
	 * 
	 * The invokee and any parameters must be provided in all invoke() calls.
	 * 
	 * @param _methodName method name
	 */
	public Functor(String _methodName) {
		this(null, null, _methodName, null, null);
	}

	/**
	 * Create a functor with the method name and argument class types.
	 * 
	 * The invokee must be provided in all invoke() calls
	 * 
	 * @param _methodName method name
	 * @param _types parameter types
	 */
	public Functor(String _methodName, Class[] _types) {
		this(null, null, _methodName, null, _types);
	}

	/**
	 * Create a functor with an invokee, method name, and argument values.
	 * 
	 * The invokee will be ignored in any invoke() calls.
	 * 
	 * @param _invokee object on which to invoke the method
	 * @param _methodName method name
	 * @param _args arguments to be passed to the method
	 */
	public Functor(Object _invokee, String _methodName, Object[] _args) {
		this(null, _invokee, _methodName, _args, null);
	}

	/**
	 * Create a functor from method name and arguments.
	 *  
	 * The class will be determined from the first invoke call.
	 * All invoke calls must include a target object;
	 * which must be of the same type as the initial invokee.
	 * 
	 * @param _methodName method name
	 * @param _args
	 */
	public Functor(String _methodName, Object[] _args) {
		this(null, null, _methodName, _args, null);
	}

	/**
	 * Create a functor from various different combinations of parameters.
	 * 
	 * @param _clazz class containing the method
	 * @param _invokee invokee to use for the method call
	 * @param _methodName the method name (required)
	 * @param _args arguments to be used
	 * @param _types types of arguments to be used
	 * 
	 * @throws IllegalArgumentException if:
	 * - methodName is null
	 * - both class and invokee are specified
	 * - both arguments and types are specified
	 */
	private Functor(Class _clazz, Object _invokee, String _methodName, Object[] _args, Class[] _types) {
		if (_methodName == null){
			throw new IllegalArgumentException("Methodname must not be null");
		}
		if (_clazz != null && _invokee != null){
			throw new IllegalArgumentException("Cannot provide both Class and Object");
		}
		if (_args != null && _types != null){
			throw new IllegalArgumentException("Cannot provide both arguments and argument types");
		}
		// If class not provided, default to invokee class, else null
		this.clazz = _clazz != null ? _clazz : (_invokee != null ? _invokee.getClass() : null);
		this.invokee = _invokee;
		this.methodName = _methodName;
		this.args = _args;
		// If types not provided, default to argument types, else null
		this.types = _types != null ? _types : (_args != null ? _getTypes(_args) : null);
	}

	//////////////////////////////////////////
	
	/*
	 * Low level invocation routine.
	 * 
	 * Should only be called after any defaults have been applied.
	 * 
	 */
	private Object doInvoke(Class _class, Object _invokee, Object[] _args) {
		Class[] argTypes = getTypes(_args);
		try {
			Method method = doCreateMethod(_class , argTypes);
			if (method == null){
	            final String message = "Can't find method "
	                +_class.getName()+"#"+methodName+typesToString(argTypes);
	            log.error(message, new Throwable());
	            throw new JMeterError(message);	                
			}
			return method.invoke(_invokee, _args);
		} catch (Exception e) {
			final String message = "Trouble functing: "
				+_class.getName()
				+"."+methodName+"(...) : "
				+" invokee: "+_invokee
				+" "+e.getMessage();
			log.warn(message, e);
			throw new JMeterError(message,e);
		}
	}

	/**
	 * Invoke a Functor, which must have been created with either a class name or object.
	 * 
	 * @return the object if any
	 */
	public Object invoke() {
		if (invokee == null) {
			throw new IllegalStateException("Cannot call invoke() - invokee not known");
		}
		// If invokee was provided, then clazz has been set up
		return doInvoke(clazz, invokee, getArgs());
	}

	/**
	 * Invoke the method on a given object.
	 * 
	 * @param p_invokee - provides the object to call; ignored if the class or object were provided to the constructor
	 * @return the value
	 */
	public Object invoke(Object p_invokee) {
		return invoke(p_invokee, getArgs());
	}

	/**
	 * Invoke the method with the provided parameters.
	 * 
	 * The invokee must have been provided in the constructor.
	 * 
	 * @param p_args parameters for the method
	 * @return the value
	 */
	public Object invoke(Object[] p_args) {
		if (invokee == null){
			throw new IllegalStateException("Invokee was not provided in constructor");
		}
		// If invokee was provided, then clazz has been set up
		return doInvoke(clazz, invokee, args != null? args : p_args);
	}

	/**
	 * Invoke the method on the invokee with the provided parameters.
	 * 
	 * The invokee must agree with the class (if any) provided at construction time.
	 * 
	 * If the invokee was provided at construction time, then this invokee will be ignored.
	 * If actual arguments were provided at construction time, then arguments will be ignored.
	 * 
	 */
	public Object invoke(Object p_invokee, Object[] p_args) {
		return doInvoke(clazz != null ? clazz : p_invokee.getClass(), // Use constructor class if present
				       invokee != null ? invokee : p_invokee, // use invokee if provided
						args != null? args : p_args);// use argumenrs if provided
	}

	/*
	 * Low-level (recursive) routine to define the method - if not already defined.
	 * Synchronized to protect access to methodToInvoke.
	 */
	private synchronized Method doCreateMethod(Class p_class, Class[] p_types) {
		if (log.isDebugEnabled()){
		    log.debug("doCreateMethod() using "+this.toString()
		    	+"class="
				+ p_class.getName()
				+ " types: " + Arrays.asList(p_types));
		}
		if (methodToInvoke == null) {
			try {
				methodToInvoke = p_class.getMethod(methodName, p_types);
			} catch (Exception e) {
				for (int i = 0; i < p_types.length; i++) {
					Class primitive = getPrimitive(p_types[i]);
					if (primitive != null) {
						methodToInvoke = doCreateMethod(p_class, getNewArray(i, primitive, p_types));
						if (methodToInvoke != null) {
							return methodToInvoke;
						}
					}
					Class[] interfaces = p_types[i].getInterfaces();
					for (int j = 0; j < interfaces.length; j++) {
						methodToInvoke = doCreateMethod(p_class,getNewArray(i, interfaces[j], p_types));
						if (methodToInvoke != null) {
							return methodToInvoke;
						}
					}
					Class parent = p_types[i].getSuperclass();
					if (parent != null) {
						methodToInvoke = doCreateMethod(p_class,getNewArray(i, parent, p_types));
						if (methodToInvoke != null) {
							return methodToInvoke;
						}
					}
				}
			}
		}
		return methodToInvoke;
	}

	/**
	 * Check if a read Functor method is valid.
	 * 
	 * @deprecated ** for use by Unit test code only **
	 * 
	 * @return true if method exists
	 */
	public boolean checkMethod(Object _invokee){
		Method m = null;
		try {
		    m = doCreateMethod(_invokee.getClass(), getTypes(args));
		} catch (Exception e){
			// ignored
		}
		return null != m;
	}

	/**
	 * Check if a write Functor method is valid.
	 * 
	 * @deprecated ** for use by Unit test code only **
	 * 
	 * @return true if method exists
	 */
	public boolean checkMethod(Object _invokee, Class c){
		Method m = null;
		try {
		    m = doCreateMethod(_invokee.getClass(), new Class[]{c});
		} catch (Exception e){
			// ignored
		}
		return null != m;
	}

	public String toString(){
		StringBuffer sb = new StringBuffer(100);
		if (clazz != null){
			sb.append(clazz.getName());
		}
		if (invokee != null){
		    sb.append("@");
		    sb.append(System.identityHashCode(invokee));
		} 
		sb.append(".");
		sb.append(methodName);
		typesToString(sb,types);
		return sb.toString();
	}

	private void typesToString(StringBuffer sb,Class[] _types) {
		sb.append("(");
		if (_types != null){
			for(int i=0; i < _types.length; i++){
				if (i>0) {
				    sb.append(",");
				}
				sb.append(_types[i].getName());
			}
		}
		sb.append(")");
	}

	private String typesToString(Class[] argTypes) {
		StringBuffer sb = new StringBuffer();
		typesToString(sb,argTypes);
		return sb.toString();
	}

	private Class getPrimitive(Class t) {
		if (t==null) {
		    return null;
		}
		if (t.equals(Integer.class)) {
			return int.class;
		} else if (t.equals(Long.class)) {
			return long.class;
		} else if (t.equals(Double.class)) {
			return double.class;
		} else if (t.equals(Float.class)) {
			return float.class;
		} else if (t.equals(Byte.class)) {
			return byte.class;
		} else if (t.equals(Boolean.class)) {
			return boolean.class;
		} else if (t.equals(Short.class)) {
			return short.class;
		} else if (t.equals(Character.class)) {
			return char.class;
		}
		return null;
	}

	private Class[] getNewArray(int i, Class replacement, Class[] orig) {
		Class[] newArray = new Class[orig.length];
		for (int j = 0; j < newArray.length; j++) {
			if (j == i) {
				newArray[j] = replacement;
			} else {
				newArray[j] = orig[j];				
			}
		}
		return newArray;
	}

	private Class[] getTypes(Object[] _args) {
		if (types == null)
		{
			return _getTypes(_args);
		}
		return types;
	}

	private static Class[] _getTypes(Object[] _args) {
		Class[] _types;
		if (_args != null) {
			_types = new Class[_args.length];
			for (int i = 0; i < _args.length; i++) {
				_types[i] = _args[i].getClass();
			}
		} else {
			_types = new Class[0];
		}
		return _types;
	}

	private Object[] getArgs() {
		if (args == null) {
			args = new Object[0];
		}
		return args;
	}
}
