function TextPhrase() {
	this.words = new Array();
	this.wordIndex = 0;

	this.addWord = TextPhrase_AddWord;
	this.length = TextPhrase_Length;
	this.reset = TextPhrase_Reset;
	this.complete = TextPhrase_Complete;
	this.match = TextPhrase_Match;
	this.toString = TextPhrase_ToString;
}

function TextPhrase_ToString() {
	return this.words.join(' ');
}

function TextPhrase_Match(word) {
	if (this.wordIndex < this.words.length && this.words[this.wordIndex] == word) {
		++this.wordIndex;
		return true;
	}
	this.reset();
	return false;
}

function TextPhrase_AddWord(word) {
	if (word.length > 0) 
		this.words[this.words.length] = word.toLowerCase();
}

function TextPhrase_Length() {
	return this.words.length;
}

function TextPhrase_Reset() {
	this.wordIndex = 0; 
}

function TextPhrase_Complete() {
	return this.wordIndex == this.words.length;
}

function TextHighlight(terms, doHighlightTerms) {
	// members
	this.config = {
		highlightBefore : '<span style="background: #fdc963">',
		highlightAfter  : '</span>'
	};
	this.termsRegExp = null;
	this.doHighlightTerms = (doHighlightTerms && (terms.length > 0));
	this.phrases = new Array();
	this.wordRegexp = new RegExp('a');
	this.idRegexp = new RegExp('a', 'g');
	this.idRegexp.compile('\\${i}', 'g');

	// methods
	this.setQueryString = TextHighlight_SetQueryString;
	this.termsHighlight = TextHighlight_TermsHighlight;
	this.phrasesHighlight = TextHighlight_PhrasesHighlight;
	this.highlight = TextHighlight_Highlight;
	this.phrasesComplete = TextHighlight_PhrasesComplete;
	this.phrasesReset = TextHighlight_PhrasesReset;
	this.phrasesMatch = TextHighlight_PhrasesMatch;
	
	// initialization
	this.wordRegexp.compile('[\\u0030-\\u0039\\u0041-\\u005a\\u0061-\\u007a\\u00aa\\u00b2\\u00b3\\u00b5\\u00b9\\u00ba\\u00bc-\\u00be\\u00c0-\\u00d6\\u00d8-\\u00f6\\u00f8-\\u021f\\u0222-\\u0233\\u0250-\\u02ad\\u02b0-\\u02b8\\u02bb-\\u02c1\\u02d0\\u02d1\\u02e0-\\u02e4\\u02ee\\u037a\\u0386\\u0388-\\u038a\\u038c\\u038e-\\u03a1\\u03a3-\\u03ce\\u03d0-\\u03d7\\u03da-\\u03f3\\u0400-\\u0481\\u048c-\\u04c4\\u04c7\\u04c8\\u04cb\\u04cc\\u04d0-\\u04f5\\u04f8\\u04f9\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05d0-\\u05ea\\u05f0-\\u05f2\\u0621-\\u063a\\u0640-\\u064a\\u0660-\\u0669\\u0671-\\u06d3\\u06d5\\u06e5\\u06e6\\u06f0-\\u06fc\\u0710\\u0712-\\u072c\\u0780-\\u07a5\\u0905-\\u0939\\u093d\\u0950\\u0958-\\u0961\\u0966-\\u096f\\u0985-\\u098c\\u098f\\u0990\\u0993-\\u09a8\\u09aa-\\u09b0\\u09b2\\u09b6-\\u09b9\\u09dc\\u09dd\\u09df-\\u09e1\\u09e6-\\u09f1\\u09f4-\\u09f9\\u0a05-\\u0a0a\\u0a0f\\u0a10\\u0a13-\\u0a28\\u0a2a-\\u0a30\\u0a32\\u0a33\\u0a35\\u0a36\\u0a38\\u0a39\\u0a59-\\u0a5c\\u0a5e\\u0a66-\\u0a6f\\u0a72-\\u0a74\\u0a85-\\u0a8b\\u0a8d\\u0a8f-\\u0a91\\u0a93-\\u0aa8\\u0aaa-\\u0ab0\\u0ab2\\u0ab3\\u0ab5-\\u0ab9\\u0abd\\u0ad0\\u0ae0\\u0ae6-\\u0aef\\u0b05-\\u0b0c\\u0b0f\\u0b10\\u0b13-\\u0b28\\u0b2a-\\u0b30\\u0b32\\u0b33\\u0b36-\\u0b39\\u0b3d\\u0b5c\\u0b5d\\u0b5f-\\u0b61\\u0b66-\\u0b6f\\u0b85-\\u0b8a\\u0b8e-\\u0b90\\u0b92-\\u0b95\\u0b99\\u0b9a\\u0b9c\\u0b9e\\u0b9f\\u0ba3\\u0ba4\\u0ba8-\\u0baa\\u0bae-\\u0bb5\\u0bb7-\\u0bb9\\u0be7-\\u0bf2\\u0c05-\\u0c0c\\u0c0e-\\u0c10\\u0c12-\\u0c28\\u0c2a-\\u0c33\\u0c35-\\u0c39\\u0c60\\u0c61\\u0c66-\\u0c6f\\u0c85-\\u0c8c\\u0c8e-\\u0c90\\u0c92-\\u0ca8\\u0caa-\\u0cb3\\u0cb5-\\u0cb9\\u0cde\\u0ce0\\u0ce1\\u0ce6-\\u0cef\\u0d05-\\u0d0c\\u0d0e-\\u0d10\\u0d12-\\u0d28\\u0d2a-\\u0d39\\u0d60\\u0d61\\u0d66-\\u0d6f\\u0d85-\\u0d96\\u0d9a-\\u0db1\\u0db3-\\u0dbb\\u0dbd\\u0dc0-\\u0dc6\\u0e01-\\u0e30\\u0e32\\u0e33\\u0e40-\\u0e46\\u0e50-\\u0e59\\u0e81\\u0e82\\u0e84\\u0e87\\u0e88\\u0e8a\\u0e8d\\u0e94-\\u0e97\\u0e99-\\u0e9f\\u0ea1-\\u0ea3\\u0ea5\\u0ea7\\u0eaa\\u0eab\\u0ead-\\u0eb0\\u0eb2\\u0eb3\\u0ebd\\u0ec0-\\u0ec4\\u0ec6\\u0ed0-\\u0ed9\\u0edc\\u0edd\\u0f00\\u0f20-\\u0f33\\u0f40-\\u0f47\\u0f49-\\u0f6a\\u0f88-\\u0f8b\\u1000-\\u1021\\u1023-\\u1027\\u1029\\u102a\\u1040-\\u1049\\u1050-\\u1055\\u10a0-\\u10c5\\u10d0-\\u10f6\\u1100-\\u1159\\u115f-\\u11a2\\u11a8-\\u11f9\\u1200-\\u1206\\u1208-\\u1246\\u1248\\u124a-\\u124d\\u1250-\\u1256\\u1258\\u125a-\\u125d\\u1260-\\u1286\\u1288\\u128a-\\u128d\\u1290-\\u12ae\\u12b0\\u12b2-\\u12b5\\u12b8-\\u12be\\u12c0\\u12c2-\\u12c5\\u12c8-\\u12ce\\u12d0-\\u12d6\\u12d8-\\u12ee\\u12f0-\\u130e\\u1310\\u1312-\\u1315\\u1318-\\u131e\\u1320-\\u1346\\u1348-\\u135a\\u1369-\\u137c\\u13a0-\\u13f4\\u1401-\\u166c\\u166f-\\u1676\\u1681-\\u169a\\u16a0-\\u16ea\\u16ee-\\u16f0\\u1780-\\u17b3\\u17e0-\\u17e9\\u1810-\\u1819\\u1820-\\u1877\\u1880-\\u18a8\\u1e00-\\u1e9b\\u1ea0-\\u1ef9\\u1f00-\\u1f15\\u1f18-\\u1f1d\\u1f20-\\u1f45\\u1f48-\\u1f4d\\u1f50-\\u1f57\\u1f59\\u1f5b\\u1f5d\\u1f5f-\\u1f7d\\u1f80-\\u1fb4\\u1fb6-\\u1fbc\\u1fbe\\u1fc2-\\u1fc4\\u1fc6-\\u1fcc\\u1fd0-\\u1fd3\\u1fd6-\\u1fdb\\u1fe0-\\u1fec\\u1ff2-\\u1ff4\\u1ff6-\\u1ffc\\u2070\\u2074-\\u2079\\u207f-\\u2089\\u2102\\u2107\\u210a-\\u2113\\u2115\\u2119-\\u211d\\u2124\\u2126\\u2128\\u212a-\\u212d\\u212f-\\u2131\\u2133-\\u2139\\u2153-\\u2183\\u2460-\\u249b\\u24ea\\u2776-\\u2793\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303a\\u3041-\\u3094\\u309d\\u309e\\u30a1-\\u30fa\\u30fc-\\u30fe\\u3105-\\u312c\\u3131-\\u318e\\u3192-\\u3195\\u31a0-\\u31b7\\u3220-\\u3229\\u3280-\\u3289\\u3400-\\u4db5\\u4e00-\\u9fa5\\ua000-\\ua48c\\uac00-\\ud7a3\\uf900-\\ufa2d\\ufb00-\\ufb06\\ufb13-\\ufb17\\ufb1d\\ufb1f-\\ufb28\\ufb2a-\\ufb36\\ufb38-\\ufb3c\\ufb3e\\ufb40\\ufb41\\ufb43\\ufb44\\ufb46-\\ufbb1\\ufbd3-\\ufd3d\\ufd50-\\ufd8f\\ufd92-\\ufdc7\\ufdf0-\\ufdfb\\ufe70-\\ufe72\\ufe74\\ufe76-\\ufefc\\uff10-\\uff19\\uff21-\\uff3a\\uff41-\\uff5a\\uff66-\\uffbe\\uffc2-\\uffc7\\uffca-\\uffcf\\uffd2-\\uffd7\\uffda-\\uffdc]');
	var tmp = '', j, i, found;
	for(i = 0; i < terms.length; ++i) {
		found = false;
		for(j = i + 1; j < terms.length; ++j) {
			if (terms[i] == terms[j]) {
				found = true;
				break;
			}
		}
		if (!found) {
			if (tmp.length > 0)
				tmp += '|';
			tmp += terms[i];
		}
	}
	if (tmp.length > 0) {
		this.termsRegExp = new RegExp('a', '');
		this.termsRegExp.compile('^(' + tmp + ')$', '');
	}
	this.index = 0;
}

function TextHighlight_PhrasesComplete() {
	for(var i = 0; i < this.phrases.length; ++i)
		if (this.phrases[i].complete())
			return true;
	return false;
}

function TextHighlight_PhrasesReset() {
	for(var i = 0; i < this.phrases.length; ++i)
		this.phrases[i].reset();
}

function TextHighlight_PhrasesMatch(word) {
	word = word.toLowerCase();
	var found = false;
	for(var i = 0; i < this.phrases.length; ++i)
		if (this.phrases[i].match(word))
			found = true;
	return found;
}

function TextHighlight_Highlight(text) {
	this.index = 0;
	if (this.doHighlightTerms)
		text = this.termsHighlight(text);
	if (this.phrases.length > 0) 
		text = this.phrasesHighlight(text);
	return text;
}

function TextHighlight_TermsHighlight(text) {
	var wordre = this.wordRegexp;
	var ch;
	var word = '';
	var outtext = '';
	var i = 0;
	text += ' ';

	if (i < text.length && (ch = text.charAt(i)) == '<') {
		outtext += ch;
		while (++i < text.length && (ch = text.charAt(i)) != '>') {
			outtext += ch;
		}
		if (i != text.length) {
			outtext += ch;
			++i;
		}
	}

	var sHighlightBefore = this.config.highlightBefore;
	var sHighlightAfter = this.config.highlightAfter;
	for (; i < text.length; ++i) {
		ch = '' + text.charAt(i);
		if (wordre.test(ch)) {
			word += ch;
		} else {
			if (word.length > 0) {
				if (this.termsRegExp.test(word)) {
					++this.index;
					outtext += sHighlightBefore.replace(this.idRegexp, ''+this.index) + word + sHighlightAfter;
				} else {
					outtext += word;
				}
				word = '';
			}
			if (ch == '<') {
				outtext += ch;
				while (++i < text.length && (ch = text.charAt(i)) != '>') {
					outtext += ch;
				}
				if (i != text.length) {
					outtext += ch;
				}
			} else {
				outtext += ch;
			}
		}
	}
	if (outtext.length > 0 && outtext.charAt(outtext.length - 1) == ' ') {
		outtext = outtext.substring(0, outtext.length - 1);
	}
	return outtext;
}

function TextHighlight_PhrasesHighlight(text) {
	var wordre = this.wordRegexp;
	var ch, word = '', outtext = '', outtext2 = '', outtext2x = '';
	var i = 0, found = -1, saveIndex = this.index;
	text += ' ';

	if (i < text.length && (ch = text.charAt(i)) == '<') {
		outtext += ch;
		while (++i < text.length && (ch = text.charAt(i)) != '>') {
			outtext += ch;
		}
		if (i != text.length) {
			outtext += ch;
			++i;
		}
	}

	var sHighlightBefore = this.config.highlightBefore;
	var sHighlightAfter = this.config.highlightAfter;
	for (; i < text.length; ++i) {
		ch = '' + text.charAt(i);
		if (wordre.test(ch)) {
			word += ch;
		} else {
			if (word.length > 0) {
				if (this.termsRegExp.test(word) && this.phrasesMatch(word)) {
					++found;
					if (this.phrasesComplete()) {
						if (outtext2x.length == 0) {
							++this.index;
							outtext2x = sHighlightBefore.replace(this.idRegexp, ''+this.index);
						}
						saveIndex = this.index;
						outtext += outtext2x;
						outtext += word;
						outtext += sHighlightAfter;
						outtext2 = '';
						outtext2x = '';
						found = false;
						this.phrasesReset();
					} else {
						if (found == 0) {
							++this.index;
							outtext2x += sHighlightBefore.replace(this.idRegexp, ''+this.index) + word;
						} else {
							outtext2x += word;
						}
						outtext2 += word;
					}
				} else {
					this.phrasesReset();
					found = -1;
					this.index = saveIndex;
					if (outtext2.length > 0) {
						outtext += outtext2;
						outtext2 = '';
					}
					outtext2x = '';
					outtext += word;
				}
				word = '';
			}
			if (ch == '<') {
				if (found > -1) {
					outtext2 += ch;
					outtext2x += sHighlightAfter + ch;
				} else {
					outtext += ch;
				}
				while (ch == '<') {
					while (++i < text.length && (ch = text.charAt(i)) != '>') {
						if (found > -1) {
							outtext2 += ch;
							outtext2x += ch;
						} else {
							outtext += ch;
						}
					}
					if (i != text.length) {
						if (found > -1) {
							outtext2 += ch;
							outtext2x += ch;
						} else {
							outtext += ch;
						}
					}
					if (i + 1 < text.length) {
						ch = text.charAt(i + 1);
					} else {
						ch = 'x';
					}
				}
				if (found > -1) {
					outtext2x += sHighlightBefore.replace(this.idRegexp, ''+this.index+'a');
				}
			} else {
				if (found > -1) {
					outtext2 += ch;
					outtext2x += ch;
				} else {
					outtext += ch;
				}
			}
		}
	}
	if (outtext2.length > 0) {
		outtext += outtext2;
	}
	if (outtext.length > 0 && outtext.charAt(outtext.length - 1) == ' ') {
		outtext = outtext.substring(0, outtext.length - 1);
	}
	return outtext;
}

function TextHighlight_SetQueryString(querystring) {
	var wordre = this.wordRegexp;
	var phrase = new TextPhrase();
	var word = '', bInQuote = false, i = 0;
	querystring += ' ';
	this.phrases = new Array();
	for (i = 0; i < querystring.length; ++i) {
		ch = '' + querystring.charAt(i);
		if (ch == '"') {
			if (word.length > 0) {
				phrase.addWord(word);
				word = '';
			}
			if (phrase.length() > 0) {
				this.phrases[this.phrases.length] = phrase;
				phrase = new TextPhrase();
			}
			bInQuote = !bInQuote;
		} else if (wordre.test(ch)) {
			word += ch;
		} else {
			if (word.length > 0) {
				phrase.addWord(word);
				word = '';
				if (!bInQuote) {
					if (phrase.length() > 0) {
						this.phrases[this.phrases.length] = phrase;
						phrase = new TextPhrase();
					}
				}
			}
		}
	}

	if (phrase.length() > 0) {
		this.phrases[this.phrases.length] = phrase;
	}
	var hasOnlyTerms = true;
	for(i = 0; i < this.phrases.length; ++i) {
		if (this.phrases[i].length() != 1) {
			hasOnlyTerms = false;
			break;
		}
	}
	if (hasOnlyTerms) {
		if (this.termsRegExp != null) {
			this.phrases = new Array();
			this.doHighlightTerms = true;
		}
	} else if (this.doHighlightTerms) {
		for(i = 0; i < this.phrases.length; ++i) {
			if (this.phrases[i].length() == 1) {
				this.phrases.splice(i, 1);
				--i;
			}
		}
	}
	if (this.phrases.length > 0) {
		this.phrases.sort(TextPhrase_Comparer);
	}
}

function TextPhrase_Comparer(a, b) {
	var ret, l, i;
	ret = b.length() - a.length();
	if (ret != 0)
		return ret;
	l = a.length();
	for(i = 0; i < l; ++i) {
		if (a.words[i] != b.words[i]) {
			return a.words[i]> b.words[i] ? -1 : 1;
		}
	}
	return 0;
}
