/*
parser.js
Copyright 2004 Brian Taylor, David Barnett

    This file is part of jsEDIT.

    jsEDIT is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    jsEDIT is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with jsEDIT; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

function TextToken(str, type, subtype, level)
{
	this.str = str;
	this.type = type;
	this.subtype = subtype;
	this.level = level;
}

function calcHash(str)
{
	var val = 0;
	var l = 0;
	for (var i = 0; i < str.length; i++)
	{
		val += str.charCodeAt(i);
		l += 1;
	}
	return val;
}

function findHash(key, table, depth, size)
{
	var hash = calcHash(key);
	if ((table[hash % size] == key) && (table[hash % size].length > 0))
		return true;
	else {
		var a = 1;
		var dest = hash % size;
		for (var i = 0; i < depth; ++i) {
			if ((table[dest % size] == key) && (table[dest % size].length > 0))
				return true;
			else {
				dest += a;
				a += 1;
			}
		}
	}
	return false;
}

// arguments: tokenIndex (int), tokens (array of objects),
// skip all tokens except those specified (true) or skip specified tokens (false),
// type (int), str (string), type (int), str (string)...
// -1 is wildcard type and null is wildcard string
function skipTokens(tokenIndex, tokens, skipSpecified)
{
	var stopSkip;
	while (tokenIndex + 1 < tokens.length)
	{
		stopSkip = true;
		for (var i = 3; i < arguments.length; i += 2)
		{
			var typeMatch = ((tokens[tokenIndex + 1].type == arguments[i]) || (arguments[i] == -1));
			var strMatch = ((tokens[tokenIndex + 1].str == arguments[i + 1]) || (arguments[i + 1] == null));

			var match = (typeMatch && strMatch);

			if (!skipSpecified)
				match = !match;

			if (match)
			{
				stopSkip = false;
				break;
			}
		}

		if (!stopSkip)
			++tokenIndex;
		else
			break;
	}
	return tokenIndex;
}

// add underline if cursor is on char
// change special characters to printable equivalents
function charFormat(formatChar, charLine, charCol)
{
	//Replace special characters for output
	if(synFind.length == synRep.length)	//If they are even arrays
		for(var i = 0; i < synFind.length; ++i)
			if (synFind[i] == formatChar)
			{
				formatChar = synRep[i];
				break;
			}
	
	if ((charCol == curCol) &&
		(charLine == curLine))	// underline if cursor
		formatChar = "<u>" + formatChar + "</u>";

	return formatChar;
}

function getFirstMatch(searchString, regExpList)
{
	var firstMatchIndex = searchString.length;
	for (var i = 0; i < regExpList.length; ++i)
		if (regExpList[i].test(searchString))
			firstMatchIndex = Math.min(firstMatchIndex, searchString.search(regExpList[i]));
	return firstMatchIndex;
}

function findVarDef(tokenIndex, tokens, scopeStack, nextScopeList, lineScopeList)
{
	for (var i = 0; i < varDefPatterns.length; ++i)
		if (tokens[tokenIndex].str.search(varDefPatterns[i]) == 0)
		{
			varDefProcessFuncs[i](tokenIndex, tokens, scopeStack, nextScopeList, lineScopeList);
			break;
		}
}

function mergeRegExps()
{
	var mergedRegExp = "";
	for (var i = 0; i < arguments.length; ++i)
	{
		var curRegExp = arguments[i].toString();
		if (curRegExp == "/(?:)/")
			curRegExp = "//";
		mergedRegExp += curRegExp.substr(1, curRegExp.length - 2);	// remove delimiters
	}
	
	return new RegExp(mergedRegExp);
}

function changeQuant(changeStr, newQuant)
{
	changeStr = changeStr.toString();
	if (changeStr != "/(?:)/")
	{
		var quant = changeStr.charAt(changeStr.length - 2);
		if (((quant == "*") || (quant == "+") || (quant == "?")) &&
			(changeStr.charAt(changeStr.length - 3) != "\\"))
			return new RegExp(changeStr.substring(1, changeStr.length - 2) + newQuant);
		return new RegExp(changeStr.substring(1, changeStr.length - 1) + newQuant);
	}
	return new RegExp(changeStr.substr(1, changeStr.length - 2));
}

function getPreTokenLength(parseStartLine, tokens)
{
	var curLine = 0;
	var tokenIndex = 0;
	var preTokenLength = 0;

	while (tokenIndex < tokens.length)
	{
		if (tokens[tokenIndex].type == TOKEN_TYPE_NEWLINE)
		{
			++curLine;
			if (curLine == parseStartLine)
			{
				preTokenLength = tokenIndex + 1;
				break;
			}
		}

		++tokenIndex;
	}
		
	return preTokenLength;
}

function getPreHaltRegExps()
{
	var preHaltRegExps = new Array();

	// add all multiline tokens and end of line token
	for (var type = 0; type < NUM_TYPES; ++type)
	{
		if ((tokenInterrupts[type].length > 0) ||
			(type == TOKEN_TYPE_COMMENT) ||
			(type == TOKEN_TYPE_OBRACE) ||
			(type == TOKEN_TYPE_CBRACE) ||
			(type == TOKEN_TYPE_NEWLINE))
			preHaltRegExps.push(mergeRegExps(tokenRegExps[type][0], tokenRegExps[type][1]));
	}

	// add variable definitions
	for (var i = 0; i < varDefPatterns.length; ++i)
		preHaltRegExps.push(varDefPatterns[i]);

	return preHaltRegExps;
}

function tokenFormat(curLine, curCol, curCharLine, tokens, displayText)
{
	var curCharCol = 0;
	var tempTokens = new Array();
	var tempText = "";

	for (var curTokenIndex = 0; curTokenIndex < tokens.length; ++curTokenIndex)
	{
		var token = tokens[curTokenIndex];
		
		if (token.type == TOKEN_TYPE_NEWLINE)
		{
			if ((curCol >= curCharCol) &&
				(curLine == curCharLine))
				tempText += charFormat(" ", curCharLine, curCharCol);
			displayText[curCharLine++] = tempText;
			curCharCol = 0;
			tempText = "";
			continue;
		}
	
		var tempToken = "";
		for (var chIndex = 0; chIndex < token.str.length; ++chIndex)
			tempToken += charFormat(token.str.charAt(chIndex), curCharLine, curCharCol++);

		if (tempToken != "")
		{
			if (token.subtype == -1)
				tempText += tokenTags[token.type][0] + tokenColors[token.type] + tokenTags[token.type][1] + tempToken + tokenTags[token.type][2];
			else
				tempText += subTokenTags[token.subtype][0] + subTokenColors[token.subtype] + subTokenTags[token.subtype][1] + tempToken + subTokenTags[token.subtype][2];
		}
	}
	if ((curCol >= curCharCol) &&
		(curLine == curCharLine))
		tempText += charFormat(" ", curCharLine, curCharCol);
	displayText[curCharLine] = tempText;
}

function preParse(parseText, tokens)
{
	var preLine = 0;
	var preCol = 0;
	var tokenIndex = 0;
	var parseString = parseText.join("\n");

	if ((tokenIndex < tokens.length) &&
		(tokens[tokenIndex] == parseString.substring(preCol, tokens[tokenIndex].str.length)))
		while (tokenIndex + 1 < tokens.length)
		{
			if (tokens[tokenIndex + 1].str != parseString.substring(preCol, tokens[tokenIndex + 1].str.length))
				break;
			else
			{
				if (tokenIndex >= 0)
				{
					if (tokens[tokenIndex].type == TOKEN_TYPE_NEWLINE)
					{
						++preLine;
						preCol = 0;
					}
					else
						preCol += tokens[tokenIndex].str.length;
	
					++tokenIndex;
				}
			}
		}

	while ((tokenIndex < tokens.length) &&
		(tokens[tokenIndex].level > 0))
	{
		if (tokens[tokenIndex].type == TOKEN_TYPE_NEWLINE)
			preCol = parseText[--preLine].length;
		else
			preCol -= tokens[tokenIndex].str.length;
		--tokenIndex;
	}

	tokens.length = tokenIndex;

	return new Array(preLine, preCol);
}

function fastParse(parseText, toLineIndex, curCharLine, curCharCol, tokens)
{
	var wrappedString = new Array();
	wrappedString[0] = parseText.join("\n");

	var firstTokenIndex = getFirstMatch(wrappedString[0], preHaltRegExps);
	wrappedString[0] = wrappedString[0].substr(firstTokenIndex);

	while ((curCharLine < toLineIndex) &&
		(wrappedString[0].length > 0))
	{
		do
		{
			var returnArray = parseToken(wrappedString, curCharCol, tokens);
			curCharLine += returnArray[0];
			curCharCol = returnArray[1];
		} while ((curCharLine < toLineIndex) &&
			(wrappedString[0].length > 0) &&
			(tokens[tokens.length - 1].type != TOKEN_TYPE_NEWLINE));

		var firstTokenIndex = getFirstMatch(wrappedString[0], preHaltRegExps);
		wrappedString[0] = wrappedString[0].substr(firstTokenIndex);
	}

	return new Array(curCharLine, curCharCol);
}

function parseToken(wrappedString, parseCol, tokens)
{
	var parseLine = 0;
	var typeStack = new Array();	// since interrupted tokens go (1--(2--(3-)-2)-1), need to keep track of previous types
	var curTokenStart;
	var lastTokenEndLength;
	var lastTokenStartLength;
	var isTokenStart = true;	// token is first opening (not continuing)

	do
	{
		for (var type = 0; type < NUM_TYPES; ++type)
		{
			// skip this type if in the middle of another token that can't be
			// interrupted by this type
			if (isTokenStart)
			{
				if (typeStack.length > 0)
					if (!contains(tokenInterrupts[typeStack[typeStack.length - 1]], type))
						continue;
			}
			else
				if (type != typeStack[typeStack.length - 1])
					continue;

			var curRegExp = (isTokenStart ? tokenRegExpsFull[type] : tokenRegExpsCont[type]);
			if (wrappedString[0].search(curRegExp) == 0)
			{
				var curRegExpNoEnd = (isTokenStart ? tokenRegExpsNoEnd[type] : tokenRegExps[type][1]);

				curTokenStart = wrappedString[0].match(curRegExp)[0].length;
				lastTokenEndLength = curTokenStart - wrappedString[0].match(curRegExpNoEnd)[0].length;
				lastTokenStartLength = (isTokenStart ? wrappedString[0].match(tokenRegExps[type][0])[0].length : 0);

				if (isTokenStart)
					typeStack.push(type);

				break;
			}
		}

		var interruptRegExps = new Array();
		var curTokenInterrupts = tokenInterrupts[typeStack[typeStack.length - 1]];

		for (var i = 0; i < curTokenInterrupts.length; ++i)
			interruptRegExps.push(tokenRegExpsNoEnd[curTokenInterrupts[i]]);

		var tempCTS = getFirstMatch(wrappedString[0].substr(lastTokenStartLength, curTokenStart), interruptRegExps) + lastTokenStartLength;
		isTokenStart = (tempCTS < curTokenStart - lastTokenEndLength);
		if (isTokenStart)
			curTokenStart = tempCTS;
			
		if (curTokenStart != 0)
		{
			tokens.push(new TextToken(wrappedString[0].substr(0, curTokenStart), typeStack[typeStack.length - 1]));
			tokens[tokens.length - 1].level = typeStack.length - 1;
		}

		// if token is closing
		if (!isTokenStart)
			typeStack.pop();

		if (curTokenStart != 0)
		{
			if (tokens[tokens.length - 1].type == TOKEN_TYPE_NEWLINE)
			{
				++parseLine;
				parseCol = 0;
			}
			else
				parseCol += tokens[tokens.length - 1].str.length;

			wrappedString[0] = wrappedString[0].substr(curTokenStart);
		}

	} while ((typeStack.length > 0) &&
		(wrappedString[0].length > 0));

	return new Array(parseLine, parseCol);
}

function assignSubTypes(tokens)
{
	var scopeStack = new Array(new Array());
	var nextScopeList = new Array();
	var lineScopeList = new Array();

	for (var i = 0; i < tokens.length; ++i)
	{
		findVarDef(i, tokens, scopeStack, nextScopeList, lineScopeList);
		if (tokens[i].type == TOKEN_TYPE_OBRACE)
		{
			scopeStack.push(copyArray(nextScopeList));
			nextScopeList.length = 0;
		}
		else if (tokens[i].type == TOKEN_TYPE_CBRACE)
		{
			scopeStack.pop();
			nextScopeList.length = 0;
		}
		else if ((tokens[i].type == TOKEN_TYPE_PARENTHESES) &&
			(tokens[i].str == ")"))
			lineScopeList.length = 0;

		tokens[i].subtype = getSubType(tokens[i], scopeStack, lineScopeList);
	}
}

function parseSyntax(parseText, displayText, parseStartLine, parseEndLine, curLine, curCol, tokens)
{
	// since the current line is where all the modifications will happen,
	// if all lines have been parsed once, only up to the current line plus
	// any remaining multiline tokens must be parsed

	var returnArray = preParse(parseText, tokens);
	var curCharLine = returnArray[0];
	var curCharCol = returnArray[1];

	var returnArray = fastParse(parseText, parseStartLine, curCharLine, curCharCol, tokens);
	curCharLine = returnArray[0];
	curCharCol = returnArray[1];

	var wrappedParseString = new Array(parseText.slice(curCharLine).join('\n').substr(curCharCol));

	while ((wrappedParseString[0].length > 0) &&
		(curCharLine <= parseEndLine))
	{
		var returnArray = parseToken(wrappedParseString, curCharCol, tokens);
		curCharLine += returnArray[0];
		curCharCol = returnArray[1];
	}

	assignSubTypes(tokens);

	var preTokenLength = getPreTokenLength(parseStartLine, tokens);
	shrinkArrayLeft(tokens, preTokenLength);

	// process tokens
	tokenFormat(curLine, curCol, parseStartLine, tokens, displayText);
}
