/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.javatest.exec;

import java.util.Hashtable;
import java.util.Vector;

import com.sun.javatest.Status;
import com.sun.javatest.TestDescription;
import com.sun.javatest.TestFilter;
import com.sun.javatest.TestResult;
import com.sun.javatest.TestResultTable;
import com.sun.javatest.util.Debug;
import com.sun.javatest.util.DynamicArray;

/**
 * Cached information about a particular tree node.
 * We want to collect pass/fail stats and other variable information so it can
 * be used to render a node quickly when requested by JTree.  Code here should
 * never run on the GUI thread.
 *
 * <p>
 * Objects of this type run to completion and are then immutable.
 */
class TT_NodeCache implements Runnable {
    /** 
     * Construct a cache object which will collect info about the given node
     * with respsect to the supplied filter.
     *
     * @param n Node to examine, should not be null.
     * @param f Filter to use when producing information.  Can be null.
     */
    TT_NodeCache(TestResultTable.TreeNode n, TestFilter f) {
	filter = f;
	node = n;

	// all the states plus filtered out
	testLists = new Vector[Status.NUM_STATES+1];
	for (int i = 0; i < Status.NUM_STATES+1; i++)
	    testLists[i] = new Vector();
    }

    /**
     * Start or resume processing.
     */
    public void run() {
	if (debug) {
	    Debug.println("TT_NodeCache starting");
	    Debug.println("   -> " + this);
	    Debug.println("   -> node " + node + "(" + node.getName() + ")");
	    Debug.println("   -> filter=" + filter);
	    Debug.println("   -> old state=" + state);
	}

	// needed for a new run, a paused object will already have one
	if (it == null)
	    it = init();
	
	if (debug)
	    Debug.println("   -> iterator= " + it);

	state = COMPUTING;

	process();
    }

    /**
     * Pause processing, and return immediately.
     * If the node has completed processing, calling this method has no
     * effect.
     * Be careful with MT activities when using <tt>pause() resume() isPaused()</tt>.
     * @see #resume()
     */
    void pause() {
	if (state < COMPLETED) {
	    state = PAUSED;
	    if (debug) {
		Debug.println("TT_NodeCache for " + node.getName() + " pausing...");
	    }
	}
	// else do nothing
    }

    /**
     * Continue processing after a pause.
     * @see #pause
     * @throws IllegalStateException if <tt>pause()</tt> was not previously called.
     */
    void resume() {
	if (state != PAUSED)
	    throw new IllegalStateException("Cache node not previously paused.");

	state = COMPUTING;
	process();
    }

    /**
     * @return True if the replacement has an effect on the statistics.  False
     *         if it does not.
     */
    synchronized boolean add(TestResultTable.TreeNode[] path, TestResult what, int index) {
	boolean result = false;
	boolean wouldAccept = false;	 // special case because of filtering
	boolean needsProcessing = false;

	// not even running yet
	if (it == null)
	    return false;

	try {
	    TestDescription td = what.getDescription();
	    TestFilter rejector = null;

	    synchronized(fObs) {
		wouldAccept = filter.accepts(td, fObs);
		if (!wouldAccept && fObs.lastTd == td) {
		    rejector = fObs.lastRejector;
		    fObs.clear();
		}
	    }	// sync

	    needsProcessing = (!it.isPending(what));

	    if (!needsProcessing) {
		// It's still going to come out of the iterator.
		// Make sure it's not the one which is committed to be next.
		// Would be nice to work around this in a different way, either
		// here or in the iterator.
		Object peek = it.peek();
		if (peek instanceof TestResult &&
		    ((TestResult)peek).getTestName().equals(what.getTestName())) {
		    TestResult junk = (TestResult)(it.next());	    // consume
		    needsProcessing= true;
		}
		else { }
	    }
	    else { }

	    if (needsProcessing) {
		if (!wouldAccept) {
		    // add to filtered out list
		    localRejectCount++;
		    testLists[testLists.length-1].add(what);
 		    rejectReasons.put(what, rejector);
		}
		else {
		    int type = what.getStatus().getType();

		    // unfortuantely the add/remove messages don't seem to
		    // be 100% symmetric, or else there is some other race
		    // condition.  I tried to find the problem, but 0-5 tests out
		    // of 10000 will end up added twice when loading a workdir at
		    // startup. 12/9/2002
		    if (!testLists[type].contains(what)) {
			stats[type]++;
			testLists[type].add(what);
		    }
		    else {
		    }
		}

		result = true;
	    }
	    else {	// will be counted later...
		result = false;
	    }

	    // send out notifications if needed
	    notify((wouldAccept ?
		    what.getStatus().getType()+TT_NodeCacheObserver.OFFSET_FROM_STATUS
		    : TT_NodeCacheObserver.MSGS_FILTERED),
		    true, path, what, index);

	    if (result)
		notifyStats();
	}
	catch (TestResult.Fault f) {
	    if (debug) {
		f.printStackTrace(Debug.getWriter());
		Debug.println("TT_NC - TR fault, purging old info. " + what.getTestName());
	    }

	    // TR is somehow corrupt, remove it
	    what = node.getEnclosingTable().resetTest(what.getTestName());
	    result = false;
	}
	catch (TestFilter.Fault f) {
	    if (debug)
		f.printStackTrace(Debug.getWriter());

	    // filter is broken, shove everything into filtered out.
	    // this behavior is similar to what would happen in the same
	    // case when the harness is iterating to select tests to run.
	    // trying to only do this if counting of this test would have 
	    // been necessary.
	    if (needsProcessing) {
		localRejectCount++;
		testLists[testLists.length-1].add(what);
		// this reject will not have a reason entry in
		// rejectReasons
	    }

	    // ignore error and don't do anything
	    result = false;
	}

	return result;
    }

    /**
     * @return True if the replacement has an effect on the statistics.  False
     *         if it does not.
     */
    synchronized boolean remove(TestResultTable.TreeNode[] path, TestResult what, int index) {
	boolean result = false;
	// special case because of filtering

	// not even running yet
	if (it == null)
	    return false;

	boolean wouldAccept = false;
	int type = what.getStatus().getType();

	if (!it.isPending(what)) {	    // check interator position
	    int[] rmList = locateTestInLists(what, type, -1);

	    if (rmList[0] != -1) {
		Object junk = testLists[rmList[0]].remove(rmList[1]);

		// decrement counter
		if (rmList[0] < stats.length) {
		    stats[rmList[0]]--;
		}
		else
		    localRejectCount--;

		// send out notifications if needed
		notify(rmList[0]+TT_NodeCacheObserver.OFFSET_FROM_STATUS, false, path, what, index);
		result = true;
	    }
	    else { }
	}
	else {	// pending in iterator
	    Object peek = it.peek();
	    if (peek instanceof TestResult &&
		((TestResult)peek).getTestName().equals(what.getTestName())) {
		TestResult junk = (TestResult)(it.next());	    // consume

		// do it again to make it happen
		result = remove(path, what, index);
	    }
	    
	}

	if (result)
	    notifyStats();

	return result;
    }

    /**
     * @return True if the replacement has an effect on the statistics.  False
     *         if it does not.
     */
    synchronized boolean replace(TestResultTable.TreeNode[] path, TestResult what,
				 int index, TestResult old) {
	boolean result = false;
	// not even running yet
	if (it == null)
	    return false;

	if (!it.isPending(what)) {	    // check interator position
	    int typeNew = what.getStatus().getType();
	    int typeOld = old.getStatus().getType();

	    // filtering of old does not work because of the status filter
	    boolean wouldAcceptNew = false;
	    TestFilter rejector = null;

	    try {
		TestDescription td = what.getDescription();

		synchronized(fObs) {
		    wouldAcceptNew = filter.accepts(td, fObs);
		    if (!wouldAcceptNew && fObs.lastTd == td) {
			rejector = fObs.lastRejector;
			fObs.clear();
		    }
		}	// sync
	    }   // try
	    catch (TestResult.Fault f) {
		if (debug)
		    f.printStackTrace(Debug.getWriter());

		// ignore error and don't do anything
		return false;
	    }	// catch
	    catch (TestFilter.Fault f) {
		if (debug)
		    f.printStackTrace(Debug.getWriter());

		// ignore error and don't do anything
		return false;
	    }	// catch

	    // inserting into one of the status lists or the filtered out list
	    int targetList = (wouldAcceptNew ? typeNew : testLists.length-1);
	    int[] rmList = null;

	    // optimization to search based on expected location of old test
	    // and optimize out null changes
	    if (what == old) {
		if (typeOld != typeNew) {	// special case optimization
		    // no change to lists
		    rmList = new int[] {-1, -1};
		}
		else {
		    rmList = locateTestInLists(old, Status.NOT_RUN, targetList);

		    if (rmList[0] == targetList) {
			// same TR object, same status
			// use -1 to specify no remove action
			rmList[0] = -1;
			rmList[1] = -1;
		    }
		}
	    }
	    else
		rmList = locateTestInLists(old, typeOld, targetList);

	    if (rmList[0] != -1) {
		Object junk = testLists[rmList[0]].remove(rmList[1]);
		testLists[targetList].add(what);

		// decrement counter
		if (rmList[0] < stats.length)
		    stats[rmList[0]]--;
		else {
		    localRejectCount--;
		    rejectReasons.remove(what);
		}

		// increment counter
		if (targetList < stats.length)
		    stats[targetList]++;
		else {
		    localRejectCount++;
		    rejectReasons.put(what, rejector);
		}

		// remove the old
		// add the new
		notify(rmList[0]+TT_NodeCacheObserver.OFFSET_FROM_STATUS,
			false, path, old, index);
		notify(targetList+TT_NodeCacheObserver.OFFSET_FROM_STATUS,
			true, path, what, index);

		result = true;
	    }
	    else {
		// must be a null change
	    }
	}
	else	// this TR has yet to be processed
	    result = false;

	if (result)
	    notifyStats();

	return result;
    }

    boolean isPaused() {
	return (state == PAUSED);
    }

    /**
     * Is this object eligible to run.
     * This translates to being either unprocessed or paused.
     */
    boolean canRun() {
	return ((state == PAUSED || state == NOT_COMPUTED) &&
	        valid == true);
    }

    void halt() {
	if (debug) {
	    Debug.println("TT_NodeCache thread stopping");
	    Debug.println("   -> " + this);
	}

	state = ABORTED;
	valid = false;
    }

    /**
     * Is info still being collected?
     * The state is not considered active if the processing is paused.
     *
     * @return True if informatiion is incomplete, false otherwise.
     * @see #isPaused()
     */
    boolean isActive() {
	return (state == COMPUTING);
    }

    /**
     * Has all the information been collected.
     * @return True if all information is up to date, and will not change
     *         unless this node is invalidated.
     */
    boolean isComplete() {
	return (state == COMPLETED);
    }

    /**
     * Invalidate any information in this node cache.
     * @see #isValid()
     */
    void invalidate() {
	valid = false;
	//node.removeObserver(watcher);
	//watcher = null;
    }

    /**
     * Has the data in this node been invalidated.
     * A node may be valid while it is active, but becomes invalid when
     * notified that the constraints that it is executing with are no longer
     * correct.  It may also become invalid if the thread is interrupted.
     * @see #invalidate()
     */
    boolean isValid() {
	return valid;
    }

    TestResultTable.TreeNode getNode() {
	return node;
    }

    TestFilter getFilter() {
	return filter;
    }

    /**
     * Get the pass fail error notrun stats.
     * The data may be in flux if the data is still being collected, use
     * <tt>isActive()</tt> to anticipate this.
     * @return An array of size Status.NUM_STATES.  This is not a copy, do not
     *         alter.
     * @see #isActive()
     * @see com.sun.javatest.Status#NUM_STATES
     */
    int[] getStats() {
	/* in case we want to do copies
	int[] copy = null;

	if (it != null) {
	    int[] stats = it.getResultStats();
	    copy = new int[stats.length];
	    System.arraycopy(stats, 0, copy, 0, stats.length);
	}
	else {
	    copy = new int[Status.NUM_STATES];
	}
	*/

	return stats;
    }

    /**
     * Find out how many tests were rejected by filters.
     * @return Number of rejected tests found in and below this node.
     */
    int getRejectCount() {
	if (it != null)
	    return it.getRejectCount() + localRejectCount;
	else
	    return localRejectCount;
    }

    TestFilter getRejectReason(TestResult tr) {
	return (TestFilter)(rejectReasons.get(tr));
    }

    /**
     * Snapshot the current data and add an observer.
     * This is an atomic operation so that you can get completely up to date
     * and monitor all changes going forward.
     *
     * @param obs The observer to attach.  Must not be null.
     * @param needSnapshot Does the caller want a snapshot of the current test lists.
     *        True if yes, false if not.
     * @return A copy of the Vectors that contain the current list of tests.  Null if
     *         <tt>needSnapshot</tt> is false.
     */
    synchronized Vector[] addObserver(TT_NodeCacheObserver obs, boolean needSnapshot) {
	// snapshot the current data
	// must be done before adding the observer to ensure correct data
	// delivery to client

	Vector[] cp = null;
	if (needSnapshot) {
	    cp = new Vector[testLists.length];
	    for (int i = 0; i < testLists.length; i++)
		cp[i] = (Vector)(testLists[i].clone());
	}

	if (obs != null)
	    observers = (TT_NodeCacheObserver[])DynamicArray.append(observers, obs);

	return cp;
    }

    // advisory - many thread access this.  you should lock this object before
    // calling.  See BP_TestListSubpanel.reset(TT_NodeCache), which locks this,
    // THEN itself (the GUI component) for proper locking sequence, since the
    // highest contention is for this cache object.
    synchronized void removeObserver(TT_NodeCacheObserver obs) {
	observers = (TT_NodeCacheObserver[])DynamicArray.remove(observers, obs);
    }

    // ------------- PRIVATE -----------------

    private void process() {
	while (state != ABORTED && state != PAUSED && it.hasNext()) {
	    try {
                synchronized (node) {       // to maintain locking order
                    synchronized (this) {	// sync to lockout during add/remove/replace
                        if (!it.hasNext())	// need to recheck after locking
                            continue;

                        TestResult tr = (TestResult)it.next();
                        TestDescription td = tr.getDescription();
                        TestFilter rejector = null;
                        boolean wouldAccept = false;

                        synchronized(fObs) {
                            try {
                                wouldAccept = filter.accepts(td, fObs);
                            }
                            catch (TestFilter.Fault f) {
                                if (debug)
                                    f.printStackTrace(Debug.getWriter());

                                // asssume it is accepted, this is what would
                                // happen for test exection as well
                                wouldAccept = true;
                            }	// catch

                            if (!wouldAccept && fObs.lastTd == td) {
                                rejector = fObs.lastRejector;
                                fObs.clear();
                            }
                        }	// sync

                        if (wouldAccept) {
                            int type = tr.getStatus().getType();

                            if (!testLists[type].contains(tr)) {
                                stats[type]++;
                                testLists[type].add(tr);

                                // XXX We are not producing
                                // the all the parameters.  it seems to be overkill
                                // for what we are currently using this event for.
                                // Perhaps the API need to be revisited.
                                notify(type+TT_NodeCacheObserver.OFFSET_FROM_STATUS,
                                   true, null, tr, -1);
                            }
                            else {
                                // duplicate, no action
                            }

                        }
                        else {
                            // filtered out list
                            testLists[testLists.length-1].add(tr);
                            rejectReasons.put(tr, rejector);
                            localRejectCount++;

                            // XXX We are not producing
                            // the all the parameters.  it seems to be overkill
                            // for what we are currently using this event for.
                            // Perhaps the API need to be revisited.
                            notify(TT_NodeCacheObserver.MSGS_FILTERED,
                               true, null, tr, -1);
                        }
                    }	// sync this
                }	// sync node
	    }   // try
	    catch (TestResult.Fault f) {
		if (debug)
		    f.printStackTrace(Debug.getWriter());

		// try to recover it?
		// ignore error and don't do anything
	    }   // catch

	    notifyStats();
	}   // while

	if (state != PAUSED)
	    cleanup();
    }

    /**
     * Find a particular test in the test lists.  You can specify which lists to
     * search first or last to help mitigate the O(n) search performance.  It is
     * assumed that synchronization for access to the lists has been taken care of.
     *
     * @param tr The test to locate.
     * @param firstListToCheck Hint of where to start looking.  -1 to specify none.
     * @param lastListToCheck Hint of where the last place to look should be.
     *		-1 to specify none.
     * @return Array describing [0] which list the item was found in, [1] at
     *         what index.  If [0] is greater than -1, then so will [1].  [0]
     *         of -1 indicates that then item was not found.
     */
    private int[] locateTestInLists(TestResult tr, int firstListToCheck,
				    int lastListToCheck) {
	int[] result = new int[2];
	result[0] = -1;
	result[1] = -1;
	
	if (firstListToCheck >= 0) {
	    int possible = testLists[firstListToCheck].indexOf(tr);
	    if (possible != -1) {
		// done with search
		result[0] = firstListToCheck;
		result[1] = possible;
	    }
	}
	else { }

	// do a more exhaustive search if not found yet
	if (result[0] == -1) {
	    for (int i = 0; i < testLists.length; i++) {
		// skip the lists which have been checked or that should be
		// checked last
		// this is just a performance optimization
		if (i == firstListToCheck || i == lastListToCheck)
		    continue;

		int possible = testLists[i].indexOf(tr);
		if (possible != -1) {
		    // found in list i, position possible
		    // done with search
		    result[0] = i;
		    result[1] = possible;
		    break;
		}
	    }	// for
	}

	// if still not found, check the list which was specified to be checked
	// last, this is just a performance optimization
	if (result[0] == -1 && lastListToCheck >= 0) {
	    int possible = testLists[lastListToCheck].indexOf(tr);
	    if (possible != -1) {
		result[0] = lastListToCheck;
		result[1] = possible;
	    }
	}

	return result;
    }

    /**
     * Determine the index of a particular test in a vector.
     */
    private int searchList(TestResult target, Vector list) {
	int possible = list.indexOf(target);
	return possible;
    }

    private void cleanup() {
	if (state == ABORTED) {
	    // no valid info
	    return;
	}

	state = COMPLETED;
     }

    /**
     * Prepare to process this node.
     * Avoid calling until it is time to process the node.
     * @return Iterator for this node, or null if not possible.
     */
    private TestResultTable.TreeIterator init() {
	if (node == null) {
	    valid = false;
	    state = ABORTED;
	    return null;
	}

	// wait for TRT to settle down
	//node.getEnclosingTable().waitUntilReady();

	//if (filter != null)
	//    return TestResultTable.getIterator(node, filter);
	//else
	    return TestResultTable.getIterator(node);
    }

    private synchronized void notify(int type, boolean isAdd,
				     TestResultTable.TreeNode[] path,
				     TestResult what, int index) {
	if (observers.length == 0) {
	    return;
	}

	for (int i = 0; i < observers.length; i++) {
	    boolean[] mask = observers[i].getEventMasks();
	    if (mask[0] || mask[type]) {
		if (isAdd) {
		    observers[i].testAdded(type, path, what, index);
		}
		else {
		    observers[i].testRemoved(type, path, what, index);
		}
	    }
	}   // for
    }

    private synchronized void notifyStats() {
	if (observers.length == 0)
	    return;
	
	for (int i = 0; i < observers.length; i++) {
	    boolean[] mask = observers[i].getEventMasks();
	    if (mask[0] || mask[TT_NodeCacheObserver.MSGS_STATS]) {
		observers[i].statsUpdated(stats);
	    }
	}   // for
    }

     /* deprecated since this info is no longer readily available
    Hashtable getFilterStats() {
	return filterInfo;
    }

    protected void finalize() {
	// cleanup in worst case
	if (watcher != null && node != null)
	    node.removeObserver(watcher);
    }
    */


    private TestResultTable.TreeNode node;
    //protected Watcher watcher;
    private TestResultTable.TreeIterator it;
    private TestFilter filter;
    private int[] stats = new int[Status.NUM_STATES];
    private int localRejectCount;
    private Hashtable rejectReasons = new Hashtable();
    private FilterObserver fObs = new FilterObserver();

    private TT_NodeCacheObserver[] observers = new TT_NodeCacheObserver[0];
    private Vector[] testLists;	    // could use unsynchronzied data structure

    private volatile int state;
    private volatile boolean valid = true;
    private volatile Object UPDATE_LOCK = new Object();

    private static final int NOT_COMPUTED = 0;
    private static final int COMPUTING = 1;
    private static final int COMPLETED = 2;
    private static final int PAUSED = 3;
    private static final int ABORTED = 4;
    private static final boolean debug = Debug.getBoolean(TT_NodeCache.class);

    static abstract class TT_NodeCacheObserver {
	public TT_NodeCacheObserver() {
	    interestList = new boolean[EVENT_LIST_SIZE];
	}

	/**
	 * Find out what messages this observer is interested in.
	 */
	public boolean[] getEventMasks() {
	    return interestList;
	}

	public abstract void testAdded(int messageType,
		TestResultTable.TreeNode[] path, TestResult what, int index);
	public abstract void testRemoved(int messageType,
		TestResultTable.TreeNode[] path, TestResult what, int index);
	public abstract void statsUpdated(int[] stats);

	protected boolean[] interestList;

	public static final int EVENT_LIST_SIZE = 7;
	public static final int MSGS_ALL = 0;
	public static final int MSGS_STATS = 1;
	public static final int MSGS_PASSED = 2;
	public static final int MSGS_FAILED = 3;
	public static final int MSGS_ERRORS = 4;
	public static final int MSGS_NOT_RUNS = 5;
	public static final int MSGS_FILTERED = 6;
	public static final int OFFSET_FROM_STATUS = 2;
    }

    static class FilterObserver implements TestFilter.Observer {
	public void rejected(TestDescription d, TestFilter rejector) {
	    lastTd = d;
	    lastRejector = rejector;
	}

	public void clear() {
	    lastTd = null;
	    lastRejector = null;
	}

	TestDescription lastTd;
	TestFilter lastRejector;
    }

    /* not useful right now
    class Watcher implements TestResultTable.TreeNodeObserver {
	public void countersInvalidated(TestResultTable.TreeNode where) {
	    Debug.println("Counters( " + node.getName() + ") - " + where.getName());
	}

	public void insertedBranch(TestResultTable.TreeNode parent, TestResultTable.TreeNode newNode, int index) {
	}

	public void insertedResult(TestResultTable.TreeNode parent, TestResult test, int index) {
	}

	public void removedBranch(TestResultTable.TreeNode parent, int index) {
	}

	public void removedResult(TestResultTable.TreeNode parent, TestResult test, int index) {
	    Debug.println("Remove - " + parent.getName());
	}

	public void replacedResult(TestResultTable.TreeNode parent, TestResult oldTest, TestResult newTest,
		   int index)  {
	    Debug.println("Replace - " + parent.getName() + " " + oldTest.getStatus().getType() + "  " + newTest.getStatus().getType());
	}
    }	// Watcher
    */
}
