// $Id: Operation.java,v 1.26 1998/06/08 11:46:47 oliva Exp $

/* Copyright 1997,1998 Alexandre Oliva <oliva@dcc.unicamp.br>
 *
 * This file is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

package BR.unicamp.Guarana;

import java.lang.reflect.Method;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

/** A meta-level representation of an Operation.  There are only two
    ways an Operation can be created: it is either reified from the
    base level or created by an OperationFactory.

    @see OperationFactory
   
    @author Alexandre Oliva
    @version $Revision: 1.26 $ */
public final class Operation {
    static {
        System.loadLibrary("guarana");
    }

    /** If <tt>(op.getOpType() == nop)</tt>, op is a do-nothing
	operation.  */
    public static final int nop = 0;

    /** If <tt>((op.getOpType() &amp; Operation.invocation) != 0)</tt>,
	op is either a method or a constructor invocation.  */
    public static final int invocation = 2;

    /** If <tt>(op.getOpType() == Operation.methodInvocation)</tt>, op
	is a method invocation.  */
    public static final int methodInvocation = 2;

    /** If <tt>(op.getOpType() ==
	Operation.constructorInvocation)</tt>, op is a constructor
	invocation.  */
    public static final int constructorInvocation = 3;

    /** If <tt>((op.getOpType() &amp; Operation.synchronization) !=
	0)</tt>, op is either a monitor enter or a monitor exit
	Operation.  */
    public static final int synchronization = 4;

    /** If <tt>(op.getOpType() == Operation.monitorEnter)</tt>, op is a
	monitor enter Operation.  */
    public static final int monitorEnter = 4;

    /** If <tt>(op.getOpType() == Operation.monitorExit)</tt>, op is a
	monitor exit Operation.  */
    public static final int monitorExit = 5;

    /** If <tt>((op.getOpType() &amp; Operation.readWriteMask) ==
	Operation.read)</tt>, op is either a field read, an array
	element read or an array length Operation.  */
    public static final int read = 8;

    /** If <tt>((op.getOpType() &amp; Operation.readWriteMask) ==
	Operation.write)</tt>, op is either a field write or an array
	element write Operation.  */
    public static final int write = 9;

    /** Should be used as a bit mask to check whether an operation is a
	read operation or a write operation.  If <tt>((op.getOpType()
	&amp; Operation.readWriteMask) == 0)</tt>, op is neither a read
	nor a write Operation.  */
    public static final int readWriteMask = read | write;

    /** If <tt>((op.getOpType() &amp; Operation.field) != 0)</tt>, op is
	either a field read or a field write Operation.  */
    public static final int field = 16;

    /** If <tt>((op.getOpType() &amp; Operation.array) != 0)</tt>, op is
	either an array element read, an array element write or an array
	length Operation.  */
    public static final int array = 32;

    /** If <tt>((op.getOpType() &amp; Operation.arrayMask) ==
	Operation.array)</tt>, op is either an array element read or an
	array element write Operation.  */
    public static final int arrayMask = 64|array;

    /** If <tt>(op.getOpType() == Operation.arrayLength)</tt>, op is an
	array length Operation.  */
    public static final int arrayLength = arrayMask|read;

    /** If <tt>(op.getOpType() == fieldRead)</tt>, op is a field read
	Operation.  */
    public static final int fieldRead = field|read;

    /** If <tt>(op.getOpType() == fieldWrite)</tt>, op is a field write
	Operation.  */
    public static final int fieldWrite = field|write;

    /** If <tt>(op.getOpType() == arrayRead)</tt>, op is an array
	element read Operation.  */
    public static final int arrayRead = array|read;

    /** If <tt>(op.getOpType() == arrayWrite)</tt>, op is an array
	element write Operation.  */
    public static final int arrayWrite = array|write;

    /** Must never be instantiated directly.  */
    private Operation() {}
  
    /** Just an alternate way of calling Guarana.perform(this).

	@return the Result of the Operation.

	@see Guarana#perform  */
    public Result perform() {
	return Guarana.perform(this);
    }

    /** Checks whether it is a valid Operation, that is, one that can be
	performed.

	@exception NoSuchMethodError if the target Object is not
	an instance of the Class that declares the Method or the
	Constructor.

	@exception AbstractMethodError if the Method is abstract.

	@exception IllegalArgumentException if the argument types do not
	match the Method or Constructor signature, or the number of
	arguments does not match.

	@exception NoSuchFieldError if the target Object is not an
	instance of the Class that declares the Field.

	@exception IllegalArgumentException if the value to be stored in
	the Field or array element is not assignable to the Field type.

	@exception IllegalArgumentException if the target Object is not
	an Array, but the Operation is an Array Operation.

	@exception ArrayIndexOutOfBoundsException if the specified array
	element does not exist.  */
    public native void validate()
	throws NoSuchMethodError, AbstractMethodError,
	       IllegalArgumentException, NoSuchFieldError,
	       ArrayIndexOutOfBoundsException;

    /** Obtains the target Object of the Operation.

	@return the target Object.  */
    public native Object getObject();
  
    /** Obtains the Thread in which this Operation is to be executed.

	@return the Thread that should execute this Operation.  */
    public native Thread getThread();
  
    /** Obtains the expected type of the result of an Operation.
	Constructor invocations and synchronization operations, as well
	as field and array element write operations, are assumed to
	return void.  Array length Operations always return int.

	@return the type the Operation is expected to Result under
	normal control flow.  */
    public native Class getType();

    /** Checks whether the Operation is a static method invocation or
	static field access.

	<p>Note: synchronization Operations are never considered static,
	since an invocation of a synchronized static method enters the
	monitor associated with the Class object that represents the
	class.

	@return true if and only if the method to be invoked or the
	field to be accessed is static.  */
    public native boolean isClassOperation();

    /** Obtains a numeric value that may be used to check whether the
	Operation is a method or constructor invocation, a
	synchronization Operation, a field or
	array element read or write Operation, or an array length
	Operation.  */
    public native int getOpType();

    /** Checks whether this is a method invocation Operation.

	@return true if and only if this is a method invocation
	Operation.  */
    public native boolean isMethodInvocation();

    /** Obtains the method that will be invoked by this Operation.

	@return the method to be invoked, or null if this is not a
	method invocation Operation.  */
    public native Method getMethod();

    /** Checks whether this is a constructor invocation Operation.

	@return true if and only if this is a constructor invocation
	Operation.  */
    public native boolean isConstructorInvocation();

    /** Obtains the constructor that will be invoked by this Operation.

	@return the constructor to be invoked, or null if this is not a
	constructor invocation Operation.  */
    public native Constructor getConstructor();

    /** Obtains the argument list to be used when invoking a Method or a
	Constructor.  Changing the references in the returned array has
	no effect on the invocation, but the references are to the
	actual arguments, so any Operation that changes such Objects
	will affect the actual invocation.

	@return the argument list that will be passed to the Method or
	to the Constructor, as an array of Objects, or null if this is
	not a Method nor Constructor invocation Operation.  */
    public native Object[] getArguments();

    /** Checks whether this is a synchronization Operation.

	@return true if and only if this is a synchronization
	Operation.  */
    public native boolean isSynchronization();

    /** Checks whether this is a monitor enter Operation.

	@return true if and only if this is a monitor enter Operation.
    */
    public native boolean isMonitorEnter();

    /** Checks whether this is a monitor exit Operation.

	@return true if and only if this is a monitor exit Operation.
    */
    public native boolean isMonitorExit();

    /** Checks whether this is a field or array element read or array
	length Operation.

	@return true if and only if this is a field or array element
	read or array length Operation.  */
    public native boolean isReadOperation();

    /** Checks whether this is a field or array element write Operation.

	@return true if and only if this is a field or array element
	change Operation.  */
    public native boolean isWriteOperation();

    /** Checks whether this is a field Operation.  Array Operations are
	not considered Field Operations

	@return true if and only if this is a field Operation.  */
    public native boolean isFieldOperation();

    /** Obtains the Field this Operation reads from or writes to.

	@return the Field, or null if this is not a Field Operation.  */
    public native Field getField();

    /** Checks whether this is an Array element read or write Operation.

	@return true if and only if this is an Array element read or
	write Operation.  */
    public native boolean isArrayOperation();

    /** Checks whether this is an Array length Operation.

	@return true if and only if this is the Operation that obtains
	the length of an Array.  */
    public native boolean isArrayLengthOperation();

    /** Obtains the array index this Operation reads from or writes to.

	@return the array index, or -1 if this is not an array
	Operation.  */
    public native int getArrayIndex();

    /** Obtains the value that will be written to the Field or Array
	element.

	@return the value to be written, or null if this is not a
	Write Operation.  The only way to distinguish between a
	non-Write Operation and a null value to be written is by
	invoking isWriteOperation().

	@see Operation#isWriteOperation */
    public native Object getValue();

    /** Checks whether this is a replacement Operation.

	@return true if and only if this is a replacement Operation.  */
    public native boolean isReplacement();

    /** Obtains the Operation this Operation replaced.

	@return a reference to the replaced Operation, or null if this
	is not a replacement one.  */
    public native Operation getReplaced();

    /** Checks whether an Operation is the same as or a replacement of
	the given Operation.

	@param operation the Operation that should be looked for in the
	replacement chain.

	@return true if the Operation is found, false otherwise.  */
    public boolean replaced(final Operation operation) {
	for(Operation op = this; op != null; op = op.getReplaced())
	    if (op == operation)
		return true;
	return false;
    }

    /** Obtains the original Operation from a replacement chain.  That
	is, if Operation n replaced Operation n-1 that replaced
	Operation n-2 ... that replaced Operation 1, obtain Operation 1.

	@return the last Operation in a replacement chain.  */
    public Operation getOriginal() {
	if (!isReplacement())
	    return this;
	else
	    return getReplaced().getOriginal();
    }

    /** Returns a String representation of the Operation.

	@exception IllegalArgumentException if <tt>getOpType()</tt>
	returns an invalid Operation type.  */
    public String toString() {
	final StringBuffer op = new StringBuffer();
	final boolean isStatic = isClassOperation();
	if (isStatic)
	    op.append(Guarana.getClassName((Class)getObject()));
	else
	    op.append(Guarana.toString(getObject()));
	switch (getOpType()) {
	case nop:
	    op.append(".<nop>");
	    break;
	case methodInvocation: {
	    op.append('.');
	    final Method m = getMethod();
	    if (!isStatic)
		op.append(Guarana.getClassName(m.getDeclaringClass())).append('.');
	    op.append(m.getName()).append('(');
	    final Class[] parms = m.getParameterTypes();
	    final Object[] args = getArguments();
	    for(int i = 0, lp = parms.length, la = args.length;
		i < lp || i < la; ++i) {
		if (i > 0)
		    op.append(',');
		if (i < lp) {
		    final Class parm = parms[i];
		    if (parm.isPrimitive()) {
			op.append(parm).append(' ');
			if (i < la)
			    op.append(args[i]);
			else
			    op.append('?');
		    } else {
			op.append(parm.getName()).append(' ');
			if (i < la)
			    op.append(Guarana.toString(args[i]));
			else
			    op.append('?');
		    }
		} else
		    op.append('?').append(' ').
			append(Guarana.toString(args[i]));
	    }
	    op.append(')');
	    break;
	}
	case constructorInvocation: {
	    op.append('.');
	    final Constructor c = getConstructor();
	    op.append(Guarana.getClassName(c.getDeclaringClass())).append('(');
	    final Class[] parms = c.getParameterTypes();
	    final Object[] args = getArguments();
	    for(int i = 0, lp = parms.length, la = args.length;
		i < lp || i < la; ++i) {
		if (i > 0)
		    op.append(',');
		if (i < lp) {
		    final Class parm = parms[i];
		    if (parm.isPrimitive()) {
			op.append(parm).append(' ');
			if (i < la)
			    op.append(args[i]);
			else
			    op.append('?');
		    } else {
			op.append(parm.getName()).append(' ');
			if (i < la)
			    op.append(Guarana.toString(args[i]));
			else
			    op.append('?');
		    }
		} else
		    op.append('?').append(' ').
			append(Guarana.toString(args[i]));
	    }
	    op.append(')');
	    break;
	}
	case monitorEnter:
	    op.append(".<monitor enter>");
	    break;
	case monitorExit:
	    op.append(".<monitor exit>");
	    break;
	case fieldRead: {
	    op.append('.');
	    final Field f = getField();
	    if (!isStatic)
		op.append(Guarana.getClassName(f.getDeclaringClass())).append('.');
	    op.append(getField().getName());
	    break;
	}
	case fieldWrite: {
	    op.append('.');
	    final Field f = getField();
	    if (!isStatic)
		op.append(Guarana.getClassName(f.getDeclaringClass())).append('.');
	    op.append(f.getName()).append('=');
	    final Object value = getValue();
	    if (f.getType().isPrimitive())
		op.append(value);
	    else
		op.append(Guarana.toString(value));
	    break;
	}
	case arrayLength:
	    op.append(".length");
	    break;
	case arrayRead:
	    op.append('[').append(getArrayIndex()).append(']');
	    break;
	case arrayWrite: {
	    op.append('[').append(getArrayIndex()).append(']').
		append('=');
	    final Object value = getValue();
	    if (Guarana.getClass(getObject()).getComponentType().isPrimitive())
		op.append(value);
	    else
		op.append(Guarana.toString(value));
	    break;
	}
	default:
	    throw new IllegalArgumentException("unknown operation type");
	}
	return op.toString();
    }
}
