/*
 * 09/30/2012
 *
 * HtmlFoldParser.java - Fold parser for HTML 5 and PHP.
 * 
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package org.fife.ui.rsyntaxtextarea.folding;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import javax.swing.text.BadLocationException;

import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.Token;


/**
 * Fold parser for HTML 5, PHP and JSP.  For HTML, we currently don't fold
 * <em>everything</em> possible, just the "big" stuff.  For PHP, we only fold
 * the "big" HTML stuff and PHP regions, not code blocks in the actual PHP.
 * For JSP we only fold the "big" HTML stuff and JSP blocks, not anything in
 * the actual Java code.
 *
 * @author Robert Futrell
 * @version 1.0
 */
public class HtmlFoldParser implements FoldParser {

	/**
	 * Constant denoting we're folding HTML.
	 */
	public static final int LANGUAGE_HTML	= -1;

	/**
	 * Constant denoting we're folding PHP.
	 */
	public static final int LANGUAGE_PHP	= 0;

	/**
	 * Constant denoting we're folding JSP.
	 */
	public static final int LANGUAGE_JSP	= 1;

	/**
	 * The language we're folding.
	 */
	private final int language;

	/**
	 * The set of tags we allow to be folded.  These are tags that must have
	 * explicit close tags in both HTML 4 and HTML 5.
	 */
	private static final Set<String> FOLDABLE_TAGS;

	private static final char[] MARKUP_CLOSING_TAG_START = "</".toCharArray();
	//private static final char[] MARKUP_SHORT_TAG_END = "/>".toCharArray();
	private static final char[] MLC_START = "<!--".toCharArray();
	private static final char[] MLC_END   = "-->".toCharArray();

	private static final char[] PHP_START = "<?".toCharArray(); // <? and <?php
	private static final char[] PHP_END = "?>".toCharArray();

	// Scriptlets, declarations, and expressions all start the same way.
	private static final char[] JSP_START = "<%".toCharArray();
	private static final char[] JSP_END = "%>".toCharArray();

	private static final char[][] LANG_START = { PHP_START, JSP_START };
	private static final char[][] LANG_END   = { PHP_END, JSP_END };

	private static final char[] JSP_COMMENT_START = "<%--".toCharArray();
	private static final char[] JSP_COMMENT_END   = "--%>".toCharArray();

	static {
		FOLDABLE_TAGS = new HashSet<String>();
		FOLDABLE_TAGS.add("body");
		FOLDABLE_TAGS.add("canvas");
		FOLDABLE_TAGS.add("div");
		FOLDABLE_TAGS.add("form");
		FOLDABLE_TAGS.add("head");
		FOLDABLE_TAGS.add("html");
		FOLDABLE_TAGS.add("ol");
		FOLDABLE_TAGS.add("pre");
		FOLDABLE_TAGS.add("script");
		FOLDABLE_TAGS.add("span");
		FOLDABLE_TAGS.add("style");
		FOLDABLE_TAGS.add("table");
		FOLDABLE_TAGS.add("tfoot");
		FOLDABLE_TAGS.add("thead");
		FOLDABLE_TAGS.add("tr");
		FOLDABLE_TAGS.add("td");
		FOLDABLE_TAGS.add("ul");
	}


	/**
	 * Constructor.
	 *
	 * @param language The language to fold, such as {@link #LANGUAGE_PHP}.
	 */
	public HtmlFoldParser(int language) {
		if (language<LANGUAGE_HTML && language>LANGUAGE_JSP) {
			throw new IllegalArgumentException("Invalid language: " + language);
		}
		this.language = language;
	}


	/**
	 * {@inheritDoc}
	 */
	public List<Fold> getFolds(RSyntaxTextArea textArea) {

		List<Fold> folds = new ArrayList<Fold>();
		Stack<String> tagNameStack = new Stack<String>();
		boolean inSublanguage = false;

		Fold currentFold = null;
		int lineCount = textArea.getLineCount();
		boolean inMLC = false;
		boolean inJSMLC = false;
		TagCloseInfo tci = new TagCloseInfo();

		try {

			for (int line=0; line<lineCount; line++) {

				Token t = textArea.getTokenListForLine(line);
				while (t!=null && t.isPaintable()) {

					// If we're folding PHP.  Note that PHP folding can only be
					// "one level deep," so our logic here is simple.
					if (language>=0 && t.getType()==Token.SEPARATOR) {

						// <?, <?php, <%, <%!, ...
						if (t.startsWith(LANG_START[language])) {
							if (currentFold==null) {
								currentFold = new Fold(FoldType.CODE, textArea, t.getOffset());
								folds.add(currentFold);
							}
							else {
								currentFold = currentFold.createChild(FoldType.CODE, t.getOffset());
							}
							inSublanguage = true;
						}

						// ?> or %>
						else if (t.startsWith(LANG_END[language])) {
							int phpEnd = t.getEndOffset() - 1;
							currentFold.setEndOffset(phpEnd);
							Fold parentFold = currentFold.getParent();
							// Don't add fold markers for single-line blocks
							if (currentFold.isOnSingleLine()) {
								removeFold(currentFold, folds);
							}
							currentFold = parentFold;
							inSublanguage = false;
							t = t.getNextToken();
							continue;
						}

					}

					if (!inSublanguage) {

						if (t.getType()==Token.COMMENT_MULTILINE) {

							// Continuing an MLC from a previous line
							if (inMLC) {
								// Found the end of the MLC starting on a previous line...
								if (t.endsWith(MLC_END)) {
									int mlcEnd = t.getEndOffset() - 1;
									currentFold.setEndOffset(mlcEnd);
									Fold parentFold = currentFold.getParent();
									// Don't add fold markers for single-line blocks
									if (currentFold.isOnSingleLine()) {
										removeFold(currentFold, folds);
									}
									currentFold = parentFold;
									inMLC = false;
								}
								// Otherwise, this MLC is continuing on to yet
								// another line.
							}
	
							// Continuing a JS MLC from a previous line
							else if (inJSMLC) {
								// Found the end of the MLC starting on a previous line...
								if (t.endsWith(JSP_COMMENT_END)) {
									int mlcEnd = t.getEndOffset() - 1;
									currentFold.setEndOffset(mlcEnd);
									Fold parentFold = currentFold.getParent();
									// Don't add fold markers for single-line blocks
									if (currentFold.isOnSingleLine()) {
										removeFold(currentFold, folds);
									}
									currentFold = parentFold;
									inJSMLC = false;
								}
								// Otherwise, this MLC is continuing on to yet
								// another line.
							}

							// Starting a MLC that ends on a later line...
							else if (t.startsWith(MLC_START) && !t.endsWith(MLC_END)) {
								if (currentFold==null) {
									currentFold = new Fold(FoldType.COMMENT, textArea, t.getOffset());
									folds.add(currentFold);
								}
								else {
									currentFold = currentFold.createChild(FoldType.COMMENT, t.getOffset());
								}
								inMLC = true;
							}

							// Starting a JSP comment that ends on a later line...
							else if (language==LANGUAGE_JSP &&
									t.startsWith(JSP_COMMENT_START) &&
									!t.endsWith(JSP_COMMENT_END)) {
								if (currentFold==null) {
									currentFold = new Fold(FoldType.COMMENT, textArea, t.getOffset());
									folds.add(currentFold);
								}
								else {
									currentFold = currentFold.createChild(FoldType.COMMENT, t.getOffset());
								}
								inJSMLC = true;
							}

						}

						// If we're starting a new tag...
						else if (t.isSingleChar(Token.MARKUP_TAG_DELIMITER, '<')) {
							Token tagStartToken = t;
							Token tagNameToken = t.getNextToken();
							if (isFoldableTag(tagNameToken)) {
								int newLine = getTagCloseInfo(tagNameToken, textArea, line, tci);
								if (tci.line==-1) { // EOF reached before end of tag
									return folds;
								}
								// We have found either ">" or "/>" with tci.
								//System.out.println(line + ", "+ tci + ", " + t);
								Token tagCloseToken = tci.closeToken;
								if (tagCloseToken.isSingleChar(Token.MARKUP_TAG_DELIMITER, '>')) {
									if (currentFold==null) {
										currentFold = new Fold(FoldType.CODE, textArea, tagStartToken.getOffset());
										folds.add(currentFold);
									}
									else {
										currentFold = currentFold.createChild(FoldType.CODE, tagStartToken.getOffset());
									}
									tagNameStack.push(tagNameToken.getLexeme());
								}
								t = tagCloseToken; // Continue parsing after tag
								line = newLine;
							}
						}

						// If we've found a closing tag (e.g. "</div>").
						else if (t.is(Token.MARKUP_TAG_DELIMITER, MARKUP_CLOSING_TAG_START)) {
							if (currentFold!=null) {
								Token tagNameToken = t.getNextToken();
								if (isFoldableTag(tagNameToken) &&
										isEndOfLastFold(tagNameStack, tagNameToken)) {
									tagNameStack.pop();
									currentFold.setEndOffset(t.getOffset());
									Fold parentFold = currentFold.getParent();
									// Don't add fold markers for single-line blocks
									if (currentFold.isOnSingleLine()) {
										removeFold(currentFold, folds);
									}
									currentFold = parentFold;
									t = tagNameToken;
								}
							}
						}

					}

					t = t.getNextToken();

				}

			}

		} catch (BadLocationException ble) { // Should never happen
			ble.printStackTrace();
		}

		return folds;
	
	}


	/**
	 * Grabs the token representing the closing of a tag (i.e.
	 * "<code>&gt;</code>" or "<code>/&gt;</code>").  This should only be
	 * called after a tag name has been parsed, to ensure  the "closing" of
	 * other tags is not identified.
	 * 
	 * @param tagNameToken The token denoting the name of the tag.
	 * @param textArea The text area whose contents are being parsed.
	 * @param line The line we're currently on.
	 * @param info On return, information about the closing of the tag is
	 *        returned in this object.
	 * @return The line number of the closing tag token.
	 */
	private int getTagCloseInfo(Token tagNameToken, RSyntaxTextArea textArea,
			int line, TagCloseInfo info) {

		info.reset();
		Token t = tagNameToken.getNextToken();

		do {

			while (t!=null && t.getType()!=Token.MARKUP_TAG_DELIMITER) {
				t = t.getNextToken();
			}

			if (t!=null) {
				info.closeToken = t;
				info.line = line;
				break;
			}

		} while (++line<textArea.getLineCount() &&
				(t=textArea.getTokenListForLine(line))!=null);

		return line;

	}


	/**
	 * Returns whether a closing tag ("<code>&lt;/...&gt;</code>") with a
	 * specific name is the closing tag of our current fold region.
	 *
	 * @param tagNameStack The stack of fold regions.
	 * @param tagNameToken The tag name of the most recently parsed closing
	 *        tag.
	 * @return Whether it's the end of the current fold region.
	 */
	private static final boolean isEndOfLastFold(Stack<String> tagNameStack,
			Token tagNameToken) {
		if (tagNameToken!=null && !tagNameStack.isEmpty()) {
			return tagNameToken.getLexeme().equalsIgnoreCase(tagNameStack.peek());
		}
		return false;
	}


	/**
	 * Returns whether a tag is one we allow as a foldable region.
	 *
	 * @param tagNameToken The tag's name token.  This may be <code>null</code>.
	 * @return Whether this tag can be a foldable region.
	 */
	private static final boolean isFoldableTag(Token tagNameToken) {
		return tagNameToken!=null &&
				FOLDABLE_TAGS.contains(tagNameToken.getLexeme().toLowerCase());
	}


	/**
	 * If this fold has a parent fold, this method removes it from its parent.
	 * Otherwise, it's assumed to be the most recent (top-level) fold in the
	 * <code>folds</code> list, and is removed from that.
	 *
	 * @param fold The fold to remove.
	 * @param folds The list of top-level folds.
	 */
	private static final void removeFold(Fold fold, List<Fold> folds) {
		if (!fold.removeFromParent()) {
			folds.remove(folds.size()-1);
		}
	}


	/**
	 * A simple wrapper for the token denoting the closing of a tag (i.e.
	 * "<code>&gt;</code>" or "<code>/&gt;</code>").
	 */
	private static class TagCloseInfo {

		private Token closeToken;
		private int line;

		public void reset() {
			closeToken = null;
			line = -1;
		}

		@Override
		public String toString() {
			return "[TagCloseInfo: " +
					"closeToken=" + closeToken +
					", line=" + line +
					"]";
		}

	}


}