/**
 *
 * PicoBrowser for NTT Docomo iAppli platform
 *
 * (C) 2001 Beartronics 
 * Author: Henry Minsky (hqm@alum.mit.edu)
 *
 * Licensed under terms "Artistic License"
 * http://www.opensource.org/licenses/artistic-license.html
 *
 */

import java.util.*;

import com.nttdocomo.ui.*;
import com.nttdocomo.io.*;
import com.nttdocomo.net.*;

import javax.microedition.io.*;
import java.io.*;


/**
 *
 * The PicoBrower is a tiny HTML browser for the NTT DoCoMo
 * Java iAppli phones.
 * <p>
 *
 * It is designed to support display and browsing of documents
 * containing a minimal subset of HTML. 
 *
 * <p>
 *
 * The purpose of this module is not so much general web browsing,
 * which can be done using the microbrowser in the phones, but rather
 * the ability to display documents within a Java IApplication. 
 *
 * <p>
 *
 * The problem is that the NTT platform framework has no integration 
 * between the phone's microbrowser and it's Java VM, so there appears to
 * be no easy way to give your Java applet the ability to display HTML
 * output. 
 * <p>
 *
 * 
 * HTML documents are a wonderful way to display information to users,
 * and to provide interaction, via clicking on hyperlinks or
 * submitting forms. Ideally the Java application could call out to
 * display HTML on the phone's microbrowser, and the phone could submit
 * data back to the application. In essence, the application would be 
 * a servlet engine. The NTT platform API designers have not yet taken 
 * this useful step, however.  So the current design dicates that if we 
 * want HTML browsing capability from out application, that we write
 * a browser in Java.
 *
 * <p>
 *
 * 
 * Unfortunately, the current size limit of 10kbytes for an IApplication force
 * the PicoBrowser to have a very bare-bones set of capabilities. 
 *
 * <p>
 *
 * The PicoBrower supports the following HTML tags. Note that there are
 * some restrictions on the syntax, as document below. Note also that
 * we are aiming at compatibility with XHTML BASIC (the subset of XHTML
 * proposed for mobile devices) at some point in the future. But you 
 * don't need to close your P tags.
 * <p>
 *
 *
 *<table border=1>
 *<tr><th>Element Tag</th><th>Action</th></tr>
 *<tr><td>BR</td><td>Linebreak</td></tr>
 *<tr><td>P</td><td>Paragraph break</td></tr>
 *
 *<tr><td>  B, STRONG</td><td>Bold font
 *</td></tr>
 *
 *<tr><td>A HREF="URL"</td><td>    A hyperlink. The URL value must
 *be enclosed within double quotes. The URL may be an absolute URL, 
 *such as "http://www.foo.com/bar.html", or it may be a relative URL,
 *such as "foo.html". Relative URLs will be resolved with respect to
 *the current document's base URL. 
 *<p>
 *The HREF value may also be a local application resource, such as "resource:///foo.gif".
 *<p>
 *Because the code for deducing the base URL is quite stupid, if you 
 *want to use relative URLs within your HTML document, you must specify the
 *target URL of the page as a full path and filename, such as "http://foo.com/index.html".
 *<p>
 *This is because the algorithm used to compute the base URL is simply to find the
 *last '/' in the target URL and to strip off everything after it. 
 *</td></tr>
 *
 *<tr><td>PRE</td><td>    Preformat text region. Whitespace is preserved, and fixed
 *width font is used.
 *</td></tr>
 *
 *<tr><td>FONT COLOR="#rrggbb"</td><td>    You may specify a font color, using
 *the hex RGB values. Double quotes are required around the value.
 *</td></tr>
 *
 *<tr><td>IMG SRC="URL"</td><td>    An inline image. The URL value must
 *be contained in double quotes. URL may be a absolute or relative URL. You may
 *also refer to a local resource of the form "resource:///foo.gif".
 *</td></tr>
 *
 * <tr><td>CENTER</td><td>Display text centered on the screen</td></tr>
 *<tr><td>I, EM</td><td> Italic font. Bold-Italic may be
 *specified by nesting the I and B tags, like &lt;B&gt;&lt;I&gt;This is bold italic&lt;/I&gt;&lt;/B&gt;.
 *</td></tr>
 *
 *<tr><td>Entities</td><td>    
 *Supported entities: &amp;amp;, &amp;lt; &amp;gt; &amp;quot; &amp;#ddd; &amp;NBSP;
 *</td></tr>
 *</table>
 * 
 * <p>
 *
 * The PicoBrowser does not support forms embedded in HTML documents.
 * <p>
 *
 *
 *
 * <b>Key Bindings</b>
 *
 * <p>
 *
 * The PicoBrowser application has the following key bindings:
 * <p>
 *
 *
 * <table border=1>
 * <tr><td>UP, DOWN</td><td>Prev, Next page/hyperlink</td></tr>
 * <tr><td>LEFT</td><td>Back (previous URL)</td></tr>
 * <tr><td>SELECT</td><td>Fetch selected hyperlink</td></tr>
 * <tr><td>RIGHT</td><td>unused</td></tr>
 * <tr><td>SOFT1</td><td>Scroll to prev page</td></tr>
 * <tr><td>SOFT2</td><td>Scroll to next page</td></tr>
 * <tr><td>1</td><td>Smooth Scroll down</td></tr>
 * <tr><td>2</td><td>Home (fetches resource:///index.html)</td></tr>
 * <tr><td>3</td><td>Smooth Scroll up</td></tr>
 * <tr><td>*</td><td>Enter New URL from keyboard</td></tr>
 * <tr><td>#</td><td>Exit Application</td></tr>
 * </table>
 *
 * The PicoBrowser could be pared down to even smaller footprint by
 * removing pieces of code that you don't need. For example, if you
 * only need to render HTML, and don't need a full browser, you could
 * get rid of the Panel UI for allowing users to enter new URLs in a
 * TextBox.  

 * @author Henry Minsky (hqm@ai.mit.edu)
 * @version $Id: HtmlCanvas.java.html,v 1.1.1.1 2004/06/25 15:42:45 hqm Exp $
 */


public class HtmlCanvas extends Canvas implements SoftKeyListener, KeyListener, ComponentListener
{
    /**
     * The list of all HTML components in the current document
     */
    Vector items = new Vector();

    static final char BULLET_CHAR = '\u00B0';

    static int        nPosX;
    static int        nPosY;
    
    static int        nScreenWidth;
    static int        nScreenHeight;
    
    static Font       font;

    /** The currently selected hyperlink item. An index into items*/
    static int selectedHyperlink = -1;

    /** The maximum Y height of the virtual page. */
    static int        maxPageHeight = 0;

    static String     proxyURL = "proxy.doit";

    /** The screen left margin */
    static int leftMargin = 0;
    /** The screen top margin */
    static int topMargin = 0;
    /** Interline padding */
    static int lineSpacing = 0;

    static String linkTarget = null;
    static String imageName = null;

    // Is there enough RAM on these devices to realistically
    // keep this info around?
    static String lastURL = null;
    static String lastPage = null;
    static String currentPage = null;

    static String currentURL = "http://nohost.com";

    /**
     *  Tell browser to load data from URL.
     * Valid protocols are "http://", "resource:///" 
     * and "local://" for servlets.
     */

    public void setURL (String url) {
	currentURL = url;
	setText(getURLData(url));
    }

    static int type  = Font.TYPE_DEFAULT;
    static int face  = Font.FACE_SYSTEM;
    static int style = Font.STYLE_PLAIN;
    static int size  = Font.SIZE_MEDIUM;

    static int xpos;
    static int ypos;

    static int maxHeight = 0;

    // Make this a separate param, to allow for border widths
    static int lineWidth;
    static int currentLine = 0;


    /**
     *
     */
    public HtmlCanvas()
    {
        setSoftLabel(Frame.SOFT_KEY_1, "PgUp");
        setSoftLabel(Frame.SOFT_KEY_2, "PgDn");
        
        nScreenWidth = getWidth();
        nScreenHeight = getHeight();
	lineWidth = nScreenWidth;
        

	font = Font.getFont(type|face|style|size);
        
	nPosX = 0;
	nPosY = 0;

	int xpos = leftMargin;
	int ypos = topMargin + font.getHeight();

    }

    /**
     * Set the HTML text of the current page. This will parse the HTML
     * into HtmlItem objects, and assign them layout positions on a 
     * virtual page.
     */
    public void setText (String text) {
	selectedHyperlink = -1;
	nPosY = 0;
	ypos = 0;
	xpos = 0;
	maxHeight = 0;
	if (text == null) {
	    text = "Error::no text";
	}
	currentPage = text;
	items.removeAllElements();
	// Parse the text into contigous runs.

	// The current tag we are parsing, if any
	String tag = null;

	/** State variables */

	boolean isBold = false;
	boolean isItalic = false;

	boolean isLink = false;
	boolean isImage = false;

	int color = Graphics.getColorOfName(Graphics.BLACK);
	int prevColor = color;

	// The x origin of the current run of text
	int height=0;
	int maxDescent = 0;

	boolean preformat = false; // preserve whitespace and newlines
	boolean center = false; // display text centered on the screen?
	boolean stopCenter = false;
	boolean seenWhitespace = false;
	boolean forceNewline = false;
	boolean forceNewItem = false;
	boolean charAdded = false;
	boolean htmlParagraph = false; // was the last newline a paragraph break?

	StringBuffer buf = new StringBuffer();

	// Start the initial item.
	HtmlItem currentItem = 
	    new HtmlItem(xpos, ypos,
			 font.stringWidth(buf.toString()), font.getHeight(),
			 font, currentLine);
	items.addElement(currentItem);

	// current index into string
	int idx = 0;
	// end of string
	int maxidx = text.length();
	boolean isClosingTag;

	while (idx < maxidx) {
	    char ch = text.charAt(idx++);

	    // look for a tag in <>'s
	    isImage = false;
	    if (ch == '<') {
		forceNewItem = true;
		// look for end of tag
		int tagend = text.indexOf('>', idx);
		if (tagend > 0) {
		    isClosingTag = (text.charAt(idx) == '/');
		    if (isClosingTag) { idx++; }

		    // Look for whitespace inside the tag body, to 
		    // separate the tag name from attribute values.
		    int whitespace = text.indexOf(' ', idx);
		    int eltEnd;
		    if (whitespace >= 0) {
			eltEnd = (whitespace < tagend ? whitespace : tagend);
		    } else {
			eltEnd = tagend;
		    }

		    String element = text.substring(idx, eltEnd).toUpperCase();
		    // We may need to close out the current item
		    // and start a new item, depending on the element type.
		    
		    if ("BR".equals(element)) {
			forceNewline = true;
		    } else if ("P".equals(element)) {
			forceNewline = true;
			htmlParagraph = true;
		    } else if ("STRONG".equals(element) || "B".equals(element)) {
			// bold
			if (isClosingTag) {
			    isBold = false;
			} else {
			    isBold = true;
			}
		    } else if ("A".equals(element)) {
			// hyperlink
			if (isClosingTag) {
			    isLink = false;
			} else {
			    // Look for the HREF target value. 
			    isLink = true;
			    linkTarget = getTagAttributeVal(text, idx, tagend);
			}
			
		    } else if ("PRE".equals(element)) {
			if (isClosingTag) {
			    preformat = false;
			} else {
			    preformat = true;
			}
		    } else if ("CENTER".equals(element)) {
			forceNewline = true;
			if (isClosingTag) {
			    stopCenter = true;
			} else {
			    center = true;
			}
		    } else if ("FONT".equals(element)) {
			if (isClosingTag) {
			    color = prevColor;
			} else {
			    // Look for the COLOR target value. 
			    // Double quotes required: <FONT color="#RRGGBB">
			    String colorVal = getTagAttributeVal(text, idx,tagend);
			    int r,g,b;
			    r = Integer.parseInt(colorVal.substring(1,3), 16);
			    g = Integer.parseInt(colorVal.substring(3,5), 16);
			    b = Integer.parseInt(colorVal.substring(5,7), 16);
			    prevColor = color;
			    color = Graphics.getColorOfRGB(r,g,b);

			}
		    } else if ("IMG".equals(element)) {
			// Image tag.
			// Look for the SRC target value. 
			// Double quotes required: <IMG src="http://foo/bar.gif">
			isImage = true;
			imageName = getTagAttributeVal(text, idx, tagend);
		    } else if ("I".equals(element) || "EM".equals(element)) {
			// italic
			if (isClosingTag) {
			    isItalic = false;
			} else {
			    isItalic = true;
			}
		    }
		    // Adjust fonts
		    if (isBold && isItalic) {
			style = Font.STYLE_BOLDITALIC;
		    } else if (isBold) {
			style = Font.STYLE_BOLD;
		    } else if (isItalic) {
			style = Font.STYLE_ITALIC;
		    } else {
			style = Font.STYLE_PLAIN;
		    }
		    
		    font = Font.getFont(type|face|style|size);
		    
		    idx = tagend+1;
		    
		}
		charAdded = false;
	    } else if (ch == '&') {
		// Look for entities of the form &xxx;
		// look for closing ;
		int semi = text.indexOf(';', idx);
		if (semi > 0) {
		    String entity = text.substring(idx, semi).toUpperCase();
		    if ("AMP".equals(entity)) {
			buf.append("&");
		    } else if ("LT".equals(entity)) {
			buf.append("<");
		    } else if ("GT".equals(entity)) {
			buf.append(">");
		    } else if ("QUOT".equals(entity)) {
			buf.append("\"");
		    } else if ("NBSP".equals(entity)) {
			buf.append(" ");
		    } else if (entity.startsWith("#")) {
			// &#65;
			char val = (char) Integer.parseInt(entity.substring(1));
			buf.append(val);
		    }
		    seenWhitespace = false;
		    
		    idx = semi+1;
		}
		charAdded = true;
	    } else {
		// We strip extra whitespace chars after the first one
		if (!preformat && (ch == ' ' || ch == '\r' || ch == '\n' || ch == '\t')) {
		    if (!seenWhitespace) {
			buf.append(" ");
			charAdded = true;
			seenWhitespace = true;
		    }
		} else {
		    if (preformat && ch == '\n') {
			forceNewline = true;
		    } else {
			buf.append(ch);
			charAdded = true;
			seenWhitespace = false;
		    }
		}
	    }
	    
	    if (isImage) {
		// Close out the current element, (it should be
		// (possibly empty text)), by design two image
		// items are never adjacent in the item list.
		currentItem.text = buf.toString();
		buf.delete(0, buf.length());
		currentItem.width = font.stringWidth(currentItem.text);
		xpos += currentItem.width;

		currentItem = 
		    new HtmlItem(xpos, ypos, 0, font.getHeight(), font, currentLine);
		currentItem.color = color;
		currentItem.image = getImage(imageName);
		currentItem.width = currentItem.image.getWidth();
		currentItem.isImage = true;
		if (isLink) {
		    currentItem.link = linkTarget;
		}
		currentItem.isLink = isLink;

		// If this image goes past the right screen margin, then 
		// wrap it to the next line.
		xpos += currentItem.width;
		boolean overrun = ((xpos + currentItem.width) > lineWidth);
		if (overrun || forceNewline) {
		    maxHeight = 0;
		    xpos = leftMargin;
		    ypos += lineSpacing;
		    currentLine++;
		    if (htmlParagraph) {
			ypos += font.getHeight()/2; // add an extra blank line for a P tag
			htmlParagraph = false;
		    }
		    currentItem.xpos = xpos;
		    currentItem.lineNum = currentLine;
		    // adjust line height to this image
		    adjustItemYPositions(currentItem);
		    xpos = currentItem.width;
		} else {
		    // otherwise, append this to the current line
		    // of items.
		    currentItem.height = currentItem.image.getHeight();
		    adjustItemYPositions(currentItem);
		}

		if (isLink) {
		    currentItem.link = linkTarget;
		}
		
		items.addElement(currentItem);
		
		if (forceNewline) {
		    seenWhitespace = true;
		}
		
		forceNewline = false;
		forceNewItem = false;

		// start a new textitem
		currentItem = 
		    new HtmlItem(xpos, ypos, 0, font.getHeight(), font, currentLine);
		currentItem.color = color;
		currentItem.isLink = isLink;
		currentItem.isImage = false;
		if (isLink) {
		    currentItem.link = linkTarget;
		}
		
		items.addElement(currentItem);

	    } else {
		// It's a text item
		currentItem.width = font.stringWidth(buf.toString());
		boolean overrun = ((xpos + currentItem.width) > lineWidth);

		// If we have reached right screen margin, handle line wrapping
		if (overrun || forceNewline || forceNewItem) {
		    char lastchar;
		
		    // If real right-margin overrun, so pull the last char
		    // off and start a new run on a new line.
		    if (overrun && (buf.length() > 0)) {
			seenWhitespace = true;
			lastchar = buf.charAt(buf.length()-1);
			buf.deleteCharAt(buf.length()-1);
		    } else {
			lastchar = 0;
		    }
		
		    // update current item in case we trimmed the last
		    // character off
		    currentItem.text = buf.toString();
		    currentItem.width = font.stringWidth(currentItem.text);
		    currentItem.height = font.getHeight();
		    adjustItemYPositions(currentItem);

		    if (overrun || forceNewline) {
			maxHeight = 0;
			// If we are centering text, go back and add an
			// xoffset to items in the current line
			if (center) {
			    centerLineItems(xpos + currentItem.width, 
					    lineWidth,
					    currentLine);
			}
			// a /CENTER tag was encountered, so stop centering 
			if (stopCenter) {
			    center = false;
			    stopCenter = false;
			}
			xpos = leftMargin;
			ypos = ypos + lineSpacing;
			currentLine++;
			if (htmlParagraph) {
			    ypos += font.getHeight()/2; // add an extra blank line for a P tag
			    htmlParagraph = false;
			}
		    } else {
			xpos += currentItem.width;
		    }

		    // start a new item on the next line
		    currentItem = 
			new HtmlItem(xpos, ypos, 0, font.getHeight(), font, currentLine);
		    currentItem.color = color;
		    currentItem.isLink = isLink;
		    currentItem.isImage = false;
		    if (isLink) {
			currentItem.link = linkTarget;
		    }
		
		    items.addElement(currentItem);
		
		    if (forceNewline) {
			seenWhitespace = true;
		    }
		
		    buf.delete(0, buf.length());
		    if (lastchar > 0) {
			if (!(lastchar == ' ' || lastchar == '\r' || lastchar == '\n' || lastchar == '\t')) {
			    buf.append(lastchar);
			    seenWhitespace = false;
			}
		    }
		
		    forceNewline = false;
		    forceNewItem = false;
		}

	    }
	    
	    isImage = false;

		
	}
	
	// Finalize any business needed for the last item added,
	if (!currentItem.isImage) {
	    currentItem.text = buf.toString();
	    currentItem.width = font.stringWidth(currentItem.text);
	}
	adjustItemYPositions(currentItem);

	// select the first hyperlink
	nextHyperlink(false);
    }

    /** When an element has been added to a line of items,
     * it's height may be higher than the current max line height.
     * If so, all the items in the line need to have their
     * ypos pushed downwards to give enough vertical space
     * in the line to hold the new item.
     *
     * Modifies item's ypos in the current line,
     * and also sets ypos and maxHeight to new values.
     * 
     */
    void adjustItemYPositions (HtmlItem currentItem) {
	int deltaY = currentItem.height - maxHeight;

	// If this item is taller than the current 
	// max height of the items in the line, then
	// push other elements on this line downwards
	// by image-height - (current line's MaxHeight)
	if (deltaY > 0) {
	    ypos += deltaY;
	    for (int i = 0; i < items.size()-1; i++) {
		HtmlItem item = (HtmlItem) items.elementAt(i);
		if (item.lineNum == currentLine) {
		    item.ypos += deltaY;
		}
	    }
	    maxHeight = currentItem.height;
	    currentItem.ypos = ypos;
	}
    }

    void centerLineItems (int itemTotalWidth, int lineWidth, int currentLine) {
	int xoffset = (lineWidth - itemTotalWidth) / 2;
	for (int i = 0; i < items.size(); i++) {
	    HtmlItem item = (HtmlItem) items.elementAt(i);
	    if (item.lineNum == currentLine) {
		item.xpos += xoffset;
	    }
	}
    }

    /** Gets the attribute value of a tag.  Assumes there's
     * only one attribute values, in double quotes.
     * All tag attribute values MUST be in double quotes,
     * (that's compliant with XHTML!)
     *
     * @param text The entire page of text
     * @param index An index into the page, where the current element tag starts
     * @param tagend The index of the closing '&gt;' character of the current element
     */

    String getTagAttributeVal (String text, int index, int tagend) {
	int startquote = text.indexOf("\"", index);
	int endquote = text.indexOf("\"", startquote+1);
	if (endquote > tagend) {
	    // mismatched quotes
	    return "";
	} else {
	    return text.substring(startquote+1, endquote);
	}
    }
    /**
     * Redisplay the current page. Highlights any selected element which is visible
     * on the current page.
     */
    public void paint(Graphics g)
    {
        g.lock();
        
	g.clearRect(0, 0, nScreenWidth, nScreenHeight);
	
	// loop over the content items, displaying them
	for (int i = 0; i < items.size(); i++) {
	    HtmlItem item = (HtmlItem) items.elementAt(i);
	    if (item.isImage) {
		if (item.isLink) {
		    if (selectedHyperlink == i) {
			// draw a border
			g.setColor(Graphics.getColorOfName(Graphics.RED));
			g.fillRect(item.xpos-1, (item.ypos - nPosY - item.height -1), item.width+2, item.height+2);
		    }
		}
		g.drawImage(item.image, item.xpos, (item.ypos - nPosY - item.height));
	    } else {
		g.setFont(item.font);
		//	    g.setColor(item.color);
		g.setColor(item.color);

		// Selected Hyperlink is shown in reverse video
		if (item.isLink) {
		    if (selectedHyperlink == i) {
			g.setColor(Graphics.getColorOfName(Graphics.RED));
			g.fillRect(item.xpos, (item.ypos - nPosY)-item.height, item.width, item.height+1);
			g.setColor(Graphics.getColorOfName(Graphics.WHITE));
		    }  else {
			g.setColor(Graphics.getColorOfName(Graphics.RED));		    
		    }
		}
		g.drawString(item.text, item.xpos, item.ypos - nPosY);
	    }

	    maxPageHeight = item.ypos;
	}
	
        g.unlock(true);
    }

    /** Advance selected hyperlink to next link. If it's not on this
     * page, scroll display to next page. If we scrolled, reselect to
     * find select first link on the page if there is one visible. 
     */
    void nextHyperlink(boolean autoScroll) {
	int start = selectedHyperlink >= 0 ? selectedHyperlink+1 : 0;
	boolean foundNext = false;
	boolean scrolldown = false;
	for (int i = start; i < items.size(); i++) {
	    HtmlItem item = (HtmlItem) items.elementAt(i);	
	    if (item.isLink) {
		foundNext = true;
		selectedHyperlink = i;
		// if it's not on the screen scroll one page forward
		if (item.ypos > (nPosY + nScreenHeight)) {
		    scrolldown = true;
		}
		break;
	    }
	}
	// if no link found, just do a pagedown
	if (autoScroll && (!foundNext || scrolldown)) {
	    if (nPosY < (maxPageHeight - nScreenHeight)) {
		nPosY += nScreenHeight;
	    }	    
	}

	// Now select first link on the new page, if there is one.
	if (autoScroll && scrolldown) {
	    for (int i = 0; i < items.size(); i++) {
		HtmlItem item = (HtmlItem) items.elementAt(i);	
		if (item.isLink) {
		    // if it's not on the screen scroll one page forward
		    if ((nPosY <= item.ypos) && (item.ypos <= (nPosY + nScreenHeight))) {
			selectedHyperlink = i;
		    }
		    break;
		}
	    }
	}
    }

    /**
     * Backup to select the previous hyperlink HtmlItem, or go to prev
     * page if no links on this page.  (Need to update this to work
     * like nextHyperlink does now) 
     */
    void prevHyperlink() {
	boolean foundPrev = false;
	int start = selectedHyperlink >= 0 ? selectedHyperlink-1 : items.size()-1;
	for (int i = start; i >= 0; i--) {
	    HtmlItem item = (HtmlItem) items.elementAt(i);
	    if (item.isLink) {
		foundPrev = true;
		selectedHyperlink = i;
		// if it's not on screen, page back one
		if (item.ypos < nPosY) {
		    nPosY -= nScreenHeight;
		    if (nPosY < 0) {
			nPosY = 0;
		    }
		}
		break;
	    }
	}

	if ( !foundPrev) {
	    nPosY -= nScreenHeight;
	    if (nPosY < 0) {
		nPosY = 0;
	    }
	}
    }

    static final String BROKEN_IMAGE = "resource:///pics/j1.gif";

    /** Resource name example: "resource:///test.gif"
     */
    Image getImage (String resourceName) {
	// If it doesn't have a ":",assume no protocol, and 
	// prepend the current URL with a "/".
	String currentURLBase = currentURL.substring(0,currentURL.lastIndexOf('/')+1);
	if (resourceName.indexOf(':') == -1) {
	    resourceName = currentURLBase + resourceName;
	}
	// If it's HTTP or something external, prepend the proxy URL
     	if (!(resourceName.toUpperCase().startsWith("RESOURCE:///"))) {
	    resourceName = IApplication.getCurrentApp().getSourceURL() 
		+ proxyURL + "?link=" + URLEncoder.encode(resourceName);
	}

 	MediaImage mi = MediaManager.getImage(resourceName);
	try {
	    mi.use();
	} catch (ConnectionException e) {
	    // This is bad idea because if the default BROKEN_IMAGE
	    // resource is not found, we will loop recursively.
	    // Just make sure there is a resource defined for this!
	    return(getImage(BROKEN_IMAGE));
	}
	return mi.getImage();
    }


    /**
     * Fetch an HTML document from a link URL.
     *
     */
    void fetchLink() {
	lastPage = currentPage;
	lastURL = currentURL;
	if (selectedHyperlink != -1) {
	    String target = ((HtmlItem)(items.elementAt(selectedHyperlink))).link;
	    // If no ":", assume it's a relative link, (no protocol),
	    // and append current page
	    if (target.indexOf(':') == -1) {
		String currentURLBase = currentURL.substring(0,currentURL.lastIndexOf('/')+1);
		target = currentURLBase + target;
	    }
	    setURL(target);
	}
    }

    /** 
     * Tries to implement the "Back" feature of browsers. Keeps the previous
     * page text cached locally. Don't know if this is a reasonable thing to
     * do, i.e., do phones have enough RAM typically to make this safe?
     */
    void prevLink() {
	if (lastPage != null) {
	    setText(lastPage);
	    currentURL = lastURL;
	    selectedHyperlink = -1;
	    nPosY = 0;
	}
    }

    Hashtable params = new Hashtable();

    /* return table of key-value pairs from a URL query string. 
     */
    Hashtable getQueryParams (String url) {
	Hashtable h = params;
	h.clear();
	int start = url.indexOf('?');
	if (start >= 0) {
	    start++; // advance past ? to first key
	    boolean done = false;
	    while (!done) {
		int equalsign = url.indexOf('=', start);
		if (equalsign == -1) {
		    break;
		}
		String key = url.substring(start, equalsign);
		int nextKey = url.indexOf('&',start);
		String val;
		if (nextKey == -1) {
		    val = url.substring(equalsign+1);
		    done = true;
		} else {
		    val = url.substring(equalsign+1, nextKey);
		    start=nextKey+1;
		}
		h.put(key,val);
	    }
	}
	return h;
    }

    /**
     * Execute servlet named NAME, return HTML result
     * <p>
     * URLs of the form "local://xxxx" are dispatched here. You can
     * modify code in this method to handle all your servlets. 
     *
     * <p>
     * You can use the utility method getQueryParams to parse the URL query
     * string of the form "servletname?key1=value1&amp;key2=value2&amp;..."
     * into a Hashtable of key-value pairs.
     * 
     */
    String handleServlet(String url) {
	if (url.startsWith("servlet")) {
	    Hashtable params = getQueryParams(url);
	    int n = Integer.parseInt((String)params.get("n"));
	    int n2 = Integer.parseInt((String)params.get("n2"));
	    return "<center>Local servlet!<br>n = " + n+"</center>"
		+ "<br><a href=\"local://servlet?n2="+n+"&n="+(n+n2)+"\">Fibonacci</a>";
	} else {
	    return "undefined servlet "+url;
	}
    }

    /** 
     * Get a generalized resource using CLDC/NTT Connector API.
     * Supports HTTP and RESOURCE protocol at the moment.
     * <p>
     * <p>
     * Your URL can be of the form 
     * <ul><li> "http://foo.com/bar.html", fetches via HTTP
     * <li> "resource:///bar.html" access the application's local JAR file
     * <li> "local://name" dispatch in this function to a "servlet"
     * </ul>
     * <p>
     */
    public String getURLData(String url) {
	if (url.toUpperCase().startsWith("LOCAL://")) {
	    return handleServlet(url.substring(8));
	} else 	if (url.toUpperCase().startsWith("RESOURCE:///")) {
	    String s = "";
	    try {
		InputStream in = Connector.openInputStream(url);
		s = readByteString(in);
		in.close();
	    } catch (Exception e) {
		return e.getMessage();
	    }
	    return s;
	} else {
	    return httpPost(url, null);
	}
    }
    
    /**
     * Get an HTML document using HTTP protocol.
     *
     * URL can be of the form
     * http://foo.bar.com/my.html
     */
    public String httpPost(String url, String postData) {
	String res = null;
	
	String proxy = IApplication.getCurrentApp().getSourceURL() 
	    + proxyURL + "?link=" + URLEncoder.encode(url);
	try {
	    HttpConnection conn = (HttpConnection)Connector.open(proxy, Connector.READ, true);
	    conn.setRequestMethod(HttpConnection.GET);
	    conn.setRequestProperty("Content-Type", "text/plain");

	    /*
	    HttpConnection conn = (HttpConnection)Connector.open(url, s != null ? Connector.READ_WRITE : Connector.READ, true);

	    conn.setRequestMethod(s != null ? HttpConnection.POST : HttpConnection.GET);
	    conn.setRequestProperty("Content-Type", "text/plain");
			
	    if (s != null) {
		OutputStream out = conn.openOutputStream();
		byte[] b = null;
		try {
		    b = postData.getBytes("SJIS");
		} catch (IOException e) {
		    b = s.getBytes();
		}
		out.write(b);
		out.close();
	    }
	    */
	    conn.connect();
	    InputStream in = conn.openInputStream();
	    if (in == null)
		throw new IOException();
	    res = readByteString(in);
	    in.close();
	    conn.close();
	} catch (Exception e) {
	    res = e.toString();
	}
	return res;
    }
    
    public static String readByteString (InputStream in) throws IOException {
	String res;
	ByteArrayOutputStream baos = new ByteArrayOutputStream();
	byte[] b = new byte[0x1000];
	for (;;) {
	    int len = in.read(b);
	    if (len < 0)
		break;
	    baos.write(b, 0, len);
	}
	try {
	    res = new String(baos.toByteArray(), "SJIS");
	} catch (IOException e) {
	    res = new String(baos.toByteArray());
	}
	return res;
    }

    /**
     * Handle a keypad event
     */
    public void processEvent(int nType, int nParm)
    {
        if(nType == Display.KEY_PRESSED_EVENT) {
            switch(nParm) {
	    case Display.KEY_SELECT:
		// select hyperlink
		fetchLink();
		break;
	    case Display.KEY_LEFT:
		// select hyperlink
		prevLink();
		break;
	    case Display.KEY_2:
		// home
		setURL("resource:///index.html");
		break;
	    case Display.KEY_UP:
		prevHyperlink();
		break;
	    case Display.KEY_DOWN:
		nextHyperlink(true);
		break;

	    case Display.KEY_SOFT1:
		if (nPosY < nScreenHeight) {
		    nPosY = 0;
		} else {
		    nPosY -= nScreenHeight;
		}
		break;
	    case Display.KEY_SOFT2:
		if (nPosY < (maxPageHeight - nScreenHeight)) {
		    nPosY += nScreenHeight;
		}
		break;

	    case Display.KEY_1:
		// scroll down
		if (nPosY > 0) {
		    nPosY-=8;
		}
		break;
	    case Display.KEY_3:
		// scroll down
 		if (nPosY < maxPageHeight) {
 		    nPosY+=8;
  		}
		break;

	    case Display.KEY_ASTERISK:
		// scroll down
		getNewURL();
		return;

	    case Display.KEY_POUND:
		IApplication.getCurrentApp().terminate();
		break;
            }
        }
        repaint();
    }

    Component button;
    TextBox url;

    /**
     * User interface to allow user to type in a URL into a textbox, and
     * then fetch that URL.
     *
     * This may not be needed for most real applications. The HTML documents
     * you generally would display are not arbitrary user-specified ones, so
     * you could probably flush this method.
     */
    void getNewURL() {
	Panel panel = new Panel();
	// Present a button for now
	url = new TextBox("resource:///index.html", 20, 1, TextBox.DISPLAY_ANY);
	panel.add(url);
	button = new Button("Submit");
	panel.add(button);

	panel.setComponentListener(this);
	panel.setKeyListener(this);
	panel.setSoftKeyListener(this);
	Display.setCurrent(panel);

    }

	// SoftKeyListener
	public void softKeyPressed(int key) {
	}
	public void softKeyReleased(int key) {
	}
	
	// KeyListener
	public void keyPressed(Panel p, int key) {
	}

	public void keyReleased(Panel p, int key) {
	}

	// ComponentListener
	public void componentAction(Component source, int type, int param) {
		if (source == button) {
		    String target = url.getText();
		    try {
			setURL(target);
		    } catch (Exception e) {
			setText(e.toString());
		    }
		    selectedHyperlink = -1;
		    nPosY = 0;
		    Display.setCurrent(this);
		}
	}

}





/** An HTML content layout item.
 * Currently the only types supported
 * are a run of text or an inline image. 
 *
 */
class HtmlItem {
    
    /** Ok, I want to find out how many bytes I save by making
	these  instead of defining accessor methods. If 
	it is not a big savings, then convert these to methods.
    */
    
    boolean isLink = false;
    boolean isImage = false;
    int lineNum;
    // xpos,ypos = lower left corner
    int xpos, ypos, width, height;

    String text = "";
    Font font = Font.getDefaultFont();
    int color = Graphics.getColorOfName(Graphics.BLACK);

    String link = null;
    Image image;
    
    HtmlItem (int x, int y, int width, int height, Font f, int lineNum) {
	font = f;
	xpos = x;
	ypos = y;
	this.lineNum = lineNum;
	this.width = width;
	this.height = height;
    }
    
    HtmlItem (String text, int x, int y, int width, int height, Font f, int lineNum) {
	this.lineNum = lineNum;
	font = f;
	xpos = x;
	ypos = y;
	this.width = width;
	this.height = height;
	this.text = text;
    }
    
}