package edu.uprm.admg.nettraveler.schema;

//JDK imports
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Arrays;

import edu.uprm.admg.nettraveler.type.MDObject;
import edu.uprm.admg.util.DataVerify;


/**
 * <code>MethodDescriptor</code> implements the mechanism used to
 * execute the user-defined functions contained in generalized projections
 * and predicates. This mechanism is based on the Java Reflection feature.
 * Each method descriptor contains the name of the method, the class
 * containing the implementation of the method, the types for the parameters
 * (if any) for the method, and the type of its result value.
 * </P>
 * Example: Evaluation of method <code>MIInteger foo(MIInteger x)</code> in 
 * class <code>MyClass.java</code>
 * <PRE>
 * . . .
 * MethodDescriptor methodDesc;
 * Class methodContainer;
 * Class[] paramType;
 * Class resType;
 * MDObject[] args;
 * MIInteger result;
 * ...
 * // Create and initialize
 * methodContainer = Class.forName("MyClass");
 * paramType = new Class[1];
 * paramType[0] = Class.forName("edu.uprm.admg.nettraveler.type.MIInteger");
 * resType = Class.forName("edu.uprm.admg.nettraveler.type.MIInteger");
 * methodDesc = new MethodDesc("foo", methodContainer, paramType, resType);
 * methodDesc.initializeMethod();
 * ...
 * // Prepare arguments and invoke the method
 * args = new MDObject[1];
 * args[0] = new MIInteger(2000);
 * result = (MIInteger) method_desc.invokeMethod(args);
 *
 * </PRE>
 * 
 * @author M. Rodriguez-Martinez
 * @see java.lang.reflect.Method
 */
public final class MethodDescriptor implements Serializable{
	/**
	 * Serial version
	 */
	private static final long serialVersionUID = -73613588447650782L;
	
	/**
	 * The name of the method.
	 */
	private String methodName;
	
	/**
	 * The Java class containing the definition of the method.
	 */
	private Class  methodClass;
	
	/**
	 * The Java classes for the parameters (if any) of this method.
	 */
	private Class[] methodParamTypes = null;
	
	/**
	 * The Java class for the return type of this method.
	 */
	private Class methodReturnType;
	
	/**
	 * The Method object used to invoke the method.
	 */
	private transient Method methodHandler = null;
	/** Tells if this method has been initialized **/
	private transient boolean initialized = false;
	
	
	/**
	 * Constructs a <code>MethodDescriptor</code> from: a method
	 * name, the <code>Class</code> object for the class that contains 
	 * the implementation of the method, an array of parameter types
	 * and a return type for the method result.
	 * @param name the name of the method to be associated with this descriptor
	 * @param container the class that contains the implementation of the method
	 * @param paramTypes the types of the parameters for the method
	 * @param returnType the type of the result returned by the method
	 */
	public MethodDescriptor(String name, Class container, 
			Class[] paramTypes, Class returnType) {
		// sanity checks
		DataVerify.VerifyObjectParam("name", name);
		DataVerify.VerifyObjectParam("container", container);
		DataVerify.VerifyObjectParam("returnType", returnType);
		// paramTypes can be null: when the method has no arguments
		this.methodName = name;
		this.methodClass = container;
		if(paramTypes != null){
			DataVerify.VerifyArrayParam("paramTypes", paramTypes);
			this.methodParamTypes = new Class[paramTypes.length];
			System.arraycopy(paramTypes, 0, this.methodParamTypes, 0, 
					this.methodParamTypes.length);
		}
		this.methodReturnType = returnType;
	}
	
	
	/**
	 * Initializes the method associated with this descriptor and makes
	 * it ready to be invoked. A <code>MethodInitializeException</code>
	 * is thrown if:
	 * <ul>
	 * <li> The method cannot be found in the container class, based on its
	 * method name and parameter types.
	 * <li> The security restrictions in place block the initialization of 
	 * the method.
	 * </ul>
	 * <p/>
	 * Calling this method on an already initialized method is a no-op.
	 * 
	 * @return <code>true</code> if the method was initialized for the first 
	 * time.
	 * @exception MethodInitializeException if an error occurs 
	 * while initializing the method (see discussion above).
	 */
	public boolean initializeMethod() throws MethodInitializeException {
		if(initialized)
			return false;
		try {
			methodHandler = methodClass.getDeclaredMethod(methodName,  
					methodParamTypes);
			initialized = true;
			return initialized;
		}
		catch(NoSuchMethodException e1){
			throw 
			new MethodInitializeException
			("Method " + methodName + " could not be found " +
					"in class" + methodClass.getName() + 
			" with the specified argument types.");
			
		}
		catch(SecurityException e2){
			throw 
			new MethodInitializeException 
			("Security restrictions blocked the initialization of " +
					" method " + methodName + " in class "  + 
					methodClass.getName());
		}
	}
	
	/**
	 * Invokes the method associated with this descriptor based on a 
	 * set of arguments and produces a result value. The arguments array
	 * must be null if the method receives no arguments.
	 * @param args the arguments for the method associated with this descriptor
	 * @return the result of invoking the method on the given arguments
	 * @exception MethodInvocationException if an error occurs while executing the body of the method.
	 */
	public MDObject invokeMethod(MDObject[] args) throws MethodInvocationException {
		MDObject result = null;
		if (!initialized){
			throw new IllegalStateException("Method " + methodName + 
			" has not been initialized.");
		}
		
		try {
			// call the method with the given argument and return result
			// Note: args can be null, for example, if the method has 
			// no arguments.
			result = (MDObject) methodHandler.invoke(null, (Object[]) args);
			return result;
		}
		catch(Exception e){
			throw new 
			MethodInvocationException("Error found while invoking method "
					+ methodName +  ". Reason: " + 
					e.toString());
		}
	} 
	
	/**
	 * Returns the name of the method associated with this 
	 * method descriptor.
	 * @return the name of the method associated with this descriptor
	 */
	public String getMethodName(){
		return methodName;
	}
	
	/**
	 * Returns the <code>Class</code> object for the class that
	 * contains the implementation of the method associated with 
	 * this descriptor.
	 * @return the class object for the class that contains the implementation of the method associated with this descriptor 
	 */
	public Class getMethodContainer(){
		return methodClass;
	}
	
	/**
	 * Returns the parameter types for the method associated with this 
	 * descriptor. The parameter types are represented as an array of
	 * <code>Class</code> objects, with one entry for each parameter to
	 * the method. A value of <code>null</code> indicates a method with
	 * no parameters.
	 * @return the parameter types for the method associated with this descriptor
	 */
	public Class[] getParamTypes(){
		return methodParamTypes;
	}
	
	/**
	 * Returns the type of the object that is returned by each call to the
	 * method associated with this descriptor.
	 * @return the result type of the method associated with this descriptor
	 */
	public Class getResultType(){
		return methodReturnType;
	}
	/**
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	public boolean equals(Object obj){
		if(obj == this)
			return true;
		if(!(obj instanceof MethodDescriptor))
			return false;
		MethodDescriptor o = (MethodDescriptor) obj;
		return ((methodName.equals(o.methodName))
				&& (methodClass.equals(o.methodClass))
				&& (methodReturnType.equals(o.methodReturnType))
				&& (Arrays.equals(methodParamTypes, o.methodParamTypes)));
	}
	/**
	 * @see java.lang.Object#hashCode()
	 */
	public int hashCode(){
		int result = 17;
		result += result * 37 + methodName.hashCode();
		result += result * 37 + methodClass.hashCode();
		result += result * 37 + methodReturnType.hashCode();
		result += result * 37 + paramTypesHashCode();
		return result;
	}
	/**
	 * 
	 */
	private int paramTypesHashCode(){
		if(methodParamTypes == null)
			return 0;
		int result = 17;
		for(int i=0; i<methodParamTypes.length; i++)
			result += result * 37 + methodParamTypes[i].hashCode();
		return result;
	}
	/**
	 * @return <code>true</code> if this method is initialized and ready
	 * to be invoked.
	 */
	public boolean isInitialized() {
		return initialized;
	}
	/**
	 * 
	 * @param in
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
		in.defaultReadObject();
		try{
			DataVerify.VerifyObjectParam("Method Name", methodName);
			DataVerify.VerifyObjectParam("Method cClass", methodClass);
			if(methodParamTypes != null){
				Class[] params = new Class[methodParamTypes.length];
				System.arraycopy(methodParamTypes,0,params,0,params.length);
				methodParamTypes = params;
				DataVerify.VerifyArrayParam("Parameter Types", methodParamTypes);
			}
			DataVerify.VerifyObjectParam("Return Type", methodReturnType);
//			this.initializeMethod();
		}catch(Exception e){
			throw new IOException(e.toString());
		}
	}
}

