// Chat Touring
// Copyright (c) 1996 by Paul Burchard
// Distributed under the terms of the GNU Library General Public License

////////////////////////////////////////////////////////////////
// Please do not use this code as an example of anything.     //
// It is a nest of workarounds for bugs in AWT and Netscape,  //
// for AWT's horrible design for event and layout management, //
// for Netscape/Sun's severely broken thread implementation,  //
// and for Java's OOP-hostile network loading strategy.       //
////////////////////////////////////////////////////////////////

import java.applet.*;
import java.awt.*;
import java.net.*;
import java.util.*;
import QueryTransaction;

public class ChaTour extends Panel implements Observer {
	public static final String initString = "Welcome to Chat Touring by Paul Burchard\n";
	public static final String infoString = "Chat Touring (c) 1996 by Paul Burchard\nDistributed under the terms of the GNU Library General Public License";
	public static final String logoString = "Chat Touring";
	
	// Parameters
	int readLine = 0, lookLine = 0;
	int showLines = 10;
	int fontSize = 12;
	public static final int LINE_LENGTH = 80;
	public static final int LINE_WIDTH = 600;
	int pauseTime = 1;
	String nickName = null, guideName = null;
	String tourDest = "help.html";
	String cgiSuffix = ".cgi";
	URL cgiBase = null;
	public static final String PARAM_PREFIX = "ChaTour.";
	public static final String IO_PARAM = "i";
	public static final String SERIAL_PARAM = "z";
	public static final String READLINE_PARAM = "r";
	public static final String LOOKLINE_PARAM = "l";
	public static final String SHOWLINES_PARAM = "s";
	public static final String FONTSIZE_PARAM = "f";
	public static final String PAUSETIME_PARAM = "p";
	public static final String NICKNAME_PARAM = "n";
	public static final String GUIDENAME_PARAM = "g";
	public static final String TOURDEST_PARAM = "t";
	public static final String CGISUFFIX_PARAM = "c";
	public static final String CGIBASE_PARAM = "b";
	
	// State
	QueryTransaction queryChain, pollChain;
	URL chatURL;
	Hashtable chatParams;
	int serial = 0;
	String nextDest = null, nextFrame = null, foundDest = null;
	boolean allowTeleport = false, nickSet = false;
	int card;
	private static final int CHAT_CARD = 0;
	private static final int SETTINGS_CARD = 1;
	
	// GUI
	Applet app;
	boolean inlineApplet = false;
	Panel chatCard, settingsCard;
	TextField inputText;
	TextArea outputText;
	Label logoLabel, settingsLabel;
	Teleporter haltButton;
	Button okButton, helpButton, settingsButton, exitButton;
	TextField nickText, guideText, pauseTimeText, fontSizeText;
	Font mainFont, logoFont;
	public static final int NONE = 0;
	public static final int ENDL = 1;
	public static final int HFIL = 2;
	public static final int VFIL = 4;
	
	// Polling for new messages
	Thread pollThread;
	ChatPoller poller;

	public ChaTour(Applet main) {
		this(main, false);
	}
	public ChaTour(Applet main, boolean isInline) {
		// Set up GUI.
		app = main;
		pauseTime = (isInline ? 4:1);
		cgiBase = app.getDocumentBase();
		readParameters(app);
		createComponents(isInline);

		// Prepare chat query.
		try { chatURL = new URL(cgiBase, "update" + cgiSuffix); }
		catch(MalformedURLException e) { throw(new RuntimeException(logoString + " configuration error: bad chat server URL")); }
		chatParams = new Hashtable();
	}
	protected void readParameters(Applet main) {
		// Load and validate parameters.
		app = main;
		try {
			int newReadLine = Integer.valueOf(main.getParameter(PARAM_PREFIX + READLINE_PARAM)).intValue();
			if(newReadLine >= 0) readLine = newReadLine;
		} catch(Exception e) {}
		try {
			int newLookLine = Integer.valueOf(main.getParameter(PARAM_PREFIX + LOOKLINE_PARAM)).intValue();
			if(newLookLine >= 0) lookLine = newLookLine;
		} catch(Exception e) {}
		if(lookLine < readLine) lookLine = readLine;
		try {
			int newShowLines = Integer.valueOf(main.getParameter(PARAM_PREFIX + SHOWLINES_PARAM)).intValue();
			if(newShowLines>=1 && newShowLines<=100) showLines = newShowLines;
		} catch(Exception e) {}
		fontSize = app.getFont().getSize();
		try {
			int newFontSize = Integer.valueOf(main.getParameter(PARAM_PREFIX + FONTSIZE_PARAM)).intValue();
			if(newFontSize>=6 && newFontSize<=24) fontSize = newFontSize;
		} catch(Exception e) {}
		try {
			int newPauseTime = Integer.valueOf(main.getParameter(PARAM_PREFIX + PAUSETIME_PARAM)).intValue();
			if(newPauseTime >= 0) pauseTime = newPauseTime;
		} catch(Exception e) {}
		String newNickName = main.getParameter(PARAM_PREFIX + NICKNAME_PARAM);
		if(newNickName != null) {
			nickName = newNickName;
			nickSet = true; allowTeleport = true;
		}
		String newGuideName = main.getParameter(PARAM_PREFIX + GUIDENAME_PARAM);
		if(newGuideName != null) guideName = newGuideName;
		String newTourDest = main.getParameter(PARAM_PREFIX + TOURDEST_PARAM);
		if(newTourDest != null) tourDest = newTourDest;
		String newCgiSuffix = main.getParameter(PARAM_PREFIX + CGISUFFIX_PARAM);
		if(newCgiSuffix != null) cgiSuffix = newCgiSuffix;
		try {
			URL newCgiBase = new URL(main.getParameter(PARAM_PREFIX + CGIBASE_PARAM));
			if(newCgiBase != null) cgiBase = newCgiBase;
		} catch(Exception e) {}
	}
	protected void createComponents(boolean isInline) {
		// Lay out GUI.
		inlineApplet = isInline;
		setLayout(new CardLayout());
		add("chat_card", (chatCard = new Panel())); {
			chatCard.setLayout(new GridBagLayout());
			addComponent(chatCard, (logoLabel = new Label(logoString, Label.LEFT)), "logo_label", 0, 0, (inlineApplet ? 8:7), 1, HFIL);
			addComponent(chatCard, (helpButton = new Button("Help")), "help_button", (inlineApplet ? 8:7), 0, 1, 1, NONE);
			addComponent(chatCard, (settingsButton = new Button("Settings")), "settings_button", (inlineApplet ? 9:8), 0, 1, 1, (inlineApplet ? ENDL:NONE));
			if(!inlineApplet) addComponent(chatCard, (exitButton = new Button("Exit")), "exit_button", 9, 0, 1, 1, ENDL);
			addComponent(chatCard, (outputText = new TextArea(showLines, LINE_LENGTH)), "output_text", 0, 1, 10, showLines+1, VFIL|HFIL|ENDL); {
				outputText.setEditable(false);
				outputText.setText(initString);
			}
			addComponent(chatCard, (inputText = new TextField(LINE_LENGTH - 6)), "input_text", 0, showLines+2, 9, 1, HFIL); {
				inputText.setEditable(true);
			}
			addComponent(chatCard, (haltButton = new Teleporter(this, pauseTime*1000)), "halt_button", 9, showLines+2, 1, 1, ENDL);
		}
		add("settings_card", (settingsCard = new Panel())); {
			settingsCard.setLayout(new GridBagLayout());
			addComponent(settingsCard, (settingsLabel = new Label(logoString + " Settings", Label.CENTER)), "settings_label", 0, 0, 2, 1, HFIL|VFIL|ENDL);
			addComponent(settingsCard, (new Label("Your Nickname: ", Label.RIGHT)), "nick_label", 0, 1, 1, 1, HFIL);
			addComponent(settingsCard, (nickText = new TextField((nickName!=null ? nickName : "anonymous"))), "nick_text", 1, 1, 1, 1, HFIL|ENDL);
			addComponent(settingsCard, (new Label("Tour Guide: ", Label.RIGHT)), "guide_label", 0, 2, 1, 1, HFIL);
			addComponent(settingsCard, (guideText = new TextField((guideName!=null ? guideName : ""))), "guide_text", 1, 2, 1, 1, HFIL|ENDL);
			addComponent(settingsCard, (new Label("Font Size: ", Label.RIGHT)), "font_label", 0, 3, 1, 1, HFIL);
			addComponent(settingsCard, (fontSizeText = new TextField(String.valueOf(fontSize))), "font_text", 1, 3, 1, 1, HFIL|ENDL);
			addComponent(settingsCard, (new Label("Teleport Delay (seconds): ", Label.RIGHT)), "pause_label", 0, 4, 1, 1, HFIL);
			addComponent(settingsCard, (pauseTimeText = new TextField(String.valueOf(pauseTime))), "pause_text", 1, 4, 1, 1, HFIL|ENDL);
			addComponent(settingsCard, (okButton = new Button("GO!")), "ok_button", 0, 5, 2, 1, ENDL);
		}
		
		// Show correct initial "screen".
		if(nickName == null) {
			((CardLayout)getLayout()).show(this, "settings_card"); layout();
			nickText.selectAll();
			nickText.requestFocus();
			card = SETTINGS_CARD;
		} else {
			((CardLayout)getLayout()).show(this, "chat_card"); layout();
			inputText.requestFocus();
			card = CHAT_CARD;
		}
		
		// Adjust font size per request.
		try { setFontSize(fontSize); }
		catch(Exception e) {}
		repaint();
	}
	public static void addComponent(Container cont, Component comp, String key, int gridx, int gridy, int gridw, int gridh, int flags) {
		// Convenience method to sanitize use GridBagLayout.
		// Container must use GridBagLayout.
		GridBagConstraints cnst = new GridBagConstraints();
		cnst.weightx = cnst.weighty = 0.0;
		cnst.gridx = gridx; cnst.gridy = gridy;
		cnst.gridwidth = gridw; cnst.gridheight = gridh;
		if((flags & ENDL) != 0) cnst.gridwidth = GridBagConstraints.REMAINDER;
		if((flags & HFIL) != 0) {
			if((flags & VFIL) != 0) {
				cnst.fill = GridBagConstraints.BOTH;
				cnst.weighty = 1.0;
			}
			else cnst.fill = GridBagConstraints.HORIZONTAL;
			cnst.weightx = cnst.weighty = 1.0;
		} else if((flags & VFIL) != 0) {
			cnst.fill = GridBagConstraints.VERTICAL;
			cnst.weighty = 1.0;
		}
		((GridBagLayout)cont.getLayout()).setConstraints(comp, cnst);
		cont.add(key, comp);
	}
	protected synchronized void setFontSize(int sz) {
		if(sz<6 || sz>24) return;
		
		//!!! Netscape 2.0 doesn't appear to honor inline applet resize requests,
		//!!! so for now we teleport when changing the font size.
		if(inlineApplet && sz!=fontSize) {
			int oldSize = fontSize;
			fontSize = sz;
			if(tourDest == null) nextDest = "help.html";
			else nextDest = tourDest;
			nextFrame = null;
			teleport();
			fontSize = oldSize;
			return;
		}
		
		//!!! Manually force everything to re-lay itself out.  So much for OOP...
		fontSize = sz;
		mainFont = app.getFont();
		Font newFont = new Font(mainFont.getName(), mainFont.getStyle(), fontSize);
		if(newFont != null) {
			mainFont = newFont;
			logoFont = new Font(mainFont.getName(), Font.BOLD, fontSize+4);
			resize(preferredSize());
			if(logoFont == null) logoFont = mainFont;
			inputText.setFont(mainFont); outputText.setFont(mainFont);
			logoLabel.setFont(logoFont); logoLabel.layout();
			settingsLabel.setFont(logoFont); settingsLabel.layout();
			haltButton.setFont(mainFont); haltButton.layout();
			okButton.setFont(logoFont); okButton.layout();
			helpButton.setFont(mainFont); helpButton.layout();
			settingsButton.setFont(mainFont); settingsButton.layout();
			if(exitButton != null) { exitButton.setFont(mainFont); exitButton.layout(); }
			nickText.setFont(mainFont); guideText.setFont(mainFont); pauseTimeText.setFont(mainFont); fontSizeText.setFont(mainFont);
			chatCard.layout();
			settingsCard.layout();
			Window frame;
			if(!inlineApplet && (frame = getWindow())!=null) {
				frame.reshape(frame.location().x, frame.location().y, frame.preferredSize().width, frame.preferredSize().height);
				frame.pack();
			}
			repaint();
		}
	}
	public Window getWindow() {
		Container win;
		for(win=this; win.getParent()!=null && win.getParent()!=win; win=win.getParent());
		if(win instanceof Window) return ((Window)win);
		else return null;
	}
	public synchronized Dimension preferredSize() {
		// This is how much we allot ourselves in HTML.
		return new Dimension(LINE_WIDTH, (showLines + 3)*((fontSize*5)/4) + 50);
	}
	public synchronized Dimension minimumSize() {
		return preferredSize();
	}
	public boolean mouseDown(Event e, int x, int y) {
		if(e.target == haltButton) return haltClick();
		else return super.mouseDown(e, x, y);
	}
	protected boolean haltClick() {
		// Don't follow teleport request.
		teleportCancel();
		inputText.requestFocus();
		return true;
	}
	public boolean action(Event e, Object arg) {
		if(e.target == inputText) return inputAction();
		else if(e.target == settingsButton) return settingsAction();
		else if(e.target == okButton) return okAction();
		else if(e.target == helpButton) return helpAction();
		else if(e.target == exitButton) return exitAction();
		else return super.action(e, arg);
	}
	protected boolean inputAction() {
		// Send a line to server and request an update.
		chat(inputText.getText());
		inputText.setText("");
		return true;
	}
	protected boolean settingsAction() {
		// Switch to settings panel.
		pauseTimeText.setText(String.valueOf(pauseTime));
		fontSizeText.setText(String.valueOf(fontSize));
		((CardLayout)getLayout()).show(this, "settings_card"); layout();
		card = SETTINGS_CARD;
		return true;
	}
	protected boolean okAction() {
		// Validate new settings.
		boolean ok = true;
		int newFontSize, newPauseTime;
		String newGuideName, newNickName;
		if((newFontSize = validateIntegerField(fontSizeText, 6, fontSize, 24)) < 6) ok = false;
		if((newPauseTime = validateIntegerField(pauseTimeText, 0, pauseTime, -1)) < 0) ok = false;
		if((newGuideName = validateNameField(guideText, 0, 16, (guideName!=null ? guideName : null))) == null) ok = false;
		if(newGuideName!=null && newGuideName.length()==0) newGuideName = null;
		if((newNickName = validateNameField(nickText, 1, 16, (nickName!=null ? nickName : "anonymous"))) == null) ok = false;

		// If OK, commit to new settings.
		if(ok) {
			boolean nickChange = !newNickName.equals(nickName);
			if(nickSet && nickChange && pollThread!=null) chat("<renamed "+newNickName+">");
			guideName = newGuideName;
			nickName = newNickName;
			haltButton.setDelay(1000*(pauseTime = newPauseTime));

			// Attempt to set font size.
			//!!! Note: this operation may teleport in current implementation, so do this setting last.
			if(newFontSize != fontSize) {
				allowTeleport = true;
				try { setFontSize(newFontSize); }
				catch(Exception exc) {}
			}
				
			// Switch back to chat panel.
			if(!nickSet) {
				nickSet = true;
				allowTeleport = true;
				if(pollThread != null) chat("<enter>");
			}
			((CardLayout)getLayout()).show(this, "chat_card"); layout();
			card = CHAT_CARD;
			inputText.requestFocus();
		}
		return true;
	}
	protected boolean helpAction() {
		boolean ok = true;
		try { app.getAppletContext().showDocument(new URL(app.getDocumentBase(), "help.html"), "ChaTour.frame.help"); }
		catch(MalformedURLException e) { ok = false; }
		return ok;
	}
	protected boolean exitAction() {
		Window frame;
		if(!inlineApplet && (frame = getWindow())!=null) frame.dispose();
		app.destroy();
		return true;
	}
	protected int validateIntegerField(TextField field, int minValue, int defaultValue, int maxValue) {
		// If field is out of range, returns value < minValue,
		// resets the field text to defaultValue, and selects it.
		// The maxValue will be ignored if < minValue.
		int newValue = defaultValue;
		try { newValue = Integer.valueOf(field.getText()).intValue(); }
		catch(NumberFormatException e) { newValue = minValue - 1; }
		if(newValue<minValue || (maxValue>=minValue && newValue>maxValue)) {
			field.setText(String.valueOf(defaultValue));
			field.selectAll();
			field.requestFocus();
			newValue = minValue - 1;
		}
		return newValue;
	}
	protected String validateNameField(TextField field, int minLength, int maxLength, String defaultValue) {
		// If field gives invalid "name", this returns null,
		// resets the field text to defaultValue (or to "", if
		// defaultValue is null), and selects it.
		// The maxLength will be ignored if < minLength.
		String newValue = field.getText().trim();
		newValue = newValue.substring(0, Math.min(maxLength, newValue.length()));
		char[] valBuf = newValue.toCharArray();
		int n = newValue.length();
		int chrs = 0;
		for(int i=0; i<n; i++) {
			char c = valBuf[i];
			if(!(Character.isSpace(c) || Character.isDigit(c) || Character.isUpperCase(c) || Character.isLowerCase(c))) valBuf[i] = '_';
			else chrs++;
		}
		if(chrs >= minLength) {
			newValue = new String(valBuf);
		} else {
			field.setText(defaultValue!=null ? defaultValue : "");
			field.selectAll();
			field.requestFocus();
			newValue = null;
		}
		return newValue;
	}
	public boolean postEvent(Event e) {
		boolean ok = super.postEvent(e);
		if(e.id == Event.MOUSE_ENTER) {
			if(card == CHAT_CARD) inputText.requestFocus();
			else if(card == SETTINGS_CARD) {
				if(nickName==null && nickText.getSelectionStart()==0 && nickText.getSelectionEnd()>0) nickText.requestFocus();
			}
		}
		return ok;
	}
	public void start() {
		pollThread = new Thread(poller = new ChatPoller(this, 5000));
		pollThread.setPriority(Thread.NORM_PRIORITY+1);
		pollThread.start();
		if(nickSet) {
			allowTeleport = true;
			chat("<enter>");
		}
	}
	public void stop() {
		if(pollThread != null) {
			if(nickSet) {
				chat("<exit>");
				allowTeleport = false;
			}
			pollThread.stop();
			pollThread = null;
		}
	}
	public void chat() {
		chat((String)null);
	}
	private synchronized void chat(String input) {
		chatParams.put(READLINE_PARAM, String.valueOf(readLine));
		chatParams.put(SERIAL_PARAM, String.valueOf(serial++));
		chatParams.put(SHOWLINES_PARAM, String.valueOf(showLines));
		chatParams.put(NICKNAME_PARAM, (nickName!=null ? nickName : "anonymous"));
		if(input != null) chatParams.put(IO_PARAM, input);
		else chatParams.remove(IO_PARAM);
		if(input != null) (queryChain = new QueryTransaction(queryChain, chatURL, chatParams)).start((Observer)this);
		else if(pollChain==null || pollChain.isComplete()) (pollChain = new QueryTransaction(null, chatURL, chatParams)).start((Observer)this);
	}
	synchronized void appendText(String s) {
		if(outputText == null) return;
		outputText.insertText(s, outputText.getText().length());
		int newlen = outputText.getText().length();
		// Force outputText to scroll to end by selecting there.
		outputText.select(newlen, newlen);
	}
	public void chatFilter(String output, int lineNum) {
		// Display wrapped text, and filter the text for URLs.
		//
		// Text filter assumes line format decoded by the Perl statement:
		//	(time, nick, text) = m!^([0-9:]*)[ ]([^:]*):[ ](.*)$!;
		//
		// Line wrap assumes fixed-width font for now...not clear
		// how expensive stringWidth() is.
		//
		int lineLength = outputText.getColumns();
		if(outputText.isShowing()) {
			int charWidth = outputText.getFontMetrics(outputText.getFont()).charWidth('x');
			if(charWidth > 0) lineLength = LINE_WIDTH/charWidth;
			else lineLength = outputText.getColumns();
		}
		int nickStart = 1 + output.indexOf(' ');
		if(nickStart < 1) return;
		int textStart = 2 + output.substring(nickStart).indexOf(':');
		if(textStart < 2) return;
		else textStart += nickStart;
		String headerText = output.substring(0, textStart);
		boolean foundGuide = false;
		if(guideName!=null && headerText.substring(nickStart).startsWith(guideName+":")) foundGuide = true;
		StringTokenizer toks = new StringTokenizer(output.substring(textStart), " \n\t\r<>\"\'", true);
		StringBuffer textLine = new StringBuffer();
		textLine.append(headerText);
		while(toks.hasMoreTokens()) {
			String t = toks.nextToken();
			if(lineNum>=lookLine && (guideName==null || foundGuide) && (t.indexOf("://")>=0 || t.startsWith("news:") || t.startsWith("mailto:") || t.startsWith("URL:") || t.startsWith("url:"))) {
				String u = t;
				if(u.startsWith("URL:") || u.startsWith("url:")) {  u = u.substring("URL:".length()); }
				try { URL url = new URL(u); }
				catch(MalformedURLException e) { u = null; }
				if(u != null) foundDest = u;
			}
			if((t.length() + textLine.length())>=lineLength && (t.length() + headerText.length())<lineLength) {
				appendText(textLine.toString() + "\n");
				textLine.setLength(headerText.length());
			}
			textLine.append(t);
		}
		appendText(textLine.toString() + "\n");
	}
	public synchronized void update(Observable o, Object arg) {
		if((o instanceof QueryTransaction) && arg!=null) {
			if(arg instanceof String) {
				// Server returns consecutively numbered lines which
				// would be decoded by the Perl statement:
				// 	(serial, timestamp, nick, input) = m!^(\d*)/(\d\d:\d\d) ([^:]*): (.*)$!;
				// where the nick and input are URL-encoded, and
				// valid serial numbers start at 1.
				StringTokenizer output = new StringTokenizer((String)arg, "\n");
				int numLines = output.countTokens();
				if(output.hasMoreTokens()) {
					String textLine = output.nextToken();
					int startLine = -1;
					try { startLine = Integer.valueOf(textLine.substring(0, textLine.indexOf('/'))).intValue(); }
					catch(Exception e) { startLine = -1; }
					if(startLine > 0) {
						if(readLine <= 0) readLine = startLine;
						if(lookLine <= 0) lookLine = startLine + numLines;
						while(startLine<readLine && output.hasMoreTokens()) {
							// Discard already-read lines.
							textLine = output.nextToken();
							startLine++;
						}
						if(startLine > readLine) {
							//!!! Out-of-order server response.
							//!!! Discard and wait for earlier responses to catch up (this wastes bandwidth).
						} else if(startLine == readLine) {
							foundDest = null;
							do {
								try { textLine = textLine.substring(1+textLine.indexOf('/')); }
								catch(IndexOutOfBoundsException ee) {}
								textLine = QueryTransaction.urlDecodeText(textLine);
								chatFilter(textLine, readLine++);
							} while(output.hasMoreTokens() && (textLine = output.nextToken())!=null);
							if(lookLine < readLine) lookLine = readLine;
							if(foundDest != null) {
								teleportRequest(foundDest, "ChaTour.frame.tour");
								foundDest = null;
								return;
							}
							// Poll fast for a while if we actually got something.
							if(poller != null) poller.resetPollInterval();
						}
					}
				}
			} else if(arg instanceof Exception) {
				appendText("!!!ERROR: "+((Exception)arg).getMessage()+"\n");
			}
		}
	}
	public synchronized void teleportRequest(String address, String frame) {
		if(allowTeleport && address!=null) {
			nextDest = address;
			nextFrame = frame;
			appendText(">>>TELEPORTING: \"" + address + "\"\n");
			haltButton.setWarning(true);
		}
	}
	public synchronized void teleportCancel() {
		haltButton.setWarning(false);
		if(nextDest != null) {
			appendText("<<<CANCELLING: \"" + nextDest + "\"\n");
			nextDest = null;
			nextFrame = null;
		}
	}
	public synchronized void teleportComplete() {
		haltButton.setWarning(false);
		if(nextDest != null) tourDest = nextDest;
		nextDest = null;
		nextFrame = null;
	}
	public synchronized void teleport() {
		if(nextDest!=null && allowTeleport) {
			if(!inlineApplet || nextFrame!=null) {
				URL jumpURL;
				try {
					jumpURL = new URL(nextDest);
				} catch(MalformedURLException e1) {
					try { jumpURL = new URL(app.getDocumentBase(), nextDest); }
					catch(MalformedURLException e2) { jumpURL = null; }
				}
				if(jumpURL == null) teleportCancel();
				else if(nextFrame == null) app.getAppletContext().showDocument(jumpURL, "ChaTour.frame.aerotour");
				else app.getAppletContext().showDocument(jumpURL, nextFrame);
				teleportComplete();
			} else {
				Hashtable jumpParams = new Hashtable();
				jumpParams.put(READLINE_PARAM, String.valueOf(readLine - showLines));
				jumpParams.put(LOOKLINE_PARAM, String.valueOf(lookLine));
				jumpParams.put(SHOWLINES_PARAM, String.valueOf(showLines));
				jumpParams.put(FONTSIZE_PARAM, String.valueOf(fontSize));
				jumpParams.put(PAUSETIME_PARAM, String.valueOf(pauseTime));
				jumpParams.put(NICKNAME_PARAM, (nickName!=null ? nickName : "anonymous"));
				if(guideName != null) jumpParams.put(GUIDENAME_PARAM, guideName);
				jumpParams.put(TOURDEST_PARAM, nextDest);
				jumpParams.put(CGISUFFIX_PARAM, cgiSuffix);
				jumpParams.put(CGIBASE_PARAM, cgiBase.toExternalForm());
				URL jumpURL;
				try { jumpURL = new URL(cgiBase, "express" + cgiSuffix); }
				catch(MalformedURLException e) { jumpURL = null; }
				if(jumpURL == null) teleportCancel();
				else {
					// Teleporting clean out of this copy of the applet.
					(queryChain = new QueryTransaction(queryChain, jumpURL, jumpParams)).start((Applet)app);
					allowTeleport = false; // avoid teleport surprises from defunct applets
				}
				teleportComplete();
			}
		}
	}
}

class ChatPoller implements Runnable {
	ChaTour chatter;
	long shortPoll;
	long pollInterval;
	
	public ChatPoller(ChaTour callback, long msec) {
		chatter = callback; pollInterval = shortPoll = msec;
	}
	public void resetPollInterval() {
		pollInterval = shortPoll;
	}
	public void run() {
		pollInterval = shortPoll;
		while(true) {
			chatter.chat();
			try { Thread.sleep(pollInterval); }
			catch(InterruptedException e) { pollInterval = shortPoll; }
			pollInterval = (pollInterval*12)/10;
			if(pollInterval > 60000) pollInterval = 60000;			
		}
	}
}
	
class Teleporter extends Canvas implements Runnable {
	ChaTour chatter;
	long delayInterval, accumInterval;
	int state;
	static final long switchInterval = 650; // Don't make too small, or we will hang in repaint() due to Netscape threading bugs.
	int fontSize;
	
	public Teleporter(ChaTour callback, long msec) {
		chatter = callback; delayInterval = msec;
		state = 0; fontSize = 12;
	}
	public synchronized void setWarning(boolean yn) {
		if(!yn) {
			state = 0;
		} else if(state == 0) {
			state = 1;
			Thread waiter = new Thread(this);
			waiter.setPriority(Thread.MAX_PRIORITY);
			waiter.start();
		}
		accumInterval = 0;
	}
	public synchronized void setFont(Font f) {
		fontSize = f.getSize();
		resize(4*fontSize, 2*fontSize);
		repaint();
	}
	public synchronized void setDelay(long msec) {
		delayInterval = msec;
	}
	public synchronized Dimension minimumSize() {
		return new Dimension(4*fontSize, 2*fontSize);
	}
	public synchronized void run() {
		for(accumInterval=0; accumInterval<delayInterval && state!=0; accumInterval+=switchInterval) {
			try { wait(switchInterval); }
			catch(InterruptedException e) {}
			state = -state;
			repaint();
		}
		if(state != 0) chatter.teleport();
	}
	public synchronized void paint(Graphics g) {
		int w = size().width;
		int h = size().height;
		if(state == 0) {
			g.setColor(Color.green);
			g.fillOval(w/2 - h/2, 0, h, h);
		} else if(state > 0) {
			g.setColor(Color.red);
			g.fillOval(0, 0, h, h);
		} else {
			g.setColor(Color.red);
			g.fillOval(w - h, 0, h, h);
		}
	}
}


	
	
