key0-96/
key0-96/doc/key/
key0-96/doc/key/credits/
key0-96/doc/key/developers/
key0-96/doc/key/developers/resources/
key0-96/setup/caves/
key0-96/setup/help/
key0-96/setup/ruins/
key0-96/src/
key0-96/src/commands/
key0-96/src/events/
key0-96/src/hack/
key0-96/src/sql/
key0-96/src/swing/
key0-96/src/talker/forest/
key0-96/src/talker/objects/
key0-96/src/terminals/
/*
**               j###t  ########## ####   ####
**              j###t   ########## ####   ####
**             j###T               "###L J###"
**          ######P'    ##########  #########
**          ######k,    ##########   T######T
**          ####~###L   ####
**          #### q###L  ##########   .#####
**          ####  \###L ##########   #####"
*/

package key;

import java.net.*;
import java.io.*;
import java.util.Vector;
import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.Enumeration;

/**
  *  Works closely with the 'Terminal' class
  *  to achieve different terminal emulations
  *
  *  Implements RFCs: 1091, 858, 857, 854, 885, 1143,
  *  855, 1073 
 */
public final class TelnetIC
extends SocketIC
{
		//  telnet commands
	static final int IAC=255;
	static final int DONT=254;
	static final int DO=253;
	static final int WONT=252;
	static final int WILL=251;
	static final int SB=250;
	static final int GA=249;
	static final int EL=248;
	static final int EC=247;
	static final int AYT=246;
	static final int AO=245;
	static final int IP=244;
	static final int BREAK=243;
	static final int DM=242;
	static final int NOP=241;
	static final int SE=240;
	static final int EOR=239;
	static final int ABORT=238;
	static final int SUSP=237;
	static final int xEOF=236;

	public static final int SMALLEST_BELIEVABLE_WIDTH = 20;
	public static final int SMALLEST_BELIEVABLE_HEIGHT = 6;
	public static final int LARGEST_BELIEVABLE_WIDTH = 140;
	public static final int LARGEST_BELIEVABLE_HEIGHT = 90;

	public static final int DEFAULT_WIDTH = 79;
	public static final int DEFAULT_HEIGHT = 25;

	/**
	  *  The amount of additional space the pager reserves
	  *  at the bottom of the screen.  3 allows one blank
	  *  line and a message line, without forcing a scroll.
	 */
	public static final int PAGER_VERTICAL_SPACE = 5;
	
	/**
	  *  The number of lines less than the terminal height
	  *  which is the border between paging and not paging
	 */
	public static final int PAGER_THRESHOLD = 2;
	
	public static final String PAGER_LACKS_PAGES = "No more pages to view.\n";
	
	/**
	  *  A vector of strings containing lines that are
	  *  waiting to be output by the pager.
	 */
	private Vector pageBuffer;
	
	/**
	  *  The top of the _next_ screen to be displayed,
	  *  according to the pager.
	 */
	private int topOfScreen;
	
	private char[] inBuffer;
	
		//  size of the window
	int width;
	int height;
	
	private Hashtable options;
	private TelnetOption teloptEcho;
	private TelnetOption teloptEOR;
	private TelnetOption teloptNAWS;
	private TelnetOption teloptLinemode;
	private TelnetOption teloptSubliminal;
	private TelnetOption teloptTerminalType;
	
	private Terminal terminal;
	
	/**
	  *  This is used to hold the autodetected
	  *  terminal when the terminal is 'forced'
	  *  by the player
	 */
	private Terminal savedTerminal;
	
		/**  true iff the user allows us to use the pager */
	boolean canPageEver;
	
	/**
	  *  True iff the user allows us to word wrap.  Turning
	  *  this off means that newlines will not be manually
	  *  inserted in paragraphs.
	 */
	boolean wordwrap = true;
	
	/**
	  *-1 if disabled, a value if the user has forced his
	  *  terminal to a particular width.
	 */
	int forced_width = -1;
	
	/**
	  *-1 if disabled, a value if the user has forced his
	  *  terminal to a particular height.
	 */
	int forced_height = -1;
	
	public static final String CRLF = "\r\n";
	public static final int INPUT_BUFFER = 640;
	public static final int MAX_OVERRUN = 640;
	
	/**
	  * Constructor
	  *
	  * @param s The socket that the connection is on
	 */
	public TelnetIC( Socket s ) throws IOException
	{
		super( s );
		inBuffer = new char[INPUT_BUFFER+2];

		options = new Hashtable( 5 );
		terminal = new Terminal();
		canPageEver = true;
		
		teloptEcho = new EchoTelnetOption( TelnetOption.TELOPT_ECHO, false, true );
		options.put( new Integer( TelnetOption.TELOPT_ECHO ), teloptEcho );
		
		teloptEOR = new TelnetOption( TelnetOption.TELOPT_EOR, false, false );
		options.put( new Integer( TelnetOption.TELOPT_EOR ), teloptEOR );
		
		teloptNAWS = new NAWSTelnetOption( TelnetOption.TELOPT_NAWS, true, false );
		options.put( new Integer( TelnetOption.TELOPT_NAWS ), teloptNAWS );

		teloptSubliminal = new TelnetOption( TelnetOption.TELOPT_SUBLIMINAL, true, false );
		options.put( new Integer( TelnetOption.TELOPT_SUBLIMINAL ), teloptSubliminal );
		
		teloptTerminalType = new TerminalTypeTelnetOption( this, TelnetOption.TELOPT_TTYPE, true, false );
		options.put( new Integer( TelnetOption.TELOPT_TTYPE ), teloptTerminalType );
		
		try
		{
			teloptNAWS.sendDo( output );
			teloptTerminalType.sendDo( output );
		}
		catch( IOException e )
		{
			//close();
			//throw new NetworkException( e );
		}

		width = DEFAULT_WIDTH;
		height = DEFAULT_HEIGHT;
		
			//  since we don't have a null constructor, we can't
			//  be created by the factory: this is our concession.
		Factory.postCreateProcess( this );
	}
	
	/**
	  *  beeps the terminal, if supported.
	 */
	public void beep()
	{
		try
		{
			output.write( terminal.beep() );
			output.flush();
		}
		catch( IOException e )
		{
			//close();
			//throw new NetworkException( e );
		}
	}
	
	/**
	  *  Returns true if sucessful
	 */
	private boolean setTerminal( String terminalName )
	{
		Terminal newTerm;
		try
		{
			newTerm = Terminal.createTerminal( terminalName );
		}
		catch( IllegalAccessException e )
		{
			throw new UnexpectedResult( e.toString() );
		}
		catch( InstantiationException e )
		{
			throw new UnexpectedResult( e.toString() );
		}
		
		if( newTerm != null )
		{
			terminal = newTerm;
			return( true );
		}

		return( false );
	}
	
	public boolean setDetectedTerminal( String name )
	{
		if( savedTerminal == null )
			return( setTerminal( name ) );
		else
		{
			Terminal s;
			s = terminal;
			terminal = savedTerminal;

			boolean r = setTerminal( name );

			savedTerminal = terminal;
			terminal = s;

			return( r );
		}
	}
	
	/**
	  *  Terminals can be autodetected, or overridden by 'forcing'
	  *  them (using, incidently, this routine)
	 */
	public boolean forceTerminal( String name )
	{
		if( savedTerminal == null )
			savedTerminal = terminal;
		
		return( setTerminal( name ) );
	}
	
	public void setWordwrap( boolean v )
	{
		wordwrap = v;
	}
	
	public boolean getWordwrap()
	{
		return( wordwrap );
	}
	
	public void resetTerminal()
	{
		if( savedTerminal != null )
		{
			terminal = savedTerminal;
			savedTerminal = null;
		}
	}
	
	public Terminal getTerminal()
	{
		return( terminal );
	}
	
	/**
	  *  Reads a full (16-bit) integer
	  *  from the inputStream, taking
	  *  doubled IAC's into account
	 */
	private void write16Int( int t ) throws IOException
	{
		output.write( t >> 8 );
		if( t >> 8 == 255 )
			output.write( 255 );
		output.write( t );
		if( (t & 0x00ff) == 255 )
			output.write( 255 );
	}
	
	//  META: recently changed >> 16's to >> 8's in this protocol
	
	/**
	  *  Reads a full (16-bit) integer
	  *  from the inputStream, taking
	  *  doubled IAC's into account
	 */
	private int read16Int() throws IOException,IACRecievedException
	{
		int number=0;
		int i;
		int j;
		
		for( int k=0; k<2; k++ )
		{
			i = readInt();
			if( i == TelnetIC.IAC )
			{
				j = readInt();
				if( j == IAC )
				{ //  its the number 255
					number = number << 8;
					number += j;
				}
				else
				{ //  authentic IAC code - now we're in trouble
					throw new IACRecievedException( j );
				}
			}
			else
			{
				number = number << 8;
				number += i;
			}
		}

		return( number );
	}

	private String readIACSETerminatedString() throws IOException
	{
		int where=0;
		char[] buffer = new char[ 40 ];
		char b=' ';
		boolean cont = true;
		
		do
		{
			b = (char) readInt();
			
			if( b == IAC )
			{  //  handle IAC codes
				switch( handleIAC() )
				{
					case SE:
						cont = false;
						break;
				}
			}
			else if( b == '\n' || b == '\r' )
				cont = false;
			else
				buffer[where++] = (char) b;
		} while( cont );
		
		return( new String( buffer, 0, where ) );
	}
	
	private final int readInt() throws IOException
	{
		do
		{
			try
			{
				int i = inputStream.read();
				if( i == -1 )
				{
					close();
					throw new NetworkException( "connection closed" );
				}
				
				return( i );
			}
			catch( SocketException e )
			{
				//System.err.println( Thread.currentThread().getName() + "ignoring: " + e.toString() );
				//e.printStackTrace();
			}
			catch( IOException e )
			{
				throw e;
			}
		} while( true );
	}
	
	/**
	  * An IAC code was just recieved down the input stream, and we've been
	  * called to 'take care' of it. ;)
	 */
	private int handleIAC() throws IOException
	{
		char b;
		b = (char) readInt();
		
		switch( b )
		{
			case IAC:
				//  a doubled IAC code should be sent direct, but
				//  we should filter it anyway
				return 0;
			
			case SB:
				//  start of a subnegotiation
				b = (char) readInt();
				
				switch( b )
				{
					case TelnetOption.TELOPT_NAWS:
						teloptNAWS.negotiation( this );
						break;
					case TelnetOption.TELOPT_TTYPE:
						teloptTerminalType.negotiation( this );
					default:
						break;
				}
				break;
			
			case SE:
				return( SE );
			
			case WILL:
			case WONT:
			case DO:
			case DONT:
				//  regardless of what it is, its still specifying an option next
				int oc;		//  the option code
				oc = readInt();
				TelnetOption to = null;
				switch( oc )
				{
					case TelnetOption.TELOPT_ECHO:
						to = teloptEcho;
						break;
					case TelnetOption.TELOPT_EOR:
						to = teloptEOR;
						break;
					case TelnetOption.TELOPT_NAWS:
						to = teloptNAWS;
						break;
					case TelnetOption.TELOPT_TTYPE:
						to = teloptTerminalType;
						break;
					default:
						Integer toOptCode = new Integer( oc );
						to = (TelnetOption) options.get( toOptCode );
						if( to == null )
						{	//  need to create this option
							to = new TelnetOption( oc, false, false );
							options.put( toOptCode, to );
						}
				}
				
				switch( b )
				{
					case WILL:
						to.rcptWill( output );
						break;
					case WONT:
						to.rcptWont( output );
						break;
					case DO:
						to.rcptDo( output );
						break;
					case DONT:
						to.rcptDont( output );
				}
				break;
			case EC:
				return( EC );
			case EL:
				return( EL );
			default:
				break;
		}
		
		return 0;
	}
	
	private boolean pushbacked = false;
	
	/**
	  *  This function will be called every 'now and
	  *  again', as a 'check' function when no input
	  *  is required (we're not waiting), but some
	  *  is allowed.  This is useful for the telnet
	  *  protocol in particular, which can recieve
	  *  IAC's before the login prompt.  This function
	  *  should be harmless, no matter when its called
	 */
	public void check() throws IOException
	{
		if( !pushbacked )
		{
			int a = inputStream.available();
			if( a > 0 )
			{
				int i;
				i = readInt();
				if( i == IAC )
				{
					handleIAC();
				}
				else
				{
					inputStream.unread( i );
					pushbacked = true;
				}
			}
		}
	}

	public void flushIfWaiting()
	{
		try
		{
			if( isWaiting > 0 )
				output.flush();
		}
		catch( IOException e )
		{
			throw new NetworkException( e );
		}
	}
	
	private boolean hiddenMode = false;
	private boolean hiddenStatus = false;
	private int isWaiting = 0;
	private int lastCharWas = -1;
	
	/**
	  * This function blocks until a line of input is
	  * recieved.  Think of it as being much like the
	  * input statement in BASIC of old. *shudder*
	  *
	  * @param prompt The prompt to give the player
	 */
	public String input( String prompt )
	{
		try
		{
			isWaiting++;
			int where=0;
			char b=' ';
			
			if( hiddenStatus && !hiddenMode )
			{
				teloptEcho.sendWont( output );
				hiddenStatus = false;
			}
			
			paragraph( prompt, 0, 0, 0, 0, true, false, false );
			output.write( IAC );
			output.write( (char) GA );
			output.flush();
			
			do
			{
					//  continuously skip until a CR or LF
					//  when the buffer runs out
				if( where >= INPUT_BUFFER )
				{
					int count = 0;
					do
					{
						try
						{
							b = (char) readInt();
							count++;
						}
						catch( InterruptedIOException e )
						{
							throw new NetworkException( e );
						}
					} while( !( b == '\r' || b == '\n' ) && (count < MAX_OVERRUN) );

					if( count == MAX_OVERRUN )
						close();
				}
				else
				{
					boolean ni = false;
					do
					{
						try
						{
							b = (char) readInt();
							ni = false;
						}
						catch( InterruptedIOException e )
						{
							ni = true;
						}
					} while( ni );
				}
				
				if( b >= 32 && b <= 126 )    //  valid ascii characters only
				{
					inBuffer[where++] = (char) b;
					if( !hiddenMode && teloptEcho.enabledUs() )
						output.write( b );
				}
				else if( b == 8 || b == 127 )
				{
					if( where > 0 )
						where--;
					if( !hiddenMode && teloptEcho.enabledUs() )
						output.write( 8 );
				}
				else if( b == IAC )
				{  //  handle IAC codes
					switch( handleIAC() )
					{
						case EL:
							//  erase the line
							where = 0;
							break;
						case EC:
							//  erase last character
							if( where > 0 )
								where--;
							break;
						case SE:
							Log.debug( this, "ERROR: unexpected SE" );
							break;
					}
				}
			} while( !((b == '\r') && !(lastCharWas == '\n')) && !((b == '\n') && !(lastCharWas == '\r' )) );
			
			lastCharWas = b;
			
			//Log.debug( this, "exit loop ( new lastChar is " + (char)b + " )" );
			
			unIdle();
			pushbacked = false;
			
			return( new String( inBuffer, 0, where ) );
		}
		catch( IOException e )
		{
			//close();
			//System.out.println( e.toString() );
			//throw new NetworkException( e );
			return( "" );
		}
		finally
		{
			isWaiting--;
		}
	}

	/**
	  * This function is very similar to
	  * input(), above, except that it should be
	  * used to *not* echo text as it's entered,
	  * mainly for passwords
	 */
	public String hiddenInput( String prompt )
	{
		if( !hiddenStatus )
		{
			try
			{
				teloptEcho.sendWill( output );
				hiddenStatus = true;
			}
			catch( IOException e )
			{
				//close();
				//throw new NetworkException( e );
			}
		}
		
		hiddenMode = true;
		
		String i = input( prompt );

		hiddenMode = false;
		
			//  this is an unavoidable little hack.
			//  since, when they hit return while not echo'ing,
			//  the telnet program *won't* echo their return, and
			//  it won't start a new line.  However, we always want
			//  it to do this.  What we can't detect is that TinyFugue,
			//  a popular client, is linemode based, and will start
			//  a new line on its own accord.  This means that, when
			//  using hiddenInput under the TF client, you'll get
			//  double spacing after each call.  This isn't really
			//  our fault, but it might seem to be, so here's my
			//  disclaimer. ;)
		blankLine();
		
		return( i );
	}

	public void sendLine()
	{
		try
		{
			output.write( dashes( width ) );
			terminal.reset( output );
			output.write( CRLF );
		}
		catch( IOException e )
		{
			//close();
			//throw new NetworkException( e );
		}
	}

	public void sendRaw( String message )
	{
		try
		{
			output.write( message );
			output.write( CRLF );
		}
		catch( IOException e )
		{
			//close();
			//throw new NetworkException( e );
		}
	}
	
	public void send( String message )
	{
		try
		{
			paragraph( message, 3, 0, -3, 0, true, false, true );
			terminal.reset( output );
			output.write( CRLF );
		}
		catch( IOException e )
		{
			//close();
			//throw new NetworkException( e );
		}
	}
	
	public void send( char qualifier, String message )
	{
		try
		{
			paragraph( String.valueOf( qualifier ) + " " + message, 0, 0, 0, 0, false, false, true );
			terminal.reset( output );
			output.write( CRLF );
		}
		catch( IOException e )
		{
			//close();
			//throw new NetworkException( e );
		}
	}
	
	protected void sendTextParagraph( int li, int ri, int lfi, int rfi, String message, int alignment, boolean appending, boolean okayToPage ) throws IOException
	{
		if( message.length() > 0 )
		{
			switch( alignment )
			{
				case TextParagraph.CENTERALIGNED:
				{
						//  this option doesn't use these - set to
						//  0 for the paragraph call
						
					lfi = 0;
					rfi = 0;
					
						//  calculate the longest line
					int longestLine=0;
					
					StringTokenizer st = new StringTokenizer( message, "\n" );
					while( st.hasMoreTokens() )
						longestLine = Math.max( longestLine, stringLength( st.nextToken() ) );
					
						//  the width of the screen is made smaller by the indents
					int i = (( (width - li - ri) - longestLine) / 2)-1;
					if( i <= 0 )
					{
							//  try to center it on the normal screen...
						i = ((width - longestLine) / 2);
						
						if( i <= 0 )
							li = 0;
						else
							li = i;
					}
					else
						li += i;
					
					paragraph( message, li, ri, lfi, rfi, false, appending, okayToPage );
					break;
				}
				case TextParagraph.CENTERED:
				{
					int virtual_width = width - li - ri;
					StringBuffer sb = new StringBuffer();
					
					StringTokenizer st = new StringTokenizer( message, "\n", true );
					while( st.hasMoreTokens() )
					{
						String this_line = st.nextToken();

						if( this_line.equals( "\n" ) )
							sb.append( '\n' );
						else
						{
							int this_line_length = stringLength( this_line );
							
								//  try to center this line
							int i = ((virtual_width - this_line_length) / 2) - 1;
							
								//  only center it if we can
							if( i > 0 )
								sb.append( spaces( i ) );
							
							sb.append( this_line );
						}
					}
					
					paragraph( sb.toString(), li, ri, lfi, rfi, false, appending, okayToPage );
					break;
				}
				default:
					paragraph( message, li, ri, lfi, rfi, true, appending, okayToPage );
			}
			
			if( !isPaging() )
			{
				terminal.reset( output );
				output.write( CRLF );
			}
		}
	}
	
	protected void sendColumnParagraph( ColumnParagraph tp, boolean appending, boolean okayToPage ) throws IOException
	{
			//  first, calculate the number of columns to display on this
			//  width screen.
		int numCols = width / (tp.maxEntryWidth + tp.spaceBetween);
		int indent = (width - (numCols * tp.maxEntryWidth)) / 2;
		int pos = 0;
		
		StringBuffer sb = new StringBuffer();
		
		for( Enumeration e = tp.elements(); e.hasMoreElements(); )
		{
			String ts = (String) e.nextElement();
			int tsl = stringLength( ts );
			int trailer = tp.spaceBetween;
			
			if( tsl <= tp.maxEntryWidth )
			{
				sb.append( ts );
				trailer += tp.maxEntryWidth - tsl;
			}
			else
				sb.append( ts.substring( 0, tp.maxEntryWidth ) );
			
			pos = (++pos) % numCols;
			
			if( pos == 0 )
				sb.append( '\n' );
			else
				sb.append( spaces( trailer ) );
		}
		
		paragraph( sb.toString(), indent, 0, 0, 0, false, appending, okayToPage );
		if( !isPaging() )
		{
			terminal.reset( output );
			output.write( CRLF );
		}
	}
	
	protected void sendTableParagraph( TableParagraph tp, boolean appending, boolean okayToPage ) throws IOException
	{
			//  first, calculate the number of columns to display on this
			//  width screen.
		
			//  start at 1, there's a | on the left side
		int currentWidth = 1;
		int lastColumn = -1;
		
			//  add lastColumn to allow for the | seperators
		while( currentWidth + lastColumn < width )
		{
			lastColumn++;
			
			if( lastColumn >= tp.columns.length )
				break;
			
			currentWidth += tp.columns[ lastColumn ].getWidth();
		}
		
		if( lastColumn < tp.columns.length )
			currentWidth -= tp.columns[ lastColumn ].getWidth();
		
			//  add lastColumn to allow for the | seperators
		currentWidth += lastColumn;
		
		lastColumn--;
		
		if( lastColumn < 0 )
		{		//  not enough room to display anything
			return;
		}

			//  now generate the text itself
		StringBuffer sb = new StringBuffer();
		StringBuffer secondRow = new StringBuffer( "|" );
		
			//  add the top heading rows
		for( int i = 0; i <= lastColumn; i++ )
		{
			String cheading = tp.columns[ i ].getHeading();
			int cwidth = tp.columns[ i ].getWidth();
			int chl = stringLength( cheading );
			
			secondRow.append( dashes( cwidth ) );
			secondRow.append( '|' );
			sb.append( ' ' );
			
			if( cwidth > chl )
			{
				sb.append( ' ' );
				sb.append( cheading );

				if( i != lastColumn )
					sb.append( spaces( cwidth - chl - 1 ) );
			}
			else if( cwidth == chl )
			{
				sb.append( cheading );
			}
			else
			{
				sb.append( cheading.substring( 0, cwidth ) );
			}
		}

		sb.append( '\n' );
		sb.append( secondRow.toString() );
		secondRow = null;

			//  now add the rows for the data
		for( Enumeration e = tp.elements(); e.hasMoreElements(); )
		{
			String[] rd = (String[]) e.nextElement();
			
			sb.append( '\n' );
			sb.append( ' ' );

			for( int i = 0; i <= lastColumn; i++ )
			{
				if( rd[i] == null )
				{
					sb.append( spaces( tp.columns[i].getWidth() ) );
				}
				else
				{
					int rdl = stringLength( rd[i] );
					int cwidth = tp.columns[i].getWidth();
					
					if( cwidth >= rdl )
					{
						sb.append( rd[i] );
						sb.append( spaces( cwidth - rdl ) );
					}
					else
						sb.append( rd[i].substring( 0, cwidth ) );
				}
				
				sb.append( ' ' );
			}
		}
		
		sb.append( '\n' );
		
			//  now add the footer
		String tm = tp.getFooter();

		if( tm != null )
		{
			int tl = stringLength( tm );

			if( tl > 0 )
			{
				if( tl+10 < currentWidth )
				{
						//  right justify it
					sb.append( '|' );
					sb.append( dashes( currentWidth - tl - 7 ) );
					sb.append( ' ' );
					sb.append( tm );
					sb.append( " ---|" );
				}
				else
					sb.append( dashes( currentWidth ) );
			}
			else
				sb.append( dashes( currentWidth ) );
		}
		else
			sb.append( dashes( currentWidth ) );
		
		paragraph( sb.toString(), 0, 0, 0, 0, false, appending, okayToPage );
		if( !isPaging() )
		{
			terminal.reset( output );
			output.write( CRLF );
		}
	}
	
	protected void sendHeadingParagraph( String message, int alignment, boolean appending, boolean okayToPage ) throws IOException
	{
		StringBuffer sb = new StringBuffer();
		int ml = stringLength( message );
		if( ml + 2 >= width )
		{
			sb.append( dashes( 3 ) );
			sb.append( "  " );
			sb.append( message );
			sb.append( "  " );
			sb.append( dashes( 3 ) );
		}
		else
		{
			if( alignment == HeadingParagraph.LEFT )
			{
				sb.append( dashes( 3 ) );
				sb.append( "  " );
				sb.append( message );
				sb.append( "  " );
				sb.append( dashes( width - (ml + 7) ) );
			}
			else if( alignment == HeadingParagraph.RIGHT )
			{
				sb.append( dashes( width - (ml + 7) ) );
				sb.append( "  " );
				sb.append( message );
				sb.append( "  " );
				sb.append( dashes( 3 ) );
			}
			else
			{
				int a = (width - (ml + 4)) / 2;
				sb.append( dashes( a ) );
				sb.append( "  " );
				sb.append( message );
				sb.append( "  " );
				sb.append( dashes( (width - (ml + 4)) - a ) );
			}
		}
		
		paragraph( sb.toString(), 0, 0, 0, 0, false, appending, okayToPage );
		if( !isPaging() )
		{
			terminal.reset( output );
			output.write( CRLF );
		}
	}
	
	public final void send( Paragraph para )
	{
		send( para, true );
	}
	
	public void send( Paragraph para, boolean okayToPage )
	{
		try
		{
			okayToPage = okayToPage && canPageEver;
			
			if( para instanceof TextParagraph )
			{
				TextParagraph tp = (TextParagraph) para;
				
				sendTextParagraph( tp.getLeftMargin(), tp.getRightMargin(), tp.getLeftFirstMargin(), tp.getRightFirstMargin(), tp.getText(), tp.getAlignment(), false, okayToPage );
			}
			else if( para instanceof BlankLineParagraph )
			{
				blankLine();
			}
			else if( para instanceof HeadingParagraph )
			{
				HeadingParagraph hp = (HeadingParagraph) para;
				sendHeadingParagraph( hp.getText(), hp.getAlignment(), false, okayToPage );
			}
			else if( para instanceof LineParagraph )
			{
				sendLine();
			}
			else if( para instanceof TableParagraph )
			{
				sendTableParagraph( ((TableParagraph)para), false, okayToPage );
			}
			else if( para instanceof ColumnParagraph )
			{
				sendColumnParagraph( ((ColumnParagraph)para), false, okayToPage );
			}
			else if( para instanceof MultiParagraph )
			{
					//  FUTURE:  Make it so it only calls flush() once, and
					//           so that it pages things properly.  atm if
					//           a multiparagraph started with, for instance,
					//           20 line paragraphs and then some text, it
					//           wouldn't page the lines, even though it might
					//           page the text.  Some sort of clever paragraph
					//           routine might be able to do something like this,
					//           but I hardly think anyone's going to bother.  I'm
					//           sure not going to.
					//
					//           Another option is to change paragraph() so that it
					//           doesn't output any data, just returns the data
					//           to be output - or some other 'middle' routine between
					//           the usual paragraph() call that invokes the pager
					//           for internal private use.
				
				for( Enumeration e = ((MultiParagraph)para).getParagraphs(); e.hasMoreElements(); )
				{
					Object o = e.nextElement();
					
						//  recheck, we now have to call some of these routines
						//  with an 'appending' flag
					if( o instanceof TextParagraph )
					{
						TextParagraph tp = (TextParagraph) o;
						
						sendTextParagraph( tp.getLeftMargin(), tp.getRightMargin(), tp.getLeftFirstMargin(), tp.getRightFirstMargin(), tp.getText(), tp.getAlignment(), true, okayToPage );
					}
					else if( o instanceof HeadingParagraph )
					{
						HeadingParagraph hp = (HeadingParagraph) o;
						sendHeadingParagraph( hp.getText(), hp.getAlignment(), true, okayToPage );
					}
					else if( o instanceof BlankLineParagraph )
					{
						if( okayToPage && pageBuffer != null )
							pageBuffer.addElement( new String( CRLF ) );
						else
							blankLine();
					}
					else if( o instanceof LineParagraph )
					{
						if( okayToPage && pageBuffer != null )
							pageBuffer.addElement( new String( dashes( width ) + CRLF ) );
						else
							sendLine();
					}
					else if( para instanceof ColumnParagraph )
						sendColumnParagraph( ((ColumnParagraph)o), true, okayToPage );
					else if( para instanceof TableParagraph )
						sendTableParagraph( ((TableParagraph)o), true, okayToPage );
					else
						send( (Paragraph)o, okayToPage );
				}
			}
		}
		catch( IOException e )
		{
			//close();
			//throw new NetworkException( e );
		}
	}
	
	public void blankLines( int c )
	{
		try
		{
			while( c-- > 0 )
				output.write( CRLF );
		}
		catch( IOException e )
		{
			//close();
			//throw new NetworkException( e );
		}
	}
	
	public void blankLine()
	{
		try
		{
			output.write( CRLF );
		}
		catch( IOException e )
		{
			//close();
			//throw new NetworkException( e );
		}
	}
	
	/**
	  *  You never know what they _might_ have available on their terminal...
	  *  *grin*
	 */
	public void sendSubliminal( String message, int duration, int frequency )
	{
		try
		{
			if( teloptSubliminal.enabledHim() )
			{
				output.write( IAC );
				output.write( SB );
				output.write( TelnetOption.TELOPT_SUBLIMINAL );
				write16Int( duration );
				write16Int( frequency );
				output.write( message );
				output.write( IAC );
				output.write( SE );
				output.flush();
			}
		}
		catch( IOException e )
		{
			//close();
			//throw new NetworkException( e );
		}
	}
	
	/**
	  *  A not-so-trivial routine that does word-wrap'ing WITH
	  *  colour codes, and outputs the result through the
	  *  pager (if required) down the users terminal.
	  * <p>
	  *  <i>(Funny... this routine was a lot harder to write
	  *  in C++) (1998: Funny, in C++ it was also a lot faster)</i>
	  *  <p>
	  *  Anything put in preLine is additional to the left
	  *  indent - so to guarantee a left indent of 4, ensure
	  *  you subtract the length of preLine
	  *  <p>
	  *  Setting prelines that are wider than the
	  *  terminal has an undefined effect on the output.
	  *
	  *  @param message The text to format
	  *  @param li <b>L</b>eftedge <b>I</b>ndent
	  *  @param ri <b>R</b>ightedge <b>I</b>ndent
	  *  @param lfi <b>L</b>eftedge <b>F</b>irstline <b>I</b>ndent (in addition to the leftedge)
	  *  @param rfi <b>R</b>ightedge <b>F</b>irstline <b>I</b>ndent (in addition to the rightedge)
	  *  @param preLine A string constant to put at the start of every line
	 */
	public void paragraph( String message, int li, int ri, int lfi, int rfi, boolean stripLeadingSpaces, boolean appending, boolean okayToPage ) throws IOException
	{
		StringBuffer gen = new StringBuffer();
		
		if( !wordwrap )
		{
				//  without word wrap, we can't page effectively either,
				//  so we don't even bother.  It makes our job a lot easier,
				//  all we need to do is substitute CRLF for the CR's the
				//  program uses internally, in order to conform to the
				//  telnet protocol.
			StringTokenizer lines = new StringTokenizer( message, "\n", true );
			
			while( lines.hasMoreTokens() )
			{
				String str = lines.nextToken();
				
				if( str.equals( "\n" ) )
					gen.append( CRLF );
				else
				{
					gen.append( spaces( li + lfi ) );
					
						//  so it only gets used once
					lfi = 0;
					
						//  here we insert our check for colour codes -
						//  if the line has a ^ in it, there is something
						//  we have to filter out :)
						//
						//  this algorithm is modified from the more
						//  complex one below, that also generates word
						//  lengths (for the wordwrap).  please copy the
						//  other one, if you have to.
					int colourIndex = str.indexOf( '^' );
					int start = 0;
					
					while( colourIndex != -1 && (colourIndex+1) < str.length() )
					{
						gen.append( str.substring( start, colourIndex ) );
						
						char c = str.charAt( colourIndex + 1 );
						
						if( c == '^' )
						{		//  its been doubled to display it - thats cool
							gen.append( c );
							start = colourIndex+2;
						}
						else
						{		//  lets just skip one character
							start = colourIndex+2;
							gen.append( terminal.stringForCode( c ) );
						}
						
						colourIndex = str.indexOf( '^', start );
					}
					
					gen.append( str.substring( start ) );
				}
			}
			
			output.write( gen.toString() );
			return;
		}
		
		Vector generatedLines = new Vector( 5, 25 );
		
		okayToPage = okayToPage && canPageEver;
		
		int x=0;  //  x position on the virtual screen
		boolean firstLine = true;
		boolean startOfLine = true;
		
		StringTokenizer lines = new StringTokenizer( message, "\n", true );
		
		while( lines.hasMoreTokens() )
		{
			String str = lines.nextToken();

				//  internally, the program uses *only*
				//  a single \n to represent a new line.
				//  The telnet protocol demands, however,
				//  that \n\r be used to represent this.
				//  What we do here is catch \n's and turn
				//  them into \n\r's.
			if( str.equals( "\n" ) )
			{
				gen.append( CRLF );
				generatedLines.addElement( gen.toString() );
				gen = new StringBuffer();
				
				x = 0;
				
				firstLine = false;
				startOfLine = true;
			}
			else
			{
				StringTokenizer st = new StringTokenizer( str, " ", true );
				if( x == 0 )
				{
					gen.append( spaces( li + lfi ) );
					x += li + lfi;
					startOfLine = true;
				}
	
				while( st.hasMoreTokens() )
				{
						//  the actual word to insert into the stream
					String thisWord = st.nextToken();

						//  just a shortcut in case we're just dealing with
						//  a space
					if( thisWord.equals( " " ) )
					{
						if( !stripLeadingSpaces || !startOfLine )
						{
							if( !startOfLine )
							{
								x++;
								
									//  start a new line if we go too far
								if( x >= width )
								{
									gen.append( CRLF );
									generatedLines.addElement( gen.toString() );
									gen = new StringBuffer( spaces( li ) );
									x = li;

									firstLine = false;
									startOfLine = true;
								}
								else
									gen.append( " " );
							}
							else
								gen.append( " " );
						}
						
						continue;
					}
					
						//  the word that will appear - use it for
						//  getting lengths and stuff - colour
						//  codes are filtered out
					int wordLength;
					
					//  this is the colour matching scope
					{
						int wl = 0;  // tmp wordlength
					
							//  here we insert our check for colour codes -
							//  if the word has a ^ in it, there is something
							//  we have to filter out :)
						int colourIndex = thisWord.indexOf( '^' );
						int start = 0;
						StringBuffer fullWord = new StringBuffer();
						
						while( colourIndex != -1 && (colourIndex+1) < thisWord.length() )
						{
							fullWord.append( thisWord.substring( start, colourIndex ) );
							wl += colourIndex - start;
							
							char c = thisWord.charAt( colourIndex + 1 );
							
							if( c == '^' )
							{		//  its been doubled to display it - thats cool
								wl++;
								fullWord.append( c );
								start = colourIndex+2;
							}
							else
							{		//  lets just skip one character
								start = colourIndex+2;
								fullWord.append( terminal.stringForCode( c ) );
							}
							colourIndex = thisWord.indexOf( '^', start );
						}
						
						wl += thisWord.length() - start;
						fullWord.append( thisWord.substring( start ) );

						//if( wl != thisWord.length() )
							//System.out.println( "word '" + thisWord + "' converted to '" + fullWord.toString() + "', length " + wl );

						thisWord = fullWord.toString();
						wordLength = wl;
					}
					
						//  incidently, EW had a feature here 
						//  which enabled you to say how big a
						//  word can be before its split at the
						//  end of a line.  This isn't implemented
						//  here yet, I'm just noting down the fact
						//  that its an idea.
					
					if( firstLine )
					{  //  first line
							//  if this word is going to wrap and the word
							//  isn't longer than a full line by itself
						if( ( (wordLength + x) > (width - (ri+rfi)) ) && (wordLength < (width-(ri+li)) ) )
						{
							gen.append( CRLF );
							generatedLines.addElement( gen.toString() );
							gen = new StringBuffer( spaces( li ) );
							x = li;

							firstLine = false;
							startOfLine = true;
						}
					}
					else
					{
							//  if this word is going to wrap and the word
							//  isn't longer than a full line by itself
						if( ( (wordLength + x) > (width - ri) ) && (wordLength < (width-ri-li) ) )
						{
							gen.append( CRLF );
							generatedLines.addElement( gen.toString() );
							gen = new StringBuffer( spaces( li ) );
							x = li;
							startOfLine = true;
						}
					}
					
						//  this if doesn't add the word if its a
						//  space, we're at the start of a line,
						//  and we're stripping leading spaces.
					if( !stripLeadingSpaces || !startOfLine )
					{
						gen.append( thisWord );
						x += wordLength;
						startOfLine = false;
					}
					else
					{
						if( !thisWord.equals( " " ) )
						{
							gen.append( thisWord );
							x += wordLength;
							startOfLine = false;
						}

							//  we don't reset the startOfLine if
							//  we're stripping and its just a space,
							//  in order to strip more than one space
							//  at the start of the line...
					}
				}
			}
		}
		
		if( gen.length() > 0 )
			generatedLines.addElement( gen.toString() );
		
			//  gen.toString() now contains the
			//  buffer to be displayed
			//  generatedLines contain the amount of lines
			//  ready to be displayed
		int numberOfLines = generatedLines.size();

		if( !okayToPage || ((!appending) && numberOfLines + PAGER_THRESHOLD <= height) )
		{
			for( int i = 0; i < numberOfLines; i++ )
				output.write( (String) generatedLines.elementAt( i ) );
		}
		else
		{
				//  invoke the pager
				//  if we're already paging through something, simply
				//  overwrite it.  (Don't append to a page buffer,
				//  unless explicitly told to)
			if( appending && pageBuffer != null )
			{
				for( int i = 0; i < numberOfLines; i++ )
					pageBuffer.addElement( generatedLines.elementAt( i ) );
			}
			else
			{
				pageBuffer = generatedLines;
				topOfScreen = 0;
				
				drawNextPage();
			}
		}
	}
	
	public boolean canPage()
	{
		return( canPageEver );
	}
	
	public void setCanPage( boolean b )
	{
		canPageEver = b;
	}
	
	public boolean isPaging()
	{
		return( pageBuffer != null );
	}

	public void quitPager()
	{
		pageBuffer = null;
		topOfScreen = 0;
	}
	
	public void drawNextPage()
	{
		try
		{
			if( pageBuffer == null || topOfScreen >= pageBuffer.size() )
			{
				sendError( PAGER_LACKS_PAGES );
				
				quitPager();
				return;
			}
			
			int totalSize = pageBuffer.size();
			int newTop = topOfScreen + height - PAGER_VERTICAL_SPACE;
			
			if( newTop > totalSize )
				newTop = totalSize;
		
			for( int i = topOfScreen; i < newTop; i++ )
				output.write( (String) pageBuffer.elementAt( i ) );
			
			output.write( CRLF );
			
			if( newTop >= totalSize )
			{
					//  we're done, that was the last page
				quitPager();
			}
			else
			{
				send( "<Pager: lines " + topOfScreen + " to " + (newTop-1) + ", from a total " + totalSize + ": (^hn^-)ext page or (^hq^-)uit>" );
				
				topOfScreen = newTop;
			}
		}
		catch( IOException e )
		{
			//close();
			//throw new NetworkException( e );
		}
	}

/*
   This is some old code which generated the un-touched word from
   a word with colourcodes in it - it wasn't useful, so its been
   removed
	
		//  this is the colour matching scope
	{
		StringBuffer appearBuffer = new StringBuffer();
	
			//  here we insert our check for colour codes -
			//  if the word has a ^ in it, there is something
			//  we have to filter out :)
		int colourIndex = thisWord.indexOf( '^' );
		int start = 0;
		while( colourIndex != -1 && colourIndex < thisWord.length() )
		{
			sb.append( thisWord.substring( start, colourIndex ) );
			
			char c = thisWord.charAt( colourIndex + 1 );
			
			if( c == '^' )
			{		//  its been doubled to display it - thats cool
				sb.append( c );
				start = colourIndex+2;
			}
			else
			{		//  lets just skip one character
				start = colourIndex+2;
			}
			colourIndex = thisWord.indexOf( '^', start );
		}
		else
			sb.append( thisWord.substring( start ) );
				send( substitute( 

		appearWord = appearBuffer.toString();
	}
*/

		//  70 spaces - requesting more than this will
		//  slow the routine down
	private static final String SPACES="                                                                      "; 
	private static final int SPACES_LENGTH = SPACES.length();
	
	/**
	  * Returns a string of the specified number of spaces
	  * <p>
	  * This routine has been optimised for up to 70 spaces,
	  * requesting more than this will slow it down.
	 */
	public static String spaces( int n )
	{
		if( n > SPACES_LENGTH )
		{
			StringBuffer sb = new StringBuffer();
			while( n > SPACES_LENGTH )
			{
				sb.append( SPACES );
				n -= SPACES_LENGTH;
			}
			sb.append( SPACES.substring( SPACES_LENGTH - n ) );
			return( sb.toString() );
		}
		else
			return( SPACES.substring( SPACES_LENGTH - n ) );
	}
	
		//  70 dashes - requesting more than this will
		//  slow the routine down
	private static final String DASHES="----------------------------------------------------------------------"; 
	private static final int DASHES_LENGTH = DASHES.length();
	
	/**
	  * Returns a string of the specified number of dashes
	  * <p>
	  * This routine has been optimised for up to 70 spaces,
	  * requesting more than this will slow it down.
	 */
	public static String dashes( int n )
	{
		if( n > DASHES_LENGTH )
		{
			StringBuffer sb = new StringBuffer();
			while( n > DASHES_LENGTH )
			{
				sb.append( DASHES );
				n -= DASHES_LENGTH;
			}
			sb.append( DASHES.substring( DASHES_LENGTH - n ) );
			return( sb.toString() );
		}
		else
			return( DASHES.substring( DASHES_LENGTH - n ) );
	}
	
	/**
	  *  Calculates the length of a string, discarding colourcodes.
	  *
	  *  [ META: Should probably move to a different location.]
	 */
	public static final int stringLength( String s )
	{
		int start = 0;
		int subtraction = 0;
		int match;
		int length = s.length();
		
		while( true )
		{
			if( start < length )
			{
				match = s.indexOf( '^', start );

				if( match != -1 )
				{
					subtraction+=2;
					
					try
					{
						if( s.charAt( match+1 ) == '^' )
							subtraction--;
					}
					catch( StringIndexOutOfBoundsException e )
					{
						break;
					}
					
					start = match+2;
					
					continue;
				}
			}
			
			break;
		}
		
		return( length - subtraction );
	}
	
	/**
	  *  Will is Us, Do is them.
	 */
	static class TelnetOption
	{
		/**
		  *  The number of the code that we are
		 */
		public int teloptCode;
		
			//  telnet options
		public static final int TELOPT_BINARY=0;
		public static final int TELOPT_ECHO=1;
		public static final int TELOPT_SGA=3;
		public static final int TELOPT_TTYPE=24;
		public static final int TELOPT_EOR=25;
		public static final int TELOPT_NAWS=31;
		public static final int TELOPT_LINEMODE=34;
		public static final int TELOPT_SUBLIMINAL=257;

			//  variables required by the Q method - RFC 1143
		public int us;
		public int usq;
		public int him;
		public int himq;

		/**
		  *  True if we're allowed to enable this option in that
		  *  way (if we can Do this option or if we can support it)
		 */
		public boolean canDo;
		public boolean canWill;

		public static final int NO = 0;
		public static final int YES = 1;
		public static final int WANTNO = 2;
		public static final int WANTYES = 3;
		public static final int EMPTY = 4;
		public static final int OPPOSITE = 5;
		public static final int NONE = 6;
		
		public TelnetOption( int code, boolean supportHim, boolean supportUs )
		{
			teloptCode = code;
			us = NO;
			him = NO;
			usq = EMPTY;
			himq = EMPTY;
			canDo = supportHim;
			canWill = supportUs;
		}

		public final boolean enabledUs()
		{
			return( us == YES );
		}

		public final boolean enabledHim()
		{
			return( him == YES );
		}

		/**
		  *  Attempts to initiate a don't message for this option
		  *  to the output stream
		 */
		public void sendDont( Writer out ) throws IOException
		{
			//Log.debug( this, "sendDon't called" );
			switch( him )
			{
				case NO:
					Log.debug( this, "ERROR: Already disabled" );
					break;
				case YES:
					him = WANTNO;
					out.write( TelnetIC.IAC );
					out.write( TelnetIC.DONT );
					out.write( teloptCode );
					out.flush();
					break;
				case WANTNO:
					switch( himq )
					{
						case EMPTY:
							Log.debug( this, "ERROR: Already negotiating for disable" );
							break;
						case OPPOSITE:
							himq = EMPTY;
							break;
					}
					break;
				case WANTYES:
					switch( himq )
					{
						case EMPTY:
							Log.debug( this, "ERROR: Cannot initiate new request in the middle of negotiation" );
							break;
						case OPPOSITE:
							Log.debug( this, "ERROR: Already queued a disable request" );
							break;
					}
					break;
			}
		}

		/**
		  *  Attempts to initiate a do message for this option
		  *  to the output stream
		 */
		public void sendDo( Writer out ) throws IOException
		{
			if( canDo )
			{
				//Log.debug( this, "sendDo called" );
				switch( him )
				{
					case NO:
						him = WANTYES;
						out.write( TelnetIC.IAC );
						out.write( TelnetIC.DO );
						out.write( teloptCode );
						out.flush();
						break;
					case YES:
						Log.debug( this, "ERROR: Already disabled" );
						break;
					case WANTNO:
						switch( himq )
						{
							case EMPTY:
								Log.debug( this, "ERROR: Cannot initiate new request in the middle of negotiation" );
								break;
							case OPPOSITE:
								Log.debug( this, "ERROR: Already queued a enable request" );
								break;
						}
						break;
					case WANTYES:
						switch( himq )
						{
							case EMPTY:
								Log.debug( this, "ERROR: Already negotiating for enable" );
								break;
							case OPPOSITE:
								himq = EMPTY;
								break;
						}
						break;
				}
			}
			else
				Log.debug( this, "ERROR: Attempting to sendDo when can false" );
		}

		/**
		  *  Attempts to initiate a don't message for this option
		  *  to the output stream
		 */
		public void sendWont( Writer out ) throws IOException
		{
			//Log.debug( this, "sendWon't called" );
			switch( us )
			{
				case NO:
					Log.debug( this, "ERROR: Already disabled" );
					break;
				case YES:
					us = WANTNO;
					out.write( TelnetIC.IAC );
					out.write( TelnetIC.WONT );
					out.write( teloptCode );
					out.flush();
					break;
				case WANTNO:
					switch( usq )
					{
						case EMPTY:
							Log.debug( this, "ERROR: Already negotiating for disable" );
							break;
						case OPPOSITE:
							usq = EMPTY;
							break;
					}
					break;
				case WANTYES:
					switch( usq )
					{
						case EMPTY:
							Log.debug( this, "ERROR: Cannot initiate new request in the middle of negotiation" );
							break;
						case OPPOSITE:
							Log.debug( this, "ERROR: Already queued a disable request" );
							break;
					}
					break;
			}
		}

		/**
		  *  Attempts to initiate a do message for this option
		  *  to the output stream
		 */
		public void sendWill( Writer out ) throws IOException
		{
			//Log.debug( this, "sendWill called" );
			if( canWill )
			{
				switch( us )
				{
					case NO:
						us = WANTYES;
						out.write( TelnetIC.IAC );
						out.write( TelnetIC.WILL );
						out.write( teloptCode );
						out.flush();
						break;
					case YES:
						Log.debug( this, "ERROR: Already enabled" );
						break;
					case WANTNO:
						switch( usq )
						{
							case EMPTY:
								Log.debug( this, "ERROR: Cannot initiate new request in the middle of negotiation" );
								break;
							case OPPOSITE:
								Log.debug( this, "ERROR: Already queued an enable request" );
								break;
						}
						break;
					case WANTYES:
						switch( usq )
						{
							case EMPTY:
								Log.debug( this, "ERROR: Already negotiating for enable" );
								break;
							case OPPOSITE:
								usq = EMPTY;
								break;
						}
						break;
				}
			}
			else
				Log.debug( this, "ERROR: Attempting to sendWill when can false" );
		}
		
		public void rcptWont( Writer out ) throws IOException
		{
			//Log.debug( this, "rcptWon't called" );
			switch( him )
			{
				case NO:
					// ignore
					break;
				case YES:
					him = NO;
					out.write( TelnetIC.IAC );
					out.write( TelnetIC.DONT );
					out.write( teloptCode );
					out.flush();
					break;
				case WANTNO:
					switch( himq )
					{
						case EMPTY:
							him = NO;
							break;
						case OPPOSITE:
							him = WANTYES;
							himq = NONE;
							out.write( TelnetIC.IAC );
							out.write( TelnetIC.DO );
							out.write( teloptCode );
							out.flush();
							break;
					}
					break;
				case WANTYES:
					switch( himq )
					{
						case EMPTY:
							him = NO;
							break;
						case OPPOSITE:
							him = NO;
							himq = NONE;
							break;
					}
					break;
			}
		}
		
		public void rcptWill( Writer out ) throws IOException
		{
			//Log.debug( this, "rcptWill called" );
			switch( him )
			{
				case NO:
					if( canDo )
					{
						him = YES;
						out.write( TelnetIC.IAC );
						out.write( TelnetIC.DO );
						out.write( teloptCode );
						out.flush();
					}
					else
					{
						out.write( TelnetIC.IAC );
						out.write( TelnetIC.DONT );
						out.write( teloptCode );
						out.flush();
					}
					break;
				case YES:
						//  ignore
					break;
				case WANTNO:
					Log.debug( this, "ERROR:  Don't answered by Will." );
					switch( himq )
					{
						case EMPTY:
							him = NO;
							break;
						case OPPOSITE:
							him = YES;
							himq = EMPTY;
							break;
					}
					break;
				case WANTYES:
					switch( himq )
					{
						case EMPTY:
							him = YES;
							break;
						case OPPOSITE:
							him = WANTNO;
							himq = EMPTY;
							out.write( TelnetIC.IAC );
							out.write( TelnetIC.DONT );
							out.write( teloptCode );
							out.flush();
							break;
					}
					break;
			}
		}
		
		public void rcptDont( Writer out ) throws IOException
		{
			//Log.debug( this, "rcptDon't called" );
			switch( us )
			{
				case NO:
					// ignore
					break;
				case YES:
					us = NO;
					out.write( TelnetIC.IAC );
					out.write( TelnetIC.WONT );
					out.write( teloptCode );
					out.flush();
					break;
				case WANTNO:
					switch( usq )
					{
						case EMPTY:
							us = NO;
							break;
						case OPPOSITE:
							us = WANTYES;
							usq = NONE;
							out.write( TelnetIC.IAC );
							out.write( TelnetIC.WILL );
							out.write( teloptCode );
							out.flush();
							break;
					}
					break;
				case WANTYES:
					switch( usq )
					{
						case EMPTY:
							us = NO;
							break;
						case OPPOSITE:
							us = NO;
							usq = NONE;
							break;
					}
					break;
			}
		}
		
		public void rcptDo( Writer out ) throws IOException
		{
			//Log.debug( this, "rcptDo called" );
			switch( us )
			{
				case NO:
					if( canWill )
					{
						us = YES;
						out.write( TelnetIC.IAC );
						out.write( TelnetIC.WILL );
						out.write( teloptCode );
						out.flush();
					}
					else
					{
						out.write( TelnetIC.IAC );
						out.write( TelnetIC.WONT );
						out.write( teloptCode );
						out.flush();
					}
					break;
				case YES:
						//  ignore
					break;
				case WANTNO:
					Log.debug( this, "ERROR:  Won't answered by Do." );
					switch( usq )
					{
						case EMPTY:
							us = NO;
							break;
						case OPPOSITE:
							us = YES;
							usq = EMPTY;
							break;
					}
					break;
				case WANTYES:
					switch( usq )
					{
						case EMPTY:
							us = YES;
							break;
						case OPPOSITE:
							us = WANTNO;
							usq = EMPTY;
							out.write( TelnetIC.IAC );
							out.write( TelnetIC.WONT );
							out.write( teloptCode );
							out.flush();
							break;
					}
					break;
			}
		}

	/*
		public String toString()
		{
			return( "[TelnetOption " + teloptCode + "]: 
		}
	*/
		
		void negotiation( TelnetIC tc ) throws IOException
		{
		}
	}

	final static class NAWSTelnetOption extends TelnetOption
	{
		public NAWSTelnetOption( int code, boolean supportHim, boolean supportUs )
		{
			super( code, supportHim, supportUs );
		}
		
		/**
		  *  This is the routine to handle the subnegotiation
		  *  of the telnet window size option
		 */
		void negotiation( TelnetIC tc ) throws IOException
		{
			try
			{
				tc.width = tc.read16Int()-1;
				tc.height = tc.read16Int();
				
				int a = tc.width;
				int b = tc.height;
				
				
				if( tc.width < TelnetIC.SMALLEST_BELIEVABLE_WIDTH || tc.width > TelnetIC.LARGEST_BELIEVABLE_WIDTH )
					tc.width = TelnetIC.DEFAULT_WIDTH;
				
				if( tc.height < TelnetIC.SMALLEST_BELIEVABLE_HEIGHT || tc.height > TelnetIC.LARGEST_BELIEVABLE_HEIGHT )
					tc.height = TelnetIC.DEFAULT_HEIGHT;
				
				if( a != tc.width || b != tc.height )
					Log.debug( this, "Didn't accepted Negotiated window size of : " + a + "x" + b + ", instead using: " + tc.width + "x" + tc.height );
				//else
					//tc.send( "Screen size of " + tc.width + "x" + tc.height + " detected" );
				
				int i;
				i = tc.readInt();
				if( i == TelnetIC.IAC )
				{
					switch( tc.handleIAC() )
					{
						case TelnetIC.SE:
							break;
						default:
							Log.debug( this, "ERROR: Didn't receive expected SE" );
							break;
					}
				}
				else
					Log.debug( this, "ERROR: Didn't receive expected SE" );
			}
			catch( IACRecievedException e )
			{
				Log.debug( this, e.toString() );
			}
		}
	}

	final static class TerminalTypeTelnetOption extends TelnetOption
	{
		static final int IS=0;  //  used in Terminal-Type negotiations
		static final int SEND=1;

		String last="";
		TelnetIC telnet;
		
		public TerminalTypeTelnetOption( TelnetIC tc, int code, boolean supportHim, boolean supportUs )
		{
			super( code, supportHim, supportUs );
			
			telnet = tc;
		}
		
		/**
		  *  This is the routine to handle the subnegotiation
		  *  of the telnet terminal type option
		 */
		void negotiation( TelnetIC tc ) throws IOException
		{
			int i = tc.readInt();
			if( i == IS )
			{
				//  receiving a terminal type
				String s = tc.readIACSETerminatedString();
				if( !telnet.setDetectedTerminal( s ) )
				{
					//Log.debug( this, "failed terminal match: " + s );
					if( !last.equals( s ) )
					{
						last = s;
						requestAnotherTerminal( tc.output );
						//Log.debug( this, "getting another terminal..." );
					}
				}
				//else
					//Log.debug( this, "made terminal match: " + telnet.getTerminal().getName() );
			}
		}

		public void rcptWill( Writer out ) throws IOException
		{
			super.rcptWill( out );

			if( enabledHim() )
			{
				requestAnotherTerminal( out );
			}
		}

		void requestAnotherTerminal( Writer out ) throws IOException
		{
			out.write( TelnetIC.IAC );
			out.write( TelnetIC.SB );
			out.write( TELOPT_TTYPE );
			out.write( SEND );
			out.write( TelnetIC.IAC );
			out.write( TelnetIC.SE );
			out.flush();
		}
	}

	/**
	  *  For echos, rather than implement some sort of complicated
	  *  queueing system for options, I choose to simply always
	  *  send do and won't requests, and to ignore the replies.  This
	  *  is what EW2 does with all options.
	  *
	  *  I understand that this isn't a particularly wonderful implementation,
	  *  however, echo gets turned on and off for passwords, and an auto-connect
	  *  client (like tinyfugue) can send the password before it responds to the
	  *  echo requests that it recieves.  We would, conceivably, only need a 
	  *  queue of length 2 in order to implement this properly, but its a
	  *  step that I'm not willing to make at the moment, for fear of screwing
	  *  it up.  I daresay that I only managed to get the options working at
	  *  all with several days effort and the help of the Q-method RFC.  (I
	  *  forget the number for it itself, its one of the ones listed at the
	  *  top of the file.  - subtle
	 */
	final static class EchoTelnetOption extends TelnetOption
	{
		public EchoTelnetOption( int code, boolean supportHim, boolean supportUs )
		{
			super( code, supportHim, supportUs );
		}
		
		/**
		  *  Attempts to initiate a don't message for this option
		  *  to the output stream
		 */
		public final void sendDont( Writer out ) throws IOException
		{
			//Log.debug( this, "sendDon't called" );
			out.write( TelnetIC.IAC );
			out.write( TelnetIC.DONT );
			out.write( teloptCode );
			out.flush();
		}

		/**
		  *  Attempts to initiate a do message for this option
		  *  to the output stream
		 */
		public final void sendDo( Writer out ) throws IOException
		{
			out.write( TelnetIC.IAC );
			out.write( TelnetIC.DO );
			out.write( teloptCode );
			out.flush();
		}

		/**
		  *  Attempts to initiate a don't message for this option
		  *  to the output stream
		 */
		public final void sendWont( Writer out ) throws IOException
		{
			out.write( TelnetIC.IAC );
			out.write( TelnetIC.WONT );
			out.write( teloptCode );
			out.flush();
		}

		/**
		  *  Attempts to initiate a do message for this option
		  *  to the output stream
		 */
		public final void sendWill( Writer out ) throws IOException
		{
			out.write( TelnetIC.IAC );
			out.write( TelnetIC.WILL );
			out.write( teloptCode );
			out.flush();
		}
		
		public void rcptWont( Writer out )
		{
			// ignore
		}
		
		public void rcptWill( Writer out )
		{
			//  ignore
		}
		
		public void rcptDont( Writer out )
		{
			// ignore
		}
		
		public void rcptDo( Writer out )
		{
			//  ignore
		}
	}

	final static class IACRecievedException extends Exception
	{
		int following;
		public IACRecievedException( int codeAfter )
		{
			super( "IAC recieved unexpectedly" );
			following = codeAfter;
		}
	}
}