 // Copyright (c) 2004  Per M.A. Bothner.
// This is free software;  for terms and warranty disclaimer see ./COPYING.

package gnu.mapping;

/**
 * The abstract parent for all Scheme functions. The implementations of this
 * abstract class define the behavior these scheme functions.
 * 
 * @author Per Bothner
 * @author Paul Wowk (Comments)
 */

public abstract class Procedure extends PropertySet
{
	// Internal key for accessing the source location, for the property set.
	private static final String sourceLocationKey = "source-location";

	// Internal key for accessing the setter, used by the property set.
	private static final String setterKey = "setter";

	/**
	 * Sets the source location and stores in the propertyset.
	 * @param file The filename of the source file.
	 * @param line The line number of the source.
	 */
	public void setSourceLocation(String file, int line)
	{
		setProperty(sourceLocationKey, file + ":" + line);
	}

	/**
	 * Gets the source location of this procedure.
	 * @return Returns the source location, should be in the format 
	 * "filename:linenumber".
	 */
	public String getSourceLocation()
	{
		Object value = getProperty(sourceLocationKey, null);
		return value == null ? null : value.toString();
	}

	/**
	 * Default constructor, does nothing.
	 */
	public Procedure()
	{
	}

	/**
	 * Constructor that takes the name of this procedure.  Calls setName() in
	 * the PropertySet class.
	 * @param n The name of this procedure, 
	 */
	public Procedure(String n)
	{
		setName(n);
	}

	/**
	 * Takes in any number of arguments, uses or does something with these
	 * objects and returns something that used the arguments.  Can manipulate
	 * the arguments or use the arguments to create new objects.  Casting will
	 * most likely be required in the implementations.
	 * 
	 * @param args Argument array, must be the right type for the implementation.
	 * @return An object, implementation defines what kind of object.
	 * @throws Throwable Blanket throwable, bad practice.
	 */
	public abstract Object applyN(Object[] args) throws Throwable;

	/**
	 * Takes in no arguments and returns a object based on the state of the 
	 * implementation.  
	 * 
	 * @return An object, implementation defines what kind of object.
	 * @throws Throwable Blanket throwable, bad practice.
	 */
	public abstract Object apply0() throws Throwable;

	/**
	 * Takes in exactly one argument, uses or does something with the object, and
	 * returns something that used the argument.  Can manipulate the supplied
	 * object and can create new objects using the supplied object.  Implementations
	 * of procedures that require more than one argument should throw an error if
	 * this method is called. 
	 *  
	 * @param arg1 The only object supplied.
	 * @return An object, implementation defines what kind of object.
	 * @throws Throwable Blanket throwable, bad practice.
	 */
	public abstract Object apply1(Object arg1) throws Throwable;

	/**
	 * Used for two arguments needed in an implementation, uses or does something
	 * with the objects and returns something that used the arguments.  Can 
	 * manipulate or create new objects using the supplied objects.  Implementations
	 * of procedure that do not use this method should throw an error if this
	 * method is called.  Arguments will most likely need to be cast in 
	 * implementations.
	 * 
	 * @param arg1 A supplied object, implementation defines type.
	 * @param arg2 A supplied object, implementation defines type.
	 * @return An object, implementation defines what kind of object.
	 * @throws Throwable Blanket throwable, bad practice.
	 */
	public abstract Object apply2(Object arg1, Object arg2) throws Throwable;

	/**
	 * Used for three arguments needed in an implementation, uses or does 
	 * something with the objects and returns something that used the arguments.
	 * Can manipulate or create new objects using the supplied objects.  
	 * Implementations of procedure that do not use this method should throw an
	 * error if this method is called.  Arguments will most likely need to be
	 * cast in implementations.
	 * 
	 * @param arg1 A supplied object, implementation defines type.
	 * @param arg2 A supplied object, implementation defines type.
	 * @param arg3 A supplied object, implementation defines type.
	 * @return An object that was manipulated or created, implementation defines
	 * @return An object, implementation defines what kind of object.
	 * @throws Throwable Blanket throwable, bad practice.
	 */
	public abstract Object apply3(Object arg1, Object arg2, Object arg3)
			throws Throwable;
	/**
	 * Used for three arguments needed in an implementation, uses or does 
	 * something with the objects and returns something that used the arguments.
	 * Can manipulate or create new objects using the supplied objects.  
	 * Implementations of procedure that do not use this method should throw an
	 * error if this method is called.  Arguments will most likely need to be
	 * cast in implementations.
	 * 
	 * @param arg1 A supplied object, implementation defines type.
	 * @param arg2 A supplied object, implementation defines type.
	 * @param arg3 A supplied object, implementation defines type.
	 * @param arg4 A supplied object, implementation defines type.
	 * @return An object that was manipulated or created, implementation defines
	 * @return An object, implementation defines what kind of object.
	 * @throws Throwable Blanket throwable, bad practice.
	 */

	public abstract Object apply4(Object arg1, Object arg2, Object arg3,
			Object arg4) throws Throwable;

	/**
	 * Defines the minimum number of objects required for this procedure.  
	 * Implementations define the number of arguments.
	 * @return The minimum number of arguments.
	 */
	public final int minArgs()
	{
		return numArgs() & 0xFFF;
	}

	/**
	 * Maximum number of arguments allowed, or -1 for unlimited. (May also return
	 * -1 if there are keyword arguments, for implementation reasons.)
	 * @return The maximum number of arguments or -1 for unlimited.
	 */
	public final int maxArgs()
	{
		return numArgs() >> 12;
	}

	/**
	 * Check that the number of arguments in a call is valid.
	 * 
	 * @param proc the Procedure being called
	 * @param argCount the number of arguments in the call
	 * @exception WrongArguments
	 *               there are too many or too few actual arguments
	 */
	public static void checkArgCount(Procedure proc, int argCount)
	{
		int num = proc.numArgs();
		if (argCount < (num & 0xFFF) || (num >= 0 && argCount > (num >> 12)))
			throw new WrongArguments(proc, argCount);
	}

	/** Return minArgs()|(maxArgs<<12). */

	/*
	 * We use a single virtual function to reduce the number of methods in the
	 * system, as well as the number of virtual method table entries. We shift by
	 * 12 so the number can normally be represented using a sipush instruction,
	 * without requiring a constant pool entry.
	 */
	
	/**
	 * Used to reduce the number of methods.
	 * @return Returns 0xfffff000
	 */
	public int numArgs()
	{
		return 0xfffff000;
	}

	/*
	 * CPS: ?? public void apply1(Object arg, CallContext stack, CallFrame rlink,
	 * int rpc) { context.value = apply1(arg); context.frame = rlink; context.pc
	 * = rpc; }
	 */

	/**
	 * Call this Procedure using the explicit-CallContext-convention. The input
	 * arguments are (by default) in stack.args; the result is written to
	 * ctx.consumer.
	 */

	/**
	 * Call this procedure using the explicit-CallContext-convention.  The input
	 * arguements are (by default) in stack.args; the result is written to 
	 * ctx.consumer.
	 * @param ctx The Callcontext for this apply.
	 * @throws Throwable Blanket Throwable, bad practice.
	 */
	public void apply(CallContext ctx) throws Throwable
	{
		apply(this, ctx);
	}

	/**
	 * Calls the correct apply method with the supplied procedure and context.  
	 * Gets the number of arguments for the procedure and calls apply on 
	 * that procedure.
	 * 
	 * @param proc The procedure that is used.
	 * @param ctx The call context that defines the number of arguments and 
	 * 		contains the arguements.
	 * @throws Throwable Blanket Throwable, bad practice.
	 */
	public static void apply(Procedure proc, CallContext ctx) throws Throwable
	{
		Object result;
		
		// Get the number of arguments from the context.
		int count = ctx.count;
		
		// Run the correct apply method.
		if (ctx.where == 0 && count != 0)
			result = proc.applyN(ctx.values);
		else
		{
			switch (count)
			{
			case 0:
				result = proc.apply0();
				break;
			case 1:
				result = proc.apply1(ctx.getNextArg());
				break;
			case 2:
				result = proc.apply2(ctx.getNextArg(), ctx.getNextArg());
				break;
			case 3:
				result = proc.apply3(ctx.getNextArg(), ctx.getNextArg(), ctx
						.getNextArg());
				break;
			case 4:
				result = proc.apply4(ctx.getNextArg(), ctx.getNextArg(), ctx
						.getNextArg(), ctx.getNextArg());
				break;
			default:
				result = proc.applyN(ctx.getArgs());
				break;
			}
		}
		ctx.writeValue(result);
	}

	/**
	 * Checks if the procedure can take zero argument.  Then sets the the context
	 * for zero arguments.
	 * 
	 * @return non-negative if the match succeeded, else negative.
	 */
	public int match0(CallContext ctx)
	{
		// In it's current state, always sets min equal to zero.  It is up to the 
		// implementation to override this behavior.
		int num = numArgs();
		int min = num & 0xFFF;
		
		if (min > 0)
			return MethodProc.NO_MATCH_TOO_FEW_ARGS | min;
		if (num < 0)
			return matchN(ProcedureN.noArgs, ctx);
		
		// Set Context variables for 0 arguements
		ctx.count = 0;
		ctx.where = 0;
		ctx.next = 0;
		ctx.proc = this;
		return 0;
	}

	/**
	 * Checks if the procedure can take one argument.  Then sets the the context
	 * for one argument.

	 * @param arg1 If successfully matched, will put this boject into the 
	 * 	CallContext.
	 * @param ctx The call context where information will be stored if matched.
	 * @return non-negative if the match succeeded, else negative.
	 */

	public int match1(Object arg1, CallContext ctx)
	{
		// In it's current state, always sets min equal to zero.  It is up to the 
		// implementation to override this behavior.
		int num = numArgs();
		int min = num & 0xFFF;

		if (min > 1)
			return MethodProc.NO_MATCH_TOO_FEW_ARGS | min;
		if (num >= 0)
		{
			int max = num >> 12;
			if (max < 1)
				return MethodProc.NO_MATCH_TOO_MANY_ARGS | max;
			
			// Set the context variables correctly.
			ctx.value1 = arg1;
			ctx.count = 1;
			ctx.where = CallContext.ARG_IN_VALUE1;
			ctx.next = 0;
			ctx.proc = this;
			return 0;
		}
		Object[] args =
		{ arg1 };
		return matchN(args, ctx);
	}

	/**
	 * Checks if the procedure can take two arguments.  Then sets the the context
	 * for two arguments.

	 * @param arg1 If successfully matched, will put this boject into the 
	 * 	CallContext.
	 * @param arg2 If successfully matched, will put this boject into the 
	 * 	CallContext.
	 * @param ctx The call context where information will be stored if matched.
	 * @return non-negative if the match succeeded, else negative.
	 */
	public int match2(Object arg1, Object arg2, CallContext ctx)
	{

		// In it's current state, always sets min equal to zero.  It is up to the 
		// implementation to override this behavior.
		int num = numArgs();
		int min = num & 0xFFF;

		if (min > 2)
			return MethodProc.NO_MATCH_TOO_FEW_ARGS | min;
		if (num >= 0)
		{
			int max = num >> 12;
			if (max < 2)
				return MethodProc.NO_MATCH_TOO_MANY_ARGS | max;
	
			// Set the number of arguements in the callcontext
			ctx.value1 = arg1;
			ctx.value2 = arg2;
			ctx.count = 2;
			ctx.where = CallContext.ARG_IN_VALUE1
					| (CallContext.ARG_IN_VALUE2 << 4);
			ctx.next = 0;
			ctx.proc = this;
			return 0;
		}
		Object[] args =
		{ arg1, arg2 };
		return matchN(args, ctx);
	}

	/**
	 * Checks if the procedure can take three arguments.  Then sets the the context
	 * for three arguments.

	 * @param arg1 If successfully matched, will put this boject into the 
	 * 	CallContext.
	 * @param arg2 If successfully matched, will put this boject into the 
	 * 	CallContext.
	 * @param arg3 If successfully matched, will put this boject into the 
	 * 	CallContext.
	 * @param ctx The call context where information will be stored if matched.
	 * @return non-negative if the match succeeded, else negative.
	 */
	public int match3(Object arg1, Object arg2, Object arg3, CallContext ctx)
	{
		int num = numArgs();
		int min = num & 0xFFF;
		if (min > 3)
			return MethodProc.NO_MATCH_TOO_FEW_ARGS | min;
		if (num >= 0)
		{
			int max = num >> 12;
			if (max < 3)
				return MethodProc.NO_MATCH_TOO_MANY_ARGS | max;
			ctx.value1 = arg1;
			ctx.value2 = arg2;
			ctx.value3 = arg3;
			ctx.count = 3;
			ctx.where = CallContext.ARG_IN_VALUE1
					| (CallContext.ARG_IN_VALUE2 << 4)
					| (CallContext.ARG_IN_VALUE3 << 8);
			ctx.next = 0;
			ctx.proc = this;
			return 0;
		}
		Object[] args =
		{ arg1, arg2, arg3 };
		return matchN(args, ctx);
	}

	/**
	 * Checks if the procedure can take four arguments.  Then sets the the context
	 * for four arguments.
	 * 
	 * @param arg1 If successfully matched, will put this boject into the 
	 * 	CallContext.
	 * @param arg2 If successfully matched, will put this boject into the 
	 * 	CallContext.
	 * @param arg3 If successfully matched, will put this boject into the 
	 * 	CallContext.
	 * @param arg4 If successfully matched, will put this boject into the 
	 * 	CallContext.
	 * @param ctx The call context where information will be stored if matched.
	 * @return non-negative if the match succeeded, else negative.
	 */
	public int match4(Object arg1, Object arg2, Object arg3, Object arg4,
			CallContext ctx)
	{
		int num = numArgs();
		int min = num & 0xFFF;
		if (min > 4)
			return MethodProc.NO_MATCH_TOO_FEW_ARGS | min;
		if (num >= 0)
		{
			int max = num >> 12;
			if (max < 4)
				return MethodProc.NO_MATCH_TOO_MANY_ARGS | max;
			ctx.value1 = arg1;
			ctx.value2 = arg2;
			ctx.value3 = arg3;
			ctx.value4 = arg4;
			ctx.count = 4;
			ctx.where = (CallContext.ARG_IN_VALUE1
					| (CallContext.ARG_IN_VALUE2 << 4)
					| (CallContext.ARG_IN_VALUE3 << 8) | (CallContext.ARG_IN_VALUE4 << 12));
			ctx.next = 0;
			ctx.proc = this;
			return 0;
		}
		Object[] args =
		{ arg1, arg2, arg3, arg4 };
		return matchN(args, ctx);
	}

	/**
	 * Checks if the procedure can take N number of arguments.  Then sets the 
	 * the context for N number of arguments.
	 * 
	 * @param args An object array with the arguments.
	 * @param ctx The call context where information will be stored if matched.
	 * @return non-negative if the match succeeded, else negative.
	 */
	public int matchN(Object[] args, CallContext ctx)
	{
		int num = numArgs();
		int min = num & 0xFFF;
		if (args.length < min)
			return MethodProc.NO_MATCH_TOO_FEW_ARGS | min;
		if (num >= 0)
		{
			switch (args.length)
			{
			case 0:
				return match0(ctx);
			case 1:
				return match1(args[0], ctx);
			case 2:
				return match2(args[0], args[1], ctx);
			case 3:
				return match3(args[0], args[1], args[2], ctx);
			case 4:
				return match4(args[0], args[1], args[2], args[3], ctx);
			default:
				int max = num >> 12;
				if (args.length > max)
					return MethodProc.NO_MATCH_TOO_MANY_ARGS | max;
			}
		}
		ctx.values = args;
		ctx.count = args.length;
		ctx.where = 0;
		ctx.next = 0;
		ctx.proc = this;
		return 0;
	}

	/**
	 * Calls match0() and throws a WrongArguments Exception if there is an argument
	 * mismatch. 
	 * @param ctx The CallContext that is used to call match0()
	 */
	public void check0(CallContext ctx)
	{
		int code = match0(ctx);
		if (code != 0)
		{
			throw MethodProc.matchFailAsException(code, this, ProcedureN.noArgs);
		}
	}

	/**
	 * Calls match1() and throws a WrongArguments Exception if there is an argument
	 * mismatch. 
	 * @param ctx The CallContext that is used to call match1()
	 */
	public void check1(Object arg1, CallContext ctx)
	{
		int code = match1(arg1, ctx);
		if (code != 0)
		{
			Object[] args =
			{ arg1 };
			throw MethodProc.matchFailAsException(code, this, args);
		}
	}

	/**
	 * Calls match2() and throws a WrongArguments Exception if there is an argument
	 * mismatch. 
	 * @param ctx The CallContext that is used to call match2()
	 */
	public void check2(Object arg1, Object arg2, CallContext ctx)
	{
		int code = match2(arg1, arg2, ctx);
		if (code != 0)
		{
			Object[] args =
			{ arg1, arg2 };
			throw MethodProc.matchFailAsException(code, this, args);
		}
	}

	/**
	 * Calls match3() and throws a WrongArguments Exception if there is an argument
	 * mismatch. 
	 * @param ctx The CallContext that is used to call match3()
	 */
	public void check3(Object arg1, Object arg2, Object arg3, CallContext ctx)
	{
		int code = match3(arg1, arg2, arg3, ctx);
		if (code != 0)
		{
			Object[] args =
			{ arg1, arg2, arg3 };
			throw MethodProc.matchFailAsException(code, this, args);
		}
	}

	/**
	 * Calls match4() and throws a WrongArguments Exception if there is an argument
	 * mismatch. 
	 * @param ctx The CallContext that is used to call match4()
	 */
	public void check4(Object arg1, Object arg2, Object arg3, Object arg4,
			CallContext ctx)
	{
		int code = match4(arg1, arg2, arg3, arg4, ctx);
		if (code != 0)
		{
			Object[] args =
			{ arg1, arg2, arg3, arg4 };
			throw MethodProc.matchFailAsException(code, this, args);
		}
	}

	/**
	 * Calls matchN() and throws a WrongArguments Exception if there is an argument
	 * mismatch. 
	 * @param args The argument array used for finding the number of arguments.
	 * @param ctx The CallContext that is used to call match0()
	 */
	public void checkN(Object[] args, CallContext ctx)
	{
		int code = matchN(args, ctx);
		if (code != 0)
		{
			throw MethodProc.matchFailAsException(code, this, args);
		}
	}

	/**
	 * Gets the setter procedure from the propertySet() for this procedure.  
	 * Throws a runtime exception if it cannot find a setter procedure.
	 * @return The setter procedure
	 */
	public Procedure getSetter()
	{
		if (!(this instanceof HasSetter))
		{
			Object setter = getProperty(setterKey, null);
			if (setter instanceof Procedure)
				return (Procedure) setter;
			throw new RuntimeException("procedure '" + getName()
					+ "' has no setter");
		}
		int num_args = numArgs();
		if (num_args == 0x0000)
			return new Setter0(this);
		if (num_args == 0x1001)
			return new Setter1(this);
		return new Setter(this);
	}

	/**
	 * Sets the procedure setter for this procedure.  Throws a runtime exception
	 * if it is not a valid setting procedure.
	 * @param setter A procedure setting.
	 */
	public void setSetter(Procedure setter)
	{
		if (this instanceof HasSetter)
			throw new RuntimeException("procedure '" + getName()
					+ "' has builtin setter - cannot be modified");
		setProperty(Procedure.setterKey, setter);
	}

	/** If HasSetter, the Procedure is called in the LHS of an assignment. */
	
	/**
	 * Gets the setter from this procedure (by calling getSetter()), if there is
	 * no setter, it throws a runtime exception.  It then calls apply1() with the 
	 * supplied argument.
	 * @param result The supplied argument.
	 * @throws Throwable Blanket throwable.
	 */
	public void set0(Object result) throws Throwable
	{
		getSetter().apply1(result);
	}

	/**
	 * Gets the setter from this procedure (by calling getSetter()), if there is
	 * no setter, it throws a runtime exception.  It then calls apply2() with the 
	 * supplied arguments.
	 * @param result A supplied argument.
	 * @param value A supplied argument.
	 * @throws Throwable Blanket throwable.
	 */
	public void set1(Object arg1, Object value) throws Throwable
	{
		getSetter().apply2(arg1, value);
	}

	/**
	 * Gets the setter from this procedure (by calling getSetter()), if there is
	 * no setter, it throws a runtime exception.  It then calls applyN() with the 
	 * supplied arguments.
	 * 
	 * @param args The supplied arguments.
	 * @throws Throwable Blanket throwable.
	 */
	public void setN(Object[] args) throws Throwable
	{
		getSetter().applyN(args);
	}

	/**
	 * Returns a string representation of this procedure.  It first creates a 
	 * string with "#procedure " and tries to get the name of this procedure by
	 * calling getName(), then tries to get the Source location by calling 
	 * getSourceLocation(), then tries the class name by getClass().getName. 
	 * The first method that does not return null will
	 * be appended to a string and returned.  The format is "#<procedure: name>" 
	 * where name is the information found by calling the above methods.
	 * 
	 * @return The string representation of this class.
	 */
	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	public String toString()
	{
		// Initialize variables
		StringBuffer sbuf = new StringBuffer();
		sbuf.append("#<procedure ");
		
		// Get some information to describe this procedure.
		String n = getName();
		if (n == null)
			n = getSourceLocation();
		if (n == null)
			n = getClass().getName();
		
		// Append the found string
		sbuf.append(n);
		sbuf.append('>');
		return sbuf.toString();
	}
}
