/*
 * Created on 17-Apr-2005
 *
 * HTMLParser is the main part of the webcoords project.
 * It provides a way to convert an html string to a "matrix" 
 * of html elements with respective coordinates.
 */

package edu.uiuc.webcoord;

import java.util.Vector;
import java.util.logging.Logger;
import java.lang.reflect.Array;
import org.htmlparser.Node;
import org.htmlparser.Parser;
import org.htmlparser.lexer.*;
import org.htmlparser.nodes.TextNode;
import org.htmlparser.nodes.TagNode;
import org.htmlparser.tags.*;
import org.htmlparser.util.NodeIterator;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;

public class HTMLParser {
	// Use logger setup by fetchHTML class
	static Logger logger = Logger.getLogger("logger.log");
	
	public static HTMLDocument parseHTML(String htmlString) {
		// Instantiate objects
		HTMLDocument htmlDoc = new HTMLDocument();
		Lexer lexer = new Lexer(htmlString);
		Parser parser = new Parser(lexer);
		
		boolean bodyFound = false; // Flag to indicate that body tag was present in html source
		
		try {
			logger.info("Iterating through all parser nodes");
			
			for (NodeIterator i = parser.elements(); i.hasMoreNodes();) {
				// Iterate through all top level nodes until html tag found
				
				Node node = i.nextNode();
				
				if (node instanceof Html) {
					logger.info("Found <html> tag...parsing its children nodes");
					
					// Iterate through all children of html node until body tag found
					NodeList nodes = node.getChildren();
					
					if (nodes != null) {
						logger.info("<html> tag contains children elements...begin iterating");
						
						for (i = (NodeIterator) nodes.elements(); i.hasMoreNodes();) {
							node = i.nextNode();
							
							if (node instanceof BodyTag) {
								logger.info("<body> tag found...parsing its children nodes");
								
								bodyFound = true;
								
								// Call main parsing loop by passing it the body node's children nodes
								parseChildren(node.getChildren(), 1, 1, "", "", null, htmlDoc);
								
							} else if (node instanceof FrameSetTag) {
								logger.warning("<frameset> tag found - aborting");
								System.out.println("Frameset tag found and is not currently supported - aborting");
								return null;
							}
						}	
					}
				}
			}
		} catch (ParserException ex) {
			logger.severe("Parser exception");
			System.out.println("Parser Exception");
			return null;
		}
		
		if (!bodyFound) {
			// This may or may not be expected.  HTMLParser may behave incorrectly
			// if new line characters or other strings are not where expected.
			// HTMLParser needs to be stress tested further.
			logger.warning("Body tag not found in parser");
			System.out.println("Body tag not found - nothing to parse");
			return null;
		}
		return htmlDoc;
	}
	
	/**
	 * 
	 * @param nodes The list of nodes to be processed
	 * @param absXCoord The absolute x coordinate at the current nesting level
	 * @param absYCoord The absolute y coordinate at the current nesting level
	 * @param relXCoord The relative (using dot notation) x coordinate of the parent node
	 * @param relYCoord The relative (using dot notation) y coordinate of the parent node
	 * @param spanArray Array used to maintain rowspan information for each column of a table
	 * @param htmlDoc HTMLDocument object reference
	 */
	private static void parseChildren(NodeList nodes, int absXCoord, int absYCoord, String relXCoord, String relYCoord, int [] spanArray, HTMLDocument htmlDoc) {
		// Parent node may not have any children.  If so, do not process any further.
		if (nodes == null){
			logger.info("No children to parse - returning to parent");
			
			return;
		}
			
		//Initialize local variables X,Y, xCoord, yCoord which are used to maintain absolute and relative
		//coordinates for the current nesting level being processed.
		int xCoord = absXCoord;
		int yCoord = absYCoord;
		String X = relXCoord;
		String Y = relYCoord;
		
		int j;
		int colspan = 1;
		int rowspan = 1;
		int [] row = new int[50];
		
		boolean prevNewLine = true; // Used to flag that a previously processed tag automatically forces a new line
		
		try {
			for (NodeIterator i = (NodeIterator) nodes.elements(); i.hasMoreNodes();) {
				Node node = i.nextNode();
				
				if (node instanceof TextNode) {
					// Any plain text within source html
					logger.info("Text processed");
					
					TextNode textNode = (TextNode) node;
					String nodeText = removeNewLine(textNode.toPlainTextString());
					
					if (nodeText.length() > 0) {
						logger.info("TextNode added to output");
						
						WEText weText = new WEText(X+Integer.toString(xCoord), Y+Integer.toString(yCoord), nodeText);
						
						htmlDoc.addWebElement(weText);
						xCoord++;  // Next element processed will be one column adjacent to current element
						
						prevNewLine = false;
					}
				} else if (node instanceof Div) {
					// Block element
					logger.info("<div> tag processed");
					
					if(!prevNewLine) {
						logger.info("Previous element does not force newline");
						yCoord++; // Previous element does not force a new line, so force it now
					}
					
					xCoord=1; // Encountering Div forces to first column
					
					// Nested structure - parse children
					logger.info("Parsing <div> children");
					parseChildren(node.getChildren(), xCoord, yCoord, X, Y, null, htmlDoc);
					
					// Div elements render on a new line - so skip line and flag it
					yCoord++;
					prevNewLine = true;
				} else if (node instanceof FormTag) {
					// Form block
					logger.info("<form> tag processed");
					
					if(!prevNewLine){
						logger.info("Previous element does not force newline");
						yCoord++; // Previous element does not force a new line, so force it now
					}
					
					// Nested structure - parse children
					logger.info("Parsing <form> children");
					
					parseChildren(node.getChildren(), xCoord, yCoord, X, Y, null, htmlDoc);
					
					// Form elements render on a new line - so skip line and flag it
					yCoord++;
					prevNewLine = true;
				} else if (node instanceof InputTag) {
					// Input element
					logger.info("<input> tag processed");
					
					InputTag inputTag = (InputTag) node;
					
					logger.info("<input> tag added to output");
					
					WEInput weInput = new WEInput(X+Integer.toString(xCoord), Y+Integer.toString(yCoord), inputTag.getAttribute("name"),inputTag.getAttribute("type"));
					
					htmlDoc.addWebElement(weInput);
					
					xCoord++; // Next element will be one column adjacent to this one
					prevNewLine = false;
				} else if (node instanceof SelectTag) {
					// Select element
					logger.info("<select> tag processed");
					
					SelectTag selectTag = (SelectTag) node;
					
					// Get all options associated with this select
					logger.info("Retrieving <select> tag options");
					
					OptionTag[] options = selectTag.getOptionTags();
					Vector optionsVec = new Vector(); // Vector used to store WEOption elements
					
					// Iterate through all options in array of OptionTags.  For each one, add a corresponding
					// WEOption in the vector.  This vector will become an attribute for the WESelect element.
					for (int v = 0;v<Array.getLength(options);v++){
						optionsVec.add(new WEOption(X+Integer.toString(xCoord), Y+Integer.toString(yCoord), (options[v]).getOptionText(), (options[v]).getValue()));
					}
					
					logger.info("<select> tag added to output");
					
					WESelect weSelect = new WESelect(X+Integer.toString(xCoord), Y+Integer.toString(yCoord), selectTag.getAttribute("name"), optionsVec );
					
					htmlDoc.addWebElement(weSelect);
					xCoord++;  // Next element will be one column adjacent to this one
					prevNewLine = false;
				} else if (node instanceof BulletList) {
					// Ordered or unordered lists (<ol>,<ul>)
					logger.info("<ul> or <ol> tag processed");
					
					if(!prevNewLine){
						logger.info("Previous element does not force newline");
						yCoord++; // Previous element does not force a new line, so force it now
					}
					
					xCoord=1; // Encountering list forces to first column
					BulletList bulletList = (BulletList) node;
					
					// Nested structure - parse children
					logger.info("Parsing <ul> or <ol> children");
					
					parseChildren(node.getChildren(), 1, 1, X+Integer.toString(xCoord)+".", Y+Integer.toString(yCoord)+".", null, htmlDoc);
					
					// Lists render on a new line - so skip line and flag it
					yCoord++;
					prevNewLine = true;
				} else if (node instanceof Bullet) {
					// List item (<li>)
					logger.info("<li> tag processed");
					
					Bullet bulletListElements = (Bullet) node;
					xCoord = 1;
					// Nested structure - parse children
					logger.info("Parsing <li> children");
					
					parseChildren(node.getChildren(), 1, 1, X+Integer.toString(xCoord)+".", Y+Integer.toString(yCoord)+".", null, htmlDoc);
					
					yCoord++; // Force to new line after having parsed all children of this list item
				} else if (node instanceof LinkTag) {
					// Link element
					logger.info("<a> tag processed");
					
					LinkTag linkTag = (LinkTag) node;
					
					logger.info("<a> tag added to output");
					
					WELink weLink = new WELink(X+Integer.toString(xCoord), Y+Integer.toString(yCoord), linkTag.getAttribute("name"),linkTag.getLinkText(),linkTag.getLink());
					
					htmlDoc.addWebElement(weLink);
					xCoord++; // Next element will be one column adjacent to this one
					prevNewLine = false;
				} else if (node instanceof TableTag) {
					// Nested structure - parse rows and children within rows
					logger.info("<table> tag processed");
					
					if (!prevNewLine){
						logger.info("Previous element does not force newline");
						yCoord++;
					}
					
					xCoord=1;
					
					for (j = 0; j < 50; j++) {
						row[j] = 1;
					}
					
					logger.info("Parsing <table> children");
					
					parseChildren(node.getChildren(),1,1,X+Integer.toString(xCoord)+".", Y+Integer.toString(yCoord)+".", row, htmlDoc);
					yCoord++;
					prevNewLine = true;
				} else if (node instanceof TableRow){
					logger.info("<tr> tag processed");
					
					xCoord = 1;
					
					logger.info("Parsing <tr> children");
					
					parseChildren(node.getChildren(), 1, 1,  X,Y+Integer.toString(yCoord)+".", spanArray, htmlDoc);
					yCoord++;
				} else if (node instanceof TableHeader){
					logger.info("Parsing <th> children");
					
					parseChildren(node.getChildren(),1,1,X+Integer.toString(xCoord)+".",Y,spanArray, htmlDoc);
					TableHeader tableHeader = (TableHeader) node;
					
					if (tableHeader.getAttribute("colspan") != null) {
						logger.info("colspan attribute > 1");
						colspan = (new Integer(tableHeader.getAttribute("colspan"))).intValue();
					} else {
						colspan = 1;
					}
					
					xCoord = xCoord + colspan;
					
					if (tableHeader.getAttribute("rowspan") != null) {
						logger.info("rowspan attribute > 1");
						rowspan = (new Integer(tableHeader.getAttribute("rowspan"))).intValue();
					} else {
						rowspan = 1;
					}
					
					for (j = xCoord - colspan; j < xCoord; j++) {
						spanArray[j - 1] = rowspan;
					}
					
				} else if (node instanceof TableColumn){
					logger.info("<td> tag processed");
					
					while (spanArray[xCoord - 1] > 1) {
						spanArray[xCoord - 1] = spanArray[xCoord - 1] - 1;
						xCoord++;
					}
					
					logger.info("Parsing <td> children");
					
					parseChildren(node.getChildren(), 1, 1, X+Integer.toString(xCoord)+".", Y, spanArray, htmlDoc);
					TableColumn tableColumn = (TableColumn) node;
					
					if (tableColumn.getAttribute("colspan") != null) {
						logger.info("colspan attribute > 1");
						colspan = (new Integer(tableColumn.getAttribute("colspan"))).intValue();
					} else {
						colspan = 1;
					}
					
					xCoord = xCoord + colspan;
					
					if (tableColumn.getAttribute("rowspan") != null) {
						logger.info("rowspan attribute > 1");
						rowspan = (new Integer(tableColumn.getAttribute("rowspan"))).intValue();
					} else {
						rowspan = 1;
					}
					
					for (j = xCoord - colspan; j < xCoord; j++) {
						spanArray[j - 1] = rowspan;
					}
				} else if (node instanceof ImageTag){
					logger.info("<img> tag processed");
					
					ImageTag imageTag = (ImageTag) node;
					
					logger.info("<img> tag added to output");
					
					WEImage weImage = new WEImage(X+Integer.toString(xCoord), Y+Integer.toString(yCoord), imageTag.getImageURL(), imageTag.getImageURL());
					
					htmlDoc.addWebElement(weImage);
					xCoord++;  // Next element processed will be one column adjacent to current element
					prevNewLine = false;
				} else if (node instanceof TextareaTag){
					logger.info("<textarea> tag processed");
					
					TextareaTag textareaTag = (TextareaTag) node;
					
					logger.info("<textarea> tag added to output");
					
					WETextBox weTextBox = new WETextBox(X+Integer.toString(xCoord), Y+Integer.toString(yCoord), textareaTag.getValue());
					
					htmlDoc.addWebElement(weTextBox);
					xCoord++;  // Next element processed will be one column adjacent to current element
					prevNewLine = false;
				} else if (node instanceof TagNode) {
					logger.info("Catch-all tag node processed");
					
					TagNode tagNode = (TagNode) node;
					if (tagNode.getTagName().equals("BR")) {
						// Line break
						logger.info("<br /> tag processed");
						
						xCoord = 1;
						yCoord++;
						prevNewLine = true;
					} else if (tagNode.getRawTagName().toUpperCase().equals("ADDRESS")){
						// Address block
						logger.info("<address> tag processed");
						
						xCoord = 1;
						
						if(!prevNewLine){
							logger.info("Previous element does not force newline");
							yCoord++;
						}
					} else if (tagNode.getRawTagName().toUpperCase().equals("/ADDRESS")){
						// Address block
						logger.info("</address> tag processed");
						
						xCoord = 1;
						yCoord++;
						prevNewLine = true;
					} else if (tagNode.getRawTagName().toUpperCase().equals("CENTER")){
						// Center text
						logger.info("<center> tag processed");
						
						xCoord = 1;
						if(!prevNewLine){
							logger.info("Previous element does not force newline");
							yCoord++;
						}
					} else if (tagNode.getRawTagName().toUpperCase().equals("/CENTER")){
						// Center text
						logger.info("</center> tag processed");
						
						xCoord = 1;
						yCoord++;
						prevNewLine = true;
					} else if (tagNode.getRawTagName().toUpperCase().equals("BLOCKQUOTE")){
						// Block quotation
						logger.info("<blockquote> tag processed");
						
						xCoord = 1;
						if(!prevNewLine){
							logger.info("Previous element does not force newline");
							yCoord++;
						}
					} else if (tagNode.getRawTagName().toUpperCase().equals("/BLOCKQUOTE")){
						// Block quotation
						logger.info("</blockquote> tag processed");
						
						xCoord = 1;
						yCoord++;
						prevNewLine = true;
					} else if (tagNode.getRawTagName().toUpperCase().equals("P")) {
						// Paragraph
						logger.info("<p> tag processed");
						
						xCoord = 1;
						
						if(!prevNewLine){
							logger.info("Previous element does not force newline");
							yCoord++;
						}
					} else if (tagNode.getRawTagName().toUpperCase().equals("/P")) {
						//Paragraph
						logger.info("</p> tag processed");
						
						xCoord = 1;
						yCoord++;
						prevNewLine = true;
					} else if (tagNode.getRawTagName().toUpperCase().equals("HR")) {
						// Horizontal rule
						logger.info("<hr /> tag processed");
						
						xCoord = 1;
						yCoord++;
						prevNewLine = true;
					} else if (tagNode.getRawTagName().toUpperCase().equals("H1")) {
						//Header type 1
						logger.info("<h1> tag processed");
						
						xCoord = 1;
						
						if(!prevNewLine){
							logger.info("Previous element does not force newline");
							yCoord++;
						}
					} else if (tagNode.getRawTagName().toUpperCase().equals("/H1")) {
						// Closing tag for header type 1
						logger.info("</h1> tag processed");
						xCoord = 1;
						yCoord++;
						prevNewLine = true;
					} else if (tagNode.getRawTagName().toUpperCase().equals("H2")) {
						// Header type 2
						logger.info("<h2> tag processed");
						xCoord = 1;
						
						if(!prevNewLine){
							logger.info("Previous element does not force newline");
							yCoord++;
						}
					} else if (tagNode.getRawTagName().toUpperCase().equals("/H2")) {
						// Closing tag for header type 2
						logger.info("</h2> tag processed");
						xCoord = 1;
						yCoord++;
						prevNewLine = true;
					} else if (tagNode.getRawTagName().toUpperCase().equals("H3")) {
						// Header type 3
						logger.info("<h3> tag processed");
						xCoord = 1;
						
						if(!prevNewLine){
							logger.info("Previous element does not force newline");
							yCoord++;
						}
					} else if (tagNode.getRawTagName().toUpperCase().equals("/H3")) {
						// Closing tag for header type 3
						logger.info("</h3> tag processed");
						xCoord = 1;
						yCoord++;
						prevNewLine = true;
					} else if (tagNode.getRawTagName().toUpperCase().equals("H4")) {
						// Header type 4
						logger.info("<h4> tag processed");
						xCoord = 1;
						
						if(!prevNewLine){
							logger.info("Previous element does not force newline");
							yCoord++;
						}
					} else if (tagNode.getRawTagName().toUpperCase().equals("/H4")) {
						// Closing tag for header type 4
						logger.info("</h4> tag processed");
						xCoord = 1;
						yCoord++;
						prevNewLine = true;
					} else if (tagNode.getRawTagName().toUpperCase().equals("H5")) {
						// Header type 5
						logger.info("<h5> tag processed");
						xCoord = 1;
						
						if(!prevNewLine){
							logger.info("Previous element does not force newline");
							yCoord++;
						}
					} else if (tagNode.getRawTagName().toUpperCase().equals("/H5")) {
						// Closing tag for header type 5
						logger.info("</h5> tag processed");
						xCoord = 1;
						yCoord++;
						prevNewLine = true;
					} else if (tagNode.getRawTagName().toUpperCase().equals("H6")) {
						// Header type 6
						logger.info("<h6> tag processed");
						xCoord = 1;
						
						if(!prevNewLine){
							logger.info("Previous element does not force newline");
							yCoord++;
						}
					} else if (tagNode.getRawTagName().toUpperCase().equals("/H6")) {
						// Closing tag for header type 6
						logger.info("</h6> tag processed");
						xCoord = 1;
						yCoord++;
						prevNewLine = true;
					} else {
						logger.info("Unsupported tag processed: " + tagNode.getRawTagName().toUpperCase());
						System.out.println("Unsupported tag processed: " + tagNode.getRawTagName().toUpperCase() + " May result in incorrect coordinate processing.");
					}
				}
			}
		} catch (ParserException ex) {
			logger.severe("Parser exception");
			System.out.println("Parser Exception!");
		}
	}
	
	/**
	 * 
	 * @param inputString String from which new line \n must replaced with whitespace.
	 * @return Reformated String value
	 */
	public static String removeNewLine(String inputString){
		if (inputString != null) {
			return (inputString.replaceAll("\n"," ")).trim();
		}
		
		return inputString;
	}
}