/*
 * AfterCmd.java --
 *
 *	Implements the built-in "after" Tcl command.
 *
 * Copyright (c) 1997 Cornell University.
 * Copyright (c) 1997 Sun Microsystems, Inc.
 *
 * See the file "license.terms" for information on usage and
 * redistribution of this file, and for a DISCLAIMER OF ALL
 * WARRANTIES.
 * 
 * RCS: @(#) $Id: AfterCmd.java,v 1.1.1.1 1998/10/14 21:09:19 cvsadmin Exp $
 *
 */

package tcl.lang;

import java.util.Vector;

/*
 * This class implements the built-in "after" command in Tcl.
 */

class AfterCmd implements Command {

/*
 * The list of handler are stored as AssocData in the interp.
 */

AfterAssocData assocData = null;

/*
 * Valid command options.
 */

static final private String validOpts[] = {
    "cancel",
    "idle",
    "info"
};

static final int OPT_CANCEL = 0;
static final int OPT_IDLE   = 1;
static final int OPT_INFO   = 2;


/*
 *----------------------------------------------------------------------
 *
 * cmdProc --
 *
 *	This procedure is invoked as part of the Command interface to
 *	process the "after" Tcl command.  See the user documentation
 *	for details on what it does.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	See the user documentation.
 *
 *----------------------------------------------------------------------
 */

public void
cmdProc(
    Interp interp,		// Current interpreter.
    TclObject argv[])		// Argument list.
throws 
    TclException 		// A standard Tcl exception.
{
    int i;
    Notifier notifier = (Notifier)interp.getNotifier();
    Object info;

    if (assocData == null) {
	/*
	 * Create the "after" information associated for this
	 * interpreter, if it doesn't already exist.
	 */

	assocData = (AfterAssocData)interp.getAssocData("tclAfter");
	if (assocData == null) {
	    assocData = new AfterAssocData();
	    interp.setAssocData("tclAfter", assocData);
	}
    }

    if (argv.length < 2) {
	throw new TclNumArgsException(interp, 1, argv, "option ?arg arg ...?");
    }

    /*
     * First lets see if the command was passed a number as the first argument.
     */

    boolean isNumber = false;
    int ms = 0;

    if (argv[1].getInternalRep() instanceof TclInteger) {
	ms = TclInteger.get(interp, argv[1]);
	isNumber = true;
    } else {
	String s = argv[1].toString();
	if ((s.length() > 0) && (Character.isDigit(s.charAt(0)))) {
	    ms = TclInteger.get(interp, argv[1]);
	    isNumber = true;
	}
    }

    if (isNumber) {
	if (ms < 0) {
	    ms = 0;
	}
	if (argv.length == 2) {
	    /*
	     * Sleep for at least the given milliseconds and return.
	     */

	    long endTime = System.currentTimeMillis() + ms;
	    while (true) {
		try {
		    Thread.sleep(ms);
		    return;
		} catch (InterruptedException e) {
		    /*
		     * We got interrupted. Sleep again if we havn't slept
		     * long enough yet.
		     */

		    long sysTime = System.currentTimeMillis();
		    if (sysTime >= endTime) {
			return;
		    }
		    ms = (int)(endTime - sysTime);
		    continue;
		}
	    }
	}

	TclObject cmd = getCmdObject(argv);
	cmd.preserve();

	assocData.lastAfterId ++;
	TimerInfo timerInfo = new TimerInfo(notifier, ms);
	timerInfo.interp = interp;
	timerInfo.command = cmd;
	timerInfo.id = assocData.lastAfterId;

	assocData.handlers.addElement(timerInfo);

	interp.setResult("after#" + timerInfo.id);

	return;
    }

    /*
     * If it's not a number it must be a subcommand.
     */

    int index;

    try {
	index = TclIndex.get(interp, argv[1], validOpts, "option", 0);
    } catch (TclException e) {
	throw new TclException(interp, "bad argument \"" + argv[1] +
		"\": must be cancel, idle, info, or a number");
    }

    switch (index) {
    case OPT_CANCEL:
	if (argv.length < 3) {
	    throw new TclNumArgsException(interp, 2, argv, "id|command");
	}
	
	TclObject arg = getCmdObject(argv);
	arg.preserve();

	/*
	 * Search the timer/idle handler by id or by command.
	 */

	info = null;
	for (i = 0; i < assocData.handlers.size(); i++) {
	    Object obj = assocData.handlers.elementAt(i);
	    if (obj instanceof TimerInfo) {
		TclObject cmd = ((TimerInfo)obj).command;

		if ((cmd == arg) || cmd.toString().equals(arg.toString())) {
		    info = obj;
		    break;
		}
	    } else {
		TclObject cmd = ((IdleInfo)obj).command;

		if ((cmd == arg) || cmd.toString().equals(arg.toString())) {
		    info = obj;
		    break;
		}
	    }
	}
	if (info == null) {
	    info = getAfterEvent(arg.toString());
	}
	arg.release();

	/*
	 * Cancel the handler.
	 */

	if (info != null) {
	    if (info instanceof TimerInfo) {
		((TimerInfo)info).cancel();
		((TimerInfo)info).command.release();
	    } else {
		((IdleInfo)info).cancel();
		((IdleInfo)info).command.release();
	    }

	    assocData.handlers.removeElement(info);
	}
	break;

    case OPT_IDLE:
	if (argv.length < 3) {
	    throw new TclNumArgsException(interp, 2, argv,"script script ...");
	}

	TclObject cmd = getCmdObject(argv);
	cmd.preserve();
	assocData.lastAfterId ++;

	IdleInfo idleInfo = new IdleInfo(notifier);
	idleInfo.interp = interp;
	idleInfo.command = cmd;
	idleInfo.id = assocData.lastAfterId;

	assocData.handlers.addElement(idleInfo);

	interp.setResult("after#" + idleInfo.id);
	break;

    case OPT_INFO:
	if (argv.length == 2) {
	    /*
	     * No id is given. Return a list of current after id's.
	     */

	    TclObject list = TclList.newInstance();
	    for (i = 0; i < assocData.handlers.size(); i++) {
		int id;
		Object obj = assocData.handlers.elementAt(i);
		if (obj instanceof TimerInfo) {
		    id = ((TimerInfo)obj).id;
		} else {
		    id = ((IdleInfo)obj).id;
		}
		TclList.append(interp, list, TclString.newInstance("after#" +
			id));
	    }
	    interp.setResult(list);
	    return;
	}
	if (argv.length != 3) {
	    throw new TclNumArgsException(interp, 2, argv, "?id?");
	}	    

	/*
	 * Return command and type of the given after id.
	 */

	info = getAfterEvent(argv[2].toString());
	if (info == null) {
	   throw new TclException(interp, "event \"" + argv[2] +
		    "\" doesn't exist");
	}
	TclObject list = TclList.newInstance();
	TclList.append(interp, list,
		((info instanceof TimerInfo) ?
		((TimerInfo)info).command : ((IdleInfo)info).command));
	TclList.append(interp, list, TclString.newInstance(
		(info instanceof TimerInfo) ? "timer" : "idle"));

	interp.setResult(list);
	break;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * getCmdObject --
 *
 *	Returns a TclObject that contains the command script passed
 *	to the "after" command.
 *
 * Results:
 *	A concatenation of the objects from argv[2] through argv[n-1].
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

private TclObject
getCmdObject(
    TclObject argv[])		// Argument list passed to the "after" command.
{
    if (argv.length == 3) {
	return argv[2];
    } else {
	TclObject cmd = TclString.newInstance(Util.concat(2, argv.length-1,
		argv));
	return cmd;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * getAfterEvent --
 *
 *	This procedure parses an "after" id such as "after#4" and
 *	returns an AfterInfo object.
 *
 * Results:
 *	The return value is an AfterInfo object.  if one is
 *	found that corresponds to "string", or null if no
 *	corresponding after event can be found.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

private Object
getAfterEvent(
    String string)		// Textual identifier for after event, such
				// as "after#6".
{
    if (!string.startsWith("after#")) {
	return null;
    }

    StrtoulResult res = Util.strtoul(string, 6, 10);
    if (res.errno != 0) {
	return null;
    }

    for (int i = 0; i < assocData.handlers.size(); i++) {
	Object obj = assocData.handlers.elementAt(i);
	if (obj instanceof TimerInfo) {
	    if (((TimerInfo)obj).id == res.value) {
		return obj;
	    }
	} else {
	    if (((IdleInfo)obj).id == res.value) {
		return obj;
	    }
	}
    }

    return null;
}

/*
 * This inner class manages the list of handlers created by the
 * "after" command. We keep the handler has an AssocData so that they
 * will continue to exist even if the "after" command is deleted.
 */

class AfterAssocData implements AssocData {

/*
 * The set of handlers created but not yet fired.
 */

Vector handlers = new Vector();

/*
 * Timer identifier of most recently created timer.	
 */

int lastAfterId = 0;


/*
 *----------------------------------------------------------------------
 *
 * disposeAssocData --
 *
 *	This method is called when the interpreter is destroyed or
 *	when Interp.deleteAssocData is called on a registered
 *	AssocData instance.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	All unfired handler are cancelled; their command objects are
 *	released.
 *
 *----------------------------------------------------------------------
 */

public void
disposeAssocData(
    Interp interp)		// The interpreter in which this AssocData
				// instance is registered in.
{
    for (int i = assocData.handlers.size() - 1; i >= 0; i--) {
	Object info = assocData.handlers.elementAt(i);
	assocData.handlers.removeElementAt(i);
	if (info instanceof TimerInfo) {
	    ((TimerInfo)info).cancel();
	    ((TimerInfo)info).command.release();
	} else {
	    ((IdleInfo)info).cancel();
	    ((IdleInfo)info).command.release();
	}
    }
    assocData = null;
}
    
} // end AfterCmd.AfterAssocData


/*
 * This inner class implement timer handlers for the "after"
 * command. It stores a script to be executed when the timer event is
 * fired.
 */

class TimerInfo extends TimerHandler {

/*
 * Interpreter in which the script should be executed.
 */

Interp interp;

/*
 * Command to execute when the timer fires.
 */

TclObject command;

/*
 * Integer identifier for command;  used to cancel it.
 */

int id;


/*
 *----------------------------------------------------------------------
 *
 * TimerInfo --
 *
 *	Constructs a new TimerInfo instance.
 *
 * Side effects:
 *	The timer is initialized by the super class's constructor.
 *
 *----------------------------------------------------------------------
 */

TimerInfo(
    Notifier n,			// The notifier to fire the event.
    int milliseconds)		// How many milliseconds to wait
				// before invoking processTimerEvent().
{
    super(n, milliseconds);
}

/*
 *----------------------------------------------------------------------
 *
 * processTimerEvent --
 *
 *	Process the timer event.
 *
 * Results:	
 *	None.
 *
 * Side effects:
 *	The command executed by this timer can have arbitrary side
 *	effects.
 *
 *----------------------------------------------------------------------
 */

public void
processTimerEvent()
{
    try {
	assocData.handlers.removeElement(this);
	interp.eval(command, TCL.GLOBAL_ONLY);
    } catch (TclException e) {
	interp.addErrorInfo("\n    (\"after\" script)");
	interp.backgroundError();
    } finally {
	command.release();
	command = null;
    }
}

} // end AfterCmd.AfterInfo


/*
 * This inner class implement idle handlers for the "after"
 * command. It stores a script to be executed when the idle event is
 * fired.
 */

class IdleInfo extends IdleHandler {

/*
 * Interpreter in which the script should be executed.
 */

Interp interp;

/*
 * Command to execute when the idle event fires.
 */

TclObject command;

/*
 * Integer identifier for command;  used to cancel it.
 */

int id;


/*
 *----------------------------------------------------------------------
 *
 * IdleInfo --
 *
 *	Constructs a new IdleInfo instance.
 *
 * Side effects:
 *	The idle handler is initialized by the super class's
 *	constructor.
 *
 *----------------------------------------------------------------------
 */

IdleInfo(
    Notifier n)			// The notifier to fire the event.
{
    super(n);
}

/*
 *----------------------------------------------------------------------
 *
 * processIdleEvent --
 *
 *	Process the idle event.
 *
 * Results:	
 *	None.
 *
 * Side effects:
 *	The command executed by this idle handler can have arbitrary side
 *	effects.
 *
 *----------------------------------------------------------------------
 */

public void
processIdleEvent()
{
    try {
	assocData.handlers.removeElement(this);
	interp.eval(command, TCL.GLOBAL_ONLY);
    } catch (TclException e) {
	interp.addErrorInfo("\n    (\"after\" script)");
	interp.backgroundError();
    } finally {
	command.release();
	command = null;
    }
}

} // end AfterCmd.AfterInfo

} // end AfterCmd
