/*******************************************************************************
 * Copyright (c) 2007 Exadel, Inc. and Red Hat, Inc.
 * Distributed under license by Red Hat, Inc. All rights reserved.
 * This program is made available under the terms of the
 * Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Exadel, Inc. and Red Hat, Inc. - initial API and implementation
 ******************************************************************************/ 
package org.jboss.tools.jsf.ui.adopt;

import java.util.*;
import org.eclipse.jface.text.IDocument;

public class JSPTokenizer {
	static int ROOT = 0;
	static int TEXT = 1;
	static int TAG = 2;
	static int TAG_CLOSING = 3;
	static int JSP = 4;
	static int DIRECTIVE = 5;
	static int COMMENT = 6;
	static int DOCTYPE = 7;

	String text;
	StringBuilder sb = new StringBuilder();
	Token root;
	
	int selectionStart;
	int selectionEnd;
	
	int start = -1;
	int end = -1;
	
	public Token parse(IDocument document) {
		selectionStart = 0;
		selectionEnd = document.getLength();
		text = document.get();
		root = new Token(ROOT, "", 0, text.length(), null); //$NON-NLS-1$
		root.indent = ""; //$NON-NLS-1$
		root.indentLevel = -1;
		tokenize();
		return root;
	}
	
	private void tokenize() {
		int cursor = 0;
		Token current = root;
		Token last = null;
		while(cursor < text.length()) {
			int p = text.indexOf('<', cursor);
			if(p < 0) {
				current.addChild(last = createTag(TEXT, "", cursor, text.length() - cursor, last)); //$NON-NLS-1$
				cursor = text.length();
			} else {
				if(p > cursor) {
					current.addChild(last = createTag(TEXT, "", cursor, p - cursor, last)); //$NON-NLS-1$
					cursor = p;
				}
				if(isStringStart(cursor, "<!DOCTYPE")) { //$NON-NLS-1$
					int l = "<!DOCTYPE".length(); //$NON-NLS-1$
					int q = text.indexOf(">", cursor); //$NON-NLS-1$
					int nc = (q < 0) ? text.length() : q + 1;
					int k = skipToName(cursor + l, nc);
					String tag = readTag(cursor + l + k);
					current.addChild(last = createTag(DOCTYPE, tag, cursor, nc - cursor, last));
					int ab = cursor + l + k + tag.length();
					int al = nc - ab;
					last.attributes = getDoctype(text, ab, al);
					cursor = nc;
				} else if(isStringStart(cursor, "<%@")) {					 //$NON-NLS-1$
					int q = text.indexOf("%>", cursor); //$NON-NLS-1$
					int nc = (q < 0) ? text.length() : q + 2;
					int k = skipToName(cursor + 3, nc);
					String tag = readTag(cursor + 3 + k);
					current.addChild(last = createTag(DIRECTIVE, tag, cursor, nc - cursor, last));
					int ab = cursor + 3 + k + tag.length();
					int al = nc - ab;
					last.attributes = getAttributes(text, ab, al);
					cursor = nc;
				} else if(isStringStart(cursor, "<%")) { //$NON-NLS-1$
					int q = text.indexOf("%>", cursor); //$NON-NLS-1$
					int nc = (q < 0) ? text.length() : q + 2;
					current.addChild(last = createTag(JSP, "", cursor, nc - cursor, last)); //$NON-NLS-1$
					cursor = nc;
				} else if(isStringStart(cursor, "<!--")) { //$NON-NLS-1$
					int q = text.indexOf("-->", cursor); //$NON-NLS-1$
					int nc = (q < 0) ? text.length() : q + 3;
					current.addChild(last = createTag(COMMENT, "", cursor, nc - cursor, last)); //$NON-NLS-1$
					cursor = nc;
				} else if(isStringStart(cursor, "<!")) { //$NON-NLS-1$
					///This is doctype
					int q = text.indexOf(">", cursor); //$NON-NLS-1$
					int nc = (q < 0) ? text.length() : q + 1;
					current.addChild(last = createTag(COMMENT, "", cursor, nc - cursor, last)); //$NON-NLS-1$
					cursor = nc;
				} else if(isStringStart(cursor, "</")) { //$NON-NLS-1$
					String tag = readTag(cursor + 2);
					int q = text.indexOf(">", cursor); //$NON-NLS-1$
					int nc = (q < 0) ? text.length() : q + 1;
					last = createTag(TAG_CLOSING, tag, cursor, nc - cursor, last);
					current = findParent(current, last);
					current.addChild(last);
					cursor = nc;
				} else {
					String tag = readTag(cursor + 1);
					int q = findTagClosingSymbol(cursor);
					int nc = (q < 0) ? text.length() : q + 1;
					last = createTag(TAG, tag, cursor, nc - cursor, last);
					int ab = cursor + 1 + tag.length();
					int al = nc - ab;
					last.attributes = getAttributes(text, ab, al);
					if(isOptionallyClosed(tag)) {
						current = findParentForOptionallyClosedTag(current, tag);
					}
					current.addChild(last);
					cursor = nc;
					if(q > 0 && text.charAt(q - 1) != '/' && areChildrenAllowed(tag)) {
						current = last;
					}
				}
			}
		}
	}
	
	private Token findParent(Token current, Token t) {
		Token c = current;
		while(c.kind != ROOT) {		
			if(c.name.equals(t.name)) return c.parent;
			c = c.parent;
		}
		return current;
	}
	
	private boolean isOptionallyClosed(String name) {
		return ".body.p.dt.dd.li.ol.option.thead.tfoot.tbody.colgroup.tr.td.th.head.html.".indexOf(name.toLowerCase()) >= 0; //$NON-NLS-1$
	}
	
	private Token findParentForOptionallyClosedTag(Token current, String name) {
		String n1 = name.toLowerCase();
		String n2 = current.name.toLowerCase();
		if("p".equals(n1)) { //$NON-NLS-1$
			if(n2.equals("p")) return current.parent; //$NON-NLS-1$
		} else if("tr".equals(n1)) { //$NON-NLS-1$
			if(n2.equals("tr")) return current.parent; //$NON-NLS-1$
			if(n2.equals("th") || n2.equals("td") || n2.equals("p")) return findParentForOptionallyClosedTag(current.parent, name); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		} else if("td".equals(n1) || "th".equals(n1)) {  //$NON-NLS-1$ //$NON-NLS-2$
			if(n2.equals("th") || n2.equals("td")) return current.parent; //$NON-NLS-1$ //$NON-NLS-2$
			if(n2.equals("p")) return findParentForOptionallyClosedTag(current.parent, name); //$NON-NLS-1$
		}
		return current;
	}
	
	private boolean areChildrenAllowed(String name) {
		return ".br.area.link.img.param.hr.input.col.isindex.base.meta.".indexOf("." + name.toLowerCase() + ".") < 0; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
	}
	
	private Token createTag(int kind, String name, int off, int length, Token previous) {
		Token t = new Token(kind, name, off, length, previous);
		t.indentLength = (kind == TEXT) ? -1 : computeIndentLength(off);
		if(previous != null && previous.kind == JSPTokenizer.TEXT && t.indentLength > 0) {
			previous.length -= t.indentLength;
			t.off -= t.indentLength;
			t.length += t.indentLength;
		}
		return t;
	}
	
	private boolean isStringStart(int c, String s) {
		if(text.length() <= c + s.length()) return false;
		for (int i = 0; i < s.length(); i++) {
			if(text.charAt(c + i) != s.charAt(i)) return false;
		}
		return true;
	}
	
	private int skipToName(int b, int e) {
		int t = b;
		while(t < e && !isNameChar(text.charAt(t))) {
			++t;
		}
		return t - b;
	}
	
	private String readTag(int c) {
		int k = c;
		while(k < text.length() && isNameChar(text.charAt(k))) ++k;
		return text.substring(c, k);
	}
	
	private boolean isNameChar(char ch) {
		return Character.isJavaIdentifierPart(ch) || ch == '-' || ch == ':';
	}
	
	private int computeIndentLength(int off) {
		off--;
		int l = 0;
		while(off >= 0) {
			char ch = text.charAt(off);
			if(ch == '\n' || ch == '\r') return l;
			if(!Character.isWhitespace(ch)) return -1;
			++l;
			--off;
		}
		return (off < 0) ? l : -1;
	}
	
	public int findTagClosingSymbol(int i) {
		int l = text.length();
		char quota = '\0';
		while(i < l) {
			char ch = text.charAt(i);
			if(quota != '\0') {
				if(ch == quota) quota = '\0';
			} else if(ch == '\'' || ch == '"') {
				quota = ch;
			} else if(ch == '>') {
				return i;
			}
			++i;
		}
		return -1;
	}

	public Token getTokenAt(int pos) {
		return getTokenAt(root, pos);
	}
	
	public Token getTokenAt(Token t, int pos) {
		if(t == null || t.off > pos) return null;
		if(t.off + t.length > pos) {
			return (t.firstChild == null || t.firstChild.off > pos) ? t : getTokenAt(t.firstChild, pos);
		}
		if(t.nextSibling != null && t.nextSibling.off <= pos) {
			return getTokenAt(t.nextSibling, pos);
		}
		return (t.firstChild == null) ? t : getTokenAt(t.firstChild, pos);
	}
	
	public boolean isInTagAttributeValue(int pos) {
		Token t = getTokenAt(root, pos);
		if(t == null || t.kind != TAG) return false;
		char quote = '\0';
		for (int i = root.off; i < text.length() && i < pos; i++) {
			char ch = text.charAt(i);
			if(ch == quote) quote = '\0';
			else if(ch == '"' || ch == '\'') quote = ch;
		}
		return (quote != '\0');
	}

    static Properties getAttributes(String text, int off, int length) {
    	Properties p = new Properties();
    	int NOTHING = 0;
    	int NAME = 1;
    	int VALUE = 2;
    	int state = 0;
    	char quote = '\0';
    	StringBuilder name = new StringBuilder();
    	StringBuilder value = new StringBuilder();
    	for (int i = 0; i < length; i++) {
    		char ch = text.charAt(i + off);
    		if(state == NOTHING) {
    			if(" \t\r\n".indexOf(ch) >= 0) continue; //$NON-NLS-1$
    			if("/\\><%".indexOf(ch) >= 0) break; //$NON-NLS-1$
    			state = NAME;
    			name.append(ch);
    		} else if(state == NAME) {
    			if(ch == '=') {
    				state = VALUE;
    			} else {
    				name.append(ch);
    			}
    		} else if(state == VALUE) {				
    			if(ch == quote) {
    				String n = name.toString().trim();
    				String v = value.toString();
    				name.setLength(0);
    				value.setLength(0);
    				p.setProperty(n, v);
    				state = NOTHING;
    				quote = '\0';
    			} else if(quote == '\0' && (ch == '"' || ch == '\'')) {
    				quote = ch;    				
    			} else if(quote != '\0') {
    				value.append(ch);
    			}
    		}
    	}
    	return p;
    }

    static Properties getDoctype(String text, int off, int length) {
    	Properties p = new Properties();
    	int b = off;
    	int i = text.indexOf("PUBLIC", off); //$NON-NLS-1$
    	if(i >= 0) {
    		int i1 = text.indexOf('"', i);
    		if(i1 >= 0) {
    			int i2 = text.indexOf('"', i1 + 1);
    			if(i2 >= 0) {
    				p.setProperty("public", text.substring(i1 + 1, i2)); //$NON-NLS-1$
    				b = i2 + 1;
    			}
    		}
    	}
    	int i1 = text.indexOf('"', b);
		if(i1 >= 0) {
			int i2 = text.indexOf('"', i1 + 1);
			if(i2 >= 0) {
				p.setProperty("system", text.substring(i1 + 1, i2)); //$NON-NLS-1$
			}
		}
    	return p;
    }

}
