/*
  Live Search for DCX Framework (C) James Brindle, 2009
*/

if (typeof(dcxLiveSearch) == 'undefined') {
  _ls = dcxLiveSearch = {};
}


if (typeof(_ls.LiveSearch) == 'undefined') {
  _ls.LiveSearch = {};
}

_ls.LiveSearch = function(id, params) {

  this.init(id, params);

  var obj = this;

  this.fld.onkeypress = function(e) { obj.onKeyPress(e); return _ls.STATIC.disableEnterKey(e); }
  this.fld.onkeyup    = function(e) { obj.onKeyUp(e); return _ls.STATIC.disableEnterKey(e); }
  this.fld.onblur     = function(e) { return obj.onBlur(e); }

  this.debugPrint('Initialised');
  var s = "";
  for (var k in this.param) { s += k + ": \"" + this.param[k] + "\", "; }
  s.replace(/,\s/, '');
  this.debugPrint("params = {" + s + "}");

  this.debugPrint("Waiting for input....");

};

_ls.LiveSearch.prototype.init = function(id, params) {
  this.keycodes = {
    CR : 13,
    TAB : 9,
    ESC : 27,
    UP : 38,
    DOWN : 40
  };

  this.defparam = {
    minlen : 1,
    debugID : 'dcxlivesearchdebug',
    displaynoresultsmsg : 1,
    noresultsmsg : 'No Results Found',
    classname : 'livesearch',
    scrollentries : 10,
    cacheresults : 1,
    cachesearchkey : 'key'
  };

  this.fldID        = id;
  this.fld          = _ls.DOM.gE(id);
  this.selObjID     = 'dcxls_' + this.fldID;
  this.param        = params ? params : {};
  this.input        = '';
  this.inlen        = 0;
  this.results      = [];
  this.highlightpos = 0;
  this.tallest      = 0;
  this.scrollpos    = 0;
  this.viewstart    = 0;
  this.viewend      = 0;

  // set up the default options
  for (var key in this.defparam) {
    if (typeof(this.param[key]) != typeof(this.defparam[key])) { this.param[key] = this.defparam[key]; }
  }

  // check some sanity in some of the default options
  if (this.param.cachesearchkey == '') { this.param.cachesearchkey = 'key'; }

};

_ls.LiveSearch.prototype.debugPrint = function(str) {
  if (typeof(this.debug) == 'undefined') {
    this.debug = _ls.DOM.gE(this.param.debugID);
    if (this.debug == 0) { return; }

    this.debug.style.height = 200;
    this.debug.style.overflow = 'scroll';

    this.debugEnabled = 1;
  }

  if (this.debugEnabled) {
    this.debug.innerHTML += this.fldID + ": " + str + "<br>\n";
    this.debug.scrollTop = this.debug.scrollHeight;
  }

};

// EVENT HANDLER - onKeyPress()
_ls.LiveSearch.prototype.onKeyPress = function(e) {
  if (!e) { e = window.event; }
	var keyCode = (window.event) ? window.event.keyCode : e.keyCode;
	var bubbleEvent = 1;

	switch (keyCode) {
	  case this.keycodes['CR']:
	    bubbleEvent = 0;
	    this.debugPrint("enter pressed - set highlighted item");
	    this.setSelected();
	    this.handleCRPressed();
	    break;
	    /*
	  case this.keycodes['ESC']:
	    bubbleEvent = 0;
	    this.debugPrint("escape pressed - cancel search and clear");
	    this.cancelList();
	    break;
	    */
	}

	if (bubbleEvent == 0) {
    e.cancelBubble = true;
    if (e.stopPropagation) { e.stopPropagation(); }
	}

	return bubbleEvent;
};

// EVENT HANDLER - onKeyUp()
_ls.LiveSearch.prototype.onKeyUp = function(e) {
  if (!e) { e = window.event; }
	var keyCode = (window.event) ? window.event.keyCode : e.keyCode;
	var bubbleEvent = 1;

	switch (keyCode) {
	  case this.keycodes['UP']:
	    bubbleEvent = 0;
	    this.debugPrint("Arrow up pressed");
	    this.keypressMoveHighlight(-1);
	    break;
	  case this.keycodes['DOWN']:
	    bubbleEvent = 0;
	    this.debugPrint("Arrow down pressed");
	    this.keypressMoveHighlight(1);
	    break;
	  case this.keycodes['CR']:
	    bubbleEvent = 0;
	    break;
	  case this.keycodes['ESC']:
	    bubbleEvent = 0;
	    this.debugPrint("escape pressed - cancel search and clear");
	    this.cancelList();
	    break;
	  default:
	    this.debugPrint("other key pressed - looking for suggestions");
	    this.doSearch(this.fld.value);
	    break;
	}

	if (bubbleEvent == 0) {
    e.cancelBubble = true;
    if (e.stopPropagation) { e.stopPropagation(); }
	}

	return bubbleEvent;
};

// EVENT HANDLER - onBlur()
_ls.LiveSearch.prototype.onBlur = function(e) {
  e = (window.event) ? window.event : e;
  this.debugPrint("onBlur event handler called");
  //this.cancelList();
  if (this.results.length == 0) { this.debugPrint("- no value, exiting!"); return false; }
  if (!this.param.onblur_callback) { this.debugPrint("- no callback handler defined, exiting!"); return false; }

  eval("var fType = typeof("+this.param.onblur_callback+");");
  if (fType != 'function') { this.debugPrint("- callback handler defined but "+this.param.onblur_callback+" is not a function "); return 0; }

  this.debugPrint("executing onblur_callback");
  var o = { value: this.fld.value };
  var ptr = this;
  o.debugPrint = function(str) { ptr.debugPrint("- "+ptr.param.onblur_callback+"() : "+str); }
  eval("var fRet = "+this.param.onblur_callback+"(o)");
  this.debugPrint("done with onblur_callback");

  return fRet;
}

_ls.LiveSearch.prototype.handleCRPressed = function() {
  this.debugPrint("CRLF event handler called");
  if (this.results.length == 0) { this.debugPrint("- bubbled from list item selection or no value, exiting!"); return 0; }
  if (!this.param.oncrlf_callback) { this.debugPrint("- no callback handler defined, exiting!"); return 0; }

  eval("var fType = typeof("+this.param.oncrlf_callback+");");
  if (fType != 'function') { this.debugPrint("- callback handler defined but "+this.param.oncrlf_callback+" is not a function "); return 0; }

  this.debugPrint("executing oncrlf_callback");
  var o = { value: this.fld.value };
  var ptr = this;
  o.debugPrint = function(str) { ptr.debugPrint("- "+ptr.param.oncrlf_callback+"() : "+str); }
  eval("var fRet = "+this.param.oncrlf_callback+"(o)");
  this.debugPrint("done with oncrlf_callback");

  return 0;
}

_ls.LiveSearch.prototype.doSearch = function(strValue) {
  // bail out if nothing has changed
  if (strValue == this.input) { return; }

  _ls.DOM.rE(this.selObjID);

  this.input = strValue;

  if (strValue.length < this.param.minlen) {
    this.results = [];
    this.inlen = strValue.length;
    return;
  }

  this.debugPrint("minimum string length reached, sending request");

  var ol = this.inlen;
  this.inlen = strValue.length ? strValue.length : 0;
  var l = this.results.length;

  if (this.inlen > ol && l && this.param.cacheresults) {
    this.debugPrint("searching existing cache as string is growing");
    this.debugPrint("cache has "+ l + " items, looking for '"+strValue.toLowerCase()+"'");
    var arr = new Array();
    for (var i=0; i<l; i++) {
      //this.debugPrint("- examining item "+ i +": "+ this.results[i][this.param.cachesearchkey].toLowerCase());
      if (this.results[i][this.param.cachesearchkey].toLowerCase().indexOf(strValue.toLowerCase()) != -1) {
        arr.push(this.results[i]);
      }
    }
    this.results = arr;
    this.createList();

    return false;

  } else {
    this.callRemoteScript(strValue);
  }

};

_ls.LiveSearch.prototype.callRemoteScript = function(strValue) {
  this.debugPrint("staring an AJAX request");

  var obj = this;
  var input = this.input;

  // don't send an empty search
  if (input.length == 0) { return; }

  // set up the URL to be called
  var url = this.param.script + "?" + this.param.varname + "=" + encodeURIComponent(input);

  // check to see if we have been told to do a callback before we send the query
  if (this.param.preajax_callback) {
    var func = this.param.preajax_callback+"(obj)";
    eval("var preajax_return = " + func);
    url += "&" + preajax_return;
  }
  var ajaxOnSuccess = function (objReq) { obj.handleResults(objReq, input); }
  var ajaxOnError   = function (err) { alert("AJAX Error : " + err); }
  var ajax = new _ls.AJAX(obj);
	ajax.doRequest(url, ajaxOnSuccess, ajaxOnError);

};

_ls.LiveSearch.prototype.handleResults = function(objReq, input) {
  if (input != this.input) { return; }

  this.debugPrint("handleResults (");
  var xml = objReq.responseXML;

  var results = xml.getElementsByTagName('results')[0].childNodes;

  this.results = new Array();

  for (var i=0; i<results.length; i++) {
    if (results[i].hasChildNodes()) {
      var t = results[i];
      var tKey = t.getAttribute('key');
      var tVal = t.getAttribute('val');
      var tHTML = "";
      var tLink = t.getAttribute('link');
      var tTarget = t.getAttribute('target');
      for (var z=0; z<t.childNodes.length; z++) {
        var node = t.childNodes[z];
        if (node.nodeType == 4) {
          tHTML = node.nodeValue;
        }
      }
      this.results.push( { key: tKey, link: tLink, target: tTarget, val: tVal, html: tHTML } );
    }
  }
  this.debugPrint(this.results.length + " items returned");

  _ls.DOM.rE(this.selObjID);
  this.debugPrint(") // END handleResults");
  this.createList();
};

_ls.LiveSearch.prototype.createList = function() {
  this.debugPrint("Starting creating the list");
  var obj = this;
  var calcpage = 0;

  if (this.results.length == 0 && !this.param.displaynoresultsmsg) { return; }

  // clear some of our storage pointers
  this.tallest = 0;
  this.highlightpos = 0;
  this.scrollpos = 0;

  // create the containing DIV and add it to the document now
  var DIV            = _ls.DOM.cE("DIV", {id : this.selObjID, className : this.param.classname});
  var pos            = _ls.DOM.getPos(this.fld);
  DIV.style.left     = pos.x + "px";
  DIV.style.top      = ( pos.y + this.fld.offsetHeight ) + "px";
  DIV.style.width    = this.fld.offsetWidth + "px";
  DIV.style.position = "absolute";
	document.getElementsByTagName("body")[0].appendChild(DIV);

  // set up for scrolling if the number of items in the list exceeds the max entries before scrolling setting
  if (this.results.length > this.param.scrollentries) {
    DIV.style.overflow = "auto";
    calcpage = 1;
  }

  // if we have some results to show then we'll build our unordered list
  if (this.results.length > 0) {

  	// create the un-ordered list and add it to the containing DIV
    var UL = _ls.DOM.cE("UL", { id : this.selObjID+"_ul"} );
    DIV.appendChild(UL);

    // loop through the items in the list
    for (var i=0; i<this.results.length; i++) {
      var item = this.results[i];
      var itemHTML = _ls.STATIC.highlightText(item.html, this.input);
      var A = _ls.DOM.cE("A", { href: '#', name: i+1 });
      var INNERDIV = _ls.DOM.cE("DIV", {}, itemHTML, true);

      //this.debugPrint("Adding item key="+item.key+", html="+item.html);
      A.onmouseover = function() { obj.setHighlight(this.name); }
      A.onclick = function() { obj.setSelected(); return false; }

      A.appendChild(INNERDIV);
      var LI = _ls.DOM.cE("LI", {}, A);

      UL.appendChild(LI);

      if (i == (this.param.scrollentries-1) && calcpage == 1) {
        DIV.style.height = DIV.offsetHeight;
        calcpage = 2;
        this.tallest = DIV.offsetHeight / this.param.scrollentries;
      }

    }

  } else {

    var INNERDIV = _ls.DOM.cE("DIV", { className : 'noresults' }, this.param.noresultsmsg, true);
    DIV.appendChild(INNERDIV);

  }

  this.debugPrint("tallest height = "+ this.tallest);

	// finally set the viewport parameters
	this.viewstart = 0;
	this.viewend = (this.results.length > this.param.scrollentries) ? this.param.scrollentries : this.results.length;

	var oDIV = _ls.DOM.gE(this.selObjID);
	this.debugPrint("DIV added, height = "+oDIV.offsetHeight);
};

/*
 * Function to cancel the current list and reset everything
 */
_ls.LiveSearch.prototype.cancelList = function(o) {
  var str = "";
  this.debugPrint("Cancel list called, "+ typeof(o) + " passed");
  if (typeof(o) == "undefined") {
    str = "";
  } else if (typeof(o) == "string") {
    str = o;
  } else if (typeof(o) == "object") {
    str = o.key;
  }
  this.fld.value = str;
  _ls.DOM.rE(this.selObjID);
  this.input   = '';
  this.inlen   = 0;
  this.results = new Array();
  this.highlightpos = 0;
  this.scrollpos = 0;
  this.viewstart = 0;
  this.viewend = 0;
  if (typeof(o) == "object") {
    if (o.link && !o.target) {
      this.debugPrint("Redirecting user to " + o.link);
      location.href = o.link;
    } else if (o.link && o.target) {
      this.debugPrint("Opening window based link to " + o.link);
      var w = window.open(o.link, o.target);
    } else if (this.param.onselect_callback) {
      eval("var fType = typeof("+this.param.onselect_callback+");");
      this.debugPrint("onselect_callback: Check if function "+ this.param.onselect_callback+ " exists - " + (fType == 'function' ? 'yes' : 'no'));
      if (fType == 'function') {
        var ptr = this;
        o.debugPrint = function(str) { ptr.debugPrint("- "+ ptr.param.onselect_callback +"(): "+str); }
        var func = this.param.onselect_callback+"(o)";
        this.debugPrint("executing onselect_callback");
        eval("var onselect_return = " + func);
        if (typeof(onselect_return) != "undefined" && onselect_return !== false) {
          this.debugPrint("callback function returned '"+ onselect_return +"'");
          this.fld.value = onselect_return;
        } else {
          this.debugPrint("nothing returned by callback function")
        }
        this.debugPrint("done with onselect_callback");
      }
    }
  }
  this.debugPrint("Cancel list complete");
};

/*
 * Function to handle cancelling a highlighted item
 *
 * This clears a previously selected highlight
 */
_ls.LiveSearch.prototype.clearHighlight = function() {

  var listUL  = _ls.DOM.gE(this.selObjID+"_ul");
  if (!listUL) { return; }

  if (this.highlightpos > 0) {
    listUL.childNodes[this.highlightpos-1].className = "";
    this.highlightpos = 0;
  }
};

/*
 * Function to handle selecting a highlighted item
 *
 * This works by storing the value from A.name within the UL construct and then setting the class of the LI object.
 * It calls clearHighligh() prior to setting it so that it can clear the highlight on the previously selected item.
 * This is better than just using the :hover class of the A tag as we can move the mouse away and the highlight
 * will stay in place.
 *
 */
_ls.LiveSearch.prototype.setHighlight = function(num) {

  var listUL  = _ls.DOM.gE(this.selObjID+"_ul");
  if (!listUL) { return; }

  if (this.highlightpos > 0) { this.clearHighlight(); }

  this.highlightpos = Number(num);
  listUL.childNodes[this.highlightpos-1].className = "highlighted";

};

_ls.LiveSearch.prototype.keypressMoveHighlight = function(direction) {

  var num;

  var listUL  = _ls.DOM.gE(this.selObjID+"_ul");
  if (!listUL) { return; }

  if (direction > 0) { // move down list
    num = this.highlightpos + 1;
  } else if (direction < 0) { // move up list
    num = this.highlightpos - 1;
  }

  if (num > listUL.childNodes.length) {
    num = listUL.childNodes.length;
  }

  if (num < 1) {
    num = 1;
  }

  // call out to our other function to actually set the highlight
  this.setHighlight(num);
  this.scrollListToItem(num, direction);

};

_ls.LiveSearch.prototype.scrollListToItem = function(num, direction) {

  var recalc = 0;
  var prevnum = num - direction;  // this would decrease with a negative direction (eg up)
  var listDIV = _ls.DOM.gE(this.selObjID);
  if (!listDIV) { return; }

  var listUL  = _ls.DOM.gE(this.selObjID+"_ul");
  if (!listUL) { return; }

  if (this.results.length <= this.param.scrollentries) { return; }

  if (num > prevnum) { // we're going down the list
    if (num > this.viewend) {
      this.viewstart = num - this.param.scrollentries;
      this.viewend   = this.viewstart + this.param.scrollentries;
      recalc = 1;
    }
  } else if (num < prevnum) { // we're going up the list
    this.debugPrint("moving up: num="+num+", prevnum="+prevnum+", viewstart="+this.viewstart+", viewend="+this.viewend);
    if ((num-1) < this.viewstart) {
      this.viewstart = num-1;
      this.viewend   = this.viewstart + this.param.scrollentries;
      recalc = 1;
    }
  }

  if (recalc) {
    var prev = this.scrollpos;
    this.scrollpos = (num == this.results.length) ? listDIV.scrollHeight : Number(this.viewstart * this.tallest);
    this.debugPrint("calculating offset from num="+num+", scrollentries="+this.param.scrollentries+", tallest="+this.tallest+", prevscollpos="+prev+", newscrollpos="+this.scrollpos);
  }

  this.debugPrint("keyboard scrolling: num="+num+", scrollpos="+this.scrollpos+", viewstart="+this.viewstart+", viewend="+this.viewend);
  listDIV.scrollTop = this.scrollpos;
};

/*
 * Function to handle the selection of an event
 */
_ls.LiveSearch.prototype.setSelected = function() {
  _ls.DOM.rE(this.selObjID);
  if (this.highlightpos == 0) { return; }

  var item = this.results[this.highlightpos-1];

  this.cancelList(item);

}

/*
 * Function to return the currently selected item as an object
 */
_ls.LiveSearch.prototype.getSelected = function() {
  if (this.highlightpos == 0) { return; }
  var item = this.results[this.highlightpos-1];
  return item;
}

/* DOM handlers */
if (typeof(_ls.DOM) == 'undefined') { _ls.DOM = {}; }

// gE = getElementById wrapper
_ls.DOM.gE = function(e) {
	var t = typeof(e);

	if (t == "undefined") {
	  return 0;
  } else if (t == "string") {
    var o = document.getElementById(e);
    if (!o) {
      return 0;
    } else if (typeof(o.appendChild) != "undefined") {
      return o;
    } else {
      return 0;
    }
  } else if (typeof(e.appendChild) != "undefined") {
    return e;
  } else {
    return 0;
  }

}

// cE = createElement wrapper
_ls.DOM.cE = function ( type, attrs, cont, html ) {
	var newElem = document.createElement( type );

	// bail out if for some reason we couldn't create the element
	if (!newElem) { return; }

	// loop thruogh the attribs and push them into the new element
	for (var a in attrs) { newElem[a] = attrs[a]; }

	var t = typeof(cont);

	if (t == "string" && !html) {
	  newElem.appendChild( document.createTextNode(cont) );
	} else if (t == "string" && html) {
		newElem.innerHTML = cont;
	} else if (t == "object") {
		newElem.appendChild( cont );
	}
	return newElem;
};

// rE = remove element
_ls.DOM.rE = function (ele) {
	var e = this.gE(ele);

	if (!e) {
	  return 0;
	}	else if (e.parentNode.removeChild(e)) {
		return true;
	} else {
		return 0;
	}

};

// getPos - returns position of an element on screen
_ls.DOM.getPos = function (e) {
	var e = this.gE(e);

	var obj = e;

	var curleft = 0;
	if (obj.offsetParent)	{
		while (obj.offsetParent) {
			curleft += obj.offsetLeft;
			obj = obj.offsetParent;
		}
	}	else if (obj.x) {
		curleft += obj.x;
  }

 	var obj = e;

	var curtop = 0;
	if (obj.offsetParent) {
	  while (obj.offsetParent) {
			curtop += obj.offsetTop;
			obj = obj.offsetParent;
		}
	}	else if (obj.y) {
		curtop += obj.y;
	}

	return {x : curleft, y : curtop};
};

/* AJAX handlers */
if (typeof(_ls.AJAX) == 'undefined') { _ls.AJAX = {}; }

_ls.AJAX = function(caller) {
  this.callingobj = caller;
  this.objReq = {};
  this.isIE = false;

  var c = this.callingobj;
  this.debugPrint = function(s) { c.debugPrint(s); }

  this.debugPrint('AJAX Initialised');
}

_ls.AJAX.prototype.doRequest = function(strURL, funcSuccess, funcError) {
  this.onComplete = funcSuccess;
  this.onError    = funcError;

  var obj = this;

  this.objReq = this.getXMLHTTPObject();

  if (this.objReq) {
    this.objReq.onreadystatechange = function () { obj.processRequestChange() };
	  this.objReq.open("GET", strURL, true);
	  this.objReq.send(null);
	  this.debugPrint("Request started : " + strURL);
  } else {
    this.debugPrint("Can't find an AJAX object");
  }
};

_ls.AJAX.prototype.processRequestChange = function() {
	// only if req shows "loaded"
	if (this.objReq.readyState == 4) {
	  this.debugPrint("Object loaded, status = " + this.objReq.status);
		// only if "OK"
		if (this.objReq.status == 200 || this.objReq.status == 0) {
		  this.debugPrint("AJAX done - passing to onComplete function");
			this.onComplete( this.objReq );
		} else {
		  this.debugPrint("AJAX done but with error - passing to onError function");
			this.onError( this.objReq.status );
		}
	}

};


_ls.AJAX.prototype.getXMLHTTPObject = function() {
  var o;

	if (window.XMLHttpRequest) { // branch for native XMLHttpRequest object
	  o = new XMLHttpRequest();
	// branch for IE/Windows ActiveX version
	} else if (window.ActiveXObject) {
		o = new ActiveXObject("Microsoft.XMLHTTP");
	} else {
	  return 0;
	}

	return o;
};

/* STATIC HANDLERS */
if (typeof(_ls.STATIC) == 'undefined') { _ls.STATIC = {}; }

_ls.STATIC.highlightText = function(str, txt) {
  var p1, p2, p3;
  if (str.length == 0) { return str; }

  if (txt.length == 0) { return str; }

  // if we can't find the construct <!>..</!> then bail out
  if ((p1 = str.indexOf('<!>', 0)) == -1) { return str; }
  if ((p2 = str.indexOf('</!>', p1)) == -1) { return str; }

  // now that we know where the construct is, we can extract it, if we can't find the string we just
  var tStr = str.substring(p1+3, p2);
  if ((p3 = tStr.toLowerCase().indexOf(txt.toLowerCase())) == -1) {

    rStr = str.replace(/\<!\>|\<\/!\>/g, '');

  } else {

    var hStr = tStr.substring(0, p3) +
      '<em>'+
      tStr.substring(p3, p3+txt.length) +
      '</em>'+
      tStr.substring(p3+txt.length);

    var rStr = str.substring(0,p1) + hStr + str.substring(p2+4);

  }
  /*
  alert("search="+txt+", length="+txt.length+
    "\ntag found starting at "+ p1 +", ending at "+p2+
    "\nstring="+tStr+
    "\nmatch at="+p3+
    "\nreplacing with="+hStr+
    "\nreturn="+rStr);
  */
  return rStr;
}

_ls.STATIC.disableEnterKey = function(e) {
  if (!e) { e = window.event; }
  var key;
  if (window.event) {
    key = window.event.keyCode;
  } else {
    key = e.which;
  }
  return (key != 13);
}
