"use strict";

function TypeAheadGenericCallbacks() {
	return new Error("Static class, can't be instantiated.");
}

TypeAheadGenericCallbacks.matchResults = function(arrDataSet) {
		var _self = this;
		/*
		* This is the generic matching function passed to the Typeahead plugin.
		* fnAsyncCallback is called when the async request completes.
		*/
		return function findMatches(strInput, fnSyncCallback, fnAsyncCallback) {

			// wait for queries to be finalized
			if(!_self._arrFullDataSet || !_self._objColumns)
				return;

			var arrMatches = [];
			_self._strCurrentText = strInput;

			// split the input into words (using spaces as delimiters, but ignoring spaces inside double quotes)
			var arrWords = strInput.match(/(?:[^\s"]+|(?:"|^)[^"]+(?:"|$))+/g);
			if (!arrWords) {
				return;
			}

			if (strInput[strInput.length - 1] == " ")
				arrWords.push("");

			// remove double quotes from the word
			for (var i = arrWords.length - 1; i >= 0; i--) {
				if(arrWords[i].indexOf("\"") == 0)
					arrWords[i] = arrWords[i].substr(1);
				if(arrWords[i].lastIndexOf("\"") == arrWords[i].length - 1)
					arrWords[i] = arrWords[i].substr(0, arrWords[i].length - 1);
				arrWords[i] = arrWords[i].replace(/\":\"/, ":");
				arrWords[i] = arrWords[i].replace(/\":/, ":");
				arrWords[i] = arrWords[i].replace(/:\"/, ":");
			};

			// build regex based on last word only
			var strCleanInput = arrWords[arrWords.length - 1];
			strCleanInput = strCleanInput.split(":");
			if(strCleanInput.length == 1) {
				strCleanInput = strCleanInput[0];
			}
			else {
				strCleanInput.shift();
				strCleanInput = strCleanInput.join(":");
			}

			// escape metacharacters, ignore + and "
			strCleanInput = strCleanInput.replace(/\+|\"/g, "");
			strCleanInput = strCleanInput.replace(/^\"|\"$/g, "");
			strCleanInput = strCleanInput.replace(/[.?*^$[\]\\(){}|-]/g, "\\$&");

			// create regex based on input
			var objRegex = new RegExp(strCleanInput, "i");
			var objFullRegex = new RegExp("^" + strCleanInput + "$", "i");

			// asynchronous call to _fnBuildSuggestions
			_self._fnBuildSuggestions (
				function testSuggestions(arrSuggestions) {
					if(!arrSuggestions) {
						fnSyncCallback([]);
						fnAsyncCallback([]);
					}
					else {
						arrMatches = [];
						// remove empty strings, null and undefined values from suggestions
						arrSuggestions = arrSuggestions.filter(function(n) { return !!n; });

						// add all suggestions that match the regex defined earlier to the actual matches
						arrSuggestions.forEach(
						function addIfCorrect(strValue)
						{
							if (objRegex.test(strValue)) {
								if (strValue.toString().indexOf(" ") > - 1)
								{
									if (!objFullRegex.test(strValue))
									{
										arrMatches.push(strValue);
									}
								}
								else
								{
									arrMatches.push(strValue);
								}
							}
						});

						// Sort all suggestions alphabetically, ignoring case. This can be removed or replaced with another sorting function
						arrMatches.sort(TypeAheadGenericCallbacks.compareCaseInsensitive);
						fnSyncCallback(arrMatches);
						fnAsyncCallback(arrMatches);
					}
				},
				arrWords
			)
		};
};

TypeAheadGenericCallbacks.buildSuggestions = 	
	/*
	* private
	* Builds suggestions based on current input words.
	*
	* @param function fnCallback.
	* @param array arrWords.
	*
	* @return array
	*/
 function(fnCallback, arrWords) {
		var _self = this;
		var arrSuggestions;
		var arrKeywords = [];

		// specific column mode, only get suggestions for the column specified before ":"
		if(arrWords[arrWords.length - 1].indexOf(":") > -1)
		{
			// find the column we are querying
			var arrTags = arrWords[arrWords.length - 1].split(":");
			var strColumnToQuery = arrTags[0];
			strColumnToQuery = strColumnToQuery.replace(/\+/g, "");

			// if we have no data for the column or the column has changed, query database again
			if (
				(strColumnToQuery != _self._strCurrentColumn)
				&& (_self._objAllAvailableColumns.hasOwnProperty(strColumnToQuery))
				&& !(_self._objColumns.hasOwnProperty(strColumnToQuery))
			)
			{
				_self._objDictionary.getRowsOnColumn(
					function checkColumnValidity(arrColumnData) {
						_self._arrColumnData = arrColumnData;
						_self._strCurrentColumn = strColumnToQuery;

						for (var i = 0; i < arrColumnData.length; i++) {
							if (_self._arrFullDataSet.indexOf(arrColumnData[i]) == -1) {
								_self._arrFullDataSet.push(arrColumnData[i]);
							}
						}

						//if the column is a "hidden" one, i.e it exists but it is not an autocomplete suggestion, make it one by adding it to objColumns
						_self._objColumns[strColumnToQuery] = arrColumnData;

						// if the column the user has entered is not valid, no suggestions are available
						if (_self._objColumns.hasOwnProperty(strColumnToQuery))
							arrSuggestions = _self._arrColumnData; // get dictionary for the needed column
						else
							arrSuggestions = [];

						_self._strCurrentWord = arrWords[arrWords.length - 1];
						arrSuggestions.sort(TypeAheadGenericCallbacks.compareCaseInsensitive);
						fnCallback(arrSuggestions);
					},
					strColumnToQuery,
					_self._objAllAvailableColumns[strColumnToQuery]
				);
			}
			else {
				_self._strCurrentColumn = strColumnToQuery;
				arrSuggestions = _self._objColumns[strColumnToQuery] || [];	
			}
		}
		else 	 // generic mode, get column names and data from any column
		{
			var bHasWrongKeywords = false;
			var arrColumnNames = Object.keys(_self._objColumns);
			if (arrWords.length > 1)
			{
					for (var i = arrWords.length - 2; i >= 0; i--) {
						var bIsMandatory = false;
						if (
							arrWords[i].indexOf("+") == 0
							|| (arrWords[i].indexOf("\"") == 0 && arrWords[i].indexOf("+") == 1)
							) {
							arrWords[i] = arrWords[i].replace("+", "");
							bIsMandatory = true;
						}

						if (arrWords[i].indexOf(":") > -1) {
							var arrAtoms = arrWords[i].split(":");
							var strColumn = arrAtoms.shift();
							if (
								(arrColumnNames.indexOf(strColumn) > -1)
								&& (_self._arrFullDataSet.indexOf(arrAtoms.join(":")) > -1)
								&& (arrKeywords.indexOf(arrWords[i]) == -1)
							)
								if (arrWords[i].indexOf(" ") > -1) {
									if(bIsMandatory) {
										arrKeywords.push("+" + strColumn + ":" + "\"" + arrAtoms.join(":") + "\"");
									}
									else {
										arrKeywords.push(strColumn + ":" + "\"" + arrAtoms.join(":") + "\"");
									}
								}
								else {
									if(bIsMandatory) {
										arrKeywords.push("+" + arrWords[i]);
									}
									else {
										arrKeywords.push(arrWords[i]);
									}
								}
							else
								bHasWrongKeywords = true;
						}
						else if (
							(_self._arrFullDataSet.indexOf(arrWords[i]) > -1)
							&& (arrKeywords.indexOf(arrWords[i]) == -1)
							) {
								if (arrWords[i].indexOf(" ") > -1) {
									if (bIsMandatory) {
										arrKeywords.push("+" + arrWords[i] + "\"");
									}
									else {
										arrKeywords.push("\"" + arrWords[i] + "\"");
									}
								}
								else {
									if (bIsMandatory)
										arrWords[i] =  "+" + arrWords[i];
									arrKeywords.push(arrWords[i]);
								}
						}
						else {
							bHasWrongKeywords = true;
						}
					}
					if (arrWords[arrWords.length - 1] != _self._strCurrentWord) {
						_self._objDictionary.getFullDictionary(
							function buildSuggestions(arrDataSet) {
								_self._fnFillDataSet(arrDataSet);
								_self._strCurrentWord = arrWords[arrWords.length - 1];
								arrSuggestions = arrColumnNames.concat(_self._arrDataSet);
								//arrSuggestions.sort(TypeAheadGenericCallbacks.compareCaseInsensitive);
								fnCallback(arrSuggestions);
							},
							arrKeywords.join(" "));
					}
					arrSuggestions = arrColumnNames.concat(_self._arrDataSet);
					//fnCallback(arrSuggestions);
			}
			else {
				arrSuggestions = arrColumnNames.concat(_self._arrFullDataSet);
			}
		}
		//arrSuggestions.sort(TypeAheadGenericCallbacks.compareCaseInsensitive);
		fnCallback(arrSuggestions);
};


// event handler needed to hide suggestion menu when cursor is not on the last word
TypeAheadGenericCallbacks.eventHandler = function(evEvent) {
	var nCaretPositionStart = this.selectionStart;
	var nCaretPositionEnd = this.selectionEnd;

	// if caret is on the last word, show suggestion menu
	if(this.value.lastIndexOf(" ") < nCaretPositionStart && this.value.lastIndexOf(" ") > -1) {
		$(this).typeahead("open");
		this.setSelectionRange(nCaretPositionStart, nCaretPositionEnd);
		var arrMenuElements = document.getElementsByClassName("tt-menu");
		for (var i = arrMenuElements.length - 1; i >= 0; i--) {
			arrMenuElements[i].style.visibility = "visible";
		}
	}
	// otherwise, hide suggestion menu
	else if(this.value.lastIndexOf(" ") > -1) {
		var arrMenuElements = document.getElementsByClassName("tt-menu");
		for (var i = arrMenuElements.length - 1; i >= 0; i--) {
			arrMenuElements[i].style.visibility = "hidden";
		};
		
		// on arrow up & arrow down, also close typeahead so a suggestion can't silently be chosen
		if (evEvent.keyCode == 38 || evEvent.keyCode == 40)
		{
			$(this).typeahead("close");
			this.setSelectionRange(nCaretPositionStart, nCaretPositionEnd);
		}
	}
	else {
		var arrMenuElements = document.getElementsByClassName("tt-menu");
		for (var i = arrMenuElements.length - 1; i >= 0; i--) {
			arrMenuElements[i].style.visibility = "visible";
		}
	}
};

TypeAheadGenericCallbacks.fillDataSet = function(objDataSet, bUpdateColumns) {
		var arrResults = [];
		if(objDataSet.hasOwnProperty("duration_milliseconds"))
			objDataSet = [objDataSet];;
		for (var objTable in objDataSet) // for each table query result
		{
			if (bUpdateColumns) {
				var arrAllAvailableColumns = objDataSet[objTable]["all_available_columns"];
				if (arrAllAvailableColumns) {
					for (var i = arrAllAvailableColumns.length - 1; i >= 0; i--) {
						this._objAllAvailableColumns[arrAllAvailableColumns[i]] = objTable;
					};
				}
			}

			var objColumnsToKeywords = objDataSet[objTable]["columns_to_keywords"];
			for (var strColumn in objColumnsToKeywords) {

				// only update if no data or we found more results
				if (!this._objColumns[strColumn] || this._objColumns[strColumn].length < objColumnsToKeywords[strColumn].length)
					this._objColumns[strColumn] = objColumnsToKeywords[strColumn];


				for (var i = objColumnsToKeywords[strColumn].length - 1; i >= 0; i--) {
					if (
						objColumnsToKeywords[strColumn][i]
						&& arrResults.indexOf(objColumnsToKeywords[strColumn][i].toString()) == -1
					) {
						arrResults.push(objColumnsToKeywords[strColumn][i].toString()); // need to stringify numbers
					}
				}
			}
		}
		this._arrDataSet = arrResults;
};

TypeAheadGenericCallbacks.replaceWithSuggestion = function(errError, strSuggestion) {
	// stringify suggestion
	strSuggestion = strSuggestion.toString();

	// when selecting a suggestion, only replace last word
	var strCurrentText = $(this).typeahead("val");
	var index = Math.max(strCurrentText.lastIndexOf(" "), strCurrentText.lastIndexOf(":"));

	// if there is no space or ":", it is the first word
	if (index < 0) {
		if (strCurrentText[0] == "+") {
			strSuggestion = "+" + strSuggestion;
		}
		strCurrentText = "";
	}
	else {
		// keep the "+" if it is at the start of the string
		if (
			strCurrentText[index] == " " 
			&& strCurrentText[index + 1] == "+"
		)
			strSuggestion = "+" + strSuggestion;
			strCurrentText = strCurrentText.substr(0, index + 1);
	}

	// if the selected suggestions contains spaces, enclose it in double quotes
	if(strSuggestion.indexOf(" ") > -1)
		strSuggestion = "\"" + strSuggestion + "\"";

	$(this).typeahead("val", strCurrentText + strSuggestion);
};

TypeAheadGenericCallbacks.compareCaseInsensitive = function(a, b)
{
	if(isNaN(parseInt(a)) && !isNaN(parseInt(b)))
	{
		return -1;
	}
	if(!isNaN(parseInt(a)) && isNaN(parseInt(b)))
	{
		return 1;
	}
	a = a.toString().toUpperCase();
	b = b.toString().toUpperCase();
	return (a < b ? -1 : (a > b ? 1 : 0));  
}
