
import java.util.*;
import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;

/**
 * the waplet. a wap simulator, in an applet.
 * shooting for (initial) size of wxh 150x185,
 * where the screen is 150x150, 5 border and
 * 150x30 controls.
 *
 * waplet classes are not currently in a package, because 
 * of strange browser/jar behavior with packages.
 */
public class waplet extends Applet implements ActionListener {

    /** attention decompilers */
    static String copyright = 
	"this code and all related object, binary and bytecode " +
	"compilations are copyright (c) 2000 pagea company. ";
  
    /** the page parser class used in rendering pages */
    Parser _parser;  

    /** a local vector holds the page contents */
    Vector _contents;

    /** this vector is used in rendering lines of text to the screen */
    Vector _buffer;

    /** 
     * a hash of links is maintained to support navigation, 
     * keyed by position index in the document.
     */
    Hashtable _links;

    /** 
     * the cookie jar is used to pass cookies back and forth in the
     * waplet. this isn't required behavior (I think), but it keeps
     * the waplet from generating millions of sessions.
     */
    Hashtable _cookiejar = new Hashtable();

    /** 
     * this image is used for page rendering-- content is drawn for 
     * the whole card, then the display shows a window on this image.
     */
    Image _offscreen;

    /** panels used to lay out components */
    Panel _p, _p2;

    /** this is a local variable for the vertical image offset */
    int _vertical;

    /** this flag represents current justification when rendering a page. */
    int _justify;
    
    /** this flag represents underline state when rendering pages. */
    boolean _underline;

    /** this flag represents inverted-text state when rendering pages. */
    boolean _invert;

    /** */
    boolean _doprev;

    /** this field maintains the current text-cursor position */
    Point _cursor;

    /** field indicates the current font, used in page rendering */
    int _currentfont;

    /** the current link index. links are keyed by position in the document */
    int _currentlinkindex;

    /** the current highlighted/selected link. */
    int _highlightlink;

    /** the graphics object for the offscreen rendering image. */
    Graphics _offg;

    /** the name of the current card, within the current deck. */
    String _currentcard;

    /** the current page, as string. */
    String _page;

    /** the link for a back button */
    String _dostring;

    /** the background image (skin?) */
    Image _bgimage = null;

    /** screen-height for one row of text */
    static final int ROWHEIGHT = 16;

    /** screen width */
    static final int SCREENWIDTH = 124;

    /** screen height -- be realistic */
    static final int SCREENHEIGHT = 83; // was 115

    /** field represents left justification */
    static final int LEFT_JUSTIFY = 0;

    /** field represents center justification */
    static final int CENTER_JUSTIFY = 1;

    /** field represents right justification */
    static final int RIGHT_JUSTIFY = 2;

    /** field represents an undefined font state */
    static final int UNDEFINED = -1;

    /** field represents plain text font */
    static final int FONT_PLAIN = 0;

    /** field represents bold text font */
    static final int FONT_BOLD = 1;

    /** field represents small font */
    static final int FONT_SMALL = 2;

    /** field represents italic font */
    static final int FONT_ITALIC = 3;

    /** 
     * this array holds references to font class instances for the
     * several fonts used.
     */
    static final Font[] FONTS = {
	new Font( "Helvetica", Font.PLAIN, 12),
	new Font( "Helvetica", Font.BOLD, 12),
	new Font( "Helvetica", Font.PLAIN, 10),
	new Font( "Helvetica", Font.ITALIC, 12)
    };

    /** the green color used as a screen background. */
    static final Color SCREEN_GREEN = new Color( 0, 234, 0);
    //    static final Color TRANSPARENT = new Color( 0, 0, 0, 0);

    /** default content, used when a page fails compilation. */
    private static String defaultContent =
	"<wml>\n" +
	"  <card id=\"init\" newcontext=\"true\">\n" +
	"    <p align=\"center\">\n" +
	"    pagea company<br/>\n" +
	"    ((( o )))<br/>\n" +
	"    </p>\n" +
	"  </card>\n" +
	"</wml>\n"
	;

    /** the background image/skin. */
    private static String BACKGROUND_IMAGE = 
	"/images/phone.gif";

    /** load content from a specified page. */
    public void loadContent(String page){
	// flag for successful loading 
	boolean success = false;
	
	if( null != page && !(page.trim().equals(""))){
	    // cut out the &amp; s...
	    int idx = -1;
	    while((idx = page.indexOf( "&amp;")) >= 0){
		page = page.substring(0, idx + 1) + page.substring( idx + 5);
	    }
	    
	    // if it starts with a ``rel:'', insert the current host...
	    if(page.startsWith("rel:")){
		java.net.URL doc = getDocumentBase();
		
	    }
	    
	    else if(page.indexOf("://") < 0 ){
		java.net.URL doc = getDocumentBase();
		// debug
		// System.out.println("document base: " + doc.toString());
		page = doc.getProtocol() + "://" +doc.getHost() + "/" + page;
		// debug
		// System.out.println("new page: " + page);
		
	    }
	    try{
		// debug
		// System.out.println("reading url: " + page);

		_contents = _parser.tree( _parser.readURL(page, _cookiejar));
		// if it hasn't thrown an exception, set the success flag.

		success = true;
		// debug
		// System.out.println("read page " + page);
	    }
	    catch(Exception e){
	        //System.out.println("page read failed.");
		e.printStackTrace(System.out);
	    }
	}
	// on content load failure, show the default content.
	if(!success){
	    try{
		_contents = _parser.tree( _parser.readString( defaultContent));
		System.out.println("using default content");
	    }
	    catch(Exception e){
		// debug
		// System.out.println("this shouldn't happen...");
		e.printStackTrace(System.out);
	    }
	}
	// if there's a card anchor, set the current card.
	// otherwise, set to init.
	if(page.indexOf('#') < 0) _currentcard = "init";
	else{
	    _currentcard = page.substring( page.indexOf('#') + 1);
	    // System.out.println("using card: " + _currentcard);
	}
    }

    public void init(){
	
	setBackground( Color.white);
	
	// get the background image
	// this is disabled, temporarily, for compatibility.
	/*
	MediaTracker mt = new MediaTracker( this);
	_bgimage = getImage( getDocumentBase(), BACKGROUND_IMAGE);
	mt.addImage( _bgimage, 0);
	try{ mt.waitForID( 0); }
	catch( InterruptedException ie){ ie.printStackTrace(); }
	*/
	// read content
	
	_parser = new Parser();
	_page = this.getParameter("page");
	
	loadContent(_page);

	/*
	  // debug: dump the content to std. out
	  System.out.println("dumping contents:");
	  Object obj = null;
	  for(Enumeration e = _contents.elements(); e.hasMoreElements(); ){
	  obj = e.nextElement();
	  if(obj instanceof String) System.out.println((String)obj);
	  else System.out.println("<" + ((Tag)obj).getName() + ">");
	  }
	*/

	// max height is set at 1024 -- ??
	_offscreen = this.createImage(SCREENWIDTH, 1024);

	// set some defaults, and render the page.

	_cursor = new Point(1, 16);
	_justify = LEFT_JUSTIFY;
	_underline = false;
	_invert = false;
	_currentfont = FONT_PLAIN;
	_currentlinkindex = -1;
	_vertical = 0;
	_buffer = new Vector();
	_links = new Hashtable();
	_highlightlink = 0;
	_doprev = false;
	_dostring = "";

	drawScreen(_currentcard);

	// define the panels/layout

	_p = new Panel(){
		public Dimension getMinimumSize(){
		    return new Dimension(SCREENWIDTH, SCREENHEIGHT);
		}
		public Dimension getPreferredSize(){
		    return getMinimumSize();
		}
		public void update(Graphics g){
		    paint(g);
		}
		public void paint(Graphics g){		    
		    g.drawImage(_offscreen, 0, 0 + _vertical, this);
		} 
	    };

	_p2 = new Panel(){
		public Dimension getMinimumSize(){
		    return new Dimension(SCREENWIDTH, 25); 
		}
		public Dimension getPreferredSize(){
		    return getMinimumSize();
		}
		public void update(Graphics g){
		    paint(g);
		}
		public void paint(Graphics g){
		    g.setColor( SCREEN_GREEN);
		    g.fillRect( 0, 0, SCREENWIDTH, 30);
		
		    g.setColor( Color.black);
		    if( linkOnScreen( _highlightlink)){
			g.setFont( FONTS[FONT_BOLD]);
			g.drawString("Link", 4, ROWHEIGHT + 2);
		    }
		    if( _doprev){
			g.setFont( FONTS[FONT_BOLD]);
			g.drawString("Back", SCREENWIDTH - 32, ROWHEIGHT + 2);
		    }
		}
	    };

	// define buttons

	Button up = new Button("/\\");
	up.addActionListener(this);
	up.setActionCommand("up");
	
	Button down = new Button("\\/");
	down.addActionListener(this);
	down.setActionCommand("down");

	Button left = new Button("--");
	left.addActionListener(this);
	left.setActionCommand("left");
	
	Button right = new Button("--");
	right.addActionListener(this);
	right.setActionCommand("right");

	// we're now using no absolute positioning,
	// no layout manager. we need to force some behavior.
	// this does lead to some complex layout definitions,
	// though.

	this.setLayout( null);
	Insets insets = this.getInsets();
	
	int centerline = 87;	
	int offset_y = 66 + insets.top;

	this.add(_p);
	_p.setBounds( centerline - ( _p.getPreferredSize().width/2), 
		      offset_y, 
		      _p.getPreferredSize().width, 
		      _p.getPreferredSize().height);
	offset_y += _p.getPreferredSize().height + 2;

	this.add(_p2);	
	_p2.setBounds( centerline - ( _p2.getPreferredSize().width/2), 
		       offset_y,
		       _p2.getPreferredSize().width, 
		       _p2.getPreferredSize().height);
	offset_y += _p2.getPreferredSize().height;
	
	int buttonwidth = 28;
	
	this.add( up);
	up.setBounds( centerline - ( buttonwidth/2), offset_y + 7,
		      buttonwidth, 20);
	
	this.add( down);
	down.setBounds( centerline - ( buttonwidth/2), offset_y + 30, 
			buttonwidth, 20);
	
	int sideoffset = 23;
	
	this.add( left);
	left.setBounds( centerline - sideoffset - buttonwidth, offset_y + 10,
			buttonwidth, 17);
	this.add( right);
	right.setBounds( centerline + sideoffset, offset_y + 10,
			buttonwidth, 17);

	String seq[] = { "1", "2", "3", "4", "5", "6", 
			 "7", "8", "9", "*", "0", "#" };
	// add some dead buttons, the phone numbers
	for( int i = 0; i< 4; i++){
	    for( int j = 0; j< 3; j++){
		Button b = new Button( seq[ i*3+j] );
		this.add( b);		
		int x = centerline - 45 + ( j * 31);
		int y = offset_y + 60 + ( i * 30);
		b.setBounds( x, y, 24, 20);
		// this is live now, just as a convenience: it 
		// routes back to the first page passed in.
		if( seq[ i*3+j].equals("#")){
		    b.addActionListener( this);
		    b.setActionCommand( "home");
		}
	    }
	}
    }
    
    /** check if a specified link is displayed on the screen. */
    public boolean linkOnScreen(int link){
	int top, bottom;
	Object obj = _links.get( new Integer(link));
	if( null == obj) return false;
	
	Tag tag = (Tag)obj;
	top = Integer.parseInt( tag.getParm("__top"));
	bottom = Integer.parseInt( tag.getParm("__bottom"));
	
	return (!( (bottom + _vertical) < 0 || 
		   (top + _vertical) > SCREENHEIGHT));	
    }

    /** reload content. */
    public void reload(){
	//	System.out.println("reload() called.");
	_highlightlink = 0;
	loadContent(_page);
	drawScreen(_currentcard);
	_vertical = 0;
	repaint();
    }

    /** overrides default applet paint method */
    public void paint( Graphics g){

	if( null != _bgimage)
	    g.drawImage( _bgimage, 0, 0, this);

    }

    /** handle button events, button-specific behavior */
    public void actionPerformed(ActionEvent e){
	
	// check the current link index. if there's
	// another link on the screen, move to that one.	
	if(e.getActionCommand().equals("left")){
	    if(linkOnScreen(_highlightlink)){
		Tag tag = (Tag)(_links.get( new Integer( _highlightlink)));
		String link = tag.getParm("href");
		// debug 
		// showStatus("follow link to: " + link);
		if( link.startsWith("#")){
		    _highlightlink = 0;
		    drawScreen(_currentcard = link.substring(1));
		    _vertical = 0;
		}
		else{
		    _highlightlink = 0;
		    loadContent(link);
		    drawScreen(_currentcard);
		    _vertical = 0;
		}
	    }
	}
	else if(e.getActionCommand().equals("right")){
	    if(_doprev && !_dostring.equals("")){
		if( _dostring.startsWith("#")){
		    _highlightlink = 0;
		    drawScreen(_currentcard = _dostring.substring(1));
		    _vertical = 0;
		}
		else{
		    _highlightlink = 0;
		    loadContent(_dostring);
		    drawScreen(_currentcard);
		    _vertical = 0;
		}
	    }
	}
	else if(e.getActionCommand().equals("up")){
	    if(0 < _highlightlink && linkOnScreen( _highlightlink - 1)){
		_highlightlink--;
		drawScreen(_currentcard);
	    }
	    else{
		_vertical += 10;
		if( _vertical > 0) _vertical = 0;
		if(0 < _highlightlink && linkOnScreen( _highlightlink - 1)){
		    _highlightlink--;
		    drawScreen(_currentcard);
		}
	    }
	}
	else if(e.getActionCommand().equals("down")){
	    if(linkOnScreen( _highlightlink + 1)){
		_highlightlink++;
		drawScreen(_currentcard);
	    }
	    else{
		_vertical -= 10;
		int tmp = Math.max(0, _cursor.y - SCREENHEIGHT);
		if(_vertical < ( 0 - tmp))
		    _vertical = ( 0 - tmp);
		if(linkOnScreen( _highlightlink + 1)){
		    _highlightlink++;
		    drawScreen(_currentcard);
		}
	    }
	}
	else if(e.getActionCommand().equals("home")){
	    _highlightlink = 0;
	    loadContent( getParameter( "page"));
	    drawScreen(_currentcard);
	    _vertical = 0;
	}
	    
	this.repaint();
    }

    /**
     * overrides the default applet update method. repaints
     * the two display panels.
     */
    public void update(Graphics g){ _p.repaint(); _p2.repaint(); }

    /**
     * this renders the screen. ideally, this will only be
     * rendered once, and then painted on the panel at
     * varying offssets.
     *
     * note that that's not true anymore: changing links now 
     * forces rendering, because of issues with the xor paint
     * mode. this certainly should be changed.
     */
    public void drawScreen(String cardid) {

	// System.out.println("drawScreen()");

	// set the offscreen image graphics object, if null
	if(null == _offg) _offg = _offscreen.getGraphics();

	// reset the _cursor
	_cursor.x = 1;
	_cursor.y = 16;

	// draw the background -- numbers should be static
	_offg.setColor( SCREEN_GREEN); 
	_offg.fillRect( 0, 0, 
	    _offscreen.getWidth(null), _offscreen.getHeight(null));	

	// render the card
	render(cardid);

    }

    /**
     * this method has been moved out of the screen
     * paint routine, so it can be adapted for better xml
     * parsing routines (or any other drawing routines).
     * also, I want to switch to a dynamic class loader, 
     * because I'm a little worried about loading times.
     * there's some card control here, it's a little bloated...
     */ 
    public void render(String cardid) {

	// System.out.println("render() called");

	Object obj = null;
	Tag tag = null;
	int flag = 0;

	_links = new Hashtable();

	for(Enumeration e = _contents.elements(); e.hasMoreElements(); ){
	    obj = e.nextElement();
	    if(obj instanceof Tag){
		tag = (Tag)obj;
		if(1 == flag){
		    // reading, so process the tag. if it's a close
		    // card tag, ++ the flag.
		    if(tag.getName().equals("card") && 
		       tag.getState() == tag.TAG_CLOSE){
			flag = 2;
		    }		
    		    else processTag(tag);
		}
		else if(0 == flag){
		    // not reading yet: check if this is an open
		    // card tag, and then if it's the right card.
		    if(tag.getName().equals("card") && 
		       tag.getState() == tag.TAG_OPEN && 
		       tag.getParm("id").equals(cardid)){
			flag = 1;
		    }
		}
	    }
	    // otherwise it's a string, so if we're reading,
	    // drop the leading ' (&apos;) and write it.
	    else if( 1 == flag){
		String tmp = ((String)obj).trim();
		//System.out.println("calling write(): " + tmp);
		//write( tmp.substring(1));
		bufferText( tmp.substring(1));
	    }
	}

	_doprev = false;
	_dostring = "";
	flag = 0;
	// check again, for do...
	Enumeration e = _contents.elements();
	while( e.hasMoreElements() && flag < 5){
	    obj = e.nextElement();
	    if( obj instanceof Tag){
		tag = (Tag)obj;
		if(flag > 0 && tag.getName().equals("card")) 
		    flag = 5;
		if(flag == 2){
		    if(tag.getName().equals("go")){
			_doprev = true;
			_dostring = tag.getParm("href");
		    }
		    flag = 5;
		}
		if(flag == 1 && tag.getName().equals("do") &&
		   tag.getState() == tag.TAG_OPEN &&
		   tag.getParm("type").equals("prev")) flag = 2;
		if(flag == 0 && tag.getName().equals("card") && 
		   tag.getState() == tag.TAG_OPEN &&
		   tag.getParm("id").equals(cardid)) flag = 1;
	    }
	}
    }

    /** handle one tag. should put together a case method for this. */
    public void processTag(Tag tag){
	String name = tag.getName();
	String parm = null;
	int state = tag.getState();
	if(name.equals("p") && state == tag.TAG_OPEN){
	    _justify = LEFT_JUSTIFY;
	    parm = tag.getParm("align");
	    if(null != parm){
		if( parm.equals("center"))
		    _justify = CENTER_JUSTIFY;
		else if( parm.equals("right"))
		    _justify = RIGHT_JUSTIFY;
	    }
	}
	else if(name.equals("a")){
	    // anchor... for open tag,
	    // store the tag, by index number;
	    // set the current link index number...
	    if(state == tag.TAG_OPEN){
		_currentlinkindex = _links.size();
		_links.put( new Integer( _currentlinkindex), tag);
	    }
	    else{
		// System.out.println("added link no. " + _currentlinkindex);
		_currentlinkindex = -1;
	    }
	}
	else if(name.equals("p") && state == tag.TAG_CLOSE){
	    flushBuffer();
	}
	else if(name.equals("b")){
	    _currentfont = (state == tag.TAG_OPEN) ? 
		FONT_BOLD : FONT_PLAIN;
	}
	else if(name.equals("small")){
	    _currentfont = (state == tag.TAG_OPEN) ? 
		FONT_SMALL : FONT_PLAIN;
	}
	else if(name.equals("i")){
	    _currentfont = (state == tag.TAG_OPEN) ? 
		FONT_ITALIC : FONT_PLAIN;
	}
	else if(name.equals("u")){
	    _underline = (state == tag.TAG_OPEN) ? 
		true : false;
	}
	else if(name.equals("br")){
	    flushBuffer();
	}
    }

    /**
     * add text to the glyph vector. the text buffer helps
     * in rendering text to the screen, because rendering 
     * is all based on content spacing, character widths, etc.
     */
    public void bufferText( String s){

	int style = _currentfont;
	_offg.setFont( FONTS[_currentfont]);
	FontMetrics fm = _offg.getFontMetrics();

	Glyph g = null;

	// note: if the buffer ends with a hard character,
	// and the next character is a hard character, insert
	// a space... 
	// BUT: there's a problem -- if underline is currently
	// on, it'll append a space underline to the front...
	// only do it, then, if the prior glyph is underlined.

	int len = s.length();
	if( _buffer.size() > 0 && len > 0 && s.charAt(0) != ' '){
	    g = new Glyph();
	    g.character = ' ';
	    g.style = style;
	    g.width = fm.charWidth( g.character);
	    g.underline = _underline &&
		((Glyph)(_buffer.lastElement())).underline;
	    _buffer.addElement(g);
	} 
	for(int i = 0; i< len; i++){
	    g = new Glyph();
	    g.character = s.charAt(i);
	    g.style = style;
	    g.width = fm.charWidth( g.character);
	    g.underline = ( _underline || _currentlinkindex > -1);
	    g.linkindex = _currentlinkindex;
	    _buffer.addElement(g);
	}
    }

    /**
     * flush the buffer: cut lines into screen width 
     * chunks, and write lines to the screen. then,
     * empty the buffer.
     */
    public void flushBuffer(){
	int width = 0;
	int len = _buffer.size();
	int idx = 0;
	int idx_start = 0;
	int space = -1;

	// create a glyph array from the vector.
	Glyph buffer[] = new Glyph[len];
	for(Enumeration e = _buffer.elements(); e.hasMoreElements(); ){
	    buffer[idx++] = (Glyph)(e.nextElement());
	}
	
	idx = 0;
	while( idx < len){
	    idx_start = idx; width = 0; space = -1;
	    while( width< SCREENWIDTH -2 && idx < len){
		if( buffer[idx].character == ' ') space = idx;
		width += buffer[idx++].width;
	    }
	    // out of the loop -- so render the line.
	    // if it's less than screen width, draw it;
	    // otherwise, look for the last space; 
	    // or finally, just draw it. (back up one?)
	    if( width < SCREENWIDTH -2) write( buffer, idx_start, idx);
	    else if( space > -1){
		write( buffer, idx_start, space);
		idx = space + 1;
	    }
	    else{
		write( buffer, idx_start, --idx);		
	    }
	}

	// now wipe the buffer.
	_buffer = new Vector();
    }

    /** 
     * this method writes a specified substring from the 
     * buffer to the screen.
     */
    public void write(Glyph[] buffer, int start, int end){
	Object obj = null;
	int width = 0;
	for(int i = start; i< end; i++) width += buffer[i].width;

	int current_style = FONT_PLAIN;
	_offg.setFont( FONTS[current_style]);
	justify( width);
	for(int i = start; i< end; i++){
	    if( buffer[i].style != current_style){
		current_style = buffer[i].style;
		_offg.setFont( FONTS[current_style]);
	    }
	    _offg.setColor( Color.black);
	    // if it's a link, I need to know where it is 
	    // on the map.
	    if(buffer[i].linkindex > -1){
		Tag tag = 
		    (Tag)(_links.get( new Integer(buffer[i].linkindex)));
		//System.out.println("tag is null? " + (null == tag));
		if(null == tag.getParm("__top")){
		    tag.setParm("__top", 
                       String.valueOf( _cursor.y - ROWHEIGHT + 4)); 
		    tag.setParm("__bottom", 
                       String.valueOf( _cursor.y + 4)); 
		}
		else{
		    tag.setParm("__bottom", 
                       String.valueOf( _cursor.y + 4)); 
		}		
	    }

	    // if it's a link, and it's hot...	    
	    if(buffer[i].linkindex == _highlightlink){
		_offg.fillRect( _cursor.x, _cursor.y - ROWHEIGHT + 4,
		    buffer[i].width + 1, ROWHEIGHT);
		_offg.setColor( SCREEN_GREEN);		
	    }
	    else{
		if(buffer[i].underline) 
		    _offg.drawLine( _cursor.x, _cursor.y + 1, 
			_cursor.x + buffer[i].width, _cursor.y + 1);
	    }
	    _offg.drawString( String.valueOf( buffer[i].character),
		_cursor.x, _cursor.y);
	    _cursor.x += buffer[i].width; 
	}
	_cursor.y += ROWHEIGHT;
    }

    /** 
     * justify the string, by moving the cursor.x position.
     */
    public void justify( int width){
	if( _justify == CENTER_JUSTIFY) 
	    _cursor.x = 1 + ((SCREENWIDTH - 2 - width)/2);
	else if( _justify == RIGHT_JUSTIFY)
	    _cursor.x = 1 + SCREENWIDTH - 2 - width; 	
	else _cursor.x = 1;
    }

    /** 
     * glyph is an internal class, because linux netscape (java 1.1.5?)
     * was throwing an access exception that I couldn't quite track
     * down. moving it in here cleared up the exception, but it's 
     * still irritating that I couldn't find it. 
     */
    class Glyph{

	int width;
	int style;
	char character;
	boolean underline;
	int linkindex;
	
	/** constructor sets default values */
	public Glyph(){
	    width = 0;
	    style = -1;
	    character = ' ';
	    underline = false;
	    linkindex = -1;
	}
    }

}
