/**
 * Copyright (C) 2006 Google Inc.
 * 
 * Licensed under the Apache 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.apache.org/licenses/LICENSE-2.0
 *      
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * This file has been very significantly modified from the original
 * download of the google version.
 *
 * Quick summary of how it works
 * =============================
 * User type-in sets timeout that triggers an ajax search.
 * Speedy type-in cancels, and reschedules the search.(Could it just update the timout?)
 * Once an ajax search has been commissioned it should never be cancelled. Read on for why.
 * The (cleaned) latest type-in text defines the string the SAYT should display values for.
 * Many AJAX searches may have been commissioned. When they return they should not necessarily
 * show their values. Only results for the value that is currently displayed cause the pop-up.
 * Further type-in cancels any displayed popup.
 *
 * Commissioning an ajax search:
 * - creates an entry in the search cache matching the cleaned input.
 * - sets an ajaxStartTime - which is used to measure how long it has been since the request was made
 * - sets up an indication that a response is awaited.
 * - the cleanedInput is the key - it really controls everything.
 *
 * New model now in development.
 * =========
 *
 * Input element
 *    events
 *       |
 *       |
 *       v
 * cleanedInputChanged()
 *       |
 *       |
 *       v
 *  queueSearch()
 *       :
 *       :keystroke
 *       :timeout
 *       :
 *       v
 *    search_()
 * Are results for the   -->   ensureAjaxRequest(cleanedInput) --> anticipate(cleanedInput)
 * cleanedInput in cache? no           :                           sets prompt
 *       |                             :asynchronously             sets animation
 *       | yes                         :                           
 *       |                             v
 *       |                      handleAjaxResponse()
 *       |                      update cache
 *       |                             |
 *       |                             |
 *       v                             v
 * queueShowResults()    <--    Does the response contain the results
 *       :               yes    for the anticipated cleanedInput?
 *       :                             |
 *       :                             |no
 *       :                             |
 *       :showResults                  v
 *       :timeout                     stop
 *       :
 *       :
 *       :
 *       :
 *       v
 *  showResults()   <------ Other events that request immediate display.
 *                          E.g. down-arrow or double-click in the input box.
 *
 *
 * Suggestions for improvement
 * ===========================
 * 1. Panning the map should invalidate the cache.
 *    - If the pan is more than say 20 km?
 *    - should be handled by supplying an API hook - there is already a cache clearance for the debug mode.
 *
 *
 * Tests
 * =====
 * Situations that are known to break the system.
 * 1. Can get "Anticipating response to..." dialog due to conflict with itnerary's anticipation system.
 */

/*
 * Search-as-you-type
 */
var searchAsYouTypeConfiguration = {
  // The path (beginning of the URL) to the place containing /images and
  // /styles. Should end with a slash. 
  // e.g. http://intranet.company.com/search-as-you-type/
  // Note: most styles and images will be accessed relative to this,
  // but see the styles/ie.css where absolute addressing is used
  // - which you'll need to fix if the code moves.
  // This is modified by initialize() to include baseUrl.
 resourcesPath: "/sayt/",
 
 // The fully qualified URL to the Ajax responder. 
 // e.g. http://intranet.company.com/search-as-you-type/responder.php
 // ajaxResponderUrl:
 //  "http://www.cyclestreets.net/sayt.js?w=-3.2204537&s=55.9289448&e=-3.1674103&n=55.9781516&z=13&street=thoday",
 
 // The fully qualified URL to the help page. Leave as empty string if
 // not available
 // e.g. http://intranet.company.com/search-as-you-type/help.html
 helpPageUrl: "",
 
 // How many results will be shown in full. If there are more than these,
 // all but "direct hits" will be summarized. Default value: 3
 maxFullResults: 4,
 
 // The delay (in ms) between pressing a key (while typing in a search 
 // query) and firing the query search. Shouldn't be too big, because the 
 // users will have to wait a long time for results. Shouldn't be too small, 
 // because it will increase the load on a server. Default value: 20
 keystrokeShortDelay: 400,
 
 // The delay to use when there are no spaces in the cleanedInput (it the trimmed
 // version of the input field value). Should be longer than keystrokeShortDelay.
 keystrokeLongDelay: 500,
 
 // The delay (in ms) between pressing a key and results being shown.
 // Shouldn't be too big, because it will be less usable, and the users 
 // will grow impatient. Shouldn't be too small, because the results will
 // flicker below as the user is typing. Please note that the actual
 // time might be bigger if the Ajax responder is slow. Default value: 200
 showResultsDelay: 200,
 
 // The distance (in pixels) that should be left from the bottom edge of 
 // the screen if there are many results. Default value: 10
 bottomPageMargin: 2,
 
    // Debug mode is activated when debugSearchAsYouType is in the URL, but is
    // overruled if this is false
 allowDebugMode: false
};

/**
 * SearchAsYouType class.
 * @constructor
 */
function SearchAsYouType() {}

/**
 * Sets up search-as-you-type on the given input element ID. An optional prompt element id
 * can receive changing messages.
 *
 * @param {int} inputFieldElId The input field element id to attach Search-as-you-type.
 * @param {int} promptElId     The prompt field element id used for help prompting.
 * @param {bool} focus Whether to set focus on this element
 * Optiontional args...
 * @param function selectResultCallback Called when the user selects a value from the list
 * @param function inputChangedCallback Called when the cleaned input text changes
 * @param function anticipateCallback   Called when before ajax request is issued, return false to block.
 * @param function unAnticipateCallback Called when a response to the request is received.
 */
SearchAsYouType.prototype.initialize = function(baseUrl, inputFieldElId, promptElId, focus, selectResultCallback, inputChangedCallback, anticipateCallback, unAnticipateCallback) {

  // Harsh sanity check to make sure the map exists already.
  if(!CS || !CS.map) {alert('SAYT: No map has been initialised.');return;}

  // Modify the resouce path to include the baseUrl;
  searchAsYouTypeConfiguration.resourcesPath = baseUrl + searchAsYouTypeConfiguration.resourcesPath;

  this.isProxied = (baseUrl == '/map' ? true : false);

  this.initializeVariables_(inputFieldElId, promptElId);

  this.detectBrowser_();
  this.attachStylesheets_();
  this.createDomElements_();

  this.restoreInputField_();
  this.ensureEventHandlers();
  this.updateDimensionsAndShadow_(null);

  if (this.debugMode) {this.activateDebugConsole_();}
  if (focus)          {this.focusInputField_();}

  this.selectResultCallback = selectResultCallback;
  this.inputChangedCallback = inputChangedCallback;
  this.anticipateCallback   = anticipateCallback;
  this.unAnticipateCallback = unAnticipateCallback;

  this.choosePlaceNameHint();

  this.initialized = true;
};

/**
 * Use this method to move all the SAYT functionality to another input field.
 * This seems to be preferred to creating a second input field.
 * It does similar things to initialize(), and retains the existing search cache.
 *
 * @param {int} inputFieldElId The input field element id to attach Search-as-you-type.
 * @param {int} promptElId     The prompt field element id used for help prompting.
 * @param {bool} focus Whether to set focus on this element
 */
SearchAsYouType.prototype.moveToField = function(inputFieldElId, promptElId, focus) {

  // Hide old results window
  this.hideResultsWindow_();

  // Bind to the new input field
  this.inputFieldEl = document.getElementById(inputFieldElId);
  this.promptEl     = document.getElementById(promptElId);

  // Make sure the event handlers are in place
  this.ensureEventHandlers();

  // Update the new position of everything.
  this.updateDimensionsAndShadow_(null);

  // Activate the existing input field value.
  // if (this.getCleanedInput() != this.cleanedInput) {this.cleanedInputChanged();}

  // May set focus
  if (focus) {this.focusInputField_();}
};


/**
 * Initialize all the variables needed for later.
 * @param {element} inputFieldEl The input field element to attach Search-as-you-type.
 */
SearchAsYouType.prototype.initializeVariables_ = function(inputFieldElId, promptElId) {
  // Location (URL) of the parent page
  this.location = "" + window.location;

  // Protocol used by the parent page ("http" or "https").
  this.protocol = this.location.substr(0, this.location.indexOf("://") + 3);

  // Path (URL beginning) to resources such as images or CSS files
  this.resourcesPath = searchAsYouTypeConfiguration.resourcesPath;
  // (...) make it understand https

  // <script> object for Ajax calls
  this.ajaxObject = null; 

  // A count of the number of ajax calls. Used for debugging.
  this.ajaxIndex = 0;

  // Search cache (containing previous responses)
  this.searchCache = []; 

  // Whether the whole as-you-type search engine has been initialized
  this.initialized = false; 
  
  // Whether search results window is hidden or visible 
  this.resultsWindowHidden = true; 
  
  // A handle to the input field
  this.inputFieldEl = document.getElementById(inputFieldElId);

  // Simon introduced this prompt field
  this.promptEl = document.getElementById(promptElId);
  if(this.promptEl) {
    this.promptElOriginalMessage = this.promptEl.innerHTML;
  }

  // The query last typed by the user, as cleaned up by getCleanedInput().
  this.cleanedInput = this.getCleanedInput(); 

  // A handler to the search results window element
  this.searchResultsEl = 0; 

  // A handler to the alternate search results window (there are two, switching
  // between them gives better visuals)
  this.alternateSearchResultsEl = 0; 

  // Whether the input field currently has focus (can be 0, 0.5 or 1) !! Very strange!!
  this.inputFieldHasFocus = 0;     

  // Whether any of the results is activated by navigating through it via
  // keyboard. -1 if no, 0 or more if yes (indicates the number of the 
  // active search result)
  this.activeResult = -1; 

  // Whether the arrow key has been processed on keydown event, and can be
  // ignored on keypress (see handleBodyKeyPress for more information on why
  // this is necessary)
  this.arrowKeyProcessed = false;

  // The code of the last pressed key
  this.lastKeyPressed = 0;

  // Used in determining whether the user is a fast typist.
  this.typingSpeedTime = -1;

  // Assume the user is a slow typist. Permitted values are (slow|fast).
  this.typistSpeed = 'slow';

  // Timer id of the JavaScript timer to show results
  this.showResultsTimeoutId = -1; 

  // The id of the JavaScript timer to fire a query after 
  // searchAsYouTypeConfiguration.keystrokeShortDelay ms have passed 
  // since the last keystroke
  this.keystrokeTimeoutId = -1; 

  // Current autocomplete value
  this.autocomplete = '';

  // Whether autocomplete has just been collapsed (i.e. turned into regular
  // regular input text by pressing Tab or right arrow)
  this.autocompleteJustCollapsed = false;

  // Whether we're in the debug mode (activated by adding 
  // ?debugSearchAsYouType to the URL)
  this.debugMode = searchAsYouTypeConfiguration.allowDebugMode && this.location.indexOf("debugSearchAsYouType") > -1;
};


/**
 * Sets the value of any attached prompt field.
 */
SearchAsYouType.prototype.setPrompt = function(message) {
  if(this.promptEl) {
    this.promptEl.innerHTML = message;
  }
};

/**
 * Restores attached prompt field to the value it had when the SAYT was attached.
 */
SearchAsYouType.prototype.resetPrompt = function() {
  this.setPrompt(this.promptElOriginalMessage);
};
      
/**
 * Figure out which browser is being used.
 */
SearchAsYouType.prototype.detectBrowser_ = function() {
  this.browserIE = false;
  this.browserFirefox = false;
  this.browserSafari = false;

  if (navigator.userAgent.indexOf("MSIE") > -1) {
    this.browserIE = true;
  } else if ((navigator.userAgent.indexOf("Firefox/") > -1)) {
    this.browserFirefox = true;
    if ((navigator.userAgent.indexOf("Firefox/1.0.") > -1)) {
      this.browserFirefox10 = true;
    } else {
      this.browserFirefox10 = false;
    }
  } else if (navigator.userAgent.indexOf("Safari") > -1) {
    this.browserSafari = true;
    if (navigator.userAgent.indexOf("Version/") > -1) {
      this.browserSafari3OrHigher = true;
    }
  }
};

/**
 * Attach the necessary CSS stylesheets to the document body. This adds
 * a generic CSS plus extra stylesheets containing exceptions for IE and 
 * Safari.
 */
SearchAsYouType.prototype.attachStylesheets_ = function() {
  this.attachStylesheet_('generic.css');
  if (this.browserIE) {
    this.attachStylesheet_((this.isProxied ? 'ie-proxied.css' : 'ie.css'));
  } else if (this.browserSafari) {
    this.attachStylesheet_('safari.css');
  }
};

/**
 * Attach a CSS stylesheet to the document body.
 * @param {String} filename Absolute URL of the stylesheet
 */
SearchAsYouType.prototype.attachStylesheet_ = function(filename) {
  var el = document.createElement('link');
  el.href = this.resourcesPath + "styles/" + filename;
  el.type = 'text/css';
  el.rel = 'stylesheet';
  document.getElementsByTagName('head').item(0).appendChild(el);
};

/**
 * Create all the necessary page elements: search results window(s),
 * shadow elements, loading, backup input element, and autocomplete.
 */
SearchAsYouType.prototype.createDomElements_ = function() {
  // A backup input field necessary to preserve the last entry when 
  // coming back to the page -- since we're disabling browser's native
  // autocomplete on the regular input field, it will always be clean when
  // entering the page
  var el = document.createElement("input");
  el.id = 'searchAsYouTypeBackupSearchField';
  el.style.display = 'none'; // in case CSS is not yet loaded
  document.body.appendChild(el);

  // Two search results canvas windows
  this.searchResultsEl = document.createElement("div");
  this.searchResultsEl.id = 'searchAsYouTypeResults1';
  this.searchResultsEl.className = 'searchResults';
  this.searchResultsEl.style.display = 'none'; 
  this.searchResultsEl.style.position = 'absolute'; 
  this.searchResultsEl.onclick = 'event.cancelBubble = true;';
  this.searchResultsEl.tabIndex = -1;

  this.alternateSearchResultsEl = document.createElement("div");
  this.alternateSearchResultsEl.id = 'searchAsYouTypeResults2';
  this.alternateSearchResultsEl.className = 'searchResults';
  this.alternateSearchResultsEl.style.display = 'none'; 
  this.alternateSearchResultsEl.style.position = 'absolute'; 
  this.alternateSearchResultsEl.onclick = 'event.cancelBubble = true;';
  this.alternateSearchResultsEl.tabIndex = -1;

  // Shadows for the current search results canvas
  this.searchResultsShadowEl = document.createElement("div");
  this.searchResultsShadowEl.id = 'searchAsYouTypeResultsShadow';
  this.searchResultsShadowEl.style.visibility = 'hidden'; 
  this.searchResultsShadowEl.style.display = 'none'; 
  this.searchResultsShadowEl.style.left = 0; 
  this.searchResultsShadowEl.style.top = 0; 
  this.searchResultsShadowEl.style.width = 0; 
  this.searchResultsShadowEl.style.height = 0; 

  el = document.createElement("div"); 
  el.id = 'searchAsYouTypeResultsShadowL';
  this.searchResultsShadowEl.appendChild(el);
  el = document.createElement("div"); 
  el.id = 'searchAsYouTypeResultsShadowR';
  this.searchResultsShadowEl.appendChild(el);
  el = document.createElement("div"); 
  el.id = 'searchAsYouTypeResultsShadowB';
  this.searchResultsShadowEl.appendChild(el);
  el = document.createElement("div"); 
  el.id = 'searchAsYouTypeResultsShadowBL';
  this.searchResultsShadowEl.appendChild(el);
  el = document.createElement("div"); 
  el.id = 'searchAsYouTypeResultsShadowBR';
  this.searchResultsShadowEl.appendChild(el);
  el = document.createElement("div"); 
  el.id = 'searchAsYouTypeResultsShadowTL';
  this.searchResultsShadowEl.appendChild(el);
  el = document.createElement("div"); 
  el.id = 'searchAsYouTypeResultsShadowTR';
  this.searchResultsShadowEl.appendChild(el);

  el = document.createElement("searchAsYouType");
  el.id = 'searchAsYouType';

  el.appendChild(this.searchResultsEl);
  el.appendChild(this.alternateSearchResultsEl);
  el.appendChild(this.searchResultsShadowEl);
  document.body.appendChild(el);

  // Loading animation (to be position in the input field)
  this.waitingForSearchResultsEl = document.createElement("img");
  this.waitingForSearchResultsEl.style.visibility = 'hidden'; 
  this.waitingForSearchResultsEl.style.position = 'absolute'; 
  this.waitingForSearchResultsEl.src = 
    this.resourcesPath + "images/loading.gif";

  document.body.appendChild(this.waitingForSearchResultsEl);

  // Autocomplete element
  this.autocompleteEl = document.createElement("div");
  this.autocompleteEl.id = 'searchAsYouTypeAutocomplete';
  this.autocompleteEl.className = 'searchAsYouTypeAutocompleteInputMatch';
  document.body.appendChild(this.autocompleteEl);
  this.autocompleteEl.onmousedown = 
    searchAsYouTypeBind(this.handleAutocompleteMouseDown, this);
  this.autocompleteEl.style.zIndex = 5000;
  this.autocompleteEl.style.display = 'none';

  // Autocomplete helper, used to calculate dimensions
  this.autocompleteHelperEl = document.createElement("div");
  this.autocompleteHelperEl.id = 'searchAsYouTypeAutocompleteHelper';
  this.autocompleteHelperEl.visibility = 'hidden';
  this.autocompleteHelperEl.className = 'searchAsYouTypeAutocompleteInputMatch';
  document.body.appendChild(this.autocompleteHelperEl);
};

/**
 * Get a query from the input field and clean it up a little bit - by trimming
 * whitespace fore and aft, and excessive whitespace in between.
 * @return {String} A cleaned up query
 */
SearchAsYouType.prototype.getCleanedInput = function() {
  // trim spaces for and aft, and convert multiple spaces to a single one.
  return this.inputFieldEl.value.replace(/^\s+/g, '').replace(/\s+$/g, '').replace(/\s\s+/g, ' ');
};

/**
 * Set focus on the input field. We do some extra gymnastics here for IE
 * so that the caret ends up at the end of the input field.
 */
SearchAsYouType.prototype.focusInputField_ = function() {
  this.inputFieldEl.focus();
  this.inputFieldHasFocus = 1;

  if (this.inputFieldEl.createTextRange && window.document.selection) {
    var sel = this.inputFieldEl.createTextRange();
    sel.collapse(true);
    sel.move("character", this.inputFieldEl.value.length);
    sel.select();
  }
};

/**
 * Clear the input field and autocomplete. [Scrapped: Prepares a random tip (we only do
 * it here so tips don't change or come and go as the user is typing).]
 */
SearchAsYouType.prototype.clearInputField_ = function() {
  this.inputFieldEl.value = '';
  this.clearAutocomplete_(true);
};

/**
 * Save the contents of the input field in case the user goes back
 * to the page.
 * The main input field has browser autocomplete turned off, because
 * the auto-complete window would cover SearchAsYouType window. 
 * Unfortunately, this has another side effect -- the contents of the 
 * input field won't be retained after the user pressed back button to 
 * go back to the homepage.
 *
 * We need to copy the value to a hidden input field (but with 
 * autocomplete) and copy it back when the page loads.
 */
SearchAsYouType.prototype.saveInputField = function(e) {
  document.getElementById('searchAsYouTypeBackupSearchField').value =  this.inputFieldEl.value;
  document.getElementById('searchAsYouTypeBackupSearchField').setAttribute("active", 1);
};

/**
 * Retain the previous text entry and put focus on the input field.
 */
SearchAsYouType.prototype.restoreInputField_ = function() {
  if (document.getElementById('searchAsYouTypeBackupSearchField').getAttribute("active")) {
    this.inputFieldEl.value = 
    document.getElementById('searchAsYouTypeBackupSearchField').value;
    this.cleanedInput = this.getCleanedInput();
  }
};

/**
 * Add necessary event handlers for the input field and the body of the page.
 */
SearchAsYouType.prototype.ensureEventHandlers = function() {

  // Simon: only add once to an input field
  if(this.inputFieldEl.getAttribute('saytEventHandersAdded')) {return;}

  // (...) event listener
  this.inputFieldEl.onkeyup     = searchAsYouTypeBind(this.handleInputKeyUp, this);
  this.inputFieldEl.onkeypress  = searchAsYouTypeBind(this.handleInputKeyPress, this);
  this.inputFieldEl.onkeydown   = searchAsYouTypeBind(this.handleInputKeyDown, this);
  this.inputFieldEl.onfocus     = searchAsYouTypeBind(this.handleInputFocus, this);
  this.inputFieldEl.onblur      = searchAsYouTypeBind(this.handleInputBlur, this);
  this.inputFieldEl.onclick     = searchAsYouTypeBind(this.handleInputClick, this);
  this.inputFieldEl.onmousedown = searchAsYouTypeBind(this.handleInputMouseDown, this);

  this.inputFieldEl.setAttribute('autocomplete', 'off');
  this.inputFieldEl.setAttribute('saytEventHandersAdded', true);

  // The following events should only be added once to the window / document.
  if(this.initialized) {return;}

  if (window.addEventListener) { // Mozilla, Netscape, Firefox
    document.body.addEventListener('click', searchAsYouTypeBind(this.handleBodyClick, this), false);
    document.addEventListener('keyup',      searchAsYouTypeBind(this.handleBodyKeyUp, this), false);
    document.addEventListener('keydown',    searchAsYouTypeBind(this.handleBodyKeyDown, this), false);
    document.addEventListener('keypress',   searchAsYouTypeBind(this.handleBodyKeyPress, this), false);
    window.addEventListener('resize',       searchAsYouTypeBind(this.handleBodyResize, this), false);
  } else { // IE
    document.body.attachEvent('onclick',   searchAsYouTypeBind(this.handleBodyClick, this));
    document.body.attachEvent('onkeyup',   searchAsYouTypeBind(this.handleBodyKeyUp, this));
    document.body.attachEvent('onkeydown', searchAsYouTypeBind(this.handleBodyKeyDown, this));
    document.onkeypress                  = searchAsYouTypeBind(this.handleBodyKeyPress, this);
    window.attachEvent('onresize',         searchAsYouTypeBind(this.handleBodyResize, this));
  }

  // The below is for Firefox 1.5's fastback feature.
  // (...) CHANGE TO event listener
  try {
    window.onpageshow = function(event) { 
      if (event.persisted) {
        searchAsYouType.restoreInputField_(); 
      }
    };
  } catch(e) {
  }

  if ((this.browserFirefox) && (!this.browserFirefox10)) {
    window.onpagehide = searchAsYouTypeBind(this.saveInputField, this);
  } else {
    window.onunload = searchAsYouTypeBind(this.saveInputField, this);
  }
};

/**
 * Selects a place name hint that is displayed briefly while typing and before search is initiated.
 */
SearchAsYouType.prototype.choosePlaceNameHint = function() {
  this.placeNameHint = "Enter <strong>street, town</strong>, a <strong>postcode</strong>, or <strong>place</strong>";
};
  
/**
 * A public function to call to fix up various parameters when the input box
 * to which SAYT has been attached has changed its size / postion.
 */
SearchAsYouType.prototype.handleResize =  function() {
  this.updateDimensionsAndShadow_(null);
};
  
  
/**
 * Calculate and update the dimensions of Search-as-you-type elements,
 * including autocomplete, loading animation and shadows
 *
 * +------------------------------------------+  
 * |                                          |
 * |   y                                      |
 * |                                          |
 * | x +---------------------------+          |
 * |   | el                    (X) |          |
 * |   +---------------------------+          |
 * |   | R1                        |          |
 * |   | R2                        |          |
 * |   | R3                        |          |
 * |   | R4                        |          |
 * |   +---------------------------+          | 
 * |                                          |  
 * |                                          |  
 * |                                          |  
 * |                                          |  
 * |                                          |  
 * |                                          |
 * +------------------------------------------+  
 *
 *
 *
 *
 *
 * @param {element} searchResultsEl A search results element to be updated
 */
SearchAsYouType.prototype.updateDimensionsAndShadow_ =  function(searchResultsEl) {
  // Figure out the absolute position of the input field element
  var el = this.inputFieldEl;
  var x = 0;
  var y = 0;
  var obj = el;
  do {
    x += obj.offsetLeft;
    y += obj.offsetTop;
    obj = obj.offsetParent;
  } while (obj);

  // Position the waiting animation, so it's inside the input field, flushed right.
  // Simon: the loading.gif is 15x15. The centre of it should appear at:
  // inputFieldEl.right - inputFieldEl.height / 2, inputFieldEl.height / 2
  var animation_half_width  = Math.round(15/2);
  var animation_half_height = Math.round(15/2);
  var input_half_height     = Math.round(el.offsetHeight/2);

  this.waitingForSearchResultsEl.style.left = 
    (x + this.inputFieldEl.offsetWidth - input_half_height - animation_half_width) + 'px';
  this.waitingForSearchResultsEl.style.top = 
    (y + input_half_height - animation_half_height) + 'px';

  // Position the autocomplete element
  this.autocompleteEl.setAttribute("originalLeft", x);
  this.autocompleteEl.style.top    = y + 'px';
  this.autocompleteEl.style.height = (this.inputFieldEl.clientHeight - 1) + 'px';

  // Position the search results canvas element
  if (searchResultsEl) {
    y += el.offsetHeight - 2;

    var w = el.offsetWidth - 2;

    // Make sure the width is not -ve here.
    if(w<0) {w=0;}

    searchResultsEl.style.left = (x + 1) + "px";
    searchResultsEl.style.top = y + "px";
    searchResultsEl.style.width  = w + "px";
    searchResultsEl.style.height = "auto";

    x = searchResultsEl.offsetLeft;
    y = searchResultsEl.offsetTop;
    w = searchResultsEl.offsetWidth;


    var h = searchResultsEl.offsetHeight;

    // Resize shadows
    this.resizeShadowEl_("",       x, y, w + 4, h + 6);
    this.resizeShadowEl_("L",     -2, 5,     2, h - 5);
    this.resizeShadowEl_("TL",    -2, 0,     2,     5);
    this.resizeShadowEl_("TR",     w, 0,     2,     5);
    this.resizeShadowEl_("R",      w, 5,     2, h - 5);
    this.resizeShadowEl_("B",      4, h, w - 8,     6);
    this.resizeShadowEl_("BL",    -2, h,     6,     6);
    this.resizeShadowEl_("BR", w - 4, h,     6,     6);
  }
};

/**
 * Resize one of the shadow elements.
 * @param {string} id Id of the shadow element (cf. "BR")
 * @param {int} x Horizontal position (in pixels)
 * @param {int} y Vertical position (in pixels)
 * @param {int} w Width (in pixels)
 * @param {int} h Height (in pixels)
 */
SearchAsYouType.prototype.resizeShadowEl_ = function(id, x, y, w, h) {
  var el = document.getElementById('searchAsYouTypeResultsShadow' + id);

  /* Wrapped around in try/catch because of an IE7 bug */
  try {
    el.style.left   = x + "px";
    el.style.top    = y + "px";
    el.style.width  = w + "px";
    el.style.height = h + "px";
  } catch(e) {
  }
};

/**
 * Decides whether to commission an ajax request, or queue up display of existing results.
 */
SearchAsYouType.prototype.search_ = function() {

  // If already in cache, use cache.
  var cacheEntry = this.getCacheEntry(this.cleanedInput);

  if (cacheEntry) {

    // If the cached entry is ready, queue it up for display.
    if(cacheEntry.status) {

      if(cacheEntry.status == 'responded') {
	this.queueShowResults();
	return;
      }
      // We're still awaiting a response from a previous request. 
      this.anticipateResults();
      return;
    }

    // The request is still 'out there', so just carry on waiting.
    return;
  }

  // Make sure that an external search has been commissioned.
  this.ensureAjaxRequest();

  return false;
};
  
/**
 * Makes sure that a request for the cleanedInput has been commissioned.
 */
SearchAsYouType.prototype.ensureAjaxRequest = function() {
  
  if (this.getCacheEntry(this.cleanedInput)) {
    // The request is still 'out there', so just carry on waiting.
    return;
  }
  
  // This can block the ajax request if the system can't be put into anticipation mode.
  if(!this.anticipateResults()) {return false;}

  // Create an entry for it in the cache, noting start time and index.
  var cacheEntry = {'status':'created', 'ajaxIndex': this.ajaxIndex++, 'ajaxTime': new Date().getTime(), 'cleanedInput':this.cleanedInput};
  this.searchCache["_" + this.cleanedInput] = cacheEntry;

  // Create the AJAX request here. - by adding a javascript element to the document.
  this.ajaxObject = document.createElement('script');
  this.ajaxObject.src = this.searchURL_();
  this.ajaxObject.type = "text/javascript";
  this.ajaxObject.charset = "utf-8";
  // Store the seach text on title
  this.ajaxObject.title = this.cleanedInput;
  
  // Add this script to the DOM, which should trigger it to load.
  document.getElementsByTagName('head').item(0).appendChild(this.ajaxObject);

};


/**
 * Crops a long string.
 */
SearchAsYouType.prototype.crop = function(string, maxLength)
{
  // Default maxLength
  if (arguments.length < 2) {maxLength=30;}


  if(string.length > maxLength) {
    string = string.substring(0, maxLength - 3) + '&nbsp;&hellip;';
  }
  return string;
};



/**
 * Notes that a result is anticipated, sets up some visual feedback accordingly.
 *
 * @return bool True if the system could be put into anticipation mode, false if not.
 */
SearchAsYouType.prototype.anticipateResults = function()
{

  // Try to apply the supplied callback if any.
  if(this.anticipateCallback){
    if(!this.anticipateCallback()) {return false;}
  }

  this.setPrompt(CS.icon_tag('magnifier') + ' Searching for <em>' + this.crop(this.cleanedInput) + '</em>.');
  this.waitingForSearchResultsEl.style.visibility = 'visible';

  return true;
};


/**
 * Resets the prompt and the wait state.
 */
SearchAsYouType.prototype.unAnticipateResults = function()
{
  this.resetPrompt();
  this.waitingForSearchResultsEl.style.visibility = 'hidden';

  if(this.unAnticipateCallback){this.unAnticipateCallback();}
};


/**
 * Simon's function to assemble the URL
 */
SearchAsYouType.prototype.searchURL_ = function() {

  // URL = searchAsYouTypeConfiguration.ajaxResponderUrl;
  var URL;

  // Commission a path and photo search
  if(CS && CS.map) {
    var bounds = CS.map.getExtent().transform(CS.map.getProjectionObject(), CS.map.displayProjection);
    
    if(!bounds) {return false;}
    URL = CS.baseUrl + '/sayt.js?w=' + CS.r7(bounds.left) + '&s=' +  CS.r7(bounds.bottom) + '&e=' +  CS.r7(bounds.right) + '&n=' +  CS.r7(bounds.top) + '&z=' + CS.map.getZoom();

  } else {
    // This is used on a test page.
    URL = '/sayt.js?w=0.113&s=52.200&e=0.123&n=52.210&z=13';
  }
  
  // !! this was ?query
  URL += "&street=" + encodeURIComponent(this.cleanedInput);
  if (this.debugMode) { URL += "&debug=1";  }

  CS.debugtagmsg('<a href="' + URL + '" target="_blank">' + URL + '</a>');

  return URL;
};

/**
 * Handle Ajax response when it's back. Add a tip if necessary, then forward
 * for processing.
 * @param {object} results Results object
 */
SearchAsYouType.prototype.handleAjaxResponse = function(response) {

  // Note the time it took to do this AJAX turnaround...
  var cacheEntry = this.expectCacheEntry(response.query);
  if(!cacheEntry) {return false;}

  cacheEntry.ajaxTime = new Date().getTime() - cacheEntry.ajaxTime;
  cacheEntry.status = 'responded';
  cacheEntry.response = response;

  // Is this ajax response still relevant?
  // I.e. is the user still expecting results for this query?
  if(response.query == this.getCleanedInput()) {
    this.queueShowResults();
  }
};

/**
 * Returns the entry from the searchCache corresponding to the given cleanedInput, or undefined.
 * Entries are stored in the cache with a prefix of '_'.
 */
SearchAsYouType.prototype.getCacheEntry = function(cleanedInput) {
  return this.searchCache["_" + cleanedInput];
};

/**
 * Returns the entry from the searchCache corresponding to the given cleanedInput, alerting if not found.
 */
SearchAsYouType.prototype.expectCacheEntry = function(cleanedInput) {
  var cacheEntry = this.getCacheEntry(cleanedInput);
  if(!cacheEntry) {alert("Search Cache reports a missing entry for:\n\n" + cleanedInput + "\n\nYou must refresh the page and try again.");}
  return cacheEntry;
};


/**
 * Called to queue up the display of the results that correspond to the current cleanedInput.
 * The results must already be in the cache.
 */
SearchAsYouType.prototype.queueShowResults = function() 
{
  this.unAnticipateResults();
  this.cancelShowResultsTimeout();

  // Work out how long it should be before the results are shown.
  var delay = 0;
  if (this.delayShowResults && this.ajaxRequestStartTime > -1) {
    // Subtract the time since the ajax request was issued from the configuration delay.
    delay = searchAsYouTypeConfiguration.showResultsDelay - (new Date().getTime() - this.ajaxRequestStartTime);
  }

  // If the wait is longer than say 100ms schedule it.
  // !! need to review this as it seems to partially disenfranchise searchAsYouTypeConfiguration.showResultsDelay
  if (delay > 100) {
    this.createShowResultsTimeout(delay);
    return;
  }

  // Otherwise display straight away.
  this.showResults();

};

/**
 * Get an HTML snippet showing the current result type. This is used if
 * we show summarized results.
 * @param {string} type Search result type (e.g. "Conference rooms")
 * @return {string} Corresponding HTML snippet
 */
SearchAsYouType.prototype.getResultTypeDescriptionHtml_ = function(type) {
  return '<h1>' + type + ": " + "</h1>";
};

/**
 * Get a CSS class name corresponding to a result type. What this does is
 * removes all of the spaces.
 * @param {string} type Search result type (e.g. "Conference rooms")
 * @return {string} Corresponding class name (e.g. "Conferencerooms")
 */
SearchAsYouType.prototype.getResultTypeClassName_ = function(type) {
  return type.replace(/\ /g, "");
};

/**
 * Get HTML markup for the results. 
 * !! Now only ever called with -1
 * @param {int} resultId Specific Search result to return (-1 if all)
 * @return {string} HTML markup for the result(s)
 */
SearchAsYouType.prototype.getResultsHtml_ = function(resultId) {

  var cacheEntry = this.expectCacheEntry(this.cleanedInput);
  if(!cacheEntry || !cacheEntry.response) {return false;}
  var response = cacheEntry.response;

  var currentResultId = 0; 
  var html = '';
  var lastType = null;
  var openDiv = false;

  var styles = ['expandedPriority', 'expanded', 'normal', 'compact'];
  
  for (var styleNo in styles) {
    for (var i = 0; i < response.results.length; i++) {
      if (response.results[i].style != styles[styleNo]) {
        continue; 
      }     

      if ((resultId == -1) || (resultId == currentResultId)) {
	var style;
        if (resultId > -1) {
	  style = 'expandedPriority';
        } else {
	  style = styles[styleNo];
        }

        if ((style != 'normal') || (lastType != response.results[i].type)) {
          if (openDiv) {
            html += '</div>';
          } 

          var className = "searchResult " + 
            this.getResultTypeClassName_(response.results[i].type);
          if (!currentResultId) {
            className += " first";
          }

          if (style == 'normal') {
            html += '<div class="' + className + ' summary" ';
            html += 'onclick="event.cancelBubble = true;" ';
            html += '>';
            lastType = response.results[i].type;

            html += 
              this.getResultTypeDescriptionHtml_(response.results[i].type);

            openDiv = true;
          }
        } else if (style == 'normal') {
          html += "&nbsp;&middot; ";
        }

        if (style != 'normal') {
          html += '<div id="searchResult' + currentResultId + '" ' +
                  'class="' + className + '" ' +
                  'originalId="' + i + '" ' +
                  'moreDetailsUrl="' + response.results[i].moreDetailsUrl + '" ' +
                  'onclick="searchAsYouType.handleSearchResultClick(event)"' +
                   '>' + response.results[i].content +
                  '</div>';
        } else {
          html += '<a ' +
                  ' id="searchResult' + currentResultId + '"' +
                  ' originalId="' + i + '" ' +
                  ' onclick="return ' +
                  ' searchAsYouType.expandSummaryResult(event, ' + 
                  currentResultId + ')" ' + 
                  ' class="command nowrap summarized" href="' + 
                  response.results[i].moreDetailsUrl + 
                  '">' + response.results[i].name + '</a>';
        }
      }

      currentResultId++; 
    }
  }

  return html;
};


/**
 * Creates a timeout that calls showResults() after delay.
 */
SearchAsYouType.prototype.createShowResultsTimeout = function(delay)
{
  this.showResultsTimeoutId = setTimeout(searchAsYouTypeBind(this.showResults, this), delay);
  alert("Timeout created, now has value: " + this.showResultsTimeoutId + " & delay = " + delay);
};
/**
 * If a timeout that displays the results has been set, cancel it.
 */
SearchAsYouType.prototype.cancelShowResultsTimeout = function()
{
  if (this.showResultsTimeoutId > -1) {
    alert("Cancelling timeout with value: " + this.showResultsTimeoutId);
    clearTimeout(this.showResultsTimeoutId);
  }
  //  alert("Timeout cancelled, now has value: " + this.showResultsTimeoutId);
};


/**
 * Show the search result window, incl. the shadow.
 */
SearchAsYouType.prototype.showResults = function() {

  if(!this.cleanedInput) {return;}

  // Make benign: if the results for the current input are not in the cache, exit.
  var cacheEntry = this.expectCacheEntry(this.cleanedInput);
  if(!cacheEntry || cacheEntry.status != 'responded') {return;}
  // If there are no results, exit.
  if(!cacheEntry.response || cacheEntry.response.results.length == 0) {
    this.setPrompt(CS.icon_tag('cross') + ' Found nothing matching <em>' + this.crop(this.cleanedInput) + '</em>.');
    return;
  }

  this.activeResult = -1;

  this.showResultsTimeoutId = -1;

  clearInterval(this.hideTimeout);

  this.resultsWindowHidden = false;

  // cleaning ids for safari
  var i = 0;
  var el;
  while (el = document.getElementById('searchResult' + i)) {
    el.id = '';           
    i++;
  }

  this.alternateSearchResultsEl.style.height = '1px';
  this.alternateSearchResultsEl.style.visibility = 'hidden';
  this.alternateSearchResultsEl.style.display = 'block';
  this.alternateSearchResultsEl.innerHTML = this.getResultsHtml_(-1);
  this.alternateSearchResultsEl.style.opacity = 0.99;

  // We go through all of the links in the results, and remove tabindex
  // and make them override an iframe, if we're in one
  var els = this.alternateSearchResultsEl.getElementsByTagName('a');
  for (var i = 0, j = els.length; i < j; i++) {
    els.item(i).tabIndex = -1;
    els.item(i).target = "_top";
  }  

  // We go through all of the images, hide them, and assign the function
  // to show them when they're fully loaded. Since an image can resize
  // a search result window, we need to make sure that we recalculate the
  // dimensions (and shadows) on image load
  var els = this.alternateSearchResultsEl.getElementsByTagName('img');
  for (var i = 0, j = els.length; i < j; i++) {
    els.item(i).style.display = 'none';
    els.item(i).onload = 
      searchAsYouTypeBind(this.handleImageOnLoad, this, els.item(i));
  }

  this.updateDimensionsAndShadow_(this.alternateSearchResultsEl);

  this.searchResultsEl.style.visibility = 'hidden';
  this.searchResultsEl.style.display = 'none';

  this.searchResultsShadowEl.style.display = 'block';
  this.searchResultsShadowEl.style.visibility = 'visible';
  this.searchResultsShadowEl.style.opacity = 1;
  this.alternateSearchResultsEl.style.visibility = 'visible';

  // Swap search result elements handlers
  var el = this.searchResultsEl;
  this.searchResultsEl = this.alternateSearchResultsEl;
  this.alternateSearchResultsEl = el;


  // Prompt user to make a selection:
  this.setPrompt(CS.icon_tag('wand') + ' Use arrow keys or mouse to select:');
};

/**
 * Show the image after it's loaded. Prevents images loading and layout
 * reflowing bit by bit -- it only shows the image if it is fully loaded.
 *
 * @param {element} el The image to be shown
 */
SearchAsYouType.prototype.handleImageOnLoad = function(el) {
  if (el) {
    el.style.display = 'inline';

    this.updateDimensionsAndShadow_(this.searchResultsEl);
  }

  return false;
};

/**
 * Hide the search results window. This initializes the fadeout.
 */
SearchAsYouType.prototype.hideResultsWindow_ = function() {
  if (this.resultsWindowHidden) {
    return;
  }

  this.clearAutocomplete_(true);

  this.hideOpacity = this.searchResultsEl.style.opacity;
  clearInterval(this.hideTimeout);
  this.fadeLastTime = new Date().getTime();
  this.hideTimeout = 
    setInterval(searchAsYouTypeBind(this.fadeResultsWindow_, this), 20);

  this.resultsWindowHidden = true;
  this.activeResult = -1;

  this.resetPrompt();
};

/**
 * Fade the search results window a little bit more. We're counting the 
 * time so it should always take the same amount of time, only perhaps be a 
 * little less smooth on less powerful machines.
 */
SearchAsYouType.prototype.fadeResultsWindow_ = function() {
  var newTime = new Date().getTime();

  this.hideOpacity -= (newTime - this.fadeLastTime) * 0.005;
  this.fadeLastTime = newTime;

  if (this.hideOpacity <= 0) {
    clearInterval(this.hideTimeout);
    this.searchResultsEl.style.display = 'none';
    this.searchResultsShadowEl.style.visibility = 'hidden';
  } else {
    this.searchResultsEl.style.opacity = this.hideOpacity;
    this.searchResultsShadowEl.style.opacity = this.hideOpacity;
  }
};

/**
 * Activate (highlight) a result. Used for keyboard navigation
 * between search results.
 * @param {int} no The number of the result to activate
 */
SearchAsYouType.prototype.highlightSearchResult_ = function(no) {
  document.getElementById('searchResult' + no).className += " highlighted";
};

/**
 * Deactivate (de-highlight) a result. Used for keyboard navigation
 * between search results.
 * @param {int} no The number of the result to deactivate
 */
SearchAsYouType.prototype.unhighlightSearchResult_ = function(no) {
  document.getElementById('searchResult' + no).className =
    document.getElementById('searchResult' + no).className.
    replace(/ highlighted/, "");
};

/**
 * Activate (highlight) a next result, if possible.
 */
SearchAsYouType.prototype.highlightNextSearchResult_ = function() {

  var cacheEntry = this.getCacheEntry(this.cleanedInput);
  if(!cacheEntry || !cacheEntry.response) {return;}
  var response = cacheEntry.response;

  if (response.results.length) {
    if (this.activeResult == -1) {
      this.activeResult = 0;
      if (this.inputFieldHasFocus) {
        this.inputFieldEl.blur();
      }
      this.highlightSearchResult_(this.activeResult);
    } else if (this.activeResult < response.results.length - 1) {
      this.unhighlightSearchResult_(this.activeResult);
      this.activeResult++;
      this.highlightSearchResult_(this.activeResult);
    }
  }
};

/**
 * Deactivate (de-highlight) a next result, if possible.
 */
SearchAsYouType.prototype.highlightPrevSearchResult_ = function() {
  var cacheEntry = this.getCacheEntry(this.cleanedInput);
  if(!cacheEntry || !cacheEntry.response) {return;}
  var response = cacheEntry.response;

  if (response.results.length) {
    if (this.activeResult == 0) {
      // Going up from the first result will get us back in the input field
      this.unhighlightSearchResult_(this.activeResult);
      this.activeResult = -1;
      this.inputFieldEl.focus();
    } else if (this.activeResult > 0) {
      this.unhighlightSearchResult_(this.activeResult);
      this.activeResult--;
      this.highlightSearchResult_(this.activeResult);
    }
  }
};


/**
 * Add autocomplete if it's available.
 * @return {boolean} true if added, false if not
 */
SearchAsYouType.prototype.addAutocompleteTextIfPossible_ = function() {
  // Simon has turned off this feature.
  return false;


  var results = this.response;

  if (!results.query) {
    return; // not there yet
  }

  var inputFieldValue = this.getCleanedInput();

  if ((results.query.toLowerCase() == 
       inputFieldValue.substr(0, results.query.length)) &&
      (inputFieldValue == 
       results.autocompletedQuery.substr(0, inputFieldValue.length).
         toLowerCase())) {
    this.autocomplete = 
      results.autocompletedQuery.substring(inputFieldValue.length);

    if (this.autocomplete) {
      var noAutocomplete = this.inputFieldEl.value.replace(/\ /, "&nbsp;");

      this.autocompleteHelperEl.style.display = 'block';
      this.autocompleteHelperEl.innerHTML = noAutocomplete;
      var noAutocompleteWidth = this.autocompleteHelperEl.offsetWidth;
      this.autocompleteHelperEl.innerHTML = this.autocomplete;
      var autocompleteWidth = this.autocompleteHelperEl.offsetWidth;
      this.autocompleteHelperEl.style.display = 'none';

      this.autocompleteEl.innerHTML = 
        this.autocomplete.replace(/\ /, "&nbsp;");
      this.autocompleteEl.style.left = 
        (parseInt(this.autocompleteEl.getAttribute("originalLeft")) + 
        noAutocompleteWidth) + "px";

      this.autocompleteEl.style.display = 'block';
    } else {
      this.autocompleteEl.style.display = 'none';
    }
    return true;
  }
  this.clearAutocomplete_(true);
  return false;
};

/**
 * Collapse autocomplete, i.e. make it part of the actual input field.
 */
SearchAsYouType.prototype.collapseAutocomplete_ = function() {
  if (this.autocomplete) {
    this.inputFieldEl.value += this.autocomplete + " ";
    this.inputFieldEl.selectionStart = this.inputFieldEl.value.length;
    this.inputFieldEl.selectionEnd = this.inputFieldEl.value.length;
    this.clearAutocomplete_(false);
  }
};

/**
 * Clear and hide autocomplete if present.
 * @param {boolean} hideResultsWindow Whether to hide the results window after
 *                                    clearing autocomplete
 */
SearchAsYouType.prototype.clearAutocomplete_ = function(hideResultsWindow) {
  if (this.autocomplete != '') {
    this.autocomplete = '';
    this.autocompleteEl.innerHTML = '';
    this.autocompleteEl.style.display = 'none';
    if (hideResultsWindow) {
      this.hideResultsWindow_();
    }
  }
};

/**
 * Handle a key press event in the input field.
 * @param {event} e Browser event
 */      
SearchAsYouType.prototype.handleInputKeyPress = function(e) {
  if (!this.initialized) {return;}

  var valueToReturn = true;

  e = e || window.event;
  var whichKey = (e.which) ? e.which : e.keyCode;
  
  switch (whichKey) {
    // Enter
  case 9: // Tab
  case 13: 

    // action the search immediately
    this.queueSearch(true);

    // Stops enter submitting the form, I think!
    valueToReturn = false;
    break;
  case 9: // Tab
    if (this.autocompleteJustCollapsed) {
      valueToReturn = false;
    }
    break;
  }
  
  if (!valueToReturn) {
    e.returnValue = false;
    if (e.preventDefault) {
      e.preventDefault();
    }
  }
  
  
  return valueToReturn;
};

/**
 * Handle a key down event in the input field.
 * @param {event} e Browser event
 */      
SearchAsYouType.prototype.handleInputKeyDown = function(e) {
  if (!this.initialized) {
    return;
  }

  e = e || window.event;
  var whichKey = (e.which) ? e.which : e.keyCode;

  // 8 is backspace, 46 is a full stop.
  if ((whichKey == 8) || (whichKey == 46)) {
    this.clearAutocomplete_(false);
  }
};

/**
 * Handle a key up event in the input field. Fire a search query if
 * applicable.
 * @param {event} e Browser event
 */      
SearchAsYouType.prototype.handleInputKeyUp = function(e) 
{
  if (!this.initialized) {return;}
  
  e = e || window.event;
  var whichKey = (e.which) ? e.which : e.keyCode;
  
  this.lastKeyPressed = whichKey;
  
  if (this.autocompleteJustCollapsed) {
    this.cleanedInput = this.lastTypedQuery = this.getCleanedInput();
    this.autocompleteJustCollapsed = false;
    return;
  }

  if (this.getCleanedInput() != this.cleanedInput) {this.cleanedInputChanged();}
};
  
  
  
/**
 * Called from event handlers when the cleanedInput has changed.
 * Queues any actions arising.
 */
SearchAsYouType.prototype.cleanedInputChanged = function(immediate)
{
  // Default immediate
  if (arguments.length < 1) {immediate = false;}

  // Record the latest version of cleaned input, so we can check later if its changed.
  this.cleanedInput = this.getCleanedInput();

  // Measure speed of typing.
  this.guageTypingSpeed();
  
  // Schedule the search
  this.queueSearch(immediate);
  
  // Run any callback.
  if(this.inputChangedCallback) {this.inputChangedCallback();}
  
  return true;
};


/**
 * By examining how long it took to type a few characters this function
 * makes a note of the user's typing speed.
 */
SearchAsYouType.prototype.guageTypingSpeed = function()
{

  // This value is set to a stopping value once the speed has been measured.
  if(this.typingSpeedTime == -2) {return;}
    
  // Used to detect the typing speed. -1 is the variable's initial value.
  if(this.typingSpeedTime == -1) {
    
    // Record the time that the first typed character occured.
    if(this.cleanedInput.length == 1) {
      this.typingSpeedTime = new Date().getTime();
    }

    return;
  }

  // If the user can type the 4th character within 700ms then they are considered a fast typist.
  if(this.cleanedInput.length == 4) {

    var elapsedTime = new Date().getTime() - this.typingSpeedTime;

    // Set to the stopping value so the measurement is only ever made once.
    this.typingSpeedTime = -2;
    
    // this.setPrompt('Time:' + this.typingSpeedTime + ' elapsed: ' + elapsedTime);
    if(elapsedTime < 700) {
      this.typistSpeed = 'fast';
      if(this.debugMode) {
	// Its a little bit too passive to be shown to the general user.
	this.setPrompt(CS.icon_tag('lightning') + ' Optimising search for a fast typist...');
      }
    }   
  }
};


/**
 * Schedule the search to happen soon after the last keystroke.
 */
SearchAsYouType.prototype.queueSearch = function(immediate) {

  // Cancel any existing keystroke timeout
  if (this.keystrokeTimeoutId != -1) {
    clearTimeout(this.keystrokeTimeoutId);
    this.keystrokeTimeoutId = -1;
  }
  
  this.hideResultsWindow_();
  
  if (!this.cleanedInput) {
    this.clearInputField_();
    return;
  }

  
  this.setPrompt(this.placeNameHint);

  // Schedule new search
  this.keystrokeTimeoutId = setTimeout(searchAsYouTypeBind(this.search_, this),
				       immediate ? 1 : this.queueSearchDelay());

};

  
/**
 * Choose a suitable delay before the search is commissioned.
 * Factors may include: (some of these are ignored).
 * 1. Amount of text already typed
 * 2. Presence of spaces in the cleaned input, indicating at least one word complete.
 * 3. Speed of typing.
 * 4. The existence of the cleaned input within the cache.
 */
SearchAsYouType.prototype.queueSearchDelay = function()
{
  if (this.getCacheEntry(this.cleanedInput)) {
    // Item is already in the cache - use the short delay.
    return searchAsYouTypeConfiguration.keystrokeShortDelay;
  }
  
  if (this.typistSpeed == 'fast') {
    // Fast typist - use the short delay.
    return searchAsYouTypeConfiguration.keystrokeShortDelay;
  }

  // If there's hardly any text Wait extra long.
  if(this.cleanedInput.length <= 3) {
    return searchAsYouTypeConfiguration.keystrokeLongDelay * 2;
  }
  
  // Otherwise use long delay.
  return searchAsYouTypeConfiguration.keystrokeLongDelay;
};
  

/**
 * Simon's own function
 */
SearchAsYouType.prototype.selectActiveResult = function(i) {

  var cacheEntry = this.getCacheEntry(this.cleanedInput);
  if(!cacheEntry || !cacheEntry.response) {return;}
  var response = cacheEntry.response;

  if(i<0) {i=0;}
  var selectedResult = response.results[i];
  
  this.inputFieldEl.value = selectedResult.name;
  this.hideResultsWindow_();

  if(this.selectResultCallback) {this.selectResultCallback(selectedResult);}
};

/**
 * Handle a key down event in the document body.
 * @param {event} e Browser event
 */
SearchAsYouType.prototype.handleBodyKeyDown = function(e) {
  var valueToReturn = true;

  if (!this.initialized) {
    return;
  }

  e = e || window.event;
  var whichKey = (e.which) ? e.which : e.keyCode;
  var targetElement = (e.target) ? e.target : e.srcElement;

  switch (whichKey) {
    case 9: // Tab
    case 13: // Enter
    case 32: // space
      if ((!this.resultsWindowHidden) && (this.activeResult >= 0)) {

        valueToReturn = false;
	
	// !! Simon's mods here
	this.selectActiveResult(this.activeResult);
      } 
      break;

    case 27: // Escape
      // Escape can do three things, in order of precedence:
      // 1. If the page with results is loading, Escape should
      //    be handled by the browser to cancel loading the page.
      // 2. If the pop-down with results is shown, Escape should
      //    remove it.
      // 3. Otherwise it should clear the field.

      if (this.inputFieldHasFocus) {
        // Safari sends Esc code twice, so we ignore the second time
        // it happens
        if (this.browserSafari && !this.browserSafari3OrHigher) {
          if (this.escapeKeyJustPressed) {
            this.escapeKeyJustPressed = false;
            break; 
          } else {
            this.escapeKeyJustPressed = true;
          }
        }

        if (!this.resultsWindowHidden) { 
          this.hideResultsWindow_();
          valueToReturn = false;
          this.inputFieldEl.focus();
          this.inputFieldHasFocus = 1;
        } else {
          this.clearInputField_();
          valueToReturn = false;
        }
      }
      break;

    case 35: // End
      if ((this.inputFieldHasFocus) && (this.autocomplete != '')) {
        this.collapseAutocomplete_();
        this.autocompleteJustCollapsed = true;
      }
      break;

    case 40: // down arrow
    case 63233: // down arrow
    case 39: // right arrow
      if (whichKey == 39) {
        if ((this.inputFieldHasFocus) && (this.autocomplete != '')) {
          this.collapseAutocomplete_();
          this.autocompleteJustCollapsed = true;
        }
      }

      // If we press down arrow in the input field, we can force the 
      // re-query 
      if ((this.resultsWindowHidden) && (this.inputFieldHasFocus) && 
          (whichKey != 39)) {
	this.showResults();
        valueToReturn = false;
      } else if ((!this.resultsWindowHidden) && 
                 ((this.activeResult >= 0) || 
                  ((whichKey != 39) && (this.inputFieldHasFocus)))) {
      // If not, right or down arrow activate the next result
        this.highlightNextSearchResult_();
        valueToReturn = false;
        this.arrowKeyProcessed = true;
      }

      break;

    case 38: // up arrow
    case 63235: // up arrow
    case 37: // left arrow
      if (whichKey == 37) {
        this.clearAutocomplete_(true);
      }

      // If we press up arrow in the input field, we hide the pop-down
      if ((!this.resultsWindowHidden) && (this.inputFieldHasFocus) && 
          (whichKey != 37)) {
        this.hideResultsWindow_();
        valueToReturn = false;
        this.arrowKeyProcessed = true;
      } else if ((!this.resultsWindowHidden) && (this.activeResult >= 0)) {
        // If not, left or up arrow activate the previous result
        this.highlightPrevSearchResult_();
        valueToReturn = false;
        this.arrowKeyProcessed = true;
      }
      break;
      /*
    case 9: // Tab
      if (this.inputFieldHasFocus && (this.autocomplete != '')) {
        this.collapseAutocomplete_();
        this.autocompleteJustCollapsed = true;
        valueToReturn = false;
      }
      break;*/
  }

  if (!this.resultsWindowHidden && valueToReturn) {
    if (((!this.inputFieldHasFocus) && ((whichKey < 37) || (whichKey > 40))) ||
        ((whichKey == 9) && (!this.autocompleteJustCollapsed))) {
      this.hideResultsWindow_();
    }
  }

  if (!valueToReturn) {
    e.returnValue = false;
    if (e.preventDefault) {
      e.preventDefault();
    }
  }
};

/**
 * Handle a key press event in the document body.
 * @param {event} e Browser event
 */
SearchAsYouType.prototype.handleBodyKeyPress = function(e) {
  if (!this.initialized) {return;}

  var valueToReturn = true;
  
  e = e || window.event;
  var whichKey = (e.which) ? e.which : e.keyCode;
  
  // Arrow keys have the same key codes here as some other characters
  // (for example, down arrow is the same as left parenthesis)
  // We have to detect whether the arrow key was pressed during key down,
  // and then ignore it here if that's the case (otherwise it'd scroll
  // the screen)
  if ((this.arrowKeyProcessed) && (whichKey >= 37) && (whichKey <= 40)) {
    this.arrowKeyProcessed = false;
    valueToReturn = false;
  }
  
  if (!valueToReturn) {
    e.returnValue = false;
    if (e.preventDefault) {
      e.preventDefault();
    }
  }
};

/**
 * Handle a key up event in the document body.
 * @param {event} e Browser event
 */
SearchAsYouType.prototype.handleBodyKeyUp = function(e) {
  var valueToReturn = true;
  
  e = e || window.event;
  var whichKey = (e.which) ? e.which : e.keyCode;
  var targetElement = (e.target) ? e.target : e.srcElement;

  this.arrowKeyProcessed = false;

  switch (whichKey) {
    case 32: // space
      if (this.inputFieldHasFocus && (this.autocomplete != '')) {
        this.clearAutocomplete_(true);
        valueToReturn = false;
      }
      break;
  }

  if (!valueToReturn) {
    e.returnValue = false;
    if (e.preventDefault) {
      e.preventDefault();
    }
  }
};    

/**
 * Handle a resize event in the document body (to recalculate the search
 * results window and its shadow).
 * @param {event} e Browser event
 */
SearchAsYouType.prototype.handleBodyResize = function(e) {
  this.updateDimensionsAndShadow_(this.searchResultsEl);
};    

/**
 * Handle input field losing focus. Remember this in a variable.
 * @param {event} e Browser event
 */
SearchAsYouType.prototype.handleInputBlur = function(e) {
  this.inputFieldHasFocus = 0;
};

/**
 * Handle input field receiving focus. Remember this in a variable.
 * @param {event} e Browser event
 */
SearchAsYouType.prototype.handleInputFocus = function(e) {
  this.inputFieldHasFocus = 0.5;
};

/**
 * Handle mouse down on the input field. Collapses autocomplete if 
 * present.
 * @param {event} e Browser event
 */
SearchAsYouType.prototype.handleInputMouseDown = function(e) {
  if (this.autocomplete) {
    this.collapseAutocomplete_();
  }
};

/**
 * Handle mouse down on an autocomplete object. Collapses autocomplete if 
 * present.
 * @param {event} e Browser event
 */
SearchAsYouType.prototype.handleAutocompleteMouseDown = function(e) {
  if (this.autocomplete) {
    this.collapseAutocomplete_();
  }
};

/**
 * Handle input field receiving a click.
 * @param {event} e Browser event
 */
SearchAsYouType.prototype.handleInputClick = function(e) {
  e = e || window.event;
  e.cancelBubble = true;

  // Clicking on the input field again when it's already active
  // shows the pop-down again
  if (this.inputFieldHasFocus == 1 && this.resultsWindowHidden) {
    this.showResults();
  } else {
    this.inputFieldHasFocus = 1;
  }
};

/**
 * Handle a click on a search result. Goes to a "more details" URL if the
 * given search result has any.
 * @param {event} e Browser event
 */
SearchAsYouType.prototype.handleSearchResultClick = function(e) {

  e = e || window.event;
  var el = (e.target) ? e.target : e.srcElement;

  while ((el.tagName != 'DIV') ||
         (el.className.indexOf('searchResult') == -1)) {
    el = el.parentNode;
  }


  return  this.selectActiveResult(el.getAttribute("originalId"));
};

/**
 * Handle a click in document body.
 * @param {event} e Browser event
 */
SearchAsYouType.prototype.handleBodyClick = function(e) {
  e = e || window.event;
  var targetElement = (e.target) ? e.target : e.srcElement;

  this.clearAutocomplete_();
  this.hideResultsWindow_();
};

   /*-----------------------------------------------------------/
   /                      Debug Console                         /
   /-----------------------------------------------------------*/
/**
 * Activate the debug mode, create the debug console.
 */
SearchAsYouType.prototype.activateDebugConsole_ = function() {
  document.write("<div onclick='event.cancelBubble = true;' " +
    "id='searchAsYouTypeDebugConsole' class='expanded'>" +
    "<div style='float: right'>" +
    "<button onclick='searchAsYouType.clearDebugConsoleTimes()'>Clear " +
    "console</button>" +
    "<button onclick='searchAsYouType.clearCache()'>Clear cache</button>" +
    "<button onclick='searchAsYouType.showCache()'>Show cache</button>" +
    "<button onclick='searchAsYouType.toggleDebugConsole(event)'>Show/hide" +
    "</button>" +
    "</div><h1>Search-as-you-type debug console</h1>" +
    "<br />" +
    "<table id='searchAsYouTypeDebugTimes'>" +
    "</table>" +
    "</div>");

 this.debugConsoleTimesHeader = 
    '<tr><th>Query</th>' +
    '<th>Auto-completed</th>' +
    '<th>No. of results</th>' +
    '<th>Delay before<br />displaying:<br />(fixed)</th>' +
    '<th title="JS: Time from launching a query to displaying it">' +
    'Total turn-around<br />client+server</th>' +
    '<th title="Ajax: Total time spent on the server">' +
    'Server:<br />Total time</th>' +
    '</tr>';

  this.clearDebugConsoleTimes();
};

/**
 * Show or hide the debug console.     
 * @param {event} e Browser event
 */
SearchAsYouType.prototype.toggleDebugConsole = function(e) {
  var debugConsoleEl = document.getElementById('searchAsYouTypeDebugConsole');

  if (debugConsoleEl.className.indexOf('expanded') != -1) {  
    debugConsoleEl.className = 
      debugConsoleEl.className.replace(/expanded/, 'contracted');
  } else {
    debugConsoleEl.className = 
      debugConsoleEl.className.replace(/contracted/, 'expanded');
  }

  e = e || window.event;
  e.cancelBubble = true;

  this.inputFieldEl.focus();
};

/**
 * Clear the debug console.
 */
SearchAsYouType.prototype.clearDebugConsoleTimes = function() {
  this.debugConsoleTimesContents = '';
  this.debugConsoleTimesCurrentLine = '';
  document.getElementById("searchAsYouTypeDebugTimes").innerHTML = 
    this.debugConsoleTimesHeader;

  this.inputFieldEl.focus();
};


/**
 * Show the contents of the search cache. Used only for debugging.
 */
SearchAsYouType.prototype.showCache = function() {
  
  // Hash table of title:accessor
  var headings = {'Index' : 'ajaxIndex', 'cleanedInput':'cleanedInput', 'Query' : 'response.query', '# results': 'response.results.length', 'Time' : 'ajaxTime', 'Status': 'status'};
  
  var html = '<tr>';
  for(var i in headings) {
    html += '<th>' + i + '</th>';
  }
  html += '</tr>';

  var datum;
  var accessors;
  // Content.
  for(var j in this.searchCache) {
    html += '<tr>';
    for(var i in headings) {
      datum = this.searchCache[j];
      // Apply the accessors in order
      accessors = headings[i].split('.');
      for(var a in accessors) {if(!datum){break;}datum = datum[accessors[a]];}
      html += '<td>' + datum + '</td>';
    }
    html += '</tr>';
  }

  document.getElementById("searchAsYouTypeDebugTimes").innerHTML = html;

  this.inputFieldEl.focus();
};

/**
 * Clear the search cache. Used only for debugging.
 */
SearchAsYouType.prototype.clearCache = function() {
  this.searchCache = [];
};

/**
 * A helper function which partially applies a function to a particular 
 * "this" object and zero or more arguments. The result is a new function 
 * with some arguments of the first function pre-filled and the value 
 * of |this| "pre-specified".
 *
 * Remaining arguments specified at call-time are appended to the pre-
 * specified ones.
 */
function searchAsYouTypeBind(fn, self, var_args) {
  var boundargs = fn.boundArgs_ || [];
  boundargs = boundargs.concat(Array.prototype.slice.call(arguments, 2));

  if (typeof fn.boundSelf_ != "undefined") {
    self = fn.boundSelf_;
  }

  if (typeof fn.foundFn_ != "undefined") {
    fn = fn.boundFn_;
  }

  var newfn = function() {
    // Combine the static args and the new args into one big array
    var args = boundargs.concat(Array.prototype.slice.call(arguments));
    return fn.apply(self, args);
  };

  newfn.boundArgs_ = boundargs;
  newfn.boundSelf_ = self;
  newfn.boundFn_ = fn;

  return newfn;
}


// Instantiating the object...
var searchAsYouType = new SearchAsYouType();

////Ends
