/* File: ProcessModel.java
 * Contains: The ACP3 process model and workflow stepper
 * Author: Jeff Dalton <J.Dalton@ed.ac.uk>
 * Created: March 1998
 * Updated: Fri Jun 15 15:44:44 2001 by Jeff Dalton
 * Copyright: (c) 1998, 2000, 2001, AIAI, University of Edinburgh
 */

/*
 * The code is based in part on an earlier Lisp version by John Levine
 * and a modification of that Lisp version by Jeff Dalton.
 */

// If you are reading this file, you should also read the file
// ProcessModels/acp3-process.lsp

package ix.ileed;

import java.util.*;
import java.io.*;

import ix.util.*;
import ix.util.lisp.*;
import ix.util.match.*;

import ix.icore.process.*;
import ix.icore.domain.*;
import ix.iface.domain.*;


/**
 * A ProcessModel contains a model of a process and handles the propagation
 * of state changes.  There are a number of interior classes so that the
 * whole thing can be contained in one file and can avoid name conflicts
 * with other parts of the panel (e.g. for class names such as "Node").<p>
 *
 * A ProcessModel receives information from a Watcher and is therefore
 * an implementation of WatcherListener.  It sends information to an
 * implementation of ProcessViewer.<p>
 *
 * Before you can do anything very interesting, you have to install a
 * viewer and read in a process definition.  For example:
 *
 * <pre>
 *    ProcessModel model = new ProcessModel();
 *    model.setViewer(someViewer);
 *    model.initFromFile("coax-process.lsp");
 * </pre>
 *
 * @see WatcherListener
 * @see ProcessViewer
 */

public class ProcessModel implements WatcherListener, StatusValues {

    // By "action" in the comments below, we mean a String that describes
    // an action in the process, e.g. "develop COA", or "create support plan".
    // A "pattern" is a LList (action coaNumber), where coaNumber is a Long.
    // (For more information, see the javadoc comments for the Node class.)

    ProcessViewer viewer;
    // /\/: Separate product viewer

    Domain domain;			        // action -> Schema, etc

    NodeTable nodeTable = new NodeTable();      // pattern -> Node
    Vector allNodes = new Vector();             // Vector of Nodes

    Hashtable rowTable = new Hashtable();       // action -> Vector of Nodes
    Hashtable overallColumn = new Hashtable();  // action -> OverallCell

    int numberOfCoas = 0;

    HashedSet changedNodes = new HashedSet();   // set of Nodes

    Vector products = new Vector(); 		// process Products
    Hashtable productTable = new Hashtable();   // for lookup

    /**
     * Create an empty ProcessModel.
     */
    public ProcessModel() {
    }

    /**
     * Install a viewer that will be sent status information when changes
     * occur.
     */
    public synchronized void setViewer(ProcessViewer viewer) {
	this.viewer = viewer;
    }

    /**
     * Read action definitions from a file.  Each action definition (or schema)
     * specifies the subactions (if any), a partial order over the subactions,
     * and any special properties of the action that affect how it is modelled.
     * This information is used when building data structures that represent
     * the actions that are present in the process being modelled.
     */
    public synchronized void initFromFile(String filename) {
	try {
	    domain = DomainParser.makeParser(filename).readDomain();
	    // Make sure there's exactly one process schema.  /\/
	    domain.getProcessSchema();
	    // Create the process products.
	    for(LList pl = domain.getAllProductSchemas();
		pl != Lisp.NIL; pl = pl.cdr()) {
		ProductSchema ps = (ProductSchema)pl.car();
		String pName = (String)ps.name;
		Debug.noteln("Product", pName);
		addProduct(new Product(pName));
	    }
	}
	catch (FileNotFoundException e) { Debug.noteException(e); }

	reset();
    }

    void addProduct(Product p) {
	Product already = (Product)productTable.get(p.name);
	Debug.assert(already == null, "Product already exists", p.name);
	productTable.put(p.name, p);
	products.addElement(p);
    }

    Product getProduct(Object name) {
	return (Product)productTable.get(name);
    }

    public Vector getAllProducts() {
	return products;
    }

    /**
     * Returns a tree that shows the action nesting of the process,
     * to the extent that this can be determined directly from the
     * schema definitions.  The tree is a list of entries of the
     * form (pattern subtree).
     */
    public synchronized LList staticExpansionTree() {
	return domain.staticExpansionTree();
    }

    /**
     * Remove all COAs, their associated actions, and all current state
     * information.  However, the definitions established by the most
     * recent call to initFromFile remain; it is not necessary to call
     * it again after a reset.
     */
    public synchronized void reset() {
	Debug.assert(domain != null, "No domain");
	Debug.assert(domain.getProcessSchema() != null, "No process schema");
	nodeTable.clear();
	allNodes.removeAllElements();
	rowTable.clear();
	overallColumn.clear();
	changedNodes.clear();
	numberOfCoas = 0;
	// addCoa(1);

	for(Enumeration pe = products.elements(); pe.hasMoreElements();) {
	    Product p = (Product)pe.nextElement();
	    p.status = PRODUCT_BLANK;
	}
    }

    /**
     * Add the actions for a new COA to the model.  Note that the new
     * COA's number must be exactly one greater than the current number
     * of COAs.  The only way to add a higher-numbered COA is to call
     * addCoa more than once.
     */
    public synchronized void addCoa(int coaNumber) {
	Debug.noteln("Adding coa", coaNumber);
	Debug.assert(coaNumber == numberOfCoas + 1);
	Long coa = new Long(coaNumber);
	Schema s = domain.getProcessSchema().forCoa(coa);
	Node tmpNode = new Node(); // for access to Node methods

	numberOfCoas++;

	// Nodes
	LList children = s.nodes.mapcar(new Function1() {
	    public Object funcall(Object nodeSpec) {
		LList spec = (LList)nodeSpec;
		LList childPattern = (LList)spec.elementAt(1);
		Node child = findNode(childPattern);
		if (child != null) {
		    // Nodes not per-coa can already exist
		    Debug.assert(childPattern.length() == 1);
		    Debug.assert(!child.schema.isForEachCoa);
		    return child;
		}
		// Create a new top-level node with null parent.
		// This will automatically create all descendents.
		// The new node and its descendents will register themselves.
		return ProcessModel.this.new Node(null, childPattern);
	    }
	});

	// Orderings
	tmpNode.processOrderings(children, s.orderings);

	// Post-ordering initialization
	for (LList cl = children; cl != Lisp.NIL; cl = cl.cdr()) {
	    Node child = (Node)cl.car();
	    child.afterOrderings();
	}

	// Initial nodes may now be ready to execute.
	for (LList inl = initialNodes(); inl != Lisp.NIL; inl = inl.cdr()) {
	    Node inode = (Node)inl.car();
	    if (inode.status == STATUS_BLANK)
		inode.changeStatus(STATUS_POSSIBLE);
	}

	// Adding the new nodes will invalidate the overall cells for the
	// corresponding rows; but it may be that we also need to invalidate
	// the overall cells that are for rows that have only one node
	// rather than one per coa.  /\/

	// Check connectivity
	checkConnectivity();

    }

    /**
     * Send the viewer a complete state description, rather than a delta.
     * This is normally a request from the viewer, for instance if it has
     * been ignoring changes (for some reason) and wants to catch up.
     */
    public synchronized void sendFullStateDescription() {
	try {
	    Debug.noteln("Sending full state description");

	    startRecordingChanges();
	    
	    // cf tellViewer()
	    ProcessStatusUpdate up = new ProcessStatusUpdate();
	    up.containsCompleteState = true;
	    up.numberOfCoas = numberOfCoas;
	    up.statusChanges = fullStateDescription();
	    up.issueCounts = issueCounts();
	    up.products = products;
	    viewer.statusUpdate(up);
	    
	    resetChangeRecord();
	    
	}
	catch (Exception e) {
	    Debug.noteException(e);
	}
    }

    protected Vector fullStateDescription() { 		// cf statusChanges()

	// Get a status description from all nodes
	Enumeration ce = Seq.map(allNodes.elements(), new Function1() {
	    public Object funcall(Object node) {
		return ((Node)node).stateRecord();
	    }
	});

	Vector state = Seq.toVector(ce);

	// Add in status descriptions from the entire overall column
	for (ce = overallColumn.elements(); ce.hasMoreElements();) {
	    OverallCell cell = (OverallCell)ce.nextElement();
	    if (cell.valid == false) {
		cell.computeStatus();
	    }
	    state.addElement(cell.stateRecord());
	}

	return state;

    }

    /* *
     * * Methods required by the WatcherListener interface.
     * */

    /**
     * Called by a watcher when it detects that an action has started.
     */
    public synchronized void startAction(String action) {
	// We'll use -1 as a convention for "no coa given".
	startAction(action, -1, -1);
    }

    /**
     * Called by a watcher when it detects that an action has started.
     */
    public synchronized void startAction(String action, int coa) {
	// We'll use -1 as a convention for "no level given".
	startAction(action, coa, -1);
    }

    /**
     * Called by a watcher when it detects that an action has started.
     */
    public synchronized void startAction(String action, int coa, int level) {
	try {
	    Debug.noteln("startAction(" + action + ", " + coa + ", " +
			                  level + ")");
	    startRecordingChanges();
	    seeIfNewCoa(coa);
	    Node n = coa == -1
		? findActionNode(action)
		: findActionNode(action, coa);
	    n.actionStarted(level);
	    tellViewer();
	    resetChangeRecord();
	}
	catch (Exception e) {
	    Debug.noteException(e);
	}
    }

    /**
     * Called by a watcher when it detects that an action has finished.
     */
    public synchronized void finishAction(String action) {
	// We'll use -1 as a convention for "no coa given".
	finishAction(action, -1, -1);
    }

    /**
     * Called by a watcher when it detects that an action has finished.
     */
    public synchronized void finishAction(String action, int coa) {
	// We'll use -1 as a convention for "no level given".
	finishAction(action, coa, -1);
    }

    /**
     * Called by a watcher when it detects that an action has finished.
     */
    public synchronized void finishAction(String action, int coa, int level) {
	try {
	    Debug.noteln("finishAction(" + action + ", " + coa + ", " +
			                   level + ")");
	    startRecordingChanges();
	    seeIfNewCoa(coa);
	    Node n = coa == -1
		? findActionNode(action)
		: findActionNode(action, coa);
	    n.actionFinished(level);
	    tellViewer();
	    resetChangeRecord();
	}
	catch (Exception e) {
	    Debug.noteException(e);
	}
    }

    protected void seeIfNewCoa(int coa) {
	// /\/: Need to tell the viewer if there are new coas.
	if (coa == -1) {
	    if (numberOfCoas == 0)
		addCoa(1);
	}
	else {
	    int delta = coa - numberOfCoas;
	    if (delta > 0) {
		Debug.noteln("Need to make " + delta + " new coa(s).");
		while (numberOfCoas < coa) {
		    addCoa(numberOfCoas + 1);
		}
	    }
	}
    }

    protected void startRecordingChanges() {
	if (!changedNodes.isEmpty()) {
	    changedNodes.clear();
	    Debug.warn("starting with nonempty changes");
	}
    }

    protected void resetChangeRecord() {
	// Reset for next time.
	changedNodes.clear();
    }

    protected void tellViewer() {
	// Tell the viewer.
	ProcessStatusUpdate up = new ProcessStatusUpdate();
	up.numberOfCoas = numberOfCoas;
	up.statusChanges = statusChanges();
	up.issueCounts = issueCounts();
	up.products = products;
	viewer.statusUpdate(up);
    }

    protected Vector statusChanges() {
	// Debug.noteln("Changed nodes:");
	// Debug.noteEnumeration(changedNodes.elements());

	// Note that we can't just create an enumeration of the changes,
	// because that's not a snapshot, and we later clear changedNodes.
	// A Vector makes it easy to add changes from the overall column,
	// so that's what we used.

	// Get a list of status changes.
	Enumeration ce = Seq.map(changedNodes.elements(), new Function1() {
	    public Object funcall(Object node) {
		return ((Node)node).stateRecord();
	    }
	});

	Vector changes = Seq.toVector(ce);

	// Add in changes from the overall column
	for (ce = overallColumn.elements(); ce.hasMoreElements();) {
	    OverallCell cell = (OverallCell)ce.nextElement();
	    if (cell.valid == false) {
		cell.computeStatus();
		changes.addElement(cell.stateRecord());
	    }
	}

	return changes;

    }

    protected int[] issueCounts() {
	// We'll sum down the columns.  The overall column is number 0.
	// The issues in common are from nodes that are not one-per-coa.
	int[] numberOfIssues = new int[numberOfCoas + 1];
	int issuesInCommon = 0;
	int total = 0;
	Enumeration ne = allNodes.elements();
	while (ne.hasMoreElements()) {
	    Node n = (Node)ne.nextElement();
	    if (n.status == STATUS_EXECUTING || n.status == STATUS_POSSIBLE) {
		// We call that one issue
		if (n.coa == StatusChange.COA_NONE)
		    issuesInCommon++;
		else
		    numberOfIssues[n.coa]++;
		total++;
	    }
	}
	// The issues in common count for every coa, but only once for
	// the total.  /\/: But in some interpretations of what the issues
	// are, they should count for each COA.
	int total_again = 0;
	for (int coa = 1; coa < numberOfIssues.length; coa++) {
	    total_again += numberOfIssues[coa];
	    numberOfIssues[coa] += issuesInCommon;
	}
	Debug.assert(total == total_again + issuesInCommon, "wrong total");

	// Issues from the overall column
	for (Enumeration oe = overallColumn.elements();
	     oe.hasMoreElements();) {
	    OverallCell c = (OverallCell)oe.nextElement();
	    if (c.status == STATUS_EXECUTING || c.status == STATUS_POSSIBLE)
		total++;
	}
	numberOfIssues[0] = total;

	return numberOfIssues;

    }

    /* *
     * * Node code
     * */

    // A node is registered by putting it in nodeTable and in allNodes.
    // However, it's important to note that registration also ensures
    // that there is a row (in rowTable) and an overall cell.  It turns
    // out to matter that nodes are registered *before* they're expanded,
    // because when some overall cells are created, they find a "parent"
    // cell by looking at a node from their row and then taking the
    // overall cell of that node's parent node.  For that, the parent
    // node and its overall cell must already be in the data structures.
    // Therefore these structures have to be created during expansion
    // "on the way down", not "on the way back up", because some overall
    // cells look at the structure "above" them at their creation-time.

    void registerNode(Node node) {
	Debug.assert(node.pattern != null,
		     "Node registration too soon");
	Debug.noteln("Registering Node", node.pattern);
	Debug.assert(nodeTable.get(node.pattern) == null,
		     "Node aleady exists");
	nodeTable.put(node.pattern, node);
	allNodes.addElement(node);
	addRowElement(node);
    }

    protected Node findNode(LList pattern) {
	return (Node)nodeTable.get(pattern);
    }

    public Node findActionNode(String action) {
	Node  n = findNode(Lisp.list(action));
	if (n == null)
	    throw new IllegalArgumentException(
	        "Can't find node for action " + action);
	return n;
    }

    public Node findActionNode(String action, int coa) {
	Node n = findNode(Lisp.list(action, new Long(coa)));
	if (n == null)
	    throw new IllegalArgumentException(
	        "Can't find node for action " + action + ", coa " + coa);
	return n;
    }

    /* *
     * * Matrix rows and the overall column
     * */

    void addRowElement(Node node) {
	Vector row = (Vector)rowTable.get(node.action);
	// Make sure the row Vector exists.
	if (row == null) {
	    Debug.assert(node.coa <= 1);
	    row = new Vector();
	    rowTable.put(node.action, row);
	}
	else {
	    Debug.assert(!row.contains(node));
	}
	// Add the node to the end of the Vector.
	row.addElement(node);
	// Invalidate the overall column entry.
	ensureOverallCell(node.action).invalidate();
    }

    OverallCell ensureOverallCell(String action) {
	// Make sure the cell exists.
	OverallCell cell = (OverallCell)overallColumn.get(action);
	if (cell == null) {
	    cell = newOverallCellInstance(action);
	    overallColumn.put(action, cell);
	}
	return cell;
    }

    OverallCell findOverallCell(String action) {
	OverallCell cell = (OverallCell)overallColumn.get(action);
	Debug.assert(cell != null, "no overall cell");
	return cell;
    }

    /* *
     * * Graph utilities
     * */

    // Some of these are used only when testing and are not normally
    // called at all.

    // Remember that Node preNode and postNode Vectors contain only
    // Nodes directly before or after (respectively) at the same "level"
    // (and so they all have the same parent).

    public LList topLevelNodes() {
	Enumeration ne = allNodes.elements();
	Enumeration fe = Seq.filter(ne, new Predicate1() {
	    public boolean trueOf(Object a) {
		Node n = (Node)a;
		return n.parent == null;
	    }
	});
	return Seq.toLList(fe);	// the LList is a snapshot
    }

    public LList initialNodes() {
	return initialNodes(allNodes.elements(), 0);
    }

    public LList initialNodes(Enumeration ne, final int level) {
	Enumeration fe = Seq.filter(ne, new Predicate1() {
	    public boolean trueOf(Object a) {
		Node n = (Node)a;
		return n.level == level && n.preNodes.isEmpty();
	    }
	});
	return Seq.toLList(fe);	// the LList is a snapshot
    }

    public LList finalNodes() {
	return finalNodes(allNodes.elements(), 0);
    }

    public LList finalNodes(Enumeration ne, final int level) {
	Enumeration fe = Seq.filter(ne, new Predicate1() {
	    public boolean trueOf(Object a) {
		Node n = (Node)a;
		return n.level == level && n.postNodes.isEmpty();
	    }
	});
	return Seq.toLList(fe);	// the LList is a snapshot
    }

    public LList nodesBefore(Node after) {
	return depthFirstFinishOrder(Lisp.list(after), new Function1() {
	    public Object funcall(Object a) {
		Node n = (Node)a;
		return n.preNodes.elements();
	    }
	});
    }

    public LList nodesAfter(Node before) {
	return depthFirstFinishOrder(Lisp.list(before), new Function1() {
	    public Object funcall(Object a) {
		Node n = (Node)a;
		return n.postNodes.elements();
	    }
	});
    }

    public LList nodesBetween(Node before, Node after) {
	return nodesAfter(before).intersect(nodesBefore(after));
    }

    public LList nodesInDepthFirstFinishOrder() {
	// LList nodes = Seq.toLList(allNodes.elements());
	return depthFirstFinishOrder(initialNodes(), new Function1() {
	    public Object funcall(Object a) {
		Node n = (Node)a;
		// The order in succList is just to make the result "nicer"
		LList postList = Seq.toLList(n.postNodes.elements());
		LList succList = n.children.append(postList).reverse();
		return succList.elements();
	    }
	});
    }

    public LList depthFirstFinishOrder(LList roots, Function1 successors) {
	Hashtable marks = new Hashtable();
	LListCollector result = new LListCollector();
	for (LList rs = roots; rs != Lisp.NIL; rs = rs.cdr()) {
	    Object root = rs.car();
	    dffoWalk_(root, successors, marks, result);
	}
	return result.contents();
    }

    private void dffoWalk_(Object at, Function1 sf, 
			   Hashtable marks, LListCollector result) {
	// The separate start and finish marks are only for debugging.
	if (marks.get(at) == null) {
	    Enumeration successors = (Enumeration)sf.funcall(at);
	    marks.put(at, "start");
	    while (successors.hasMoreElements())
		dffoWalk_(successors.nextElement(), sf, marks, result);
	    result.pushElement(at);
	    // marks.put(at, "finish");
	}
    }

    /**
     * Checks that we can reach all of the nodes in various ways.
     * This method is not called during the normal operation of the
     * process model but can be called when testing.
     */
    public synchronized void checkConnectivity() {
	boolean ok = true;
	Enumeration ne;
	HashedSet hs;

	Debug.noteln("Checking connectivity ...");

	// See if the descendents of top level nodes reach all nodes.
	hs = new HashedSet();
	for (LList tl = topLevelNodes(); tl != Lisp.NIL; tl = tl.cdr()) {
	    Node n = (Node)tl.car();
	    hs.addElement(n);
	    Seq.addEnumeration(hs, n.descendents.elements());
	}
	ok = ok
	    & checkNodesReached("Top's descendents", allNodes, hs.elements());

	// See if nodesInDepthFirstFinishOrder() contains all nodes.
	ne = nodesInDepthFirstFinishOrder().elements();
	ok = ok & checkNodesReached("DFFO", allNodes, ne);

	if (!ok) Debug.warn("Connectivity failure");
	else Debug.noteln("... finished connectivity check.");
    }

    private boolean 
    checkNodesReached(String test, Vector all, Enumeration reached) {
	boolean ok = true;
	Mark mAll = new Mark("checkNodesReached all nodes");
	Mark mReached = new Mark("checkNodesReached reached nodes");

	// Mark all nodes
	Enumeration a = all.elements();
	while (a.hasMoreElements())
	    ((Node)a.nextElement()).setMark(mAll);

	// Go through reached nodes
	while (reached.hasMoreElements()) {
	    Node n = (Node)reached.nextElement();

	    // Check that it isn't reached twice and is one of "all nodes".
	    if (n.hasMark(mReached)) {
		Debug.noteln(test + " twice reached", n.pattern);
		ok = false;
	    }
	    else if (!n.hasMark(mAll)) {
		Debug.noteln(test + " reached a disconnected", n.pattern);
		ok = false;
	    }
	    // Mark it as reached
	    n.setMark(mReached);
	}

	// Check that all nodes were reached.
	a = all.elements();
	while (a.hasMoreElements()) {
	    Node n = (Node)a.nextElement();
	    if (!n.hasMark(mReached)) {
		Debug.noteln(test + " didn't reach", n.pattern);
		ok = false;
	    }
	}

	return ok;
    }

    /* *
     * * Classes in the process model
     * */

    /**
     * Create an instance of an appropriate OverallCell class.
     */
    OverallCell newOverallCellInstance(String action) {	// factory
	Schema s = domain.getActionSchema(action);
	Debug.assert(s != null, "Can't find schema for overall");
	if (s.getProperty("overall-collects") != null)
	    return this.new OverallCollectionCell(action);
	else
	    return this.new OverallCell(action);
    }

    /**
     * Overall column cells, a member class of ProcessModel.  An OverallCell
     * summarizes a row in a 2-D tabular (or matrix) description of the
     * process.  All entries on a row are for the same action, but each
     * can be in a different COA.  ("Can be", because some actions appear
     * only once no matter how many COAs there are.  Some rows therefore
     * have only one entry, but if the matrix were displayed, that entry
     * would probably be duplicated in every column.)  Each column represents
     * a COA.  There is also an overall column that contains OverallCells.<p>
     *
     * The ProcessModel method newOverallCellInstance(String action) is
     * a factory method that should be used to create an instance of the
     * appropriate OverallCell class for a action-name (= row).<p>
     *
     * Instances of OverallCell are "standard" cells that compute a status
     * that is the "least" status of any Node in their row, where "less"
     * means roughly "would occur earlier in execution".  So, for instance,
     * EXECUTING is less-than COMPLETE.  Subclasses of OverallCell may
     * implement different rules.
     */

    class OverallCell implements StatusValues {

	String action;
	int status = STATUS_BLANK;
	boolean valid = false;
	String text = null;

	OverallCell(String action) {
	    this.action = action;
	}

	void invalidate() {
	    valid = false;
	}

	// Override computeStatus() in a subclass if you want a different
	// way of determining the status.

	void computeStatus() {
	    status = statusRule();
	    valid = true;
	}

	// Override statusRule in a subclass if you just want a different rule.

	protected int statusRule() {
	    Vector row = (Vector)ProcessModel.this.rowTable.get(action);
	    Debug.assert(row != null, "row doesn't exist for overall cell");
	    int result = STATUS_BLANK;
	    Enumeration re = row.elements();
	    while (re.hasMoreElements()) {
		Node n = (Node)re.nextElement();
		// It just so happens that higher-numbered status values
		// should win...  (except that BLANK trumps them).
		if (n.status == STATUS_BLANK) {
		    result = STATUS_BLANK;
		    break;
		}
		else if (n.status > result)
		    result = n.status;
	    }
	    // But N/A becomes BLANK overall
	    if (result == STATUS_NA)
		result = STATUS_BLANK;
	    return result;
	}

	StatusChange stateRecord() {
	    StatusChange c = new StatusChange();
	    c.action = action;
	    c.coa = StatusChange.COA_OVERALL;
	    c.iteration = 0;
	    c.status = status;
	    return c;
	}

    }

    /**
     * A subclass of OverallCell that reports the collective status of
     * some earlier row.  For instance, it may list all COAs that have
     * completed the action "develop COA".
     */

    class OverallCollectionCell extends OverallCell implements SchemaSymbols {

	String collects = null;		// from schema overall-collects
	boolean requireAll = false;	// from schema ovarall-at-least
	int requiredCollectionSize = 0;	// from schema ovarall-at-least

	OverallCell parentCell = null; 	// set by the constructor(s)

	OverallCollectionCell(String action) {
	    super(action);
	    Debug.noteln("New overall collection for", action);
	    setCollectionProperties();
	    parentCell = getParentCell();
	}

	protected void setCollectionProperties() {
	    // Sets collects, requireAll, and requiredCollectionSize.
	    Schema s = ProcessModel.this.domain.getActionSchema(action);
	    collects = s.getProperty("overall-collects");
	    if (collects != null) {
		Debug.assert(ProcessModel.this.domain.getActionSchema(collects)
			     != null, "Unknown action to collect");
		Object value = s.getPropertyObject("overall-at-least");
		if (value == K_ALL)
		    requireAll = true;
		else if (value != null) {
		    Debug.assert(value instanceof Number);
		    requiredCollectionSize = ((Number)value).intValue();
		}
	    }
	}

	OverallCell getParentCell() {
	    // This indirectly requires that nodes register themselves
	    // before recursively expanding.
	    Vector row = (Vector)ProcessModel.this.rowTable.get(action);
	    Node anyNode = (Node)row.firstElement();
	    if (anyNode.parent == null) {
		// Debug.warn("No parent cell for \"" + action + "\"");
		return null;
	    }
	    else
		return ProcessModel.this.findOverallCell(
		    anyNode.parent.action);
	}

	void computeStatus() {
	    status = statusRule();
	    // Leave invalid so the status will always be recomputed,
	    // because we don't handle invalidation for this case.
	    // valid = true;
	}

	protected int statusRule() {
	    // N.B. Side-effects the text and requiredCollectionSize fields.

	    Vector row = (Vector)ProcessModel.this.rowTable.get(collects);
	    Debug.assert(row != null, "collected row doesn't exist");

	    if (requireAll)
		requiredCollectionSize = ProcessModel.this.numberOfCoas;

	    // Look at the row we're "collecting" to count the number
	    // of nodes on that row that have status COMPLETE.  If
	    // numberComplete >= requiredCollectionSize, then we can
	    // have status POSSIBLE and can have the text field list
	    // the COAs that have completed the required action.

	    int numberComplete = 0;
	    Enumeration re = row.elements();
	    while (re.hasMoreElements()) {
		Node n = (Node)re.nextElement();
		Debug.assert(n.coa >= 1);       // ensure this earlier /\/
		if (n.status == STATUS_COMPLETE) {
		    numberComplete++;
		}
	    }

	    // Set the text field.
	    text = null;
	    if (numberComplete >= requiredCollectionSize && !requireAll) {
		for (re = row.elements(); re.hasMoreElements();) {
		    Node n = (Node)re.nextElement();
		    if (n.status == STATUS_COMPLETE) {
			if (text == null)
			    text = "COA-" + String.valueOf(n.coa);
			else
			    text += "," + String.valueOf(n.coa);
		    }
		}
	    }

	    // Determine the status

	    // If the parent cell has status COMPLETE, so do we.
	    if (parentCell != null) {
		int parentStatus = parentCell.valid 
		    ? parentCell.status
		    : parentCell.statusRule();
		if (parentStatus == STATUS_COMPLETE)
		    return STATUS_COMPLETE;
	    }

	    // Otherwise, it depends on how many COAs have completed
	    // the required action.
	    if (numberComplete >= requiredCollectionSize)
		return STATUS_POSSIBLE;
	    else
		return STATUS_BLANK;

	}

	StatusChange stateRecord() {
	    StatusChange result = super.stateRecord();
	    result.text = text;
	    return result;
	}

    }

    /**
     * Nodes are stored in a NodeTable, indexed by pattern.
     *
     * @see ProcessModel#registerNode
     */
    static class NodeTable extends Hashtable {
    }

    /**
     * Nodes represent actions in the process.  Each node is based on a
     * schema.  If the node is for a particular COA, the schema must
     * be instantiated for that coa by calling
     * <pre>
     *    schema.forCoa(coaNumber)
     * </pre>
     * where coaNumber is a Long.  (It's a Long, because that's how 
     * integer values are represented by the Lisp tokenizer, and hence
     * in the patterns that appear in Schemas.)<p>
     *
     * Note that Node is a member class of the ProcessModel class, and
     * so each Node is associated with a particular "containing" instance
     * of ProcessModel.  To make it clearer when Node methods are referring
     * to fields or methods of the containing instance, we use the syntax
     * that makes that explicit:
     * <pre>
     *    ProcessModel.this.field
     *    ProcessModel.this.method(...)
     * </pre>
     * We also explicitly specify the containing model when creating a new
     * Node.
     */
    class Node implements Markable, StatusValues, EffectSymbols {

	Schema schema = null;	// the schema
	LList pattern = null;	// e.g. ("review COA" 3), as in the schema

	String action;		// from the pattern
	int coa;		// from the pattern or else COA_NONE

	// /\/: Need a better name than "level" because "level"
	// is often a local var.

	int level = 0;		// expansion level, 1 + the parent's
	int status = STATUS_BLANK;
	int iteration = 0;	// number of times this action has executed

	int planLevel = -1;      // plan level reached by the monitored process

	boolean isOrJoin = false;
	boolean isDecisionSplit = false;
	boolean isLoopEntry = false;

	boolean setsParentPlanLevel = false; // depends on action String

	boolean isLoopBack = false; // loopBacks.isEmpty() + a patch /\/

	Node parent = null;
	LList children = Lisp.NIL;
	LList descendents = Lisp.NIL;

	Vector preNodes = new Vector();	 // nodes directly before this
	Vector postNodes = new Vector(); // nodes directly after this
	Vector loopBacks = new Vector(); // nodes this loops back to
	Vector loopFroms = new Vector(); // nodes that loop back to this

	LList loopNodes = Lisp.NIL;       // only when isLoopEntry

	Mark mark = null;

	Node () {
	    // Only for temp node used when adding a coa.  /\/
	}

	Node(Node parent, LList pattern) {

	    // Debug.noteln("New node:", pattern);

	    this.parent = parent;
	    this.pattern = pattern;
	    this.schema = getSchema(pattern);

	    this.action = (String)pattern.car();
	    this.coa = pattern.cdr() != Lisp.NIL
		? ((Number)pattern.elementAt(1)).intValue()
		: StatusChange.COA_NONE;

	    if (parent != null)
		level = parent.level + 1;

	    this.isOrJoin = this.schema.isTrue("or-join");
	    this.isDecisionSplit = this.schema.isTrue("decision-split");
	    this.isLoopEntry = this.schema.isTrue("loop-entry");

	    this.setsParentPlanLevel =
		(action.endsWith("to one more level") ||
		 action.endsWith("plan-cycle"));

	    registerSelf();	// must be before expansion

	    String statusName = this.schema.getProperty("initial-status");
	    if (statusName != null) {
		int statusValue = ViewColor.statusValue(statusName);
		Debug.assert(statusValue > 0);
		changeStatus(statusValue); // this must be registered 1st
	    }

	    expand();

	}

	protected void registerSelf() {
	    ProcessModel.this.registerNode(this);
	}

	protected Schema getSchema (LList pattern) {
	    String action = (String)pattern.car();
	    Schema s = ProcessModel.this.domain.getActionSchema(action);
	    Debug.assert(s != null, "Can't find schema");
	    if (s.isForEachCoa) {
		return s.forCoa(patternCoa(pattern)); // instantiate schema
	    }
	    else {
		Debug.assert(pattern.length() == 1, "bogus pattern");
		return s;
	    }
	}

	protected Long patternCoa(LList pattern) {
	    Debug.assert(pattern.length() == 2, "No coa in pattern");
	    return (Long)pattern.elementAt(1);
	}

	void expand() {

	    final ProcessModel ourModel = ProcessModel.this;

	    // Create the child nodes specified by the schema.
	    // A nodeSpec is a LList (Long:number LList:pattern).
	    children = schema.nodes.mapcar(new Function1() {
		public Object funcall(Object nodeSpec) {
		    LList spec = (LList)nodeSpec;
		    LList childPattern = (LList)spec.elementAt(1);
		    return ourModel.new Node(Node.this, childPattern);
		}
	    });

	    // Add the orderings
	    processOrderings(children, schema.orderings);

	    // If the children were expanded here, rather as they're
	    // constructed, the overall process might be easier to
	    // follow.  We'd see a level of nodes created, and then
	    // the orderings added for that level, before recursion
	    // to the next (lower) level of nodes.  Right now, instead,
	    // the Node constructor calls expand(), and so when expand()
	    // creates a child, the entire subtree under it is created,
	    // before any orderings are added; and then orderings
	    // are added level-by-level as the recursive calls return.
	    // If this changes, remember to change ProcessModel#addCoa.

	    for (LList cl = children; cl != Lisp.NIL; cl = cl.cdr()) {
		Node child = (Node)cl.car();
		// child.expand();
		child.afterOrderings();
	    }

	    // Collect all descendents
	    descendents = getDescendents();

	}

	void processOrderings(LList nodes, LList orderings) {
	    for (Enumeration ords = orderings.elements();
		 ords.hasMoreElements();) {
		LList ord = (LList)ords.nextElement();
		int n1 = ((Number)ord.car()).intValue() - 1;
		int n2 = ((Number)ord.cdr().car()).intValue() -1;
		Node before = (Node)nodes.elementAt(n1);
		Node after = (Node)nodes.elementAt(n2);
		if (n2 > n1)
		    before.linkBefore(after);
		else
		    before.loopBackTo(after);
	    }
	}

	void linkBefore(Node after) {
	    // Debug.noteln("  " + this + "\n  --> " + after);
	    postNodes.addElement(after);
	    after.preNodes.addElement(this);
	    // Since after has a new preNode, its status may change.
	    if (after.status == STATUS_POSSIBLE & !after.isReady())
		after.changeStatus(STATUS_BLANK);
	}

	void loopBackTo(Node after) {
	    // Debug.noteln("Loopback " + this + "\n  --> " + after);
	    Debug.assert(after.isLoopEntry, "loop back to non-LoopEntry");
	    loopBacks.addElement(after);
	    after.loopFroms.addElement(this);
	    if (loopBacks.size() != 1 || after.loopFroms.size() != 1)
		Debug.warn("Complex loop involving " + this +
			   " and " + after);
	}

	void afterOrderings() {
	    // Here we do any initialization that can't happen until
	    // after orderings have been added.  Remember that for
	    // nodes that are not 1-per-coa, new orderings can be
	    // added when there's a new coa.
	    if (isLoopEntry) {
		// Debug.noteln("Nodes that loop back to", this);
		// Debug.noteEnumeration(loopFroms.elements());
		if (loopFroms.size() != 1) {
		    Debug.warn("More than one node loops back to " + this);
		}
		else {
		    loopNodes = getLoopNodes();
		    // Debug.noteln("Loop contents for", this);
		    // Debug.noteEnumeration(loopNodes.elements());
		}
	    }
	    if (setsParentPlanLevel) {
		LList inodes = initialNodes(parent.children.elements(), level);
		// It's not necessary that sPPL nodes be initial,
		// but we should probably think about any that aren't.  /\/
		Debug.assert(parent != null,     "sPPL node has parent");
//		Debug.assert(isLoopEntry,        "sPPL node is loop-entry");
		Debug.assert(preNodes.isEmpty(), "sPPL node is initial");
		Debug.assert(inodes.length()==1, "sPPL node is only initial");
		Debug.assert(inodes.car()==this, "sPPL node found initial");
	    }
	    // isLoopBack is a faster test, but the proximate reason is because
	    // there are problems in the acp3-process.lsp model so that we've
	    // left out the actual link back from "reject..." nodes.  /\/
	    isLoopBack = !loopBacks.isEmpty()
		      || schema.isTrue("treat as loopback"); // gak! /\/
	}

	LList getLoopNodes() {
	    Node loopEnd = (Node)loopFroms.firstElement();
	    return ProcessModel.this.nodesBetween(this, loopEnd);
	}

	LList getDescendents() {
	    LListCollector d = new LListCollector();
	    walkDescendents(d.elementAdder());
	    return d.contents();
	}

	void walkDescendents(Function1 f) {
	    for (LList cl = children; cl != Lisp.NIL; cl = cl.cdr()) {
		Node child = (Node)cl.car();
		f.funcall(child);
		child.walkDescendents(f);
	    }
	}

	// actionStarted(int level) and actionFinished(int level) are
	// called when the ProcessMonitor receives something from the
	// watcher, and they do all the interesting work.

	// The "level" (planLevel) is the plan level being developed. 
	// This level starts at 0, and Nodes initially have iteration == 0.
	// But when we find out that an action has started, we increment
	// the iteration count; so the iteration count is normally level + 1.
	// Since it's level that the viewers display, they're given
	// iteration - 1 when the number is based on the iteration count.

	// Note that the Node field "level" is something different --
	// expansion depth in the model (ie, how many Node.parent links
	// you can follow upwards).  There are separate Node fields 
	// "level", "planLevel", and "iteration".

	// /\/: expansionLevel and planningLevel might be better names.

	void actionStarted(int level) {
	    // We've discovered that this node's action has started.
	    // Since this is called only when we receive an event from
	    // the watcher, we know it has definitely happened.
	    // So the parent action must have started too, and all
	    // actions that must complete before this one can start
	    // must have finished.  But it's possible we already
	    // handled those things when processing earlier events.
	    // So if the current status of these other nodes is already
	    // what it should be, we leave them alone.

	    // The level is used only as a check.  Perhaps it should
	    // override the iteration count instead.  /\/
	    if (level >= 0 && level != iteration)
		Debug.noteln("Started " + this + " iteration " + iteration +
			     " but level " + level);
	    actionStarted();
	}

	void actionFinished(int level) {
	    // We've discovered that this node's action has finished.
	    // Since this is called only when we receive an event from
	    // the watcher, we know it has definitely happened.

	    // The level is used only as a check.  /\/
	    if (level >= 0 && level != iteration)
		Debug.noteln("Finished " + this + " iteration " + iteration +
			     " but level " + level);
	    actionFinished();
	}

	void actionStarted() {
	    // We've decided this node's action has started.

	    // In some cases, we report a level to the parent Node.
	    for (Node n = this; n.parent != null; n = n.parent) {
		if (n.setsParentPlanLevel)
		    n.parent.setPlanLevel(iteration);
		else
		    break;
	    }

	    // If the status is already EXECUTING or COMPLETE, then this
	    // is a new iteration of a loop.
	    if (status == STATUS_EXECUTING || status == STATUS_COMPLETE) {
		if (isLoopEntry) {
		    newLoopIteration();
		    return;
		}
		else if (parent.isLoopEntry) {
		    // /\/: Should allow the parent to be anywhere in a loop.
		    iteration++;
		    changeStatus(STATUS_EXECUTING);
		    parent.newLoopIteration();
		    return;
		}
		else
		    Debug.warn("Looping to non-loop-entry " + this);
	    }

	    // From here on, we can assume we're not yet in a loop 
	    // (though a loop back might still happen later on).

	    if (status != STATUS_BLANK && status != STATUS_POSSIBLE) {
		Debug.warn("Action start for " + this +
			   " with status " + statusName(status));
	    }

	    iteration++;
	    changeStatus(STATUS_EXECUTING);

	    // Figure out what must have happened at nodes above and before
	    propagateActionStarted();

	    // Children without predecessors should be at least ready
	    // to exectute.  (In fact, they may have already started,
	    // or even finished, because we may be working up from one
	    // of them.)
	    for (LList cl = children; cl != Lisp.NIL; cl = cl.cdr()) {
		Node child = (Node)cl.car();
		if (child.preNodes.isEmpty() &&
		    child.status != STATUS_EXECUTING &&
		    child.status != STATUS_COMPLETE)
		  child.changeStatus(STATUS_POSSIBLE);
	    }
	}

	void newLoopIteration() {
	    // For now, this is the only point at which we try to handle
	    // a new iteration for a node that has already started or
	    // completed execution.
	    Debug.assert(isLoopEntry);
	    Debug.assert(loopNodes.length() > 0);

	    // First, all nodes after this in the loop, and their descendents,
	    // must have completed the previous iteration.  We make the
	    // changes directly, without any propagation.
	    for (LList ll = loopNodes; ll != Lisp.NIL; ll = ll.cdr()) {
		Node n = (Node)ll.car();
		if (n == this)
		    continue;
		if (n.iteration < iteration) {
		    // This node hasn't noticed the current iteration
		    if (n.iteration < iteration-1)
			Debug.noteln(n + " has fallen behind");
		    n.iteration = iteration;
		    n.changeStatus(STATUS_COMPLETE);
		    // Update the descendents too.
		    n.completeDescendentExecution(iteration);
		}
	    }

	    // The descendents of this node must also have completed
	    // the previous iteration.
	    completeDescendentExecution(iteration);

	    // Now this node (the loop-entry) starts the new iteration.
	    iteration++;
	    changeStatus(STATUS_EXECUTING);
	}

	void completeDescendentExecution(int minIteration) {
	    // A node has executed; therefore all the nodes under it
	    // must have executed at least once in such a way that
	    // they exited any loops.  So we assume the loopBack
	    // nodes didn't execute.  /\/: Really it should be all nodes
	    // on loopBack *paths*, but fortunately we don't have any
	    // of those of length > 1.  We also assume there aren't any
	    // xor-parallel paths only one of which can be followed.
	    // An alternative would be to make all descendents
	    // complete (and invalidate their iterations counts?),
	    // which is roughly the view we take in actionFinished().
	    for (LList dl = descendents; dl != Lisp.NIL; dl = dl.cdr()) {
		Node d = (Node)dl.car();
		if (!d.isLoopBack) {
		    if (d.iteration < minIteration) {
			d.iteration = minIteration;
			d.changeStatus(STATUS_COMPLETE);
		    }
		}
	    }
	}

	void propagateActionStarted() {
	    // The parent action must have started too, but maybe
	    // we've already handled that.
	    if (parent != null
		&& (parent.status == STATUS_BLANK
		    || parent.status == STATUS_POSSIBLE)) {
		parent.actionStarted();
	    }
	    // The actions that had to complete before this one could
	    // start must have completed.  But which actions are those?
	    // If this is an and-join, it's all the nodes directly before
	    // this one.  If this node is an or-join, we can't really tell.
	    // In any case, if the prerequisites already have status 
	    // COMPLETE, we leave them alone.
	    for (Enumeration pre = preNodes.elements();
		 pre.hasMoreElements();) {
		Node p = (Node)pre.nextElement();
		if (p.status != STATUS_COMPLETE) {
		    if (this.isOrJoin) {
			// Not clear what to do
			Debug.noteln("Can't tell which actions before " +
				     pattern + " must have completed.");
			break;
		    }
		    p.actionFinished();
		}
	    }
	    // If one of the nodes directly before this one is a
	    // decision-split, then, since this node has started
	    // executing, all other direct successors of the d-split
	    // cannot execute.  We should probably do something about
	    // this.  /\/
	}

	void actionFinished() {
	    boolean isNewIteration = false;

	    // We've decided that this node's action has finished.
	    // If its status is EXECUTING, we'll assume this is the
	    // completion of the same iteration.  If the status is
	    // BLANK, we'll assume the start wasn't monitored.
	    // If the status is already COMPLETE, then either we
	    // mistakenly inferred completion or we're in some kind
	    // of loop.  We don't yet have a way to distinguish the
	    // already-complete cases or to handle properly a loop
	    // detected here.
	    if (status == STATUS_BLANK) {
		isNewIteration = true;
		iteration++;
	    }
	    else if (status == STATUS_EXECUTING) {
		isNewIteration = false;
	    }
	    else if (status == STATUS_COMPLETE) {
		// Let's try this ... /\/
		Debug.noteln("Complete when already complete", this);
		isNewIteration = true;
		iteration++;
	    }

	    // The status is now COMPLETE.
	    changeStatus(STATUS_COMPLETE);

	    // The entire tree under this node must be complete too.
	    // Since there can be no order-links with nodes outside
	    // the subtree, we can just change the status of all the
	    // subtree nodes without worrying about propagation.
	    // /\/: It's not clear that this is really right, because
	    // there may be decision-splits in the subtree.  Only one
	    // branch can have been taken, so only one can be COMPLETE.
	    // There may also be or-paths in parallel, only some of
	    // which have executed.  cf completeDescendentExecution().
	    for (LList dl = descendents; dl != Lisp.NIL; dl = dl.cdr()) {
		Node d = (Node)dl.car();
		if (isNewIteration)
		    d.iteration++;
		d.changeStatus(STATUS_COMPLETE);
	    }

	    // Everything that has to happen before the action can
	    // even start must have happened.  (See actionStarted.)
	    propagateActionStarted();

	    // Moreover, some nodes after this one may now be ready
	    // to execute.
	    for (Enumeration post = postNodes.elements();
		 post.hasMoreElements();) {
		Node p = (Node)post.nextElement();
		if (p.status != STATUS_EXECUTING &&
		    p.status != STATUS_COMPLETE  &&
		    p.isReady())
		  p.changeStatus(STATUS_POSSIBLE);
	    }

	}

	boolean isReady() {
	    // Does not handle the initial-node case.  (An initial node has
	    // no preNodes and is ready iff its parent has started execution.)
	    if (this.isOrJoin) {
		for (Enumeration pre = preNodes.elements();
		     pre.hasMoreElements();) {
		    Node p = (Node)pre.nextElement();
		    if (p.status == STATUS_COMPLETE)
			return true;
		}
		return false;
	    }
	    else { // An and-join
		for (Enumeration pre = preNodes.elements();
		     pre.hasMoreElements();) {
		    Node p = (Node)pre.nextElement();
		    if (p.status != STATUS_COMPLETE && p.status != STATUS_NA)
			return false;
		}
		return true;
	    }
	}

	void changeStatus(int newStatus) {
	    // Always record the change even if the status will not actually
	    // be different.  This is because the iteration count may have
	    // changed.  /\/
	    if (status == STATUS_NA)
		return;
	    // Debug.noteln("Status " + this +
	    //              " --> " + statusName(newStatus));
	    ProcessModel.this.changedNodes.addElement(this);
	    status = newStatus;
	    findOverallCell(action).invalidate();

	    // Handle effects here, because it's the only place
	    // that sees all status changes.  /\/
	    handleEffects(schema.effects, newStatus);
	}

	void setPlanLevel(int newLevel) {
	    ProcessModel.this.changedNodes.addElement(this);
	    planLevel = newLevel;
	}

	String statusName(int status) {
	    String[] name =
	        {"BLANK", "COMPLETE", "EXECUTING",
		 "POSSIBLE", "IMPOSSIBLE", "N/A"};
	    return name[status];
	}

	StatusChange stateRecord() {
	    StatusChange c = new StatusChange();
	    c.action = action;
	    c.coa = coa;
	    c.iteration = iteration;
	    c.status = status;
	    c.planLevel = planLevel;
	    return c;
	}

	void handleEffects(LList effects, int nodeStatus) {
	    for(LList el = effects; el != Lisp.NIL; el = el.cdr()) {
		handleEffect((Effect)el.car(), nodeStatus);
	    }
	}

	void handleEffect(Effect e, int nodeStatus) {
	    if (nodeStatus == STATUS_EXECUTING & e.when == S_START) {
		handleEffect(e);
	    }
	    else if (nodeStatus == STATUS_COMPLETE & e.when == S_FINISH) {
		handleEffect(e);
	    }
	}

	// Here's where we finally do what the Effect says to do.

	/* static */ Object effectPatSyntax = Lisp.readFromString(
	  "(status (?name))"
	);

	/* static */ Symbol Q_NAME = Symbol.intern("?name");

	void handleEffect(Effect e) {
	    MatchEnv env = SimpleMatcher.mustMatch(effectPatSyntax, e.pattern);
	    Object name = env.get(Q_NAME);
	    Product p = ProcessModel.this.getProduct(name);
	    Debug.assert(p != null, "No product named", name);

	    Debug.noteln("Changing status of product "
			 + p.name + " to " + e.value);

	    // Change the Product's status
	    if (e.value == K_COMPLETE)
		p.status = PRODUCT_COMPLETE;
	    else if (e.value == K_DRAFT)
		p.status = PRODUCT_DRAFT;
	    else
		Debug.warn("Can't handle effect " + e);

	}

	// /\/: A more spohisticated marking mechanism is needed.

	public void setMark(Mark m) {
	    this.mark = m;
	}

	public boolean hasMark(Mark m) {
	    return mark == m;
	}

	public String toString(){
	    return "#<node level " + level + " " + pattern + ">";
	}

    }

}
