// Copyright 2010 MEDSEEK Inc.  All Rights Reserved.

/** This script is a heavily modified version of Google's ss.js script. The logic from the original file
 * has been encapsulated into a new class called, smm_google_ss to allow the code to be used more
 * than once per page.
 *
 * ---- Google's original description of the suggestion parameters. ----
 * @fileoverview JavaScript for GSA Suggest (Core).
 *
 * List of global variables defined in other files. We define these variables
 * in an XSLT accessible to customers so that they can customize it.
 * Look at the stylesheet_template.enterprise for detailed descriptions of
 * these variables. Listing here with short descriptions:
 * <ul>
 * <li> ss_form_element {string} Name of search form.
 * <li> ss_popup_element {string} Name of search suggestion drop down.
 * <li> ss_seq {array} Types of suggestions to include.
 * <li> ss_g_one_name_to_display {string} name to display to user.
 * <li> ss_g_more_names_to_display {string} name to display to user.
 * <li> ss_g_max_to_display {number} Max number of query suggestions to display.
 * <li> ss_max_to_display {number} Max number of all types of suggestions to
 * display.
 * <li> ss_wait_millisec {number} Idling internval for fast typers.
 * <li> ss_delay_millisec {number} Delay time to avoid contention when drawing
 * the suggestion box by various par  allel processes.
 * <li> ss_gsa_host {string} Host name or IP address of GSA.
 * <li> SS_OUTPUT_FORMAT_LEGACY {string} Constant that contains the value for
 * legacy output format.
 * <li> SS_OUTPUT_FORMAT_OPEN_SEARCH {string} Constant that contains the value
 * for OpenSearch output format.
 * <li> SS_OUTPUT_FORMAT_RICH {string} Constant that contains the value for rich
 * output format.
 * <li> ss_g_protocol {string} Output format protocol to use.
 * <li> ss_allow_debug {boolean} Whether debugging is allowed.
 * </ul>
 */
 
/*** Array prototype extension ***/
if (!Array.indexOf) {
  /**
   * Custom implementation of indexOf for browsers that do not support it.
   * For example, IE6 and IE7 do not support.
   *
   * @param {Object} obj The element to be searched in the array.
   *
   * @return {number} The index if the element is found, -1 otherwise.
   */
  Array.prototype.indexOf = function(obj) {
    for (var i = 0; i < this.length; i++) {
      if (this[i] == obj) {
        return i;
      }
    }
    return -1;
  };
}

/**
 * smm_google_ss class.
 *
 * @constructor
 */
function smm_google_ss(objSettings)
{
	/**
	 * HTML element names for the search form, the spellchecking suggestion, and the
	 * cluster suggestions. The search form must have the following input elements:
	 * "q" (for search box), "site", "client".
	 * @type {string}
	 */
	this.ss_form_element = (!objSettings.ss_form_element)?'gs':objSettings.ss_form_element; // search form
	
	/**
	 * Name of search input element.
	 * @type {string}
	 */
	this.ss_input_element = (!objSettings.ss_input_element)?'q':objSettings.ss_input_element; // the input element that holds teh query.
	
	/**
	 * Name of search suggestion drop down.
	 * @type {string}
	 */
	this.ss_popup_element = (!objSettings.ss_popup_element)?('search_suggest'):(objSettings.ss_popup_element);
	
	/**
	 * Types of suggestions to include.  Just one options now, but reserving the
	 * code for more types
	 *   g - suggest server
	 * Array sequence determines how different suggestion types are shown.
	 * Empty array would effectively turn off suggestions.
	 * @type {object}
	 */
	this.ss_seq = (!objSettings.ss_seq)?([ 'g' ]):(objSettings.ss_seq);
	
	/**
	 * Suggestion type name to display when there is only one suggestion.
	 * @type {string}
	 */
	
	this.ss_g_one_name_to_display = (!objSettings.ss_g_one_name_to_display)?('Suggestion'):(objSettings.ss_g_one_name_to_display);
	/**
	 * Suggestion type name to display when there are more than one suggestions.
	 * @type {string}
	 */
	this.ss_g_more_names_to_display = (!objSettings.ss_g_more_names_to_display)?('Suggestions'):(objSettings.ss_g_more_names_to_display);
	
	/**
	 * The max suggestions to display for different suggestion types.
	 * No-positive values are equivalent to unlimited.
	 * For key matches, -1 means using GSA default (not tagging numgm parameter),
	 * 0 means unlimited.
	 * Be aware that GSA has a published max limit of 10 for key matches.
	 * @type {number}
	 */
	this.ss_g_max_to_display = (!objSettings.ss_g_max_to_display)?(10):(objSettings.ss_g_max_to_display);
	
	/**
	 * The max suggestions to display for all suggestion types.
	 * No-positive values are equivalent to unlimited.
	 * @type {number}
	 */
	this.ss_max_to_display = (!objSettings.ss_max_to_display)?(12):(objSettings.ss_max_to_display);
	
	/**
	 * Idling interval for fast typers.
	 * @type {number}
	 */
	this.ss_wait_millisec = (!objSettings.ss_wait_millisec)?(300):(objSettings.ss_wait_millisec);
	
	/**
	 * Delay time to avoid contention when drawing the suggestion box by various
	 * parallel processes.
	 * @type {number}
	 */
	this.ss_delay_millisec = (!objSettings.ss_delay_millisec)?(30):(objSettings.ss_delay_millisec);
	
	/**
	 * Host name or IP address of GSA.
	 * Null value can be used if the JS code loads from the GSA.
	 * For local test, use null if there is a &lt;base> tag pointing to the GSA,
	 * otherwise use the full GSA host name
	 * @type {string}
	 */
	this.ss_gsa_host = (!objSettings.ss_gsa_host)?(''):(objSettings.ss_gsa_host);
	
	/**
	 * The url scheme to use when querying the GSA host.
	 * The default is 'http'
	 * @type {string}
	 */
	this.ss_gsa_host_scheme = (!objSettings.ss_gsa_host_scheme)?('http'):(objSettings.ss_gsa_host_scheme);
	
	/**
	 * What suggest request API to use.
	 *   legacy - use current protocol in 6.0
	 *            Request: /suggest?token=&lt;query>&amp;max_matches=&lt;num>&amp;use_similar=0
	 *            Response: [ "&lt;term 1>", "&lt;term 2>", ..., "&lt;term n>" ]
	 *                   or
	 *                      [] (if no result)
	 *   os -     use OpenSearch protocol
	 *            Request: /suggest?q=&lt;query>&amp;max=&lt;num>&amp;site=&lt;collection>&amp;client=&lt;frontend>&amp;access=p&amp;format=os
	 *            Response: [
	 *                        "&lt;query>",
	 *                        [ "&lt;term 1>", "&lt;term 2>", ... "&lt;term n>" ],
	 *                        [ "&lt;content 1>", "&lt;content 2>", ..., "&lt;content n>" ],
	 *                        [ "&lt;url 1>", "&lt;url 2>", ..., "&lt;url n>" ]
	 *                      ] (where the last two elements content and url are optional)
	 *                   or
	 *                      [ &lt;query>, [] ] (if no result)
	 *   rich -   use rich protocol from search-as-you-type
	 *            Request: /suggest?q=&lt;query>&amp;max=&lt;num>&amp;site=&lt;collection>&amp;client=&lt;frontend>&amp;access=p&amp;format=rich
	 *            Response: {
	 *                        "query": "&lt;query>",
	 *                        "results": [
	 *                          { "name": "&lt;term 1>", "type": "suggest", "content": "&lt;content 1>", "style": "&lt;style 1>", "moreDetailsUrl": "&lt;url 1>" },
	 *                          { "name": "&lt;term 2>", "type": "suggest", "content": "&lt;content 2>", "style": "&lt;style 2>", "moreDetailsUrl": "&lt;url 2>" },
	 *                          ...,
	 *                          { "name": "&lt;term n>", "type": "suggest", "content": "&lt;content n>", "style": "&lt;style n>", "moreDetailsUrl": "&lt;url n>" }
	 *                        ]
	 *                      } (where type, content, style, moreDetailsUrl are optional)
	 *                   or
	 *                      { "query": &lt;query>, "results": [] } (if no result)
	 * If unspecified or null, using legacy protocol.
	 * @type {string}
	 */
	this.ss_protocol = (!objSettings.ss_protocol)?(smm_google_ss.SS_OUTPUT_FORMAT_RICH):(objSettings.ss_protocol);
	
	/**
	 * Whether to allow non-query suggestion items.
	 * Setting it to false can bring results from "os" and "rich" responses into
	 * backward compatible with "legacy".
	 * @type {boolean}
	 */
	this.ss_allow_non_query = (!objSettings.ss_allow_non_query)?(true):(objSettings.ss_allow_non_query);
	
	/**
	 * Default title text when the non-query suggestion item does not have a useful
	 * title.
	 * The default display text should be internalionalized.
	 * @type {string}
	 */
	this.ss_non_query_empty_title = (!objSettings.ss_non_query_empty_title)?('No Title'):(objSettings.ss_non_query_empty_title);
	
	/**
	 * Whether debugging is allowed.  If so, toggle with F2 key.
	 * @type {boolean}
	 */
	this.ss_allow_debug = (!objSettings.ss_allow_debug)?(false):(objSettings.ss_allow_debug);
	
	/**
	 * Object that stores which all type of suggestions to display.
	 * @type {object}
	 */
	this.ss_use = {};
	this.ss_use.g = this.ss_seq.indexOf('g') >= 0 ? true : false;
	
	/**
	 * Cached array that stores processed results for typed queries.
	 * @type {array}
	 */
	this.ss_cached = [];
	
	/**
	 * Cached query when using up and down arrows to move around the suggestion box.
	 * When the user escapes from the suggestion box, the typed query is restored
	 * from here.
	 * @type {string}
	 */
	this.ss_qbackup = null;
	
	/**
	 * The query for which suggestions are displayed.
	 * @type {string}
	 */
	this.ss_qshown = null;
	
	/**
	 * The table row location of the selected suggestion entry.
	 * @type {number}
	 */
	this.ss_loc = -1;
	
	/**
	 * Lock to prevent painting the suggestion box for an expired query after the
	 * required delay.
	 * @type {number}
	 */
	this.ss_waiting = 0;
	
	/**
	 * Lock to prevent contention when drawing the suggestion box, especially for
	 * the concurrent AJAX calls.
	 * @type {boolean}
	 */
	this.ss_painting = false;
	
	/**
	 * Pending key handling request holder.
	 */
	this.ss_key_handling_queue = null;
	
	/**
	 * Pending painting request holder.
	 */
	this.ss_painting_queue = null;
	
	/**
	 * Global flag to indicate whether the search box is currently dismissed.
	 * The suggestion box must not be drawn if it is false.
	 * @type {boolean}
	 */
	this.ss_dismissed = false;
	
	/**
	 * Low-level raw information including AJAX requests and responses shown via
	 * rudimental alert().
	 * @type {boolean}
	 */
	this.ss_panic = false;
	
	/**
	 * Constant for the name of class for a row in suggestions drop down.
	 * @type {string}
	 */
	this.SS_ROW_CLASS = 'ss-gac-a';
	
	/**
	 * Constant for the name of class for a selected row in suggestions drop down.
	 * @type {string}
	 */
	this.SS_ROW_SELECTED_CLASS = 'ss-gac-b';
	
	var objFormElement = document.getElementById(this.ss_form_element);

	// append a client hidden input element. This has to be in the form, otherwise the suggestion url won't generated correctly.
	this.ss_client = (!objSettings.ss_client)?(''):(objSettings.ss_client);
	if(!objFormElement.client) {
		var objClient = document.createElement("input");
		objClient.type = "hidden";
		objClient.name = "client";
		objClient.value = this.ss_client;		
		objFormElement.appendChild(objClient);
		// for ie only
		if (!objFormElement.client) {
			objFormElement.client = objClient;
		}
	}
	
	// append a site hidden input element. This has to be in the form, otherwise the suggestion url won't generated correctly.
	this.ss_site = (!objSettings.ss_site)?(''):(objSettings.ss_site);
	if(!objFormElement.site) {
		var objSite = document.createElement("input");
		objSite.type = "hidden";
		objSite.name = "site";
		objSite.value = this.ss_site;
		objFormElement.appendChild(objSite);
		// for ie only
		if (!objFormElement.site) {
			objFormElement.site = objSite;
		}
	}

	var objInputElement = objFormElement[this.ss_input_element];
	objInputElement.setAttribute("autocomplete","off");
	
	/** 
	 * Wire Events
	**/
	var _this = this;
	smm_google_ss.ss_wireEvent(objInputElement,"keyup",function(objEvent){_this.ss_handleKey(objInputElement,objEvent);},false);
	smm_google_ss.ss_wireEvent(document,"keyup",function(objEvent){_this.ss_handleAllKey(document,objEvent);},false);
}

/* Static Declarations */
 
/**
 * Constant that represents legacy output format.
 * @type {string}
 */
smm_google_ss.SS_OUTPUT_FORMAT_LEGACY = 'legacy';

/**
 * Constant that represents OpenSearch output format.
 * @type {string}
 */
smm_google_ss.SS_OUTPUT_FORMAT_OPEN_SEARCH = 'os';

/**
 * Constant that represents rich output format.
 * @type {string}
 */
smm_google_ss.SS_OUTPUT_FORMAT_RICH = 'rich';

/**
	 * Instance of debugger.
	 * @type {ss_Debugger}
 */
smm_google_ss.ss_debug = new ss_Debugger();

/**
 * Wires an event handler to an event. 
 *
 * @param {Element} objElement The object to which the event handler will be wired.
 * @param {string} strEventName The name of the event.
 * @param {Element} objEventHandler event handler function.
 * @param {boolean} bolBubbleup Bubble up the event if true (FF/Chrome only).
 */
smm_google_ss.ss_wireEvent = function(objElement,strEventName,objEventHandler,bolBubbleup) {
	if (objElement.addEventListener) {
		objElement.addEventListener (strEventName,objEventHandler,bolBubbleup);
    // ie
    }else if (objElement.attachEvent) {
		objElement.attachEvent ("on" + strEventName,objEventHandler);
	// old browsers
    }else {
		objElement["on" + strEventName] = objEventHandler;
	}
};

/* Method Declarations */

/**
 * Composes the suggest URI to be sent to EnterpriseFrontend. Extracts the user
 * input from the suggest form and then formats the URI based on that.
 *
 * @param {string} qVal The query string.
 * @param {Element} suggestForm The suggest form node.
 *
 * @return {string} The composed URI.
 */
smm_google_ss.prototype.ss_composeSuggestUri = function ss_composeSuggestUri(qVal, suggestForm) {
  var siteVal = suggestForm.site ? suggestForm.site.value : null;
  var clientVal = suggestForm.client ? suggestForm.client.value : null;
  
  if (!qVal || !siteVal || !clientVal) {
    return null;
  }
  var accessVal = (suggestForm.access && suggestForm.access.value) ?
      suggestForm.access.value : 'p';
  var uri = '/suggest';
  if (smm_google_ss.ss_OUTPUT_FORMAT_LEGACY == this.ss_protocol) {
    uri = uri + '?token=' + encodeURIComponent(qVal) +
        '&max_matches=' + this.ss_g_max_to_display;
  } else {
    // Same param names for other two formats.
    uri = uri + '?q=' + encodeURIComponent(qVal) +
        '&max=' + this.ss_g_max_to_display;
  }
  uri = uri +
      '&site=' + encodeURIComponent(siteVal) +
      '&client=' + encodeURIComponent(clientVal) +
      '&access=' + encodeURIComponent(accessVal) +
      '&format=' + encodeURIComponent(this.ss_protocol);
  return uri;
};

/**
 * Submits a suggest query to the EnterpriseFrontend.
 *
 * Also defines a nested function handler that is called when suggest results
 * are fetched. The handler function parses the JSON response to extract
 * dynamic result clusters, and document matches.
 *
 * @param {string} qVal The query that user enters.
 */
// TODO: This function is too big and needs to be re-factored.
smm_google_ss.prototype.ss_suggest = function (qVal) {
  var startTimeMs = new Date().getTime();
  if (!this.ss_cached[qVal]) {
    this.ss_cached[qVal] = {};
  }
  var suggestForm = document.getElementById(this.ss_form_element);
  
  var uri = this.ss_composeSuggestUri(qVal, suggestForm);
  if (!uri) {
   return;
  }
  var url = this.ss_gsa_host ? this.ss_gsa_host_scheme + '://' + this.ss_gsa_host + uri : uri;
	
  if (this.ss_panic) {
    alert('ss_suggest() AJAX: ' + url);
  }
  
       
  var _this = this;
  var xmlhttp = XH_XmlHttpCreate();
  var handler = function() {
    if (xmlhttp.readyState == XML_READY_STATE_COMPLETED) {
	  if (_this.ss_panic) {
        alert('ss_suggest() AJAX: ' + xmlhttp.responseText);
      }
      var suggested;
      try {
	  	suggested = eval('(' + xmlhttp.responseText + ')');
      } catch (e) {
		_this.ss_cached[qVal].g = null;

        // Always try to show suggestion box even if there is no results
        // because previous attempt may be skipped due to concurrent ajax
        // processing.
        _this.ss_show(qVal);
        return;
      }
	   
	  if (_this.ss_use.g) {
        try {
		  switch (_this.ss_protocol) {
            case smm_google_ss.SS_OUTPUT_FORMAT_LEGACY:
            default:
              var suggestions = suggested;
              if (suggestions && suggestions.length > 0) {
                var found = false;
                _this.ss_cached[qVal].g = [];
                var max = (_this.ss_g_max_to_display <= 0) ?
                    suggestions.length :
                    Math.min(_this.ss_g_max_to_display, suggestions.length);
                for (var si = 0; si < max; si++) {
                  _this.ss_cached[qVal].g[si] = { 'q': suggestions[si] };
                  found = true;
                }
                if (!found) {
                  _this.ss_cached[qVal].g = null;
                }
              } else {
                _this.ss_cached[qVal].g = null;
              }
              break;
            case smm_google_ss.SS_OUTPUT_FORMAT_OPEN_SEARCH:
              if (suggested.length > 1) {
                var suggestions = suggested[1];
                if (suggestions && suggestions.length > 0) {
                  var found = false;
                  _this.ss_cached[qVal].g = [];
                  var max = (_this.ss_g_max_to_display <= 0) ?
                      suggestions.length :
                      Math.min(_this.ss_g_max_to_display, suggestions.length);
                  for (var si = 0; si < max; si++) {
                    if (suggestions[si] && suggestions[si] != suggested[0]) {
                      _this.ss_cached[qVal].g[si] = { 'q': suggestions[si] };
                      found = true;
                    } else if ((suggested.length > 3) && _this.ss_allow_non_query) {
                      var title = (suggested[2].length > si) ?
                          null : suggested[2][si];
                      var url = (suggested[3].length > si) ?
                          null : suggested[3][si];
                      if (url) {
                        title = !title ? _this.ss_non_query_empty_title : title;
                        _this.ss_cached[qVal].g[si] = { 't': title, 'u': url };
                        found = true;
                      }
                    }
                  }
                  if (!found) {
                    _this.ss_cached[qVal].g = null;
                  }
                } else {
                  _this.ss_cached[qVal].g = null;
                }
              } else {
                _this.ss_cached[qVal].g = null;
              }
              break;
            case smm_google_ss.SS_OUTPUT_FORMAT_RICH:
              var suggestions = suggested.results;
			  if (suggestions && suggestions.length > 0) {
                var found = false;
                _this.ss_cached[qVal].g = [];
                var max = (_this.ss_g_max_to_display <= 0) ?
                    suggestions.length :
                    Math.min(_this.ss_g_max_to_display, suggestions.length);
                for (var si = 0; si < max; si++) {
                  if (suggestions[si].name &&
                      suggestions[si].name != suggested.query) {
                    _this.ss_cached[qVal].g[si] = { 'q': suggestions[si].name };
                    found = true;
                  } else if (_this.ss_allow_non_query) {
                    var title = suggestions[si].content;
                    var url = suggestions[si].moreDetailsUrl;
                    if (url) {
                      title = !title ? _this.ss_non_query_empty_title : title;
                      _this.ss_cached[qVal].g[si] = { 't': title, 'u': url };
                      found = true;
                    }
                  }
                }
                if (!found) {
                  _this.ss_cached[qVal].g = null;
                }
              } else {
                _this.ss_cached[qVal].g = null;
              }
              break;
          }
        } catch (e) {
          _this.ss_cached[qVal].g = null;
        }
      }
      if (_this.ss_allow_debug && smm_google_ss.ss_debug && smm_google_ss.ss_debug.getDebugMode()) {
        var stopTimeMs = new Date().getTime();
        smm_google_ss.ss_debug.addRequestDebugLine(qVal, 'suggest',
                                     stopTimeMs - startTimeMs, _this.ss_cached[qVal]);
      }

      // Always try to show suggestion box even if there is no results
      // because previous attempt may be skipped due to concurrent ajax
      // processing.
      _this.ss_show(qVal);
    }
  };
  XH_XmlHttpPOST(xmlhttp, url, '', handler);
};

/**
 * Determines if the query has been processed.
 *
 * @param {string} qVal The query that user enters.
 * @return {boolean} True if this query is already in cache.
 */
smm_google_ss.prototype.ss_processed = function ss_processed(qVal) {
  if (!this.ss_cached[qVal] && this.ss_use.g) {
    return false;
  }
  return true;
};

/**
 * Handles key stroke events for turning debug console on and off.
 */
smm_google_ss.prototype.ss_handleAllKey = function ss_handleAllKey(objElement,objEvent) {
  var kid = (window.event) ? window.event.keyCode : objEvent.keyCode;
  switch (kid) {
    case 40:  // "key down".
    case 38:  // "key up".
      // If the next line is activated, key down and up will bring search box
      // into focus which is useful if the user happens to click the mouse
      // outside of the search box and the suggestions, but it may not be
      // desirable if you want to use keyboard to scroll the page also, once the
      // key is trapped here, it won't starts move the selection unless we add
      // suggestion movement code here, which would bring side effect to the
      // search box key stroke trapping.
      break;
    case 9:  // "tab".
    case 16:  // "shift-tab".
      this.ss_qbackup = null;
      this.ss_dismissed = true;
      this.ss_clear(true);  // Focusing away, must not grab focus back to the search
                       // box.
	  var qry = document.getElementById(this.ss_form_element)[this.ss_input_element].value;
	  
      if (!this.ss_processed(qry)) {
        // Fire new searches for the selected suggestion
        // useful for potential lucky guess.
        if (this.ss_panic) {
          alert('run ajax when key off');
        }
        this.ss_suggest(qry);
      }
      break;
    case 113:  // "F2".
      if (!this.ss_allow_debug) {
        break;
      }
      if (smm_google_ss.ss_debug && smm_google_ss.ss_debug.getDebugMode()) {
        smm_google_ss.ss_debug.deactivateConsole();
      } else {
        smm_google_ss.ss_debug.activateConsole();
      }
      break;
    default:
      break;
  }
};

/**
 * Handles key stroke events for the search box.
 */
smm_google_ss.prototype.ss_handleKey = function (objElement,objEvent) {
  var kid = (window.event) ? window.event.keyCode : objEvent.keyCode;
  var fo = document.getElementById(this.ss_form_element);
  var qnow = (!this.ss_qbackup) ? fo[this.ss_input_element].value : this.ss_qbackup;
  var sum = 0;
  var tbl = document.getElementById(this.ss_popup_element);
  
  switch (kid) {
    case 40:  // "key down".
      this.ss_dismissed = false;
      if (this.ss_processed(qnow)) {
        sum = this.ss_countSuggestions(qnow);
        if (sum > 0) {
          if (tbl.style.visibility == 'hidden') {
            this.ss_show(qnow);
            break;
          }
          if (this.ss_qbackup) {
            this.ss_loc++;
          } else {
            this.ss_qbackup = qnow;
            this.ss_loc = 0;
          }
          while (this.ss_loc >= sum)
            this.ss_loc -= sum;
          var rows = tbl.getElementsByTagName('tr');
          for (var ri = 0; ri < rows.length - 1; ri++) {
            if (ri == this.ss_loc) {
              rows[ri].className = this.SS_ROW_SELECTED_CLASS;
            } else {
              rows[ri].className = this.SS_ROW_CLASS;
            }
          }

          // Find out what type of suggestion it is.
          var suggestion = this.ss_locateSuggestion(qnow, this.ss_loc);

          // Adjust the query in the search box.
          if (suggestion.q) {
            fo[this.ss_input_element].value = suggestion.q;
          } else {
            fo[this.ss_input_element].value = this.ss_qbackup;
          }
        }
      } else {
        // May be here if using back button.
        if (this.ss_panic) {
          alert('run ajax when key down');
        }
        this.ss_suggest(qnow);
      }
      break;
    case 38:  // "key up".
      this.ss_dismissed = false;
      if (this.ss_processed(qnow)) {
        sum = this.ss_countSuggestions(qnow);
        if (sum > 0) {
          if (tbl.style.visibility == 'hidden') {
            this.ss_show(qnow);
            break;
          }
          if (this.ss_qbackup) {
            this.ss_loc--;
          } else {
            this.ss_qbackup = qnow;
            this.ss_loc = -1;
          }
          while (this.ss_loc < 0)
            this.ss_loc += sum;
          var rows = tbl.getElementsByTagName('tr');
          for (var ri = 0; ri < rows.length - 1; ri++) {
            if (ri == this.ss_loc) {
              rows[ri].className = this.SS_ROW_SELECTED_CLASS;
            } else {
              rows[ri].className = this.SS_ROW_CLASS;
            }
          }

          // Find out what type of suggestion it is.
          var suggestion = this.ss_locateSuggestion(qnow, this.ss_loc);

          // Adjust the query in the search box.
          if (suggestion.q) {
            fo[this.ss_input_element].value = suggestion.q;
          } else {
            fo[this.ss_input_element].value = this.ss_qbackup;
          }
        }
      } else {
        // May be here if using back button.
        if (this.ss_panic) {
          alert('run ajax when key up');
        }
        this.ss_suggest(qnow);
      }
      break;
    case 13:  // "enter".
      var url = null;
      if (this.ss_processed(qnow) && this.ss_qbackup && this.ss_loc > -1) {
        // Find out what type of suggestion it is.
        var suggestion = this.ss_locateSuggestion(this.ss_qbackup, this.ss_loc);
        // Adjust the query in the search box.
        if (suggestion.u) {
          url = suggestion.u;
        }
      }
      this.ss_qbackup = null;
      this.ss_dismissed = true;
      this.ss_clear();
      if (url) {
        window.location.href = url;
      }
      break;
    case 27:  // "escape".
      if (this.ss_qbackup) {
        fo[this.ss_input_element].value = this.ss_qbackup;
        this.ss_qbackup = null;
      }
      this.ss_dismissed = true;
      this.ss_clear();
      break;
    case 37:  // "key left".
    case 39:  // "key right".
    case 9:  // "tab".
    case 16:  // "shift-tab".
      break;
    default:
	  this.ss_dismissed = false;
      if (fo[this.ss_input_element].value == this.ss_qshown) {
        // The key stroke has not changed the searched text.
      } else {
        if (this.ss_key_handling_queue) {
          // Ignore pending key handling request delayed earlier.
          clearTimeout(this.ss_key_handling_queue);
        }
        this.ss_qbackup = null;
        this.ss_loc = -1;
        // Flow through for delayed AJAX calls.
        this.ss_waiting++;
        if (this.ss_allow_debug && smm_google_ss.ss_debug && smm_google_ss.ss_debug.getDebugMode()) {
          smm_google_ss.ss_debug.addWaitDebugLine(fo[this.ss_input_element].value, 'queue', this.ss_wait_millisec);
        }
		
		var _this = this;
		this.ss_key_handling_queue = setTimeout(function() {_this.ss_handleQuery(_this.ss_escape(fo[_this.ss_input_element].value),_this.ss_waiting);}, this.ss_wait_millisec);
      }
      break;
  }
};
 
/**
* Triggers fetch for query suggestions or triggers the display depending on
* whether the query has already been processed earlier or not.
*
* @param {string} query The query whose suggestions are needed.
* @param {number} waiting1 The value to match the lock so as not to handle
*     queries that are no longer valid.
*/
smm_google_ss.prototype.ss_handleQuery = function ss_handleQuery(query, waiting1) {
 
  if (waiting1 != this.ss_waiting) return;
  this.ss_waiting = 0;
  if (query == '') {
    this.ss_clear();
  } else if (!this.ss_processed(query)) {
    if (this.ss_panic) {
      alert('run ajax when key change');
    }
	this.ss_suggest(query);
  } else {
    this.ss_show(query);
  }
};

/**
 * Puts search box in focus.
 */
smm_google_ss.prototype.ss_sf = function ss_sf() {
  document.getElementById(this.ss_form_element)[this.ss_input_element].focus();
  this.ss_dismissed = false;
};

/**
 * Clears search suggestions.
 *
 * @param {boolean} nofocus The flag to indicate whether the search box must not
 *     be in focus, such as when user uses the tab key to move away to the
 *     search button(s).
 */
smm_google_ss.prototype.ss_clear = function ss_clear(nofocus) {
  this.ss_qshown = null;
  var fo = document.getElementById(this.ss_form_element);
  var qnow = (!this.ss_qbackup) ? fo[this.ss_input_element].value : this.ss_qbackup;
  this.ss_hide(qnow);
  if (!nofocus) {
    this.ss_sf();
  }
};

/**
 * Hides search suggestions.
 *
 * @param {string} qry The query to which suggestions to be closed.
 */
smm_google_ss.prototype.ss_hide = function ss_hide(qry) {
  var tbl = document.getElementById(this.ss_popup_element);
  if (tbl.style.visibility == 'visible') {
    if (this.ss_panic) {
      alert('close suggestion box');
    }
    if (this.ss_allow_debug && smm_google_ss.ss_debug && smm_google_ss.ss_debug.getDebugMode()) {
      smm_google_ss.ss_debug.addHideDebugLine(qry, 'hide');
    }
    tbl.style.visibility = 'hidden';
  }
};

/**
 * Shows search suggestions.
 *
 * @param {string} qry The query to which suggestions to be presented.
 */
smm_google_ss.prototype.ss_show = function ss_show(qry) {
  var currentQry = document.getElementById(this.ss_form_element)[this.ss_input_element].value;
  if (currentQry != qry) {
    // The query whose suggestions to be shown does not match the current query
    // this happens when the previous query takes much longer to process.
    if (this.ss_allow_debug && smm_google_ss.ss_debug && smm_google_ss.ss_debug.getDebugMode()) {
      smm_google_ss.ss_debug.addHideDebugLine(qry, 'skip');
    }
    return;
  }

  var startTimeMs = new Date().getTime();
  if (this.ss_dismissed) {
    // The suggestion box has been dismissed by mouse close or key
    // escape/enter/tab.
    this.ss_qshown = null;
    this.ss_hide(qry);
    return;
  }

  if (!this.ss_processed(qry)) {
    // Not all ajax calls have been processed, skip instead.
    return;
  }

  if (qry == '') {
    // Empty query should not have much to suggest, close if not already.
    this.ss_hide(qry);
    return;
  }

  var g = this.ss_cached[qry] ? this.ss_cached[qry].g : null;
  var disp = false;
  if (this.ss_use.g && g) {
    disp = true;
  }
  if (!disp) {
    // Nothing to show for.
    this.ss_qshown = null;
    this.ss_hide(qry);
    return;
  }
  // Check the lock.
  if (this.ss_painting) {
    if (this.ss_painting_queue) {
      // Ignore potential painting request delayed earlier.
      clearTimeout(this.ss_painting_queue);
    }
    // Postpone the call for later time.
    if (this.ss_allow_debug && smm_google_ss.ss_debug && smm_google_ss.ss_debug.getDebugMode()) {
      smm_google_ss.ss_debug.addWaitDebugLine(qry, 'delay', this.ss_delay_millisec);
    }
	
	var _this = this;
	this.ss_painting_queue = setTimeout(function() {_this.ss_show(_this.ss_escape(qry));},_this.ss_delay_millisec);
    return;
  } else {
    // Set the lock, which may not be fool-proof when more than another thread
    // checks the lock just before.
    this.ss_painting = true;
  }
  var tbl = document.getElementById(this.ss_popup_element);
  for (var ri = tbl.rows.length - 1; ri > -1; ri--) {
    tbl.deleteRow(ri);
  }
  var cnt = 0;
  for (var z = 0; z < this.ss_seq.length; z++) {
    switch (this.ss_seq[z]) {
      case 'g':
        cnt += this.ss_showSuggestion(g, cnt, tbl);
        break;
    }
    if (this.ss_max_to_display > 0 && cnt >= this.ss_max_to_display) {
      break;
    }
  }
  if (cnt > 0) {
    var row = tbl.insertRow(-1);
    row.className = 'ss-gac-e';
    var cls = document.createElement('td');
    cls.colSpan = 2;
	var _this = this;
    var clsTxt = document.createElement('span');
	clsTxt.onclick = function() {
      _this.ss_qbackup = null;
      _this.ss_clear();  // This will always turn off ss_dismiss after bring search
                   // box into focus.
      var query = document.getElementById(_this.ss_form_element)[_this.ss_input_element].value;
      if (!_this.ss_processed(query)) {
        // Fire new searches for the selected suggestion
        // useful for potential lucky guess.
        _this.ss_dismissed = true;
        if (_this.ss_panic) {
          alert('run ajax when mouse close');
        }
        _this.ss_suggest(query);
      }
    };
    clsTxt.appendChild(document.createTextNode('close'));
    cls.appendChild(clsTxt);
    row.appendChild(cls);
    tbl.style.visibility = 'visible';
    this.ss_qshown = qry;
    if (this.ss_panic) {
      alert('open suggestion box for ' + qry);
    }
    if (this.ss_allow_debug && smm_google_ss.ss_debug && smm_google_ss.ss_debug.getDebugMode()) {
      var stopTimeMs = new Date().getTime();
      smm_google_ss.ss_debug.addShowDebugLine(qry, stopTimeMs - startTimeMs,
                                this.ss_cached[qry], cnt);
    }
  } else {
    this.ss_hide(qry);
  }
  // Release the lock.
  this.ss_painting = false;
};

/**
 * Draws suggestion.
 *
 * @param {oject} g The suggest server entry.
 * @param {number} cnt The current row index to start drawing.
 * @param {object} tbl The suggestion box element.
 * @return {number} Returns the number of suggestions actually drawn.
 */
smm_google_ss.prototype.ss_showSuggestion = function(g, cnt, tbl) {
  if (this.ss_max_to_display > 0 && cnt >= this.ss_max_to_display) {
    return 0;
  }
  if (g && g.length > 0) {
    var _this = this;
	for (var i = 0; i < g.length; i++) {
      row = tbl.insertRow(-1);
	  
	  var objClickHandler = new function(){
		var objElement = row;
		this.handle = function(objEvent){
			_this.ss_handleMouseC(objElement,objEvent);
		};
	  };
	   /* 
	  * Wire onclick event 
	  *
	  * This is some rediculous looking syntax, but the wacky looking objClickHandler is necessary 
	  * because this is running in a loop. Otherwise, the row passed to ss_handleMouseC will always be the last one.)
	  */
	  smm_google_ss.ss_wireEvent(row,"click",objClickHandler.handle,false);
		
  	  var objMouseMoveHandler = new function(){
		var objElement = row;
		this.handle = function(objEvent){
			_this.ss_handleMouseM(objElement,objEvent);
		};
	  };
	  
	  /* 
	  * Wire onmousemove event 
	  *
	  * This is some rediculous looking syntax, but the wacky looking objMouseMoveHandler is necessary 
	  * because this is running in a loop. Otherwise, the row passed to ss_handleMouseM will always be the last one.)
	  */
	  smm_google_ss.ss_wireEvent(row,"mousemove",objMouseMoveHandler.handle,false);
	  
	  row.className = this.SS_ROW_CLASS;
      var alt = document.createElement('td');
      if (g[i].q) {
        alt.appendChild(document.createTextNode(g[i].q));
      } else {
        alt.innerHTML = '<i>' + g[i].t + '</i>';
      }
      alt.className = 'ss-gac-c';
      row.appendChild(alt);
      var clue = '';
      if (i == 0 && g.length == 1) {
        clue = this.ss_g_one_name_to_display;
      } else if (i == 0) {
        clue = this.ss_g_more_names_to_display;
      }
      var typ = document.createElement('td');
      typ.appendChild(document.createTextNode(clue));
      typ.className = 'ss-gac-d';
      row.appendChild(typ);
	  if (this.ss_max_to_display > 0 && cnt + i + 1 >= this.ss_max_to_display) {
        return i + 1;
      }
    }
    return g.length;
  }
  return 0;
};

/**
 * Handles mouse movement. To be attached to the row on mouse-over.
 * @return {boolean} Always returns true after handling the event.
 * @this {Element}
 */
smm_google_ss.prototype.ss_handleMouseM = function(objElement,objEvent) {
  var fo = document.getElementById(this.ss_form_element);
  var tbl = document.getElementById(this.ss_popup_element);
  var rows = tbl.getElementsByTagName('tr');

  for (var ri = 0; ri < rows.length - 1; ri++) {
	if (rows[ri] == objElement && rows[ri].className != this.SS_ROW_SELECTED_CLASS) {
      // Select the row.
      rows[ri].className = this.SS_ROW_SELECTED_CLASS;
      // Back up the original query if not already, and adjust the reference
      // index.
      if (!this.ss_qbackup) {
        this.ss_qbackup = fo[this.ss_input_element].value;
      }
      this.ss_loc = ri;
      // Find out what type of suggestion it is.
      var suggestion = this.ss_locateSuggestion(this.ss_qbackup, this.ss_loc);
      // Adjust the query in the search box.
      if (suggestion.q) {
        fo[this.ss_input_element].value = suggestion.q;
      } else {
        fo[this.ss_input_element].value = this.ss_qbackup;
      }
    } else if (rows[ri] != objElement) {
      rows[ri].className = this.SS_ROW_CLASS;
    }
  };
  // Bring the search box back into focus to allow the next key down and key up.
  this.ss_sf();
  return true;
};

/**
 * Handles mouse pressing, while keeping the history in the browser in case back
 * button is used. To be attached to the row on mouse clicking.
 * @this {Element}
 */
smm_google_ss.prototype.ss_handleMouseC = function(objElement,objEvent) {
  var fo = document.getElementById(this.ss_form_element);
  var tbl = document.getElementById(this.ss_popup_element);
  var rows = tbl.getElementsByTagName('tr');
  
  for (var ri = 0; ri < rows.length - 1; ri++) {
    if (rows[ri] == objElement) {
      // Back up the original query if not already, and adjust the reference
      // index.
      if (!this.ss_qbackup) {
        this.ss_qbackup = fo[this.ss_input_element].value;
      }
      this.ss_loc = ri;
      // Find out what type of suggestion it is.
      var suggestion = this.ss_locateSuggestion(this.ss_qbackup, this.ss_loc);
      // Adjust the query in the search box.
      if (suggestion.q) {
        fo[this.ss_input_element].value = suggestion.q;
        fo.submit();
      } else {
        fo[this.ss_input_element].value = this.ss_qbackup;
        if (suggestion.u) {
          window.location.href = suggestion.u;
        }
      }
	  this.ss_clear();
      break;
    }
  }
};

/**
 * Counts the total number of suggestions for the typed query.
 *
 * @param {string} query The typed query.
 * @return {number} The number of suggestions we have for displaying.
 */
smm_google_ss.prototype.ss_countSuggestions = function ss_countSuggestions(query) {
  var cnt = 0;
  for (var i = 0; i < this.ss_seq.length; i++) {
    switch (this.ss_seq[i]) {
      case 'g':
        cnt += this.ss_cached[query].g ? this.ss_cached[query].g.length : 0;
        break;
    }
    if (this.ss_max_to_display > 0 && cnt >= this.ss_max_to_display) {
      return this.ss_max_to_display;
    }
  }
  return cnt;
};

/**
 * Looks up the suggestion for the typed query.
 *
 * @param {string} query The typed query.
 * @param {number} loc The location index of the current suggestion selection.
 *
 * @return {string} The suggestion term for given query at the given loc.
 */
smm_google_ss.prototype.ss_locateSuggestion = function ss_locateSuggestion(query, loc) {
  var cnt1 = 0;
  var cnt2 = 0;
  var type = null;
  for (var z = 0; z < this.ss_seq.length; z++) {
    switch (this.ss_seq[z]) {
      case 'g':
        cnt2 += this.ss_cached[query].g ? this.ss_cached[query].g.length : 0;
        break;
    }
    if (loc >= cnt1 && loc < cnt2) {
      switch (this.ss_seq[z]) {
        case 'g':
          var qV = this.ss_cached[query].g[loc - cnt1].q;
          if (qV) {
            return { 'q': qV };
          } else {
            return { 'u': this.ss_cached[query].g[loc - cnt1].u };
          }
      }
      break;
    }
    cnt1 = cnt2;
  }
  return null;
};

/**
 * Escapes query to be used in setTimeout().
 *
 * @param {string} query The query whose suggestions are needed.
 * @return {string} The escaped query.
 */
smm_google_ss.prototype.ss_escape = function ss_escape(query) {
  return query.replace(/\\/g, '\\\\').replace(/\"/g, '\\\"');
};

/**
 * Escapes query to be used in debugging display.
 *
 * @param {string} query The query whose suggestions are needed.
 * @return {string} The escaped query.
 */
smm_google_ss.prototype.ss_escapeDbg = function ss_escapeDbg(query) {
  var escapedQuery = '';
  var ch = query.split('');
  for (var i = 0; i < ch.length; i++) {
    switch (ch[i]) {
      case '&':
        escapedQuery += '&amp;';
        break;
      case '<':
        escapedQuery += '&lt;';
        break;
      case '>':
        escapedQuery += '&gt;';
        break;
      default:
        escapedQuery += ch[i];
        break;
    }
  }
  return escapedQuery;
};

/**
 * Debugger class.
 *
 * @constructor
 */
function ss_Debugger() {
  this.debugMode = false;
}

/**
 * Id of debug console in the DOM Tree.
 * @type {string}
 */
ss_Debugger.DEBUG_CONSOLE_ID = 'ss_debug_console';

/**
 * Id of content node of debug console in the DOM Tree.
 * @type {string}
 */
ss_Debugger.DEBUG_CONTENT_ID = 'ss_debug_content';

/**
 * Id of the button that minimizes/maximizes the debug console.
 * @type {string}
 */
ss_Debugger.DEBUG_TOGGLE_ID = 'ss_debug_toggle';

/**
 * Getter method for debugMode member variable.
 * @return {boolean} The value of debugMode variable.
 */
ss_Debugger.prototype.getDebugMode = function() {
  return this.debugMode;
};

/**
 * Activates debugger console.
 */
ss_Debugger.prototype.activateConsole = function() {
  var console = document.getElementById(ss_Debugger.DEBUG_CONSOLE_ID);
  if (console) {
    console.style.display = 'block';
  } else {
    var dc = document.createElement('div');
    dc.id = ss_Debugger.DEBUG_CONSOLE_ID;
    dc.zIndex = 100;
    dc.className = 'expanded';
    var title = document.createElement('h1');
    title.appendChild(document.createTextNode('GSA Suggest Debug Console'));
    title.style.display = 'inline';
    dc.appendChild(title);
    var actn = document.createElement('div');
    actn.style.float = 'right';
    var btn = document.createElement('button');
    btn.onclick = function(event) {
      var debugContent = document.getElementById(ss_Debugger.DEBUG_CONTENT_ID);
      if (debugContent) {
        for (var ri = debugContent.rows.length - 1; ri > 0; ri--) {
          debugContent.deleteRow(ri);
        }
      }
    };
    btn.appendChild(document.createTextNode('Clear console'));
    actn.appendChild(btn);
    btn = document.createElement('button');
    btn.onclick = function(event) {
      ss_cached = [];
    };
    btn.appendChild(document.createTextNode('Clear cache'));
    actn.appendChild(btn);
    btn = document.createElement('button');
    btn.id = ss_Debugger.DEBUG_TOGGLE_ID;
    btn.onclick = function(event) {
      var debugConsole = document.getElementById(ss_Debugger.DEBUG_CONSOLE_ID);
      if (debugConsole) {
        var b = document.getElementById(ss_Debugger.DEBUG_TOGGLE_ID);
        if (debugConsole.className.indexOf('expanded') != -1) {
          debugConsole.className = debugConsole.className.replace(
              /expanded/, 'contracted');
          b.innerHTML = 'Maximize';
        } else {
          debugConsole.className = debugConsole.className.replace(
              /contracted/, 'expanded');
          b.innerHTML = 'Minimize';
        }
      }
    };
    btn.appendChild(document.createTextNode('Minimize'));
    actn.appendChild(btn);
    actn.style.display = 'inline';
    dc.appendChild(actn);
    dc.appendChild(document.createElement('br'));
    var pane = document.createElement('table');
    pane.id = ss_Debugger.DEBUG_CONTENT_ID;
    var dhr = pane.insertRow(-1);
    var dhc = document.createElement('th');
    dhc.innerHTML = 'Query';
    dhr.appendChild(dhc);
    dhc = document.createElement('th');
    dhc.innerHTML = 'Type';
    dhr.appendChild(dhc);
    dhc = document.createElement('th');
    dhc.innerHTML = 'Time';
    dhr.appendChild(dhc);
    dhc = document.createElement('th');
    dhc.innerHTML = 'g';
    dhr.appendChild(dhc);
    dhc = document.createElement('th');
    dhc.innerHTML = 'Total';
    dhr.appendChild(dhc);
    dc.appendChild(pane);
    document.body.appendChild(dc);
  }
  this.debugMode = true;
};

/**
 * De-activates debugger console.
 */
ss_Debugger.prototype.deactivateConsole = function() {
  var console = document.getElementById(ss_Debugger.DEBUG_CONSOLE_ID);
  if (console) {
    console.style.display = 'none';
  }
  this.debugMode = false;
};

ss_Debugger.prototype.addRequestDebugLine = function(query, type, time, obj) {
  var debugContent = document.getElementById(ss_Debugger.DEBUG_CONTENT_ID);
  if (debugContent) {
    var currentRow = debugContent.insertRow(1);
    var currentCell = document.createElement('td');
    currentCell.innerHTML = '&lt;' + ss_escapeDbg(query) + '&gt;';
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentCell.innerHTML = type;
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentCell.className = 'no';
    currentCell.innerHTML = time + ' ms';
    currentRow.appendChild(currentCell);
    switch (type) {
      case 'suggest':
        currentCell = document.createElement('td');
        currentCell.className = 'no';
        currentCell.innerHTML = (obj.g ? obj.g.length : 0);
        currentRow.appendChild(currentCell);
        currentCell = document.createElement('td');
        currentRow.appendChild(currentCell);
        break;
      default:
        currentCell = document.createElement('td');
        currentRow.appendChild(currentCell);
        currentCell = document.createElement('td');
        currentRow.appendChild(currentCell);
        break;
    }
  }
};

ss_Debugger.prototype.addShowDebugLine = function(query, time, o, total) {
  var debugContent = document.getElementById(ss_Debugger.DEBUG_CONTENT_ID);
  if (debugContent) {
    var currentRow = debugContent.insertRow(1);
    var currentCell = document.createElement('td');
    currentCell.innerHTML = '&lt;' + ss_escapeDbg(query) + '&gt;';
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentCell.innerHTML = '<i>show</i>';
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentCell.className = 'no';
    currentCell.innerHTML = time + ' ms';
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentCell.className = 'no';
    currentCell.innerHTML = (o ? (o.g ? o.g.length : 0) : 0);
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentCell.className = 'no';
    currentCell.innerHTML = total;
    currentRow.appendChild(currentCell);
  }
};

ss_Debugger.prototype.addHideDebugLine = function(query, type) {
  var debugContent = document.getElementById(ss_Debugger.DEBUG_CONTENT_ID);
  if (debugContent) {
    var currentRow = debugContent.insertRow(1);
    var currentCell = document.createElement('td');
    currentCell.innerHTML = '&lt;' + ss_escapeDbg(query) + '&gt;';
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentCell.innerHTML = '<i>' + type + '</i>';
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentCell.className = 'no';
    currentCell.innerHTML = '0 ms';
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentRow.appendChild(currentCell);
  }
};

ss_Debugger.prototype.addWaitDebugLine = function(query, type, time) {
  var debugContent = document.getElementById(ss_Debugger.DEBUG_CONTENT_ID);
  if (debugContent) {
    var currentRow = debugContent.insertRow(1);
    var currentCell = document.createElement('td');
    currentCell.innerHTML = '&lt;' + ss_escapeDbg(query) + '&gt;';
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentCell.innerHTML = '<i>' + type + '</i>';
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentCell.className = 'no';
    currentCell.innerHTML = time + ' ms';
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentRow.appendChild(currentCell);
    currentCell = document.createElement('td');
    currentRow.appendChild(currentCell);
  }
};