/**
 * LICENCE[[
 * Version: MPL 2.0/GPL 3.0/LGPL 3.0/CeCILL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 2.0 (the "License"); you may not use this file except in compliance with the
 * License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is kelis.fr code.
 *
 * The Initial Developer of the Original Code is 
 * samuel.monsarrat@kelis.fr
 *
 * Portions created by the Initial Developer are Copyright (C) 2010-2017
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 3.0 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 3.0 or later (the "LGPL"),
 * or the CeCILL Licence Version 2.1 (http://www.cecill.info),
 * in which case the provisions of the GPL, the LGPL or the CeCILL are
 * applicable instead of those above. If you wish to allow use of your version
 * of this file only under the terms of either the GPL, the LGPL or the CeCILL,
 * and not to allow others to use your version of this file under the terms of
 * the MPL, indicate your decision by deleting the provisions above and replace
 * them with the notice and other provisions required by the GPL, the LGPL or
 * the CeCILL. If you do not delete the provisions above, a recipient may use
 * your version of this file under the terms of any one of the MPL, the GPL,
 * the LGPL or the CeCILL.
 * ]]LICENCE
 */

/* ====================== SCENARI Search service ===============================
   Client-side service that manages SCENARI-generated search indexes.
   This service loads, parses and interrogates multiple search indexes. The most
   common usages of this service are calling find() that either returns an array
   of result objects or a resultSet object and calling propose() that returns an
   array of possible words.
   The main purpose is to return a list of urls of pages that are positive
   maches for the given search string.
   Most functions require the index url to be used as their first argument.
   A result object describes a given page and is of the form {url,cat,idxCat}
   where 'url' if the url of the page, 'cat' is the page category count if only
   one index is concerned and 'idxCat' is an object that contains the category
   counts for all indexes concerned by the result.
   A resultSet is an object of the form {ctrl,list,or,neg} where 'list' is an
   array of result objects and 'ctrl' is an object that provides the offset of
   each page in the list. 'or' and 'neg' are optional flags that are used when
   consolidating resultSets and enable consolidateResults() to merge or negate 
   resultSets.
   A search string may contain several words and a limited search grammar. By
   default find() requires all words to be present on a given page for a match
   to be made.
   Here is an example of different search string and what they will return :
    * "foo bar" : pages that contain the strings foo and bar
    * "foo OR bar" : pages that contain the strings foo or bar (OU and | are 
                     also valid) 
    * "+foo -bar" : pages that contain the exact word foo (but not foot) and 
                    do not contain the string bar
    * "*bar" : pages that contain words starting with bar (barrel but not 
               embargo)

   Public functions : 
    * onLoad : SCENARI onLoad system
    * declareIndex : Declare an index file
    * registerListener : Declare a listener function that is called when an 
                         index is successfully loaded
    * query : Execute a query made up of several parts: successive and 
              consolidated find() calls
    * getLastQueryList : Return the last query for a given index
    * getLastQueryResults : Return the last query results for a given index
    * resetLastQuery : Reset the last query and results for a given index
    * find : Find a string in a given index
    * getLastSearch : Return the last searched string for a given index
    * getLastResults : Return the last search results for a given index
    * resetLastSearch : Reset the last searched string and results for a given
                        index
    * propose : Propose a list of possible words from a given index
    * buildTokens : Build an array of search tokens from a given string and a
                    given index
    * consolidateResults : Parse and consolidate an array of resultSets
    * normalizeString : Normalize a string according to a given index's prefs
    * getPages : Retreive an object that lists all the pages available in a
                 given index
    * getCategories : Retreive an array of the categories available in a given
                      index
    * hasIntersections : Returns true if any page is referenced more than once
    * isLoadable : Retruns false when an exception occured during loading
 */
scServices.scSearch = scOnLoads[scOnLoads.length] = {
	fIdxs : {},
	fListeners : [],
	fDebug : false,
	fIsLocal : window.location.protocol == "file:",

	/* == onLoad =================================================================
	   PUBLIC - SCENARI onload system. */
	onLoad: function(){
		this.fStore = new this.xLocalStore("scsearch");
		if (!String.prototype.trim) {
			String.prototype.trim = function () {return this.replace(/^\s+|\s+$/g, '');};
		}
	},
	/* == declareIndex ===========================================================
	   PUBLIC - Declare an index file.
	   pUrl : url to index file
	   pAsync : load asynchonusly
	   return : true if the index is ready */
	declareIndex: function(pUrl,pAsync){
		if (!pUrl) throw new Error("scServices.scSearch.declareIndex() must be called with a url.");
		if (!this.fIdxs[pUrl]) return this.xLoadIndex(pUrl, (typeof pAsync == "undefined" ? true : pAsync));
		else return true;
	},
	/* == registerListener =======================================================
	   PUBLIC - Declare a listener function that is called when an index is successfully loaded.
	   pFunc : function to call when an index is loaded the index id (url) is passed to the listener */
	registerListener: function(pFunc){
		this.fListeners.push(pFunc);
	},
	/* == query ==================================================================
	   PUBLIC - Execute a query made up of several parts.
	   pQryLst : array of query objects : {id,str,opt,or,neg}
	   pOpt : optionnal - object containing search options : see find()
	   return : resultSet object*/
	query: function(pQryLst, pOpt){
		if (typeof pQryLst == "string") pQryLst = JSON.parse(pQryLst);
		if (typeof pQryLst != "array" && typeof pQryLst != "object") throw new Error("scServices.scSearch.query() must be called with a query array or object.");
		this.xLog("query");
		var vQryLst = pQryLst;
		if (typeof pQryLst.length == "undefined") {
			vQryLst = [];
			vQryLst.push(pQryLst)
		}
		var vResults = [];
		for (var i=0; i<vQryLst.length; i++) {
			var vQry = vQryLst[i];
			var vQryOpt = {singleToken:false,matchExact:false,matchStartOnly:false,catMask:null,returnResultSet:true};
			if (typeof vQry.opt != "undefined"){
				if (typeof vQry.opt.singleToken != "undefined") vQryOpt.singleToken = vQry.opt.singleToken;
				if (typeof vQry.opt.matchExact != "undefined") vQryOpt.matchExact = vQry.opt.matchExact;
				if (typeof vQry.opt.matchStartOnly != "undefined") vQryOpt.matchStartOnly = vQry.opt.matchStartOnly;
				if (typeof vQry.opt.catMask != "undefined") vQryOpt.catMask = vQry.opt.catMask;
				if (typeof vQry.opt.cat != "undefined") vQryOpt.cat = vQry.opt.cat;
			}
			var vResult = this.find(vQry.id, vQry.str, vQryOpt);
			if (vResult) {
				vResult.or = vQry.or;
				vResult.neg = vQry.neg;
				vResults.push(vResult);
			}
		}
		this.fQueryList = pQryLst;
		this.fStore.set("QueryList", JSON.stringify(pQryLst));
		this.fQueryResults = this.consolidateResults(vResults, {returnResultSet:true});
		return this.fQueryResults;
	},
	/* == getLastQueryList =======================================================
	   PUBLIC - Return the last query list.
	   return : last searched array of query objects */
	getLastQueryList: function(){
	if (!this.fQueryList) this.fQueryList = JSON.parse(this.fStore.get("QueryList"));
		return this.fQueryList;
	},
	/* == getLastQueryResults ====================================================
	   PUBLIC - Return search results for the last query list.
	   return : array of result objects {url,cat,idxCat} or resultSet */
	getLastQueryResults: function(){
		if (this.getLastQueryList() && !this.fQueryResults) this.query(this.fQueryList);
		return this.fQueryResults;
	},
	/* == resetLastQuery =========================================================
	   PUBLIC - Reset the last query.
	   return : true if a reset was needed */
	resetLastQuery: function(){
		if (typeof this.fQueryList == "undefined") return false;
		this.fQueryList = null;
		this.fQueryResults = null;
		this.fStore.set("QueryList", "");
		return true;
	},
	/* == find ===================================================================
	   PUBLIC - Find a string in a given index.
	   pId : index id 
	   pStr : string to search
	   pOpt : optionnal - object containing search options :
	           singleToken : boolean, treat the string as a single token (do not split)
	           matchExact : boolean, search for an exact token match
	           matchStartOnly : boolean, search for for entries beginning with each token
	           cat : comma seperated string of categories to search
	           returnResultSet : boolean, return internal resultSet object
	   return : 1) simple array of result objects {url,cat,idxCat}
	            2) internal resultSet object if pOpt.returnResultSet = true */
	find: function(pId, pStr, pOpt){
		if (!pId) throw new Error("scServices.scSearch.find() must be called with an index ID.");
		this.xLog("find");
		var vIdx = this.xGetIndex(pId);
		if (!vIdx) return null;
		var vOpt = {singleToken:false,matchExact:false,matchStartOnly:false,catMask:null,returnResultSet:false};
		if (typeof pOpt != "undefined"){
			if (typeof pOpt.singleToken != "undefined") vOpt.singleToken = pOpt.singleToken;
			if (typeof pOpt.matchExact != "undefined") vOpt.matchExact = pOpt.matchExact;
			if (typeof pOpt.matchStartOnly != "undefined") vOpt.matchStartOnly = pOpt.matchStartOnly;
			if (typeof pOpt.returnResultSet != "undefined") vOpt.returnResultSet = pOpt.returnResultSet;
			if (typeof pOpt.cat != "undefined") vOpt.catMask = this.xBuildCatMask(vIdx,pOpt.cat);
			else if (typeof pOpt.catMask != "undefined") vOpt.catMask = pOpt.catMask;
		}
		vIdx.fStr = pStr;
		var i=0;
		var vToken = {};
		var vWrdResult = {};
		var vRawResults = [];
		var vTokens = this.xBuildTokens(vIdx,pStr, vOpt.singleToken);
		for (i = 0; i < vTokens.length; i++){
			vToken = vTokens[i];
			vWrdResult = this.xFindWord(vIdx,vToken.wrd,{matchExact:vToken.exact||vOpt.matchExact,matchStartOnly:vToken.start||vOpt.matchStartOnly,catMask:vOpt.catMask});
			if (vWrdResult) {
				vWrdResult.or = vToken.or;
				vWrdResult.neg = vToken.neg;
				vRawResults.push(vWrdResult);
			}
		}
		vIdx.fResults = this.consolidateResults(vRawResults, {returnResultSet:vOpt.returnResultSet});
		return vIdx.fResults;
	},
	/* == getLastSearch ==========================================================
	   PUBLIC - Return the last searched string for a given index.
	   pId : index id 
	   return : last searched string */
	getLastSearch: function(pId){
		if (!pId) throw new Error("scServices.scSearch.getLastSearch() must be called with an index ID.");
		var vIdx = this.xGetIndex(pId);
		if (!vIdx) return null;
		return vIdx.fStr;
	},
	/* == getLastResults =========================================================
	   PUBLIC - Return the last search results for a given index.
	   pId : index id 
	   return : array of result objects {url,cat,idxCat} or resultSet */
	getLastResults: function(pId){
		if (!pId) throw new Error("scServices.scSearch.getLastResults() must be called with an index ID.");
		var vIdx = this.xGetIndex(pId);
		if (!vIdx) return null;
		return vIdx.fResults;
	},
	/* == resetLastSearch ========================================================
	   PUBLIC - Reset the last searched string and results for a given index.
	   pId : index id 
	   return : true if a reset was needed */
	resetLastSearch: function(pId){
		if (!pId) throw new Error("scServices.scSearch.resetLastSearch() must be called with an index ID.");
		var vIdx = this.xGetIndex(pId);
		if (!vIdx) return false;
		if (typeof vIdx.fStr == "undefined") return false;
		vIdx.fStr = null;
		vIdx.fResults = null;
		return true;
	},
	/* == propose ================================================================
	   PUBLIC - Propose a list of possible words from a given index.
	   pId : index id 
	   pWrd : word to look up
	   pOpt : optionnal - propose options :
	           async : boolean - load the index async (false).
	           matchStartOnly : boolean - mtche the start of words only (true)
	           maxNum : int - number of matches over which null is returned (20)
	           filter : resultSet - can be used to filter matches contained in the resultset
	   return : array of proposed strings */
	propose: function(pId, pWrd, pOpt){
		if (!pId) throw new Error("scServices.scSearch.propose() must be called with an index ID.");
		var vOpt = {async:false,matchStartOnly:true,maxNum:20,filter:null};
		if (typeof pOpt != "undefined"){
			if (typeof pOpt.async != "undefined") vOpt.async = pOpt.async;
			if (typeof pOpt.matchStartOnly != "undefined") vOpt.matchStartOnly = pOpt.matchStartOnly;
			if (typeof pOpt.maxNum != "undefined") vOpt.maxNum = pOpt.maxNum;
			if (typeof pOpt.filter != "undefined") vOpt.filter = pOpt.filter;
		}
		var vIdx = this.xGetIndex(pId, pOpt.async);
		if (!vIdx) return null;
		this.xLog("propose");
		var vWrd = this.xFilterWord(vIdx, pWrd);
		var vRet = [];
		if (!vWrd) return vRet;
		if (!vIdx.fSortedWords) this.xSortIndex(vIdx);

		var vRegFind = null;
		if (vOpt.matchStartOnly) vRegFind = new RegExp("\\b"+vWrd);
		else vRegFind = new RegExp(pWrd);

		var sCheckFilter = function(pWord, pFilter){
			if (!pFilter || !pFilter.ctrl || (pFilter.list && pFilter.list.length==0)) return true;
			for (var i = 0; i<pWord.urls.length; i++){
				if (typeof pFilter.ctrl[pWord.urls[i]] != "undefined") return true;
			}
			return false;
		}
		for (var i = 0; i<vIdx.fSortedWords.length	;i++){
			if(vRegFind.test(vIdx.fSortedWords[i].wrd)){
				if (sCheckFilter(vIdx.fSortedWords[i], vOpt.filter)) vRet.push(vIdx.fSortedWords[i]);
				if (vOpt.maxNum>0 && vRet.length==vOpt.maxNum) return null;
			}
		}
		return vRet;
	},
	/* == buildTokens ============================================================
	   PUBLIC - Build an array of search tokens from a given string.
	   pId : index id 
	   pStr : string to search
	   pSingleToken : optionnal - boolean, treat the string as a single token (do not split)
	   return : array of search tokens {wrd:string,neg:boolean,exact:boolean,or:boolean} */
	buildTokens: function(pId, pStr, pSingleToken){
		if (!pId) throw new Error("scServices.scSearch.buildTokens() must be called with an index ID.");
		var vIdx = this.xGetIndex(pId);
		if (!vIdx) return null;
		if (typeof pSingleToken == "undefined") pSingleToken = false;
		return this.xBuildTokens(vIdx, pStr, pSingleToken);
	},
	/* == consolidateResults =====================================================
	   PUBLIC - Parse and consolidate an array of resultSets, concat ORs, seperate all negated results, AND remaining results.
	   pRawResults : Array of result-sets to be consolidated.
	   pOpt : optionnal - consolidating options
	   return : array of consolidated result set. */
	consolidateResults: function(pRawResults,pOpt){
		// Consolidate individual word results (concat ORs and seperate all negated results)
		var vOpt = {returnResultSet:false};
		if (typeof pOpt != "undefined"){
			if (typeof pOpt.returnResultSet != "undefined") vOpt.returnResultSet = pOpt.returnResultSet;
		}
		var vWrdResult = {};
		var vNegResults = [];
		var vWrdResults = [];
		var i=0, j=0, k=0;
		for (i = 0; i < pRawResults.length; i++){
			vWrdResult = pRawResults[i];
			if (vWrdResult.neg) vNegResults.push(vWrdResult);
			else if (vWrdResults.length>0 && vWrdResults[vWrdResults.length-1].or) vWrdResults[vWrdResults.length-1] = this.xConcatResults(vWrdResults[vWrdResults.length-1], vWrdResult);
			else vWrdResults.push(vWrdResult);
		}
		// Calculate final result set
		var vPge;
		var vInAllWords = true;
		var vInNegWords = false;
		var vResults = {ctrl:{},list:[]};
		for (i = 0; i < vWrdResults.length; i++){
			vWrdResult = pRawResults[i];
			for (j = 0; j < vWrdResult.list.length; j++){
				vPge = vWrdResult.list[j];
				vInAllWords = true;
				vInNegWords = false;
				for (k = 0; k < vWrdResults.length; k++){
					if (typeof vWrdResults[k].ctrl[vPge.url] == "undefined") vInAllWords = false;
				}
				for (k = 0; k < vNegResults.length; k++){
					if (typeof vNegResults[k].ctrl[vPge.url] != "undefined") vInNegWords = true;
				}
				if (vInAllWords && !vInNegWords){
					if(typeof vResults.ctrl[vPge.url] != "undefined") {
						this.xMergePageCategories(vResults.list[vResults.ctrl[vPge.url]], vPge);
					} else {
						vResults.ctrl[vPge.url] = vResults.list.length;
						vResults.list.push(vPge);
					}
				}
			}
		}
		return vOpt.returnResultSet ? vResults : vResults.list;
	},
	/* == normalizeString ========================================================
	   PUBLIC - Normalize a string according to a given index's prefs.
	   pId : index id 
	   pStr : string to normalize
	   return : string normalized according to the index's prefs, ready for highlighting searches for example */
	normalizeString: function(pId, pStr){
		if (!pId) throw new Error("scServices.scSearch.normalizeString() must be called with an index ID.");
		var vIdx = this.xGetIndex(pId);
		if (!vIdx) return null;
		var vFilters = vIdx.fParams.filters;
		var vStr;
		for (var i = 0; i < vFilters.length; i++){
			var vFilter = vFilters[i];
			switch (vFilter.type){
			case "filterAccentedLatinLetters":
				vStr = this.xfilterAccentedLatinLetters(pStr, true);
				break;
			}
		}
		return vStr;
	},
	/* == getPages ===============================================================
	   PUBLIC - Retreive an object listing all the pages in a given index.
	   pId : index id 
	   return : page list object */
	getPages: function(pId){
		if (!pId) throw new Error("scServices.scSearch.getPages() must be called with an index ID.");
		var vIdx = this.xGetIndex(pId);
		if (!vIdx) return null;
		return vIdx.fPgeIdx;
	},
	/* == getCategories ==========================================================
	   PUBLIC - Retreive an array of the categories available in a given index.
	   pId : index id 
	   return : array of category names */
	getCategories: function(pId){
		if (!pId) throw new Error("scServices.scSearch.getCategories() must be called with an index ID.");
		var vIdx = this.xGetIndex(pId);
		if (!vIdx) return null;
		return vIdx.fCatIdx;
	},
	/* == isLoadable =============================================================
	   PUBLIC - Returns 'null' if index non existant, 'false' if an exception occured when loading the index and 'true' if the index was loadable.
	   pId : index id 
	   return : boolean */
	isLoadable: function(pId){
		if (!pId) throw new Error("scServices.scSearch.isLoadable() must be called with an index ID.");
		if (!this.fIdxs[pId]) this.xLoadIndex(pId,false);
		var vIdx = this.fIdxs[pId];
		if (!vIdx) return null;
		return !vIdx.fLoadException;
	},
	/* == hasIntersections =======================================================
	   PUBLIC - Determines if a given index has intersections, i.e. if no page is
	            referenced more than once then there are deemed to be no
	            intersections.
	   pId : index id 
	   return : true if any page is referenced more than once */
	hasIntersections: function(pId){
		if (!pId) throw new Error("scServices.scSearch.hasIntersections() must be called with an index ID.");
		var vIdx = this.xGetIndex(pId);
		if (!vIdx) return null;
		if (typeof vIdx.fHasIntersections == "undefined"){
			vIdx.fHasIntersections = false;
			var vCtrl = {};
			var vRawLines = vIdx.fRaw.split("\n");
			for (var i=0; i<vRawLines.length; i++){
				var vRawLine = vRawLines[i].split("\t");
				for (var j = 1; j < vRawLine.length; j++){
					if (vCtrl[vRawLine[j]]){
						vIdx.fHasIntersections = true;
						return vIdx.fHasIntersections;
					}
					vCtrl[vRawLine[j]] = true;
				}
			}
		}
		return vIdx.fHasIntersections;
	},
	/* == xBuildCatMask ===========================================================
	   PRIVATE - Build a category mask for a given index from a comma seperated string of categories
	   pIdx : index 
	   pStr : comma seperated string of categories
	   return : category mask */
	xBuildCatMask: function(pIdx, pCat){
		var vCats = pCat.split(",");
		var vCatMask = "";

		for (var i=0;i<pIdx.fCatIdx.length;i++){
			var vCatIdx = pIdx.fCatIdx[i];
			var vMatch = false;
			for (var k=0;k<vCats.length;k++){
				if (vCatIdx == vCats[k]) vMatch = true;
			}
			vCatMask += vMatch ? "1" : "0";
		}
		return vCatMask;
	},
	/* == xBuildTokens ===========================================================
	   PRIVATE - Build an array of search tokens from a given string (interpret grammar)
	   pIdx : index 
	   pStr : string to search
	   pSingleToken : boolean, treat the string as a single token (do not split)
	   return : array of search tokens */
	xBuildTokens: function(pIdx, pStr, pSingleToken){
		var vRegWords = new RegExp(pIdx.fParams.wordPattern,"gm");
		var vRegOr = new RegExp("^(OR|OU|\\|)$");
		var vCorrectPipe = new RegExp("([^ ])\\|([^ ])","g");
		var vStrs = pSingleToken ? [pStr] : pStr.replace(vCorrectPipe, "$1 | $2").split(" ");
		var i=0, j=0;
		var vStr;
		var vWrd;
		var vWords = [];
		var vToken;
		var vTokens = [];
		for (i = 0; i < vStrs.length; i++){
			vStr = vStrs[i];
			if (vStr.length>0 && !vRegOr.test(vStr)) {
				vWords = vStr.match(vRegWords);
				if (vWords){
					for (j = 0; j < vWords.length; j++){
						if (vWords[j].length>0) {
							vWrd = this.xFilterWord(pIdx, vWords[j]);
							if(vWrd) {
								vToken = {wrd:vWrd};
								vToken.neg = vStr.indexOf("-")==0;
								vToken.exact = vStr.indexOf("+")==0;
								vToken.start = vStr.indexOf("*")==0;
								vToken.or = (i < vStrs.length-2 && vRegOr.test(vStrs[i+1]));
								vTokens.push(vToken);
							}
						}
					}
				}
			}
		}
		return vTokens;
	},
	/* == xFindWord ==============================================================
	   PRIVATE - Find a single word in the given index 
	   pIdx : index 
	   pWrd : string to search
	   pOpt : optionnal - search options
	   return : array of result objects {url,cat,idxCat} */
	xFindWord: function(pIdx, pWrd, pOpt){
		var vMatchExact = pWrd.search(/\D/) < 0 || pOpt.matchExact;
		var vRegFind = null;
		if (vMatchExact) vRegFind = new RegExp("^"+pWrd+"\t[^\\n]*","gm");
		else if (pOpt.matchStartOnly) vRegFind = new RegExp("\\b"+pWrd+"[^\\t]*\t[^\\n]*","gm");
		else vRegFind = new RegExp("^[^\\t]*"+pWrd+"[^\\t]*\t[^\\n]*","gm");
		var vRetList = pIdx.fRaw.match(vRegFind);
		var vResult = {ctrl:{},list:[]};
		var vDiscard = false;
		if (vRetList) {
			for (var i = 0; i < vRetList.length; i++){
				var vRetLine = vRetList[i].trim().split("\t");
				for (var j = 1; j < vRetLine.length; j++){
					var vRetIdx = vRetLine[j];
					var vPgeIdx = null;
					var vPgeCat = "";
					if (vRetIdx.indexOf(".")>0) {
						vPgeIdx = vRetIdx.split(".")[0];
						vPgeCat = vRetIdx.split(".")[1];
					} else {
						vPgeIdx = vRetIdx;
					}
					if (vPgeCat.length>0 && pOpt.catMask && vPgeCat.length == pOpt.catMask.length){
						vDiscard = true;
						for (var k=0; k<vPgeCat.length; k++){
							if (vPgeCat.charCodeAt(k)!=48 && pOpt.catMask.charCodeAt(k)!=48) {
								vDiscard=false;
								break;
							}
						}
					}
					if (!vDiscard){
						var vUrl = pIdx.fPgeIdx[vPgeIdx];
						var vPge = {url:vUrl,cat:vPgeCat,idxCat:{}};
						vPge.idxCat[pIdx.id] = vPgeCat;
						if (typeof vResult.ctrl[vUrl] != "undefined"){
							this.xMergePageCategories(vResult.list[vResult.ctrl[vUrl]], vPge, true);
						} else {
							vResult.list.push(vPge)
							vResult.ctrl[vUrl] = vResult.list.length-1;
						}
					}
				}
			}
		}
		return vResult;
	},
	/* == xFilterWord ============================================================
	   PRIVATE - Filter a single word based on an index's filters
	   pIdx : index 
	   pWrd : string to filter
	   return : filtered word */
	xFilterWord: function(pIdx, pWrd){
		var vFilters = pIdx.fParams.filters;
		var vWrd = pWrd;
		for (var i = 0; i < vFilters.length; i++){
			var vFilter = vFilters[i];
			switch (vFilter.type){
			case "filterAccentedLatinLetters":
				vWrd = this.xfilterAccentedLatinLetters(vWrd);
				break;
			case "lowerCase":
				vWrd = vWrd.toLowerCase();
				break;
			case "ignoreWords":
				if (vFilter.words[vWrd]) return null;
				break;
			case "excludeShortWords":
				if (vFilter.forWords){
					var vRegFilter = new RegExp(vFilter.forWords,"");
					if (vRegFilter.test(vWrd) && vWrd.length < vFilter.min) return null;
				} else if (vWrd.length < vFilter.min) return null;
				break;
			case "cutLongWords":
				if (vFilter.forWords){
					var vRegFilter = new RegExp(vFilter.forWords,"");
					if (vRegFilter.test(vWrd)) vWrd = vWrd.substring(0,vFilter.max);
				} else vWrd = vWrd.substring(0,vFilter.max);
				break;
			}
		}
		return vWrd;
	},
	/* == xGetIndex ==============================================================
	   PRIVATE - Retreive a given index object.
	   pId : index ID
	   pAsync : optionnal - load asynchonusly default false
	   return : index object */
	xGetIndex : function(pId,pAsync){
		if (typeof pAsync == "undefined") pAsync = false;
		if (!this.fIdxs[pId]) this.xLoadIndex(pId,pAsync);
		var vIdx = this.fIdxs[pId];
		if (typeof vIdx != "undefined" && !vIdx.fLoaded) return null;
		else return vIdx;
	},
	/* == xLoadIndex =============================================================
	   PRIVATE - Load an index file : fetch, read and interprate the file
	   pUrl : url to index file
	   pAsync : load asynchonusly
	   return : true if the index is ready */
	xLoadIndex: function(pUrl,pAsync){
		this.xLog("xLoadIndex: url="+pUrl+" Async="+pAsync);
		var vIdx = this.fIdxs[pUrl] = {id:pUrl,fLoaded:false,fLoadException:false};
		var vReq = new XMLHttpRequest();

		function iSetupIndex(pIdx,pRaw) {
			scServices.scSearch.xLog("iSetupIndex");
			try {
				var vRegIdx = new RegExp("^([^\\n]*)\\n([^\\n]*)\\n([^\\n]*)");
				var vStrIdx = vRegIdx.exec(pRaw);
				pIdx.fRaw = pRaw.substring(vStrIdx[1].length + vStrIdx[2].length + vStrIdx[3].length + 3);
				pIdx.fPgeIdx = JSON.parse(vStrIdx[1].trim());
				pIdx.fCatIdx = JSON.parse(vStrIdx[2].trim());
				pIdx.fParams = JSON.parse(vStrIdx[3].trim());
				pIdx.fLoaded = true;
				for (var i=0; i< scServices.scSearch.fListeners.length; i++) try{scServices.scSearch.fListeners[i](pIdx.id)}catch(e){scServices.scSearch.xLog("iSetupIndex : listener error : "+e)};
			} catch(e){
				scServices.scSearch.xLog("ERROR : unable to process search index "+pIdx.id);
				pIdx.fLoadException = true;
			}
			return true;
		};
		if (pAsync) {
			vReq.onreadystatechange = function () {
				if (vReq.readyState != 4) return;
				if (vReq.status != 0 && vReq.status != 200 && vReq.status != 304) {
					alert("ERROR : unable de retreive search index "+pUrl+": " + vReq.status);
					return;
				}
				scServices.scSearch.xLog("xAsyncLoadIndex");
				if (!vIdx.fLoaded) iSetupIndex(vIdx,vReq.responseText);
			}
		}
		vReq.open("GET",pUrl,pAsync);
		try {
			vReq.send();
		} catch(e){
			this.xLog("ERROR : unable to load search index "+vIdx.id);
			vIdx.fLoadException = true;
		}
		if (!pAsync) return iSetupIndex(vIdx,vReq.responseText);
		else return false;
	},

	/* == xSortIndex =============================================================
	   PRIVATE - Sort an index file : add a sorted array of known words
	   pIdx : index */
	xSortIndex : function(pIdx){
		try {
			var vRegFind = new RegExp("^[^\\n]*","gm");
			var vLines = pIdx.fRaw.match(vRegFind);
			pIdx.fSortedWords = [];
			for (var i = 0; i<vLines.length; i++){
				var vLine = vLines[i].split("\t");
				var vUrls = [];
				for (var j = 1; j < vLine.length; j++){
					var vPgeIdx = vLine[j];
					if (vPgeIdx.indexOf(".")>0) {
						vPgeIdx = vPgeIdx.split(".")[0];
					}
					vUrls.push(pIdx.fPgeIdx[vPgeIdx]);
				}
				pIdx.fSortedWords.push({wrd:vLine[0],urls:vUrls,num:vUrls.length});
			}
			pIdx.fSortedWords.sort(function (p1, p2){
				if(scCoLib.isIE) return p1.wrd.localeCompare(p2.wrd||"");
				try{
					return p1.wrd > p2.wrd||"" ? 1 : p1.wrd == p2.wrd ? 0 : -1;
				}catch(e){
					return p1.wrd.localeCompare(p2.wrd||"");
				}
			}
		);
		} catch(e){alert("ERROR : unable de process search index "+pIdx);}
	},
	/* == xConcatResults =========================================================
	   PRIVATE - Concatinate two result objects
	   pRes1 : input result object
	   pRes2 : input result object
	   return : Concatinated result object */
	xConcatResults : function(pRes1, pRes2){
		if (typeof pRes1 == "undefined") return null;
		var vResult = pRes1;
		if (typeof pRes2 == "undefined" || !pRes2.list || pRes2.list.length==0) {
			if (typeof pRes2 != "undefined") vResult.or = (pRes2.or ? true : false);
			else vResult.or = false;
			return vResult;
		}
		for (var i = 0; i<pRes2.list.length; i++){
			var vPage2 = pRes2.list[i];
			if (typeof vResult.ctrl[vPage2.url] != "undefined"){
				this.xMergePageCategories(vResult.list[vResult.ctrl[vPage2.url]], vPage2);
			} else {
				vResult.list.push(vPage2)
				vResult.ctrl[vPage2.url] = vResult.list.length-1;
			}
		}
		vResult.or = (pRes2.or ? true : false);
		return vResult;
	},
	/* == xMergePageCategories ===================================================
	   PRIVATE - Merge two page category counts and update de first page object
	   pPage1 : input page object
	   pPage2 : input page object
	   pAdd : optionnal - if "true" categories are added not averaged
	   return : Page object */
	xMergePageCategories : function(pPage1, pPage2, pAdd){
		if (typeof pAdd == "undefined") pAdd = false;
		for (var vIdx in pPage2.idxCat){
			var vIdxCat2 = pPage2.idxCat[vIdx];
			if (typeof pPage1.idxCat[vIdx] == "undefined"){
				pPage1.idxCat[vIdx] = pPage2.idxCat[vIdx];
				pPage1.cat = null;
			} else {
				var vIdxCat1 = pPage1.idxCat[vIdx];
				var vCat = "";
				for (var i=0; i<vIdxCat2.length; i++){
					vCat += pAdd ? Math.min(9,scCoLib.toInt(vIdxCat1.charAt(i))+scCoLib.toInt(vIdxCat2.charAt(i))) : Math.round((scCoLib.toInt(vIdxCat1.charAt(i))+scCoLib.toInt(vIdxCat2.charAt(i)))/2);
				}
				pPage1.idxCat[vIdx] = vCat;
				if (typeof pPage1.cat == "string") pPage1.cat = vCat;
			}
		}
		return pPage1;
	},
	/* == xfilterAccentedLatinLetters ============================================
	   PRIVATE - must be synchonized with com.scenari.s.fw.utils.HCharSeqUtil.stringWithoutAccent().
	   pStr : input string
	   pKeepUnknown : optionnal - if true keep all unknown characters (used in highlighting the case of special punctuation)
	   return : filtered string */
	xfilterAccentedLatinLetters : function(pStr, pKeepUnknown){
		if (typeof pKeepUnknown == "undefined") pKeepUnknown = false;
		var vLen = pStr.length;
		var i = 0;
		for (; i < vLen; i++) {
			if (pStr.charCodeAt(i) >= 128) break;
		}
		if (i < vLen) {
			var vBuf = [];
			for (var j = 0; j < i; j++)
				vBuf.push(pStr.charAt(j));
			for (var j = i; j < vLen; j++) {
				var vCh = pStr.charAt(j);
				if (vCh.charCodeAt(0) < 128) {
					vBuf.push(vCh);
					continue;
				}
				switch (vCh) {
				case 'à':
				case 'á':
				case 'â':
				case 'ä':
				case 'ã':
				case 'å':
				case 'ª':
				case '\u0101': // a macron
				case '\u0103': // a breve
				case '\u0105': // a tail
					vBuf.push('a');
					break;
				case 'æ':
					vBuf.push('a');
					vBuf.push('e');
					break;
				case 'ç':
				case '\u010D': // c caron
				case '\u0107': // c acute
					vBuf.push('c');
					break;
				case '\u010F': // d caron
				case '\u0111': // d stroke
					vBuf.push('d');
					break;
				case 'é':
				case 'è':
				case 'ê':
				case 'ë':
				case '\u0113': // e macron
				case '\u0119': // e tail
					vBuf.push('e');
					break;
				case '\u0123': // g cedilla
				case '\u011F': // g breve
					vBuf.push('g');
					break;
				case 'î':
				case 'ï':
				case '\u0129': // i macron
					vBuf.push('i');
					break;
				case '\u013E': // l caron
				case '\u0142': // l stroke
					vBuf.push('l');
					break;
				case 'ñ':
				case '\u0148': // n caron
				case '\u0144': // n acute
					vBuf.push('n');
					break;
				case 'ò':
				case 'ó':
				case 'ô':
				case 'ö':
				case 'õ':
				case 'ø':
					vBuf.push('o');
					break;
				case '\u0153': // oe
					vBuf.push('o');
					vBuf.push('e');
					break;
				case '\u0155': // r acute
					vBuf.push('r');
					break;
				case '\u0161': // s caron
				case '\u0219': // s comma
				case '\u015B': // s acute
				case '\u015F': // s cedilla
					vBuf.push('s');
					break;
				case 'ß':
					vBuf.push('s');
					vBuf.push('s');
					break;
				case '\u021B': // t comma
				case '\u0165': // t caron
					vBuf.push('t');
					break;
				case 'û':
				case 'ù':
				case 'ü':
				case '\u0171': // u double acute
					vBuf.push('u');
					break;
				case 'ý':
				case 'ÿ':
					vBuf.push('y');
					break;
				case '\u017E': // z caron
				case '\u017A': // z acute
				case '\u017C': // z dot above
					vBuf.push('z');
					break;
				case 'À':
				case 'Á':
				case 'Â':
				case 'Ã':
				case 'Ä':
				case 'Å':
				case '\u0100': // A macron
				case '\u0102': // A breve
				case '\u0104': // A tail
					vBuf.push('A');
					break;
				case 'Æ':
					vBuf.push('A');
					vBuf.push('E');
					break;
				case 'Ç':
				case '\u010C': // C caron
				case '\u0106': // C acute
					vBuf.push('C');
					break;
				case '\u010E': // D caron
				case '\u0110': // D stroke
					vBuf.push('D');
					break;
				case 'É':
				case 'È':
				case 'Ê':
				case 'Ë':
				case '\u0112': // E macron
				case '\u0118': // E tail
					vBuf.push('E');
					break;
				case '\u0122': // G cedilla
				case '\u011E': // G breve
					vBuf.push('G');
					break;
				case 'Ì':
				case 'Í':
				case 'Î':
				case 'Ï':
				case '\u012A': // I macron
					vBuf.push('I');
					break;
				case '\u013D': // L caron
				case '\u0141': // L stroke
					vBuf.push('L');
					break;
				case 'Ñ':
				case '\u0147': // N caron
				case '\u0143': // N acute
					vBuf.push('N');
					break;
				case 'Ò':
				case 'Ó':
				case 'Ô':
				case 'Õ':
				case 'Ö':
				case 'Ø':
					vBuf.push('O');
					break;
				case '\u0152': // OE
					vBuf.push('O');
					vBuf.push('E');
					break;
				case '\u0154': // R acute
					vBuf.push('R');
					break;
				case '\u0160': // S caron
				case '\u0218': // S comma
				case '\u015A': // S acute
				case '\u015E': // S cedilla
					vBuf.push('S');
					break;
				case '\u021A': // T comma
				case '\u0164': // T caron
					vBuf.push('T');
					break;
				case 'Ù':
				case 'Ú':
				case 'Û':
				case 'Ü':
				case '\u0170': // U double acute
					vBuf.push('U');
					break;
				case 'Ý':
					vBuf.push('Y');
					break;
				case '\u017D': // Z caron
				case '\u0179': // Z acute
				case '\u017B': // z dot above
					vBuf.push('Z');
					break;
				case '¹':
					vBuf.push('1');
				case '²':
					vBuf.push('2');
				case '³':
					vBuf.push('3');
					break;
				case '\u20A4': // Lira sign
					vBuf.push('L');
					break;
				case '\u20AC': // euro sign
					vBuf.push('E');
					break;
				case '\u00A2': // cent sign
					vBuf.push('c');
					break;
				case '\u00A3': // pound sign
					vBuf.push('P');
					break;
				case '\u00A5': // yen sign
					vBuf.push('Y');
					break;
				default:
					if(pKeepUnknown) vBuf.push(vCh);
				}
			}
			if (vBuf.length == 0) vBuf.push('x');
			return vBuf.join("");
		}
		return pStr;
	},
	/* == xLocalStore ============================================================
	   PRIVATE - Local Storage API (localStorage/userData/cookie)
	   pId : local storage Id*/
	xLocalStore : function (pId){
		if (pId && !/^[a-z][a-z0-9]+$/.exec(pId)) throw new Error("Invalid store name");
		this.fId = pId || "";
		this.fRootKey = scServices.scLoad.getRootUrl();
		if (typeof localStorage != "undefined") {
			this.get = function(pKey) {var vRet = localStorage.getItem(this.fRootKey+this.xKey(pKey));return (typeof vRet == "string" ? unescape(vRet) : null)};
			this.set = function(pKey, pVal) {localStorage.setItem(this.fRootKey+this.xKey(pKey), escape(pVal))};
		} else if (window.ActiveXObject){
			this.get = function(pKey) {this.xLoad();return this.fIE.getAttribute(this.xEsc(pKey))};
			this.set = function(pKey, pVal) {this.fIE.setAttribute(this.xEsc(pKey), pVal);this.xSave()};
			this.xLoad = function() {this.fIE.load(this.fRootKey+this.fId)};
			this.xSave = function() {this.fIE.save(this.fRootKey+this.fId)};
			this.fIE=document.createElement('div');
			this.fIE.style.display='none';
			this.fIE.addBehavior('#default#userData');
			document.body.appendChild(this.fIE);
		} else {
			this.get = function(pKey){var vReg=new RegExp(this.xKey(pKey)+"=([^;]*)");var vArr=vReg.exec(document.cookie);if(vArr && vArr.length==2) return(unescape(vArr[1]));else return null};
			this.set = function(pKey,pVal){document.cookie = this.xKey(pKey)+"="+escape(pVal)};
		}
		this.xKey = function(pKey){return this.fId + this.xEsc(pKey)};
		this.xEsc = function(pStr){return "LS" + pStr.replace(/ /g, "_")};
	},
	/* == xLog ===================================================================
	   PRIVATE - Log a message to console
	   pStr : input string*/
	xLog : function(pStr){
		try{
			if (this.fDebug) console.log("scServices.scSearch: "+pStr);
		} catch(e){}
	},
	loadSortKey: "0search"
}
