/*
 *	$Id$
 */

package  mlterm ;


import  java.util.Properties ;
import  java.io.* ;
import  org.eclipse.swt.* ;
import  org.eclipse.swt.dnd.* ;
import  org.eclipse.swt.custom.* ;
import  org.eclipse.swt.events.* ;
import  org.eclipse.swt.graphics.* ;
import  org.eclipse.swt.layout.* ;
import  org.eclipse.swt.widgets.* ;
import  org.eclipse.swt.browser.* ;


public class MLTerm extends StyledText
{
	/* --- private --- */

	final private static boolean  DEBUG = false ;

	private MLTermListener  listener = null ;
	private MLTermPty  pty = null ;
	private int  ptyCols = 80 ;
	private int  ptyRows = 24 ;
	private boolean  isSelecting = false ;
	private Clipboard  clipboard = null ;	/* StyledText.clipboard is not accessible. */
	private int  lineHeight = 0 ;
	private int  columnWidth = 0 ;
	private int  numOfScrolledOutLines = 0 ;
	private int  scrolledOutCache = 0 ;

	private static Color[]  colors = null ;
	private static RedrawRegion  region = null ;
	private static Font  font = null ;

	private static MLTermPty[]  pooledPtys = new MLTermPty[32] ;
	private static int  numOfPooledPtys = 0 ;

	private static boolean  readyReadPty = false ;

	private boolean  pushPty( MLTermPty  p)
	{
		if( numOfPooledPtys == pooledPtys.length)
		{
			return  false ;
		}

		p.setListener( null) ;
		pooledPtys[numOfPooledPtys] = p ;
		numOfPooledPtys ++ ;

		return  true ;
	}

	private MLTermPty  popPty()
	{
		if( numOfPooledPtys == 0)
		{
			return  null ;
		}

		numOfPooledPtys -- ;
		MLTermPty  p = pooledPtys[numOfPooledPtys] ;
		pooledPtys[numOfPooledPtys] = null ;

		return  p ;
	}

	private MLTermPty  getNextPty()
	{
		if( numOfPooledPtys == 0)
		{
			return  null ;
		}

		/* 012 3 => 123 0 => 230 1 */

		MLTermPty  p = pooledPtys[0] ;
		if( numOfPooledPtys > 1)
		{
			System.arraycopy( pooledPtys , 1 , pooledPtys , 0 , numOfPooledPtys - 1) ;
		}
		numOfPooledPtys -- ;

		return  p ;
	}


	private void moveCaret()
	{
		int  row = pty.getCaretRow() + numOfScrolledOutLines ;
		int  lineCount = getLineCount() ;
		int  offset ;
		if( row >= lineCount)
		{
			for( ; lineCount <= row ; lineCount++)
			{
				append( String.valueOf( '\n')) ;
			}

			offset = getCharCount() -  1 ;
		}
		else
		{
			offset = getOffsetAtLine( row) + pty.getCaretCol() ;
		}

		if( ! isSelecting)
		{
			setCaretOffset( offset) ;
		}
	}

	private void startBrowser( final MLTerm  mlterm , String  uri)
	{
		final Composite  composite = new Composite( mlterm.getParent() , SWT.NONE) ;
		composite.setLayout( new GridLayout( 2 , false)) ;
		composite.setLayoutData( mlterm.getLayoutData()) ;

		Button  button = new Button( composite , SWT.NONE) ;
		button.setText( "Exit") ;
		button.addSelectionListener(
			new  SelectionAdapter()
			{
				public void widgetSelected( SelectionEvent  e)
				{
					mlterm.setParent( composite.getParent()) ;
					composite.dispose() ;
					mlterm.getParent().layout() ;
				}
			}) ;

		final Text  urlinput = new Text( composite , SWT.BORDER) ;
		urlinput.setLayoutData( new GridData( GridData.FILL_HORIZONTAL)) ;

		final Browser  browser = new Browser( composite , SWT.NONE) ;
		browser.setLayoutData( new GridData( SWT.FILL , SWT.FILL , true , true , 2 , 1)) ;
		browser.addLocationListener(
			new LocationAdapter()
			{
				public void changed( LocationEvent  e)
				{
					urlinput.setText( e.location) ;
				}
			}) ;

		urlinput.addKeyListener(
			new KeyAdapter()
			{
				public void keyPressed( KeyEvent  e)
				{
					if( e.keyCode == 13)
					{
						browser.setUrl( urlinput.getText()) ;
					}
				}
			}) ;

		mlterm.setParent( browser) ;
		browser.setUrl( uri) ;

		composite.getParent().layout() ;
	}

	private void  closePty()
	{
		if( pty != null)
		{
			pty.close() ;
			pty = null ;

			if( listener != null)
			{
				listener.ptyClosed() ;
			}
		}
	}

	private int  getColumnWidth()
	{
		GC  gc = new GC( this) ;
		int  width = gc.stringExtent( "W").x ;
		gc.dispose() ;

		return  width ;
	}

	private void checkCellSize( boolean  invokeEvent)
	{
		int  height = getLineHeight() ;
		int  width = getColumnWidth() ;

		if( lineHeight == height && columnWidth == width)
		{
			return ;
		}
		else if( DEBUG)
		{
			System.err.printf( "Line height %d is changed to %d\n" , lineHeight , height) ;
			System.err.printf( "Column width %d is changed to %d\n" , columnWidth , width) ;
		}

		lineHeight = height ;
		columnWidth = width ;

		if( invokeEvent && listener != null)
		{
			listener.sizeChanged() ;
		}
	}

	private void  resetScrollBar()
	{
		if( numOfScrolledOutLines > 0)
		{
			setTopIndex( numOfScrolledOutLines) ;
		}
	}

	private void  checkTextLimit( int  num)
	{
		if( getTextLimit() != -1 && getTextLimit() < getCharCount() + num)
		{
			/* Clear bufOffset, bufStr and bufReplaceLen before removing old text. */
			replaceTextBuffering( 0 , 0 , null , null) ;

			/* 500 is for buffer. */
			getContent().replaceTextRange( 0 , getCharCount() + num + 500 - getTextLimit() , "") ;

			if( numOfScrolledOutLines > getLineCount() - ptyRows)
			{
				numOfScrolledOutLines = getLineCount() - ptyRows ;
			}
		}
	}

	private int  bufOffset = 0 ;
	private StringBuilder  bufStr = null ;
	private int  bufReplaceLen = 0 ;
	private Object[]  bufStylesArray = null ;
	private int  numOfBufStylesArray = 0 ;
	private int  numOfBufStyles = 0 ;
	private void  replaceTextBuffering( int  offset , int  replaceLen ,
					String  str , StyleRange[]  styles)
	{
		if( DEBUG)
		{
			/* Simple way (output each line) */

			if( str != null)
			{
				getContent().replaceTextRange( offset , replaceLen , str) ;
			}

			if( styles != null)
			{
				replaceStyleRanges( offset , str.length() , styles) ;
			}
		}
		else
		{
			if( styles != null && bufStr != null)
			{
				for( int  count = 0 ; count < styles.length ; count++)
				{
					styles[count].start += (bufStr.length() - bufReplaceLen) ;
				}
			}

			if( ( str == null || bufOffset + bufReplaceLen != offset) && bufStr != null)
			{
				getContent().replaceTextRange( bufOffset , bufReplaceLen , bufStr.toString()) ;

				if( DEBUG)
				{
					System.err.printf( "OUTPUT %d characters.%n" , bufStr.length()) ;
				}

				if( numOfBufStylesArray > 0)
				{
					StyleRange[]  bufStyles = new StyleRange[numOfBufStyles] ;
					int  destPos = 0 ;
					for( int  count = 0 ; count < numOfBufStylesArray ; count++)
					{
						System.arraycopy( (StyleRange[])bufStylesArray[count] , 0 ,
								bufStyles , destPos ,
								((StyleRange[])bufStylesArray[count]).length) ;
						destPos += ((StyleRange[])bufStylesArray[count]).length ;
					}

					if( destPos != numOfBufStyles)
					{
						System.err.printf( "Illegal styles, not applied.\n") ;
					}
					else
					{
						replaceStyleRanges( bufOffset , bufStr.length() , bufStyles) ;
					}

					numOfBufStylesArray = 0 ;
					numOfBufStyles = 0 ;
				}

				if( str == null)
				{
					bufStr = null ;
					bufOffset = 0 ;
					bufReplaceLen = 0 ;
				}
				else
				{
					bufOffset = offset + bufStr.length() - bufReplaceLen ;
					bufStr = new StringBuilder( str) ;
					bufReplaceLen = replaceLen ;
				}
			}
			else if( str != null)
			{
				if( bufStr == null)
				{
					bufStr = new StringBuilder( str) ;
					bufOffset = offset ;
				}
				else
				{
					bufStr.append( str) ;
				}

				bufReplaceLen += replaceLen ;
			}

			if( styles != null)
			{
				if( bufStylesArray == null || bufStylesArray.length < ptyRows)
				{
					bufStylesArray = new Object[ptyRows] ;
				}

				bufStylesArray[numOfBufStylesArray] = styles ;
				numOfBufStylesArray ++ ;
				numOfBufStyles += styles.length ;
			}
		}
	}

	private void  redrawPty()
	{
		if( scrolledOutCache > 0)
		{
			numOfScrolledOutLines += scrolledOutCache ;
			setRedraw( false) ;		/* Stop moving scrollbar */
		}

		for( int  row = 0 ; row < ptyRows ; row ++)
		{
			if( ! pty.getRedrawString( row , region))
			{
				continue ;
			}

			if( region.start > 0)
			{
				replaceTextBuffering( 0 , 0 , null , null) ;
			}

			if( row == ptyRows - 1)
			{
				/* Remove '\n' which region.str always contains. */
				region.str = region.str.substring( 0 , region.str.length() - 1) ;
			}

			/* If lineCount is 0, max num of '\n' appended right below is row. */
			checkTextLimit( region.str.length() + row) ;

			int  lineCount = getLineCount() - numOfScrolledOutLines ;
			if( lineCount <= row)
			{
				replaceTextBuffering( 0 , 0 , null , null) ;

				do
				{
					append( String.valueOf( '\n')) ;
					lineCount++ ;
				}
				while( lineCount <= row) ;
			}

			/*
			 *   $: new line
			 *
			 *   0123 -> insert "e$" at 7 -> 012345678
			 *   abc$                        abc    e$
			 *
			 *   1) offset: 7, offsetNextRow: 4
			 *   2) insert 4(offset - offsetNextRow + 1) spaces at offsetNextRow - 1.
			 *   3) offsetNextRow: 4 -> Modified to 8.
			 *   4) replace 1(offsetNextRow - offset) at offset.
			 *
			 *   012 -> insert "e$" at 7 -> 012345678
			 *   abc                        abc    e$
			 *
			 *   1) offset: 7, offsetNextRow: 3
			 *   2) insert 4(offset - offsetNextRow) spaces at offsetNextRow.
			 *   3) offsetNextRow: 3 -> Modified to 7.
			 *   4) replace 0(offsetNextRow - offset) at offset.
			 */

			int  offset = getOffsetAtLine( row + numOfScrolledOutLines) + region.start ;

			int  offsetNextRow ;
			boolean  hasNewLine = true ;
			if( row + 1 >= lineCount)
			{
				offsetNextRow = getCharCount() ;

				if( offsetNextRow == offset - region.start ||
				    ! getTextRange( offsetNextRow - 1 , 1).equals( "\n"))
				{
					hasNewLine = false ;
				}
			}
			else
			{
				offsetNextRow = getOffsetAtLine( row + numOfScrolledOutLines + 1) ;
			}

			if( offset >= offsetNextRow)
			{
				int  padding = offset - offsetNextRow ;
				if( hasNewLine)
				{
					padding ++ ;
				}

				if( padding > 0)
				{
					char[]  spaces = new char[padding] ;
					for( int  count = 0 ; count < padding ; count++)
					{
						spaces[count] = ' ' ;
					}

					getContent().replaceTextRange( offset - padding , 0 , new String(spaces)) ;

					offsetNextRow += padding ;
				}

				if( DEBUG)
				{
					System.err.printf( "%s row %d lineCount %d offset %d offsetNextRow %d%n" ,
						region.str , row , lineCount , offset , offsetNextRow) ;
				}
			}

			StyleRange[]  styles = null ;
			if( region.styles != null)
			{
				styles = new StyleRange[region.styles.length] ;

				for( int  count = 0 ; count < region.styles.length ; count++)
				{
					if( colors[ region.styles[count].fg_color] == null)
					{
						colors[ region.styles[count].fg_color] =
							new Color( getDisplay() ,
									(region.styles[count].fg_pixel >> 16) & 0xff ,
									(region.styles[count].fg_pixel >> 8) & 0xff ,
									region.styles[count].fg_pixel & 0xff) ;
					}

					if( colors[ region.styles[count].bg_color] == null)
					{
						colors[ region.styles[count].bg_color] =
							new Color( getDisplay() ,
									(region.styles[count].bg_pixel >> 16) & 0xff ,
									(region.styles[count].bg_pixel >> 8) & 0xff ,
									region.styles[count].bg_pixel & 0xff) ;
					}

					styles[count] = new StyleRange(
										region.styles[count].start + offset ,
										region.styles[count].length ,
										colors[ region.styles[count].fg_color] ,
										colors[ region.styles[count].bg_color]) ;
					styles[count].underline = region.styles[count].underline ;
					styles[count].strikeout = region.styles[count].strikeout ;
					styles[count].fontStyle = (region.styles[count].bold ?
												SWT.BOLD : SWT.NORMAL) |
					                          (region.styles[count].italic ?
											    SWT.ITALIC : SWT.NORMAL) ;
				}
			}

			replaceTextBuffering( offset , offsetNextRow - offset , region.str , styles) ;
		}

		replaceTextBuffering( 0 , 0 , null , null) ;

		checkCellSize(true) ;

		if( scrolledOutCache > 0)
		{
			scrolledOutCache = 0 ;
			resetScrollBar() ;
			setRedraw( true) ;
		}
	}

	private boolean  reportMouseTracking( int  x , int  y , int  button , int  stateMask ,
						boolean  isMotion , boolean  isReleased)
	{
		if( ( stateMask & (SWT.CONTROL|SWT.SHIFT)) != 0 ||
			/* This method can be called just after closing pty. */
		    pty == null || ! pty.isTrackingMouse( button , isMotion))
		{
			return  false ;
		}

		int  row = y / lineHeight + numOfScrolledOutLines ;
		int  charIndex ;

		try
		{
			Point  p = new Point( x , y) ;
			charIndex = getLine( row).codePointCount( 0 ,
								getOffsetAtLocation( p) - getOffsetAtLine( row)) ;
		}
		catch( Exception  e)
		{
			/* There is no character at the specified location of getOffsetAtLocation(). */
			if( row < getLineCount())
			{
				charIndex = getLine( row).length() ;
				if( charIndex > 0)
				{
					charIndex -- ;
				}
				charIndex += (x - getLocationAtOffset( getOffsetAtLine( row) + charIndex).x) /
								columnWidth ;
			}
			else
			{
				charIndex = x / columnWidth ;
			}
		}

		row -= numOfScrolledOutLines ;

		pty.reportMouseTracking( charIndex , row , button , stateMask , isMotion , isReleased) ;

		return  true ;
	}

	private void attachPty( boolean  createContent)
	{
		ptyCols = pty.getCols() ;
		ptyRows = pty.getRows() ;

		StyledTextContent  content = (StyledTextContent)pty.getAuxData() ;
		if( content == null)
		{
			if( createContent)
			{
				/* New content */
				StyledText  text = new StyledText( MLTerm.this , 0) ;
				setContent( text.getContent()) ;
				text.dispose() ;
			}

			numOfScrolledOutLines = 0 ;
		}
		else
		{
			setContent( content) ;

			int  rows = content.getLineCount() ;
			if( rows > ptyRows)
			{
				numOfScrolledOutLines = rows - ptyRows ;
			}
			else
			{
				numOfScrolledOutLines = 0 ;
			}
		}

		/*
		 * XXX
		 * If scrolledOutCache > 0 , redrawPty() should be called, but
		 * such a case rarely seems to happen.
		 */
		scrolledOutCache = 0 ;

		pty.setListener(
			new MLTermPtyListener()
			{
				public void  executeCommand( String  cmd)
				{
					String[]  argv = cmd.split( " ") ;
					if( argv.length == 2 && argv[0].equals( "browser"))
					{
						startBrowser( MLTerm.this , argv[1]) ;
					}
				}

				public void  linesScrolledOut( int  size)
				{
					scrolledOutCache += size ;

					if( scrolledOutCache == ptyRows * 2 / 5)
					{
						redrawPty() ;

						Display  display = getDisplay() ;
						while( display.readAndDispatch()) ;
					}
				}

				public void  resize( int  width , int  height , int  cols , int  rows)
				{
					if( width > 0 && height > 0)
					{
						resizePty( width , height) ;
					}
					else if( cols > 0 && rows > 0)
					{
						ptyCols = cols ;
						ptyRows = rows ;
					}
					else
					{
						return ;
					}

					listener.sizeChanged() ;
				}

				public void  bell()
				{
					String  mode = getProperty( "bel_mode") ;
					if( mode == null || mode.equals( "sound"))
					{
						getDisplay().beep() ;
					}
				}
			}) ;

		if( content != null || createContent)
		{
			/* Content was changed */
			listener.sizeChanged() ;
		}
	}

	private void init( final String  host , final String  pass , final int  cols ,
					final int  rows , final String  encoding , final String[]  argv)
	{
		pty = new MLTermPty() ;

		if( ! pty.open( host , pass , cols , rows , encoding , argv))
		{
			pty = null ;

			return ;
		}

		attachPty( false) ;

		if( font == null)
		{
			String  fontFamily = getProperty( "font") ;
			if( fontFamily == null)
			{
				if( System.getProperty( "os.name").indexOf( "Windows") >= 0)
				{
					fontFamily = "Terminal" ;
				}
				else
				{
					fontFamily = "monospace" ;
				}
			}

			int  fontSize = 10 ;
			try
			{
				fontSize = Integer.parseInt( getProperty( "fontsize")) ;
			}
			catch( NumberFormatException  e)
			{
			}

			font = new Font( getDisplay() , fontFamily , fontSize , SWT.NORMAL) ;
		}

		setFont( font) ;
		checkCellSize( false) ;

		if( colors == null)
		{
			colors = new Color[512] ;

			String  color = getProperty( "fg_color") ;
			if( color != null)
			{
				long  pixel = MLTermPty.getColorRGB( color) ;
				if( pixel != -1)
				{
					colors[0x1f0] = new Color( getDisplay() , (int)((pixel >> 16) & 0xff) ,
										(int)((pixel >> 8) & 0xff) , (int)(pixel & 0xff)) ;
					setForeground( colors[0x1f0]) ;
				}
			}

			if( colors[0x1f0] == null)
			{
				colors[0x1f0] = getForeground() ;
			}

			color = getProperty( "bg_color") ;
			if( color != null)
			{
				long  pixel = MLTermPty.getColorRGB( color) ;
				if( pixel != -1)
				{
					colors[0x1f1] = new Color( getDisplay() , (int)((pixel >> 16) & 0xff) ,
										(int)((pixel >> 8)) & 0xff , (int)(pixel & 0xff)) ;
					setBackground( colors[0x1f1]) ;
				}
			}

			if( colors[0x1f1] == null)
			{
				colors[0x1f1] = getBackground() ;
			}


			/* font and color objects will be disposed when display is disposed. */
			getDisplay().addListener( SWT.Dispose ,
				new Listener()
				{
					public void handleEvent( Event  event)
					{
						font.dispose() ;
						font = null ;

						for( int  count = 0 ; count < colors.length ; count++)
						{
							if( colors[count] != null)
							{
								colors[count].dispose() ;
								colors[count] = null ;
							}
						}
						colors = null ;
					}
				}) ;
		}

		setTextLimit( 100000) ;
		setMargins( 1 , 1 , 1 , 1) ;

		clipboard = new Clipboard( getDisplay()) ;

		addListener( SWT.Dispose ,
			new Listener()
			{
				public void handleEvent( Event  event)
				{
					if( pty != null)
					{
						if( pushPty( pty))
						{
							pty.setAuxData( getContent()) ;
						}
						else
						{
							closePty() ;
						}
					}
				}
			}) ;

		addMouseListener(
			new  MouseListener()
			{
				public void mouseDown( MouseEvent  event)
				{
					if( ! reportMouseTracking( event.x , event.y ,
								event.button , event.stateMask , false , false))
					{
						isSelecting = true ;
					}
				}

				public void mouseUp( MouseEvent  event)
				{
					if( ! reportMouseTracking( event.x , event.y ,
								event.button , event.stateMask , false , true))
					{
						isSelecting = false ;
						if( getSelectionCount() > 0)
						{
							copy() ;
						}
					}
				}

				public void mouseDoubleClick( MouseEvent  event)
				{
				}
			}) ;

		addMouseMoveListener(
			new  MouseMoveListener()
			{
				public void mouseMove( MouseEvent  event)
				{
					int  button ;

					if( ( event.stateMask & SWT.BUTTON1) != 0)
					{
						button = 1 ;
					}
					else if( ( event.stateMask & SWT.BUTTON2) != 0)
					{
						button = 2 ;
					}
					else if( ( event.stateMask & SWT.BUTTON3) != 0)
					{
						button = 3 ;
					}
					else
					{
						button = 0 ;
					}

					reportMouseTracking( event.x , event.y , button , event.stateMask ,
								true , false) ;
				}
			}) ;

		addMouseWheelListener(
			new  MouseWheelListener()
			{
				public void mouseScrolled( MouseEvent  event)
				{
					int  button ;

					if( event.count > 0)
					{
						button = 4 ;
					}
					else
					{
						button = 5 ;
					}

					reportMouseTracking( event.x , event.y , button , event.stateMask , 
								false , false) ;
				}
			}) ;

		addTraverseListener(
			new TraverseListener()
			{
				public void keyTraversed( TraverseEvent  event)
				{
					if( false)
					{
						System.out.printf( "MASK %x KCODE %x\n" ,
							event.stateMask , event.keyCode) ;
					}

					if( event.stateMask == SWT.SHIFT && event.keyCode == SWT.TAB)
					{
						pty.write( "\u001b[Z") ;
					}
				}
			}) ;

		addVerifyKeyListener(
			new VerifyKeyListener()
			{
				public void verifyKey( VerifyEvent	event)
				{
					String  str ;

					if( false)
					{
						System.out.printf( "MASK %x KCODE %x CHAR %x\n" ,
							event.stateMask , event.keyCode , (int)event.character) ;
					}

					if( (event.keyCode & 0x70000) != 0)
					{
						/* Control, Shift, Alt */

						return ;
					}
					else if( event.stateMask == SWT.CONTROL && event.keyCode == SWT.F2)
					{
						if( pushPty( pty))
						{
							pty.setAuxData( getContent()) ;

							pty = new MLTermPty() ;

							if( pty.open( host , pass , cols , rows , encoding , argv))
							{
								attachPty( true) ;
							}
							else
							{
								/* restore */
								pty = popPty() ;
							}
						}

						return ;
					}
					else if( event.stateMask == SWT.CONTROL && event.keyCode == SWT.F3)
					{
						MLTermPty  nextPty = getNextPty() ;
						if( nextPty != null)
						{
							pushPty( pty) ;
							pty.setAuxData( getContent()) ;

							pty = nextPty ;
							attachPty( false) ;
						}

						return ;
					}
					else if( false && event.stateMask == SWT.SHIFT && event.keyCode == SWT.TAB)
					{
						/* XXX Shift+tab event is received only at TraverseListener. */
						return ;
					}
					else if( event.stateMask == SWT.SHIFT && event.keyCode == SWT.INSERT)
					{
						str = (String) clipboard.getContents(
										TextTransfer.getInstance() , DND.CLIPBOARD) ;
					}
					else
					{
						int  otherkey ;
						int  modcode ;

						otherkey = (int)event.character ;
						modcode = 0 ;
						if( ( event.stateMask & SWT.SHIFT) == SWT.SHIFT)
						{
							modcode |= 1 ;
						}
						if( ( event.stateMask & SWT.ALT) == SWT.ALT)
						{
							modcode |= 2 ;
						}
						if( ( event.stateMask & SWT.CONTROL) == SWT.CONTROL)
						{
							modcode |= 4 ;
							if( otherkey < 0x20 && 0x20 <= event.keyCode && event.keyCode < 0x80)
							{
								otherkey = event.keyCode ;
								/* For win32 which doesn't tell upper case from lower case. */
								if( 0x61 <= otherkey && otherkey <= 0x7a &&
								    ( event.stateMask & SWT.SHIFT) == SWT.SHIFT)
								{
									otherkey -= 0x20 ;
								}
							}
							/* 2, ' ' and @ has been converted to "" above. */
							else if( '3' <= otherkey && otherkey <= '7')
							{
								event.character = (char)((int)otherkey - 0x18) ;
							}
							else if( otherkey == '8')
							{
								event.character = '\u007f' ;
							}
							else if( otherkey == '^')
							{
								event.character = '\u001d' ;
							}
							else if( /* otherkey == '_' || */ otherkey == '/')
							{
								event.character = '\u001f' ;
							}
							else if( otherkey == ' ' || otherkey == '2' || otherkey == '@')
							{
								event.character = '\u0000' ;
							}
						}
						if( modcode > 0)
						{
							modcode ++ ;
						}

						if( modcode > 0 && otherkey < 0x80 &&
							pty.writeModifiedKey( otherkey , modcode))
						{
							str = null ;
						}
						else if( pty.writeSpecialKey( event.keyCode , modcode))
						{
							str = null ;
						}
						else if( event.character == '\u0000')
						{
							str = "" ;
						}
						else
						{
							str = String.valueOf( event.character) ;
						}
					}

					if( str != null)
					{
						if( event.stateMask == SWT.ALT)
						{
							pty.write( "\u001b") ;
						}

						pty.write( str) ;
					}
					event.doit = false ;
				}
			}) ;

		setCaret( null) ;

		DropTarget  target = new DropTarget( this , DND.DROP_MOVE|DND.DROP_COPY|DND.DROP_LINK) ;
		target.setTransfer( new Transfer[] { FileTransfer.getInstance() }) ;
		target.addDropListener(
			new DropTargetAdapter()
			{
				public void  dragEnter( DropTargetEvent event)
				{
					if( event.detail == DND.DROP_NONE)
					{
						event.detail = DND.DROP_LINK ;
					}
				}

				public void  dragOperationChanged( DropTargetEvent event)
				{
					if( event.detail == DND.DROP_NONE)
					{
						event.detail = DND.DROP_LINK ;
					}
				}

				public void  drop( DropTargetEvent event)
				{
					if( event.data == null)
					{
						event.detail = DND.DROP_NONE ;

						return ;
					}

					String[]  files = (String[])event.data ;
					for( int  count = 0 ; count < files.length ; count++)
					{
						if( count > 0)
						{
							pty.write( "\n") ;
						}
						pty.write( files[count]) ;
					}
				}
			}) ;

		region = new RedrawRegion() ;
	}


	/* --- public --- */

	public MLTerm( Composite  parent , int  style , String  host , String  pass ,
				int  cols , int  rows , String  encoding , String[]  argv)
	{
		/* If SWT.READ_ONLY is specified, tab key doesn't work. */
		super( parent , style|SWT.NO_BACKGROUND) ;

		init( host , pass , cols , rows , encoding , argv) ;
	}

	public static void  startPtyWatcher( final Display  display)
	{
		(new Thread(
			new  Runnable()
			{
				public void run()
				{
					while( true)
					{
						display.wake() ;

						if( readyReadPty)
						{
							if( false)
							{
								Thread.yield() ;
							}
							else
							{
								try
								{
									Thread.sleep(1) ;
								}
								catch( InterruptedException  e)
								{
								}
							}

							if( readyReadPty)
							{
								continue ;
							}
						}

						if( ! MLTermPty.waitForReading() /* block until pty is ready to be read. */
							|| display.isDisposed())
						{
							break ;
						}

						readyReadPty = true ;
					}
				}
			})).start() ;
	}

	public boolean  isActive()
	{
		if( pty == null)
		{
			return  false ;
		}
		else if( ! pty.isActive())
		{
			MLTermPty  nextPty = popPty() ;
			if( nextPty != null)
			{
				pty.close() ;
				pty = nextPty ;
				attachPty( false) ;

				return  true ;
			}
			else
			{
				closePty() ;

				return  false ;
			}
		}
		else
		{
			return  true ;
		}
	}

	public void  setListener( MLTermListener  lsn)
	{
		listener = lsn ;
	}

	public void  resetSize()
	{
		int  width = columnWidth * ptyCols + getLeftMargin() +
						getRightMargin() + getBorderWidth() * 2 +
						getVerticalBar().getSize().x ;
		int  height = lineHeight * ptyRows + getTopMargin() +
						getBottomMargin() + getBorderWidth() * 2 ;

		setSize( width , height) ;
		resetScrollBar() ;
	}

	public void  resizePty( int  width , int  height)
	{
		int  cols =  (width - getLeftMargin() - getRightMargin() - getBorderWidth() * 2 -
						getVerticalBar().getSize().x) / columnWidth ;
		int  rows =  (height - getTopMargin() - getBottomMargin() - getBorderWidth() * 2)
						/ lineHeight ;

		if( cols == ptyCols && rows == ptyRows)
		{
			return ;
		}

		ptyCols = cols ;
		ptyRows = rows ;

		int  start = getOffsetAtLine( numOfScrolledOutLines) ;
		int  length = getCharCount() - start ;
		StringBuilder  str = new StringBuilder() ;
		for( int  count = 0 ; count < ptyRows - 1 ; count++)
		{
			str.append( "\n") ;
		}

		replaceTextRange( start , length , str.toString()) ;

		pty.resize( ptyCols , ptyRows) ;
	}

	public boolean  updatePty()
	{
		readyReadPty = false ;

		if( ! isActive())
		{
			return  false ;
		}
		else if( pty.read())
		{
			redrawPty() ;
			moveCaret() ;
		}

		return  true ;
	}

	private static  Properties  prop = null ;
	private static void  loadProperties()
	{
		prop = new Properties() ;

		try
		{
			prop.load( new FileInputStream( MLTermPty.getConfigDirectory() + "main")) ;
		}
		catch( IOException  e)
		{
			System.err.println( "mlterm/main is not found") ;
		}
	}

	public static String  getProperty( String  key)
	{
		if( prop == null)
		{
			loadProperties() ;
		}

		return  prop.getProperty( key) ;
	}

	public static void  setProperty( String  key , String  value)
	{
		if( prop == null)
		{
			loadProperties() ;
		}

		prop.setProperty( key , value) ;
	}


	/* --- static methods --- */

	private static MLTerm[]  mlterms = new MLTerm[32] ;
	private static int  numOfMLTerms = 0 ;
	private static Point  decoration = null ;

	private static void  resetWindowSize( MLTerm  mlterm)
	{

		mlterm.resetSize() ;
		Point  p = mlterm.getSize() ;

		Shell  shell = mlterm.getShell() ;

		if( decoration == null)
		{
			shell.setSize( p) ;
			Rectangle  r = shell.getClientArea() ;

			decoration = new Point( p.x - r.width , p.y - r.height) ;
		}

		shell.setSize( p.x + decoration.x , p.y + decoration.y) ;
	}

	private static void startMLTerm( final Shell  shell , final String  host ,
								final String  pass , final int  cols , final int  rows ,
								final String  encoding , final String[]  argv)
	{
		final MLTerm  mlterm = new MLTerm( shell , SWT.BORDER|SWT.V_SCROLL ,
									host , pass , cols , rows , encoding , argv) ;
		if( ! mlterm.isActive())
		{
			MessageBox  box = new MessageBox( shell , SWT.OK) ;
			box.setMessage( "Failed to open pty.") ;
			box.open() ;

			return ;
		}

		mlterm.setListener(
			new MLTermListener()
			{
				public void  sizeChanged()
				{
					resetWindowSize( mlterm) ;
				}

				public void  ptyClosed()
				{
					for( int  count = 0 ; count < numOfMLTerms ; count++)
					{
						if( mlterms[count] == mlterm)
						{
							mlterms[count] = mlterms[--numOfMLTerms] ;
							break ;
						}
					}
				}
			}) ;

		shell.addListener( SWT.Dispose ,
			new Listener()
			{
				public void handleEvent( Event  e)
				{
					for( int  count = 0 ; count < numOfMLTerms ; count++)
					{
						if( mlterms[count] == mlterm)
						{
							mlterms[count] = mlterms[--numOfMLTerms] ;
							break ;
						}
					}
				}
			}) ;

		mlterm.addVerifyKeyListener(
			new VerifyKeyListener()
			{
				public void verifyKey( VerifyEvent	event)
				{
					if( event.stateMask == SWT.CONTROL &&
					    event.keyCode == SWT.F1 &&
						numOfMLTerms < mlterms.length)
					{
						Shell s = new Shell( shell.getDisplay()) ;
						s.setText( "mlterm") ;
						s.setLayout( new FillLayout()) ;

						startMLTerm( s , host , pass , cols , rows , encoding , argv) ;
					}
				}
			}) ;

		resetWindowSize( mlterm) ;

		shell.addListener( SWT.Resize ,
			new Listener()
			{
				private boolean  processing = false ;
				private int  prevWidth = 0 ;
				private int  prevHeight = 0 ;

				public void handleEvent( Event  e)
				{
					if( ! processing)
					{
						Rectangle  r = shell.getClientArea() ;
						if( r.width != prevWidth || r.height != prevHeight)
						{
							prevWidth = r.width ;
							prevHeight = r.height ;

							mlterm.resizePty( r.width , r.height) ;

							processing = true ;
							resetWindowSize( mlterm) ;
							processing = false ;
						}
					}
				}
			}) ;

		shell.open() ;

		mlterms[numOfMLTerms++] = mlterm ;
	}

	public static void main (String [] args)
	{
		String  host = MLTerm.getProperty( "default_server") ;
		String  pass = null ;
		String  encoding = null ;
		String[]  argv = null ;
		boolean  openDialog = false ;
		int  cols = 80 ;
		int  rows = 24 ;

		if( System.getProperty( "os.name").indexOf( "Windows") >= 0 || host != null)
		{
			openDialog = true ;
		}

		String  geom = MLTerm.getProperty( "geometry") ;

		for( int  count = 0 ; count < args.length ; count ++)
		{
			if( args[count].equals( "-h"))
			{
				System.out.println( "usage: java -jar mlterm.jar [options] -e ...") ;
				System.out.println( "options: ") ;
				System.out.println( " -dialog       : show dialog to specify a server.") ;
				System.out.println( " -fn <font>    : specify font family.") ;
				System.out.println( " -g <col>x<row>: specify size.") ;
				System.out.println( " -km <encoding>: specify character encoding.") ;
				System.out.println( " -w <point>    : specify font size.") ;
				System.out.println( " -serv proto://user@host: specify a server.") ;

				return ;
			}
			else if( args[count].equals( "-dialog"))
			{
				openDialog = true ;
			}
			else if( count + 1 < args.length)
			{
				if( args[count].equals( "-serv"))
				{
					host = args[++count] ;
					openDialog = true ;
				}
				else if( args[count].equals( "-g"))
				{
					geom = args[++count] ;
				}
				else if( args[count].equals( "-km"))
				{
					encoding = args[++count] ;
				}
				else if( args[count].equals( "-w"))
				{
					MLTerm.setProperty( "fontsize" , args[++count]) ;
				}
				else if( args[count].equals( "-fn"))
				{
					MLTerm.setProperty( "font" , args[++count]) ;
				}
				else if( args[count].equals( "-fg"))
				{
					MLTerm.setProperty( "fg_color" , args[++count]) ;
				}
				else if( args[count].equals( "-bg"))
				{
					MLTerm.setProperty( "bg_color" , args[++count]) ;
				}
				else if( args[count].equals( "-e"))
				{
					argv = new String[args.length - (++count)] ;
					for( int  count2 = 0 ; count2 < argv.length ; count2++)
					{
						argv[count2] = args[count + count2] ;
					}
					break ;
				}
			}
		}

		if( geom != null)
		{
			String[]  array = geom.split( "x") ;
			if( array.length == 2)
			{
				try
				{
					int  c = Integer.parseInt( array[0]) ;
					int  r = Integer.parseInt( array[1]) ;

					cols = c ;
					rows = r ;
				}
				catch( NumberFormatException  e)
				{
					System.err.println( geom + " geometry is illegal.") ;
				}
			}
		}

		Display  display = new Display() ;

		Shell  shell = new Shell(display) ;
		shell.setText( "mlterm") ;
		shell.setLayout( new FillLayout()) ;

		if( openDialog)
		{
			ConnectDialog  dialog = new ConnectDialog( shell) ;
			String[]  array = dialog.open( host) ;
			if( array == null)
			{
				shell.dispose() ;

				return ;
			}

			host = array[0] ;
			pass = array[1] ;
			if( array[2] != null)
			{
				encoding = array[2] ;
			}
			if( array[3] != null)
			{
				argv = array[3].split( " ") ;
			}
		}

		startMLTerm( shell , host , pass , cols , rows , encoding , argv) ;

		MLTerm.startPtyWatcher( display) ;

		while( ! display.isDisposed() && numOfMLTerms > 0)
		{
			while( display.readAndDispatch()) ;

			/*
			 * count must be descending-order because numOfMLTerms can be
			 * decreased inside this loop.
			 */
			for( int  count = numOfMLTerms - 1 ; count >= 0 ; count--)
			{
				shell = mlterms[count].getShell() ;
				/* updatePty() can call ptyClosed() via isActive() -> closePty() */
				if( ! mlterms[count].updatePty())
				{
					shell.dispose() ;
				}
			}

			if( ! display.readAndDispatch())
			{
				display.sleep() ;
			}
		}

		display.dispose() ;
	}
}
