/*
 * $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.agent;

import java.awt.Button;
import java.awt.Choice;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Event;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Label;
import java.awt.List;
import java.awt.Panel;
import java.awt.ScrollPane;
import java.awt.TextArea;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.IOException;
import java.net.URL;
import java.util.Vector;

import com.sun.javatest.ProductInfo;
import com.sun.javatest.Status;

/**
 * A component to monitor the status of a Agent. This is normally displayed
 * in either an {@link AgentFrame} or an {@link AgentApplet}.
 */
class AgentPanel extends ScrollPane 
{
    public interface MapReader {
	public abstract Map read(String name) throws IOException;
    };
	
    /**
     * Create a standard AgentPanel to control and monitor a Agent.
     *
     * @see Agent
     */
    public AgentPanel(ModeOptions[] modeOptions, MapReader mr) {
	mapReader = mr;
	initGUI(modeOptions);
    }

    /**
     * Set the mode of the agent.
     *
     * @arg mode	"active" or "passive"
     * @see Agent
     */
    public synchronized void setMode(String mode) {
	if (currAgent != null)
	    throw new IllegalStateException();

	paramPanel.setMode(mode);
    }

    /**
     * Set the maximum number of requests that will be serviced at any
     * one time.
     *
     * @arg conc	the maximum number of requests
     *
     * @see Agent
     */
    public void setConcurrency(int conc) {
	if (currAgent != null)
	    throw new IllegalStateException();

	paramPanel.setConcurrency(conc);
    }

    /**
     * Set the map file used to translate the arguments of incoming requests.
     *
     * @arg mapFile	the map file
     *
     * @see Agent#setMap
     */
    public void setMapFile(String mapFile) {
	if (currAgent != null)
	    throw new IllegalStateException();

	paramPanel.setMapFile(mapFile);
    }

    /**
     * Set the retry delay for active agents.
     *
     * @arg delay	the interval (in seconds) to wait after
     *			a connection has been refused
     *
     * @see Agent
     */
    public void setRetryDelay(int delay) {
	if (currAgent != null)
	    throw new IllegalStateException();

	paramPanel.setRetryDelay(delay);
    }

    /**
     * Set the number of requests remembered in the history.
     *
     * @arg limit	the maximum number of requests to be remembered
     *
     * @see Agent
     */
    public void setHistoryLimit(int limit) {
	if (currAgent != null)
	    throw new IllegalStateException();

	historyList.setMaxTasks(limit);
    }

    /**
     * Add a user observer to the agent when it is created.
     * @param userObs an observer to monitor the agent
     */
    public void addObserver(Agent.Observer userObs) {
	if (userObs == null)
	    throw new NullPointerException();
	if (this.userObs != null)
	    throw new IllegalStateException("just one observer can be registered");
	this.userObs = userObs;
    }

    /**
     * Start a agent running, based on the parameters in the panel.
     */
    public synchronized void start() {
	try {
	    currAgent = paramPanel.createAgent();
	    currAgent.addObserver(agentObs);
	    if (userObs != null)
		currAgent.addObserver(userObs);

	    Thread t = new Thread(currAgent, "AgentPanel worker thread");
	    t.start();

	    paramPanel.setEnabled(false);
	    buttonPanel.setStartEnabled(false);
	    buttonPanel.setStopEnabled(true);
	}
	catch (BadValue e) {
	    error(e.getMessages());
	}
	catch (SecurityException e) {
	    String[] msgs = {"Security Exception", e.getMessage()};
	    error(msgs);
	}
	catch (ConnectionFactory.Fault e) {
	    String[] msgs = {"cannot create connection", e.getMessage()};
	    error(msgs);
	}
	catch (IOException e) {
	    String[] msgs = {"problem reading map file", e.getMessage()};
	    error(msgs);
	}
    }

    /**
     * Stop a agent running. Initially, the thread running the agent is
     * interrupted. If that doesn't work, after a short timeout, the thread
     * is stopped.  If that doesn't work, after another short timeout,
     * the thread is abandoned.
     */
    public synchronized void stop() {
	buttonPanel.setStopEnabled(false);
	if (currAgent != null)
	    currAgent.interrupt();
	// the panel will be set idle when the agent reports (via the AgentObserver)
	// that it is finished
    }



    //-------------------------------------------------------------------

    public Dimension getPreferredSize() {
	Insets i = getInsets();
	Dimension d = deck.getPreferredSize();
	return new Dimension(i.left + d.width + i.right, i.top + d.height + i.bottom);
    }

    //-------------------------------------------------------------------

    void showTask(TaskState task) {
	mainFolder.show(taskPanel);
	taskPanel.setTask(task);
    }

    //-------------------------------------------------------------------
    //
    // The following are private methods for the GUI

    private void initGUI(ModeOptions[] modeOptions) {
	agentObs = new AgentObserver();
	deck = new Deck();
	initMainPanel(modeOptions);
	deck.add(mainPanel);
	errorPanel = new ErrorPanel();
	deck.add(errorPanel);
	helpPanel = new HelpPanel();
	deck.add(helpPanel);
	add(deck);
    }

    Component initMainPanel(ModeOptions[] modeOptions) {
	mainPanel = new Panel(new GridBagLayout());

	GridBagConstraints c = new GridBagConstraints();
	c.fill = GridBagConstraints.BOTH;
	c.gridwidth = GridBagConstraints.REMAINDER;
	c.weightx = 1;
	c.weighty = 1;

	mainFolder = new Folder();

	paramPanel = new ParamPanel(modeOptions);
	mainFolder.add("parameters", paramPanel);

	statsPanel = new StatsPanel();
	mainFolder.add("statistics", statsPanel);

	historyList = new HistoryList();
	mainFolder.add("history", historyList);

	taskPanel = new TaskPanel();
	mainFolder.add("selected task", taskPanel);

	c.insets.top = c.insets.left = c.insets.right = 10;
	mainPanel.add(mainFolder, c);

	buttonPanel = new ButtonPanel();
	c.weighty= 0;
	c.insets.bottom = 20;
	mainPanel.add(buttonPanel, c);

	return mainPanel;
    }

    private void setIdle() {
	paramPanel.setEnabled(true);
	buttonPanel.setStartEnabled(true);
	buttonPanel.setStopEnabled(false);
	currAgent = null;
	notifyAll();
    }

    private void showHelp() {
	deck.show(helpPanel);
    }

    private void error(String[] msgs) {
	for (int i = 0; i < msgs.length; i++)
	    System.out.println(msgs[i]);
	errorPanel.show(msgs);
	deck.show(errorPanel);
    }

    private void ackError() {
	deck.show(mainPanel);
    }

    private static int getInt(String label, TextField field) throws BadValue {
	try {
	    return Integer.parseInt(field.getText(), 10);
	}
	catch (NumberFormatException e) {
	    throw new BadValue("bad value in `" + label + "' field");
	}
    }


    private MapReader mapReader;

    private Deck deck;
    private ErrorPanel errorPanel;
    private HelpPanel helpPanel;
    private Panel mainPanel;
    private Folder mainFolder;
    private ButtonPanel buttonPanel;
    private ParamPanel paramPanel;
    private StatsPanel statsPanel;
    private TaskPanel taskPanel;
    private HistoryList historyList;
    private AgentObserver agentObs;
    private Agent.Observer userObs;

    private Agent currAgent;

    private static final String[] statusCodes =
    	{"PASS", "FAIL", "CHCK", "ERR ", "!RUN"};



    //-------------------------------------------------------------------



    //-------------------------------------------------------------------

    private static class TaskState
    {
	TaskState(Connection c) {
	    connection = c;
	}

	Connection connection;
	int mode;
	String tag;
	String className;
	String[] args;
	Status status;

	static final int TEST = 0, COMMAND = 1, MAIN=2;
    }


    //-------------------------------------------------------------------

    private class AgentObserver implements Agent.Observer {
	public void started(Agent sl) {
	    synchronized (AgentPanel.this) {
		if (sl != currAgent)
		    return;
		
		historyList.removeAll();
		taskPanel.setTask(null);
		statsPanel.reset();
	    }
	}
	
	public void errorOpeningConnection(Agent sl, Exception e) {
	    synchronized (AgentPanel.this) {
		if (sl != currAgent)
		    return;
		
		String[] msgs = {"error opening connection", e.getMessage()};
		error(msgs);
	    }
	}
	
	public void finished(Agent sl) {
	    synchronized (AgentPanel.this) {
		if (sl != currAgent)
		    return;
		
		setIdle();
	    }
	}
	
	public void openedConnection(Agent sl, Connection c) {
	    synchronized (AgentPanel.this) {
		if (sl != currAgent)
		    return;
		
		TaskState ts = new TaskState(c);
		historyList.addTask(ts);
		if (taskPanel.getTask() == null) 
		    taskPanel.setTask(ts);
		statsPanel.startedTask(ts);
	    }
	}
	
	public void execTest(Agent sl, Connection c, String tag, String className, String[] args) {
	    synchronized (AgentPanel.this) {
		if (sl != currAgent)
		    return;
		
		TaskState ts = historyList.getTask(c);
		ts.mode = TaskState.TEST;
		ts.tag = tag;
		ts.className = className;
		ts.args = args;
		historyList.update(ts, "EXEC", tag);
		if (ts == taskPanel.getTask())
		    taskPanel.update();
	    }
	}
	
	public void execCommand(Agent sl, Connection c, String tag, String className, String[] args) {
	    synchronized (AgentPanel.this) {
		if (sl != currAgent)
		    return;
		
		TaskState ts = historyList.getTask(c);
		ts.mode = TaskState.COMMAND;
		ts.tag = tag;
		ts.className = className;
		ts.args = args;
		historyList.update(ts, "EXEC", tag);
		if (ts == taskPanel.getTask())
		    taskPanel.update();
	    }
	}
	
	public void execMain(Agent sl, Connection c, String tag, String className, String[] args) {
	    synchronized (AgentPanel.this) {
		if (sl != currAgent)
		    return;
		
		TaskState ts = historyList.getTask(c);
		ts.mode = TaskState.MAIN;
		ts.tag = tag;
		ts.className = className;
		ts.args = args;
		historyList.update(ts, "EXEC", tag);
		if (ts == taskPanel.getTask())
		    taskPanel.update();
	    }
	}
	
	public void result(Agent sl, Connection c, Status r) {
	    synchronized (AgentPanel.this) {
		if (sl != currAgent)
		    return;
		
		TaskState ts = historyList.getTask(c);
		// don't update history list until data passed back to AgentClient OK
		// (i.e. in completed(...)
		ts.status = r;
		if (ts == taskPanel.getTask())
		    taskPanel.update();
	    }
	}
	
	public void exception(Agent sl, Connection c, Throwable t) {
	    synchronized (AgentPanel.this) {
		if (sl != currAgent)
		    return;
		
		TaskState ts = historyList.getTask(c);
		if (ts.tag == null) 
		    historyList.removeTask(ts);
		else
		    historyList.update(ts, "*IO*", ts.tag);
		statsPanel.finishedTask(ts, false);
	    }
	}
	
	public void completed(Agent sl, Connection c) {
	    synchronized (AgentPanel.this) {
		if (sl != currAgent)
		    return;
		
		TaskState ts = historyList.getTask(c);
		historyList.update(ts, statusCodes[ts.status.getType()], ts.tag);
		statsPanel.finishedTask(ts, true);
	    }
	}
    }

    //-------------------------------------------------------------------

    private class ButtonPanel extends Panel implements ActionListener {	
	ButtonPanel() {
	    setLayout(new GridBagLayout());
	    GridBagConstraints c = new GridBagConstraints();
	    c.weightx = 1.0;
	    
	    startButton = new Button("Start");
	    startButton.addActionListener(this);
	    add(startButton, c);
	    
	    stopButton = new Button("Stop");
	    stopButton.addActionListener(this);
	    stopButton.setEnabled(false);
	    add(stopButton, c);
	    
	    helpButton = new Button("Help");
	    helpButton.addActionListener(this);
	    add(helpButton, c);
	}

	public void actionPerformed(ActionEvent e) {
	    Object src = e.getSource();
	    if (src == startButton) {
		start();
	    }
	    else if (src == stopButton) {
		stop();
	    }
	    else if (src == helpButton) {
		showHelp();
	    }
	}

	void setStartEnabled(boolean b) {
	    startButton.setEnabled(b);
	}

	void setStopEnabled(boolean b) {
	    stopButton.setEnabled(b);
	}

	private Button startButton;
	private Button stopButton;
	private Button helpButton;
    }

    //-------------------------------------------------------------------

    private class ErrorPanel extends Panel implements ActionListener {
	/**
	 * Display a multi-line message in the error panel.
	 *
	 * @param msgs	The lines of the message to display
	 */
	public synchronized void show(String[] msgs) {
	    removeAll();
	    
	    setLayout(new GridBagLayout());
	    
	    GridBagConstraints c = new GridBagConstraints();
	    c.gridwidth = GridBagConstraints.REMAINDER;
	    c.insets = new Insets(30, 30, 0, 30); // top, left, bottom, right
	    
	    for (int i = 0; i < msgs.length; i++) {
		Label msg = new Label(msgs[i]);
		add(msg, c);
		c.insets.top = 0;
	    }
	    
	    c.insets.top = 30;
	    c.insets.bottom = 30;
	    Button okBtn = new Button("OK");
	    okBtn.addActionListener(this);
	    add(okBtn, c);
	    validate();
	}

	public void actionPerformed(ActionEvent e) {
	    ackError();
	}
    }

    //-------------------------------------------------------------------

    private class HelpPanel extends Panel implements ActionListener {
	HelpPanel() {
	    setLayout(new GridBagLayout());
	    setBackground(Color.white);
	    
	    Panel logoPanel = new Panel(new GridBagLayout());
	    GridBagConstraints lpc = new GridBagConstraints();
	    lpc.gridwidth = GridBagConstraints.REMAINDER;
	    lpc.weighty = 1;
	    lpc.insets = new Insets(20, 20, 20, 20);
	    
	    Icon logoJava = createIcon("jticon.gif");
	    if (logoJava != null) {
		lpc.anchor = GridBagConstraints.NORTH;
		logoPanel.add(logoJava, lpc);
	    }
	    
	    /*
	    Icon logo100pc = createIcon("100percent.gif");
	    if (logo100pc != null) {
		lpc.anchor = GridBagConstraints.SOUTH;
		logoPanel.add(logo100pc, lpc);
	    }
	    */

	    GridBagConstraints lrc = new GridBagConstraints();
	    lrc.weighty = 1;
	    lrc.fill = GridBagConstraints.BOTH;
	    add(logoPanel, lrc);

	    GridBagConstraints c = new GridBagConstraints();
	    c.gridwidth = GridBagConstraints.REMAINDER;
	    Panel infoPanel = new Panel(new GridBagLayout());
	    Label nameLabel = new Label("JT Harness Agent");
	    nameLabel.setFont(new Font("SansSerif", Font.BOLD, 32));
	    infoPanel.add(nameLabel, c);

	    Label versionLabel = new Label(ProductInfo.getVersion());
	    versionLabel.setFont(new Font("SansSerif", Font.BOLD, 24));
	    infoPanel.add(versionLabel, c);
	    
	    Label copyLabel1 = new Label("Copyright (c) 1996-2008 Sun Microsystems, Inc.  Use is subject to license terms.");
	    infoPanel.add(copyLabel1, c);
	  
	    Label copyLabel2 = new Label("Use is subject to license terms.");
	    infoPanel.add(copyLabel2, c);
	    
	    String helpText = 
		"  For full information on using the\n" +
		"  JT Harness Agent, please refer to the\n" +
		"     JT Harness User's Guide\n" +
		"  and to the documentation for the\n" +
		"  test suite you are using.\n";
		
	    TextArea helpArea = new TextArea(helpText, 5, 35, TextArea.SCROLLBARS_NONE);
	    helpArea.setEditable(false);
	    c.weightx = 1;
	    c.weighty = 1;
	    infoPanel.add(helpArea, c);

	    Button okBtn = new Button("OK");
	    okBtn.addActionListener(this);
	    c.weightx = 0;
	    c.weighty = 0;
	    c.insets.bottom = 10;
	    infoPanel.add(okBtn, c);

	    lrc.weightx = 1;
	    add(infoPanel, lrc);
	}

	private Icon createIcon(String name)  {
	    try {
		URL url = getClass().getResource(name);
		if (url == null) 
		    return null;
		Icon icon = new Icon(url);
		return icon;
	    }
	    catch (RuntimeException e) {
		return null;
	    }
	    catch (Error e) {
		return null;
	    }
		

	}

	public void actionPerformed(ActionEvent e) {
	    ackError(); // hmm, not the most obvious name, but it will work
	}
    }

    //-------------------------------------------------------------------

    private class HistoryList extends List implements ItemListener {
	HistoryList() {
	    super(5, false);
	    super.addItemListener(this);
	}

	public synchronized void addTask(TaskState task) {
	    if (tasks.size() >= maxTasks) {
		for (int i = 0; i < tasks.size(); i++) {
		    String s = (String) getItem(i);
		    // skip over active tasks that will be updated later
		    if (s.startsWith("CONN") || s.startsWith("EXEC"))
			continue;

		    tasks.removeElementAt(i);
		    super.remove(i);
		}
	    }
	    
	    tasks.addElement(task);
	    super.add("CONN " + task.connection.getName());
	}

	public synchronized TaskState getTask(Connection c) {
	    for (int i = 0; i < tasks.size(); i++) {
		TaskState ts = (TaskState)(tasks.elementAt(i));
		if (ts.connection == c) 
		    return ts;
	    }
	    throw new Error("AgentPanel.HistoryList.getTask: can't find socket");
	}

	public synchronized TaskState getSelectedTask() {
	    return (TaskState)(tasks.elementAt(super.getSelectedIndex()));
	}

	public synchronized void removeAll() {
	    tasks.setSize(0);
	    if (getItemCount() > 0)
		// calling List.removeAll on empty list generates motif
                // warning message
		super.removeAll();
	}

	public synchronized void removeTask(TaskState task) {
	    int index = tasks.indexOf(task);
	    tasks.removeElementAt(index);
	    super.remove(index);
	}

	public synchronized void setMaxTasks(int n) {
	    maxTasks = n;
	}

	public synchronized void update(TaskState task, String state, String detail) {
	    super.replaceItem(getKey(state, detail), tasks.indexOf(task));
	}

	public void itemStateChanged(ItemEvent e) {
	    Object src = e.getItemSelectable();
	    if (src == this) {
		switch (e.getStateChange()) {
		case ItemEvent.SELECTED:
		    showTask(getSelectedTask());
		    break;
		case Event.LIST_DESELECT:
		    showTask(null);
		    break;
		}
	    }
	}

	private String getKey(String state, String detail) {
	    return (state + " " + detail);
	}

	private int maxTasks = 10;
	private Vector tasks = new Vector();
    }


    //-------------------------------------------------------------------

    private class ParamPanel extends Panel  implements ItemListener {
	ParamPanel(ModeOptions[] modeOptions) {
	    setLayout(new GridBagLayout());

	    GridBagConstraints lc = new GridBagConstraints();
	    lc.anchor = GridBagConstraints.EAST;
	    
	    GridBagConstraints fc = new GridBagConstraints();
	    fc.fill = GridBagConstraints.HORIZONTAL;
	    fc.gridwidth = GridBagConstraints.REMAINDER;
	    fc.weightx = 1;
	    fc.weighty = 0;
	    
	    
	    // First row: mode choice, and the mode options deck
	    add(new Label("mode:"), lc);
	    
	    modeChoice = new Choice();
	    modeDeck = new Deck();

	    for (int i = 0; i < modeOptions.length; i++) {
		ModeOptions m = modeOptions[i];
		modeChoice.addItem(m.getModeName());
		modeDeck.add(m);
	    }

	    // choices added later by modeOption objects
	    modeChoice.addItemListener(this);
	    fc.weightx = 0;
	    fc.gridwidth = 1;
	    add(modeChoice, fc);

	    fc.insets.left = 20;
	    fc.fill = GridBagConstraints.HORIZONTAL;
	    fc.gridwidth = GridBagConstraints.REMAINDER;
	    fc.weightx = 1;
	    add(modeDeck, fc);
	    fc.insets.left = 0;
	    
	    // Second row: the optional map file field
	    add(new Label("map:"), lc);
	    
	    mapFileField = new TextField();
	    add(mapFileField, fc);
	    
	    // Third row: the concurrency field
	    Label concurrencyLabel = new Label("concurrency:");
	    add(concurrencyLabel, lc);
	    
	    concurrencyField = new TextField("1", 3);
	    fc.gridwidth = 1;
	    fc.weightx = 0;
	    add(concurrencyField, fc);
	    
	    fc.gridwidth = GridBagConstraints.REMAINDER;
	    fc.weightx = 1;
	    add(new Label(), fc); // dummy filler
	    
	}
	
	public void setEnabled(boolean b) {
	    modeChoice.setEnabled(b);
	    for (int i = 0; i < modeDeck.getComponentCount(); i++) {
		ModeOptions o = (ModeOptions)(modeDeck.getComponent(i));
		o.setEnabled(b);
	    }
	    mapFileField.setEnabled(b);
	    concurrencyField.setEnabled(b);
	}

	public void setMode(String modeName) {
	    modeChoice.select(modeName);
	    for (int i = 0; i < modeDeck.getComponentCount(); i++) {
		ModeOptions m = (ModeOptions)(modeDeck.getComponent(i));
		if (modeName.equals(m.getModeName())) {
		    modeDeck.show(m);
		    return;
		}
	    }
	}

	public void setConcurrency(int conc) {
	    concurrencyField.setText(Integer.toString(conc));
	}

	public void setMapFile(String mapFile) {
	    mapFileField.setText(mapFile);
	}

	public void setRetryDelay(int delay) {
	    retryDelay = delay; // not available in GUI
	}

	public Agent createAgent() throws BadValue, ConnectionFactory.Fault, IOException {
	    // should consider catching IllegalArgumentException in here
	    // and IllegalState?
	    Map map = mapReader.read(mapFileField.getText());
	    int concurrency = getInt("concurrency", concurrencyField);
	    if (concurrency < 0)
		throw new BadValue("bad value for `concurrency field': " + concurrency);

	    ModeOptions mo = (ModeOptions)(modeDeck.getCurrentCard());
	    ConnectionFactory cf = mo.createConnectionFactory(concurrency);
	    Agent agent = new Agent(cf, concurrency);

	    if (retryDelay > 0)
		agent.setRetryDelay(retryDelay);

	    agent.setMap(map);

	    return agent;
	}

	private Deck createModeDeck(Choice modeChoice) {
	    return deck;
	}

	public void itemStateChanged(ItemEvent e) {
	    Object src = e.getItemSelectable();
	    if (src == modeChoice) {
		String modeName = ((Choice)src).getSelectedItem();
		for (int i = 0; i < modeDeck.getComponentCount(); i++) {
		    ModeOptions m = (ModeOptions)(modeDeck.getComponent(i));
		    if (modeName.equals(m.getModeName())) {
			modeDeck.show(m);
			return;
		    }
		}
	    }
	}

	private Choice modeChoice;
	private Deck modeDeck;
	private TextField mapFileField;
	private Label concurrencyLabel;
	private TextField concurrencyField;
	private int retryDelay; // not currently in GUI
    }


    //-------------------------------------------------------------------

    private class StatsPanel extends Panel {
	StatsPanel() {
	    setLayout(new GridBagLayout());

	    GridBagConstraints lc = new GridBagConstraints();
	    lc.anchor = GridBagConstraints.EAST;
	    GridBagConstraints fc = new GridBagConstraints();
	    fc.weightx = 1;
	    fc.anchor = GridBagConstraints.WEST;
	    fc.gridwidth = GridBagConstraints.REMAINDER;

	    activeField = addField("currently active:", lc, fc);
	    statusFields[Status.PASSED] = addField("passed:", lc, fc);
	    statusFields[Status.FAILED] = addField("failed:", lc, fc);
	    statusFields[Status.ERROR] = addField("error:", lc, fc);
	    statusFields[Status.NOT_RUN] = addField("not run:", lc, fc);
	    exceptionsField = addField("exceptions:", lc, fc);
	}
	
	void reset() {
	    tasks.setSize(0);
	    for (int i = 0; i < statusCounts.length; i++)
		statusCounts[i] = 0;
	    exceptionsCount = 0;
	    activeField.setText("0");
	    for (int i = 0; i < statusCounts.length; i++)
		statusFields[i].setText("0");
	    exceptionsField.setText("0");
	}

	void startedTask(TaskState task) {
	    tasks.addElement(task);
	    activeField.setText(String.valueOf(tasks.size()));
	}
	
	void finishedTask(TaskState task, boolean completedNormally) {
	    int index = tasks.indexOf(task);
	    if (index == -1)
		return;
	    tasks.removeElementAt(index);
	    activeField.setText(String.valueOf(tasks.size()));
	    if (!completedNormally)
		exceptionsField.setText(String.valueOf(++exceptionsCount));
	    if (task.status != null) {
		int t = task.status.getType();
		statusFields[t].setText(String.valueOf(++statusCounts[t]));
	    }
	}

	private TextField addField(String name, GridBagConstraints lc, GridBagConstraints fc) {
	    add(new Label(name), lc);
	    TextField f = new TextField("0", 10);
	    f.setEditable(false);
	    add(f, fc);
	    return f;
	}


	private Vector tasks = new Vector();

	private int[] statusCounts = new int[Status.NUM_STATES];
	private int exceptionsCount;
	
	private TextField activeField;
	private TextField[] statusFields = new TextField[Status.NUM_STATES];
	private TextField exceptionsField;
    }

    //-------------------------------------------------------------------

    private class TaskPanel extends Panel {
	TaskPanel() {
	    setLayout(new GridBagLayout());

	    GridBagConstraints lc = new GridBagConstraints();
	    lc.anchor = GridBagConstraints.EAST;
	    GridBagConstraints fc = new GridBagConstraints();
	    fc.weightx = 1;
	    fc.fill = GridBagConstraints.HORIZONTAL;
	    fc.gridwidth = GridBagConstraints.REMAINDER;
	    
	    Label clientLabel = new Label("client:");
	    add(clientLabel, lc);
	    
	    clientField = new Label("");
	    add(clientField, fc);
	    
	    Label tagLabel = new Label("request:");
	    add(tagLabel, lc);
	    
	    tagField = new Label("");
	    add(tagField, fc);
	    
	    Label classLabel = new Label("class:");
	    add(classLabel, lc);
	    
	    classField = new Label("");
	    add(classField, fc);
	    
	    Label argsLabel = new Label("args:");
	    add(argsLabel, lc);
	    
	    argsField = new TextField("");
	    // make it a text field to support scrolling of very long fields
	    argsField.setEditable(false);
	    add(argsField, fc);
	    
	    Label resultLabel = new Label("result:");
	    add(resultLabel, lc);
	    
	    resultField = new Label("");
	    add(resultField, fc);
	}

	TaskState getTask() {
	    return task;
	}

	void setTask(TaskState ts) {
	    task = ts;
	    update();
	}

	void update() {
	    if (task == null) {
		clientField.setText("");
		tagField.setText("");
		classField.setText("");
		argsField.setText("");
		resultField.setText("");
	    } else {
		clientField.setText(task.connection.getName());
		tagField.setText(task.tag);
		classField.setText(task.className);
		String a = "";
		if (task.args != null) {
		    for (int i = 0; i < task.args.length; i++)
			a += task.args[i] + " ";
		}
		argsField.setText(a);
		if (task.status == null)
		    resultField.setText("");
		else
		    resultField.setText(task.status.toString());
	    }
	}

	private TaskState task;

	private Label clientField;
	private Label tagField;
	private Label classField;
	private TextField argsField;
	private Label resultField;
    }
}
