/*
 * Created on Apr 8, 2003
 *
 */
package ca.uvic.gild.servlet;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Stack;

import javax.swing.text.BadLocationException;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.HTML.Tag;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;

import ca.uvic.gild.activeHTML.ActiveHTMLPlugin;
import ca.uvic.gild.activeHTML.MethodSignature;

/**
 * @author Adrian, Rob Lintern
 *
 * This is the html content presentation class. It parses the source html file
 * and adds different tags or info required for the functionality of
 * Gild Active HTML. 
 *	
 * Example:
 * <pre>
 * 			Writer out = new StringWriter ();
 * 			HtmlParser p = new HtmlParser(out);
 * 			File htmlFile = new File("test.html");
 *			FileReader in = new FileReader(htmlFile);
 *			(new ParserDelegator ()).parse(in, p, false);
 * 
 * </pre>
 * 
 * 
 */
public class HtmlParser extends HTMLEditorKit.ParserCallback {
	private final static String ACTIVE_HTML_COLOR = "orange";
	public Writer out = null;
	//private String filePathRelativeToProjectRoot = null; // the name of the file being parsed
	//private String projectName = null;
	private IFile fileToParse;
	
	
	private String host = null;
	
	//used to find out whether or not we need to add a <BASE> tag
	//to resolve relative paths to images, etc.
	private boolean hasBaseTag;
	private IFile workspaceBase;
	private URL baseURL;
	
	private Stack endAnchorStack;
	private Stack endCodeStack;
	
	private static long nextID = 0;

	/**
	 * @param out
	 */
	public HtmlParser(Writer out) {
		this.out = out;
		fileToParse = null;
		hasBaseTag = false;
		endAnchorStack = new Stack ();
		endCodeStack = new Stack();
	}

	/**
	 * @param fileName
	 *
	public void filePathRelativeToProjectRoot(String filePathRelativeToProjectRoot) {
		this.filePathRelativeToProjectRoot = filePathRelativeToProjectRoot;
	}

	/**
	 * @param projectName
	 *
	public void setProjectName(String projectName) {
		this.projectName = projectName;
	}*/
	
	/**
	 * Sets the file to parse.
	 * @param file the file to parse.
	 */
	public void setParseFile(IFile file) {
		this.fileToParse = file;
	}
	
	public void setHost (String host) {
		this.host = host;
	}
	
	/**
	 * @see javax.swing.text.html.HTMLEditorKit.ParserCallback#handleStartTag(javax.swing.text.html.HTML.Tag, javax.swing.text.MutableAttributeSet, int)
	 */
	public void handleStartTag(HTML.Tag tag, MutableAttributeSet attrs, int pos) {
		if (tag.equals(HTML.Tag.BODY)) {
			handleBodyStartTag(tag);
		} else if (tag.equals(HTML.Tag.HEAD)) {
			handleHeadStartTag();
		} else if (tag.equals(HTML.Tag.CODE)) {
			handleCodeStartTag(tag, attrs);
		} else if (tag.equals(HTML.Tag.A)) {
			handleAnchorStartTag(tag, attrs);
		}  else {
			handleAnyOtherStartTag(tag, attrs);
		}
	}

	/**
	 * Sets the base path for the parser based on the &lt;BASE&gt; tag.
	 * @param tag the base tag.
	 * @param attrs the attributes of the tag.
	 */
	private void handleBaseTag(Tag tag, MutableAttributeSet attrs) {
		Enumeration names = attrs.getAttributeNames();
		while (names.hasMoreElements()) {
			Object attrName = names.nextElement();
			Object attrValue = attrs.getAttribute(attrName);
			if (attrName.equals(HTML.Attribute.HREF)) {
				try {
					baseURL = new URL(attrValue.toString());
					initializeWorkspaceBase();
					hasBaseTag = true;
				} catch (MalformedURLException e) {
				}
			}
		}
		handleAnyOtherStartTag(tag, attrs);
		
	}

	/**
	 * Creates an IPath based on the baseURL if the baseURL is in the
	 * workspace.
	 */
	private void initializeWorkspaceBase() {
		if (baseURL == null)
			return;
		if (!"FILE".equals(baseURL.getProtocol().toUpperCase()))
			return; //the workspace must be in the local file system.
		IPath baseLocation = new Path(baseURL.getPath());
		
		IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
		IFile[] files = root.findFilesForLocation(baseLocation);
		if (files != null && files.length > 0) {
			//check the actual location of the file against the
			//base location
			IFile baseFile = null;
			for (int i = 0; i < files.length && baseFile==null; i++) {
				IFile currentFile = files[i];
				if (baseLocation.equals(currentFile.getLocation()))
					baseFile = currentFile;
			}
			if (baseFile != null)
				workspaceBase = baseFile;
		}
		
	}

	/**
	 * @see javax.swing.text.html.HTMLEditorKit.ParserCallback#handleEndTag(javax.swing.text.html.HTML.Tag, int)
	 */
	public void handleEndTag(HTML.Tag tag, int pos) {
		if (tag.equals(HTML.Tag.A)) {
			handleAnchorEndTag(tag);
		} else if (tag.equals(HTML.Tag.CODE)) {
			handleCodeEndTag(tag);
		} else if (tag.equals(HTML.Tag.HEAD) && !hasBaseTag) {
			insertBaseTag();
			handleAnyOtherEndTag(tag);
		} else {
			handleAnyOtherEndTag(tag);
		}
	}


	/**
	 * Inserts a &lt;BASE&gt; tag to resolve relative paths to files. 
	 */
	private void insertBaseTag() {
		try {
			baseURL = fileToParse.getLocation().toFile().toURL();
			workspaceBase = fileToParse;
		} catch (MalformedURLException e) {
			baseURL = null;
		}
		if (baseURL != null) {
			String str = "<BASE href=\"" +
				baseURL.getProtocol() +":" +
				((baseURL.getPort()> 0) ? ""+baseURL.getPort() : "") + "/" +
				baseURL.getPath() + "\"/>\n";
			writeString(str);
		}
	}

	/**
	 * @param tag
	 */
	private void handleAnchorEndTag(HTML.Tag tag) {
		handleAnyOtherEndTag(tag);
		if (!endAnchorStack.isEmpty()) {
			String [] projectAndFile = (String []) endAnchorStack.pop();	
			String project = projectAndFile[0];		
			String file = projectAndFile[1];
			String openFileLink = "<a href=\"#\" onclick=\"revealInEditor('" + project + "', '" + file + "');return false;\">\n";	
			writeString("\n" + openFileLink);
			writeString("<font size='-1' face='Sans-Serif' color='" + ACTIVE_HTML_COLOR + "'>");					
			writeString("\n (Open in editor)");
			writeString("</font><a>");
		}
	}
	
	private void handleAnyOtherEndTag (HTML.Tag tag) {
		writeString("</");
		writeString(tag.toString());
		writeString(">\n");
	}

	/**
	 * @param tag
	 * @param attrs
	 */
	private void handleAnchorStartTag(HTML.Tag anchorTag, MutableAttributeSet attrs) {
		Enumeration enum = attrs.getAttributeNames();
		String hrefStr = null;
		while (enum.hasMoreElements()) {
			Object attrName = enum.nextElement();
			Object attrValue = attrs.getAttribute(attrName);
			if (attrName.equals(HTML.Attribute.HREF)) {
				hrefStr = attrValue.toString();	
				// see if this href links to a file within the current workspace
				// if so, we should add a hyperlink to open it within Gild
				IProject targetProject = null;
				IPath hrefPath = new Path(hrefStr);
				IFile targetFile = makeRelativeToWorkspaceBase(hrefPath);
				if (targetFile != null)
					targetProject = targetFile.getProject();
				//IProject targetProject = workSpaceRoot.getProject(projectName);
				
				if (targetProject!=null && targetProject.exists()) {
					/*IPath thisProjectRelativePath = targetFile.getProjectRelativePath();//new Path (filePathRelativeToProjectRoot); // the path of the file we are currently parsing
					thisProjectRelativePath = thisProjectRelativePath.makeRelative();
					thisProjectRelativePath = thisProjectRelativePath.removeLastSegments(1); // get the location of the file we are currently parsing
					
					IPath pathOfLink = thisProjectRelativePath.append(hrefStr); // location of the file we are linking to 
					
					IFile file = targetProject.getFile(pathOfLink);*/
					if (targetFile.getFileExtension().toLowerCase().equals("java")){ 
						String [] projectAndFile = new String [] {/*projectName*/targetProject.getName(), "" + targetFile.getProjectRelativePath()};
						String editorLink = "<a href='javascript:revealInEditor(\"" + targetProject.getName() + "\",\"" + targetFile.getProjectRelativePath() + "\")'>Open " + targetFile.getName() + " in Eclipse " + "</a>, ";
						writeString(editorLink);
					}
					String openFileLink = "<a href='javascript:navigate(\"" + /*projectName*/targetProject.getName() + "\", \"" + targetFile.getProjectRelativePath() + "\")'>\n";	
					writeString(openFileLink);
						

					return;
				}				
			}
		}
		
		handleAnyOtherStartTag(anchorTag, attrs);
	}

	private void handleBodyStartTag(HTML.Tag tag) {		
		//write the <script> tag (Note: Mozilla doesnt like references to local files get the script file via gild web app
		String javaScripts = "http://" + host + "/" + ActiveHTMLPlugin.GILD_WEB_APP_NAME + "/web_requests.js";
		writeString("<script language=\"JavaScript\" src=\"" + javaScripts + "\">" + "</script> ");
		
		//write the <body> tag
		writeString("\n<" + tag + ">\n");
	
		// write the hidden <iframe> where all the actions are sent to.
		writeString("<iframe name=\"commandFrame\" style=\"visibility:hidden\" " + "tabindex=\"-1\" frameborder=\"no\" width=\"0\" height=\"0\" " + "scrolling=\"no\"></iframe>");
		
		// write the hidden <form> that contains a hidden field with the name of the current project
		writeString("<form	method=\"post\" id=\"GildHiddenForm\" name=\"HiddenGildForm\">" + "<input type=\"hidden\" name=\"CurrentProject\" value=\"" + /*projectName*/fileToParse.getProject().toString() + "\">\n" + "</form>");
	}

	private void handleHeadStartTag() {
		// set the base to the location of this file
		writeString("<head>\n");
		//if (projectName != null) {
			//writeString("<base href=\"" + projectName + "\">\n");
		//}
	}
	
	

	/**
	 * @see javax.swing.text.html.HTMLEditorKit.ParserCallback#handleSimpleTag(javax.swing.text.html.HTML.Tag, javax.swing.text.MutableAttributeSet, int)
	 */
	public void handleSimpleTag(HTML.Tag tag, MutableAttributeSet attrs, int pos) {
		if (attrs.getAttributeCount() > 0 && attrs.toString().equalsIgnoreCase("endtag=true ")) {
			writeString("</" + tag + ">");
		} else if ((tag.toString().compareToIgnoreCase("javasource") == 0) && (attrs.getAttributeCount() > 0)) {
			handleJavaSourceTag(attrs);
		} else if (tag.equals(HTML.Tag.BASE)) {
			handleBaseTag(tag, attrs);
			hasBaseTag = true;
		} else {
			handleAnyOtherStartTag(tag, attrs);
		}
	}

	private void handleAnyOtherStartTag(HTML.Tag tag, MutableAttributeSet attrs) {
		writeString("<");		
		int index = attrs.toString().indexOf("javascript");
		if (index != -1) {
			// surround the javascript attribute value with single quotes
			writeString(tag.toString() + " " + attrs.toString().substring(0, index) + "'" + attrs.toString().substring(index) + "'");
		} else {
			writeString(tag.toString() + " " + attrs.toString());
		}
		writeString (">");			
	}

	
	/**
	 * @see javax.swing.text.html.HTMLEditorKit.ParserCallback#handleText(char[], int)
	 */
	public void handleText(char[] data, int pos) {
		try {
			out.write(data);
		} catch (IOException ex) {
			ex.printStackTrace();
		}
	}
	
	// for debugging
	private void printTag (PrintStream ps, HTML.Tag tag, MutableAttributeSet attrs, int pos) {
		ps.println(tag.toString());
		if (attrs != null) {
			Enumeration enum = attrs.getAttributeNames();
			while (enum.hasMoreElements()) {
				Object attrName = enum.nextElement();
				Object attrValue = attrs.getAttribute(attrName);
				ps.println("\t" + attrName + " = " + attrValue);	
			}
		}
	}
	
	/**
	 * @param t
	 * @param a
	 * @param pos
	 */
	private void handleCodeStartTag(HTML.Tag tag, MutableAttributeSet attrs) {	
		String isSnippet = (String) attrs.getAttribute("snippet");
		if (isSnippet != null && isSnippet.equals("true")) {
			// give this code element a unique id so we can refer to it later
			Object codeElementID = attrs.getAttribute(HTML.Attribute.ID);
			if (codeElementID == null) {
				codeElementID = "'code" + nextID++ + "'";
				attrs.addAttribute (HTML.Attribute.ID, codeElementID);
			}
			endCodeStack.push(codeElementID); 
		}
		handleAnyOtherStartTag(tag, attrs);
	}
	
	private void handleCodeEndTag(HTML.Tag tag) {
		handleAnyOtherEndTag(tag);
		if (!endCodeStack.isEmpty()) {
			String codeElementID = (String)endCodeStack.pop();
			writeString ("<p>\n");		
			writeString ("<a href=\"#\" onclick=\"runCode(" + codeElementID + ");return false;\">\n");	
			writeString("<font face=\"Sans-Serif\" color=\"" + ACTIVE_HTML_COLOR + "\">\n");
			writeString("Run code in Eclipse...\n");
			writeString("</font>\n</a>\n</p></td>\n");
		} 
	}


	/**
	 * @param a
	 */
	private void handleJavaSourceTag(MutableAttributeSet a)  {
		String errMsg = null;
		String targetProjectName = (String) a.getAttribute("project");
		if (targetProjectName == null) {
			// use the project of this file currently being parsed
			targetProjectName = fileToParse.getProject().toString();//projectName;
		}
		String javaFileName = (String) a.getAttribute("file");
		if ((targetProjectName != null) && (javaFileName != null)) {
			IWorkspaceRoot workSpaceRoot = ResourcesPlugin.getWorkspace().getRoot();
			IProject targetProject = workSpaceRoot.getProject(targetProjectName);
		
			if (targetProject.exists()) {
				IFile resourceFile = targetProject.getFile(javaFileName);
				if (resourceFile.exists()) {
					String methodSig = (String) a.getAttribute("method_sig");
					if (methodSig != null) {
						ICompilationUnit myJavaFile = JavaCore.createCompilationUnitFrom(resourceFile);
						MethodSignature methodSignature = new MethodSignature();
						errMsg = methodSignature.initialize(methodSig);
						if (errMsg != null) {
							writeString("ERROR: " + errMsg);
						} else {
							IType containerType = myJavaFile.getType(methodSignature.getClassName());
							if (containerType.exists()) {
		
								IMethod myMethod = containerType.getMethod(methodSignature.getMethodName(), methodSignature.getMethodArguments());
								try {
									writeString("<pre>\n");
									writeString(myMethod.getSource());
								} catch (JavaModelException ex) {
									writeString("Cannot display method:");
									writeString(ex.getMessage());
								} finally {
									writeString("</pre>\n");
								}
		
							} else {
								errMsg = "ERROR: Cannot find the class for method:" + methodSignature;
							}
						}
		
					} else {
						String absFile = resourceFile.getLocation().toString();
						writeSourceFile(absFile);
					}
				} else {
					errMsg = "ERROR: Cannot open the java source file";
				}
			} else {
				errMsg = "ERROR: Cannot open the project for the java source file";
			}
		} else if (javaFileName != null) { // javaFile = full path to the file
			writeSourceFile(javaFileName);
		} else { // the source file cannot be specified
			errMsg = "ERROR: Source of the java file not specified";
		}
		if (errMsg != null) {
			writeString(errMsg + "\n");
		}
	}

	/**
	 * @param javaFile
	 */
	private void writeSourceFile(String javaFile) {
		try {
			FileInputStream in = new FileInputStream(javaFile);
			writeString("<pre>");
			int c;
			while ((c = in.read()) != -1) {
				out.write(c);
			}
			writeString("</pre>");
			in.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * @param str
	 */
	public void writeString(String str) {
		try {
			char[] c = str.toCharArray();
			out.write(c);
		} catch (IOException ex) {
			ex.printStackTrace();
		}
	}

	/**
	 * @see javax.swing.text.html.HTMLEditorKit.ParserCallback#handleComment(char[], int)
	 */
	public void handleComment(char[] data, int pos) {
		writeString("<!--");
		try {
			out.write(data);
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		writeString("-->\n");
	}

	/**
	 * @see javax.swing.text.html.HTMLEditorKit.ParserCallback#handleError(java.lang.String, int)
	 */
	public void handleError(String errorMsg, int pos) {
		errorMsg = pos + " " + errorMsg;
		//System.err.println("errorMsg: " + errorMsg);
	}

	/**
	 * @see javax.swing.text.html.HTMLEditorKit.ParserCallback#handleEndOfLineString(java.lang.String)
	 */
	public void handleEndOfLineString(String eol) {
		writeString(eol);
	}

	

	/**
	 * @see javax.swing.text.html.HTMLEditorKit.ParserCallback#flush()
	 */
	public void flush() throws BadLocationException {
		super.flush();
	}
	
	/**
	 * Converts a path string to an IPath that is relative to the base
	 * of this page.
	 *
	 * @param hrefStr the path to convert.
	 * @return the path relative to the base in the workspace. Null if the
	 * base is not in the workspace, or the path could not be found in
	 * the workspace.
	 */
	private IFile makeRelativeToWorkspaceBase(IPath href) {
		IPath relativeHref = null;
		if (href.isAbsolute()) {
			relativeHref = href; //no need to make relative.
		} else {
			if (!isBaseOnWorkspace())
				return null;
			if (baseURL == null)
				return null; //could not find a relative path.
			
			IPath localBasePath = workspaceBase.getLocation().removeLastSegments(1);
			Path backPath = new Path("..");
			while (href.matchingFirstSegments(backPath) == 1 && localBasePath.segmentCount() > 0) {
				href = href.removeFirstSegments(1);
				localBasePath = localBasePath.removeFirstSegments(1);
			}
			if (localBasePath.segmentCount() == 0) {
				//the file doesn't exist in the file system, return null
				return null;
			}
			//remove remaining back paths... assume that the 
			relativeHref = localBasePath.append(href);
		}
		//try toto find the result in the workspace.
		IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
		IFile[] files = root.findFilesForLocation(relativeHref);
		IFile resultFile = null;
		if (files != null && files.length > 0) {
			for (int i = 0; i < files.length && resultFile == null; i++) {
				if (files[i].exists())
					resultFile = files[i];
			}
		}
		if (resultFile != null)
			return resultFile;
		return null;
		
	}
	
	private boolean isBaseOnWorkspace() {
		return workspaceBase != null;
	}

}
