var ConsilioSuggest =
{ suggestfields: []
, requestdelay: 150
, doccountmethod: "search" // Default document count method
, default_url: "/tollium_todd/consilio_suggest.shtml"


////////////////////////////////////////////////////////////////////////////////
//
//  Public API

  /** @short Initialize Consilio suggestions for an input field
      @param input The input field (<input> object or id) to add suggestions to
      @param options Options to customize the suggestions (either url or catalog has to be specified, any of the other
                     options can be left out)
      @cell options.url The callback URL to get suggestions from
      @cell options.catalog The catalog to get suggestions for
      @cell options.autofocus Set to true to automatically focus the input field (defaults to false)
      @cell options.document_count Set to true to add the number of matching documents to suggestions (defaults to true)
      @cell options.type Set to "inline" for inline suggestions in the input field, "dropdown" for a dropdown list with
                         suggestions or "combo" for a combination of these types (defaults to "dropdown")
      @cell options.count The number of suggestions to show (defaults to 10)
      @cell options.enabled If the suggestions are enabled
      @cell options.extradata Extra data to send to the callback URL
  */
, Initialize: function(input, options)
  {
    // Get the requested input field by id or reference
    if (typeof input == "string")
      input = document.getElementById(input);
    if (!input || typeof input != "object")
      return null;

    // Check if we're already observing this input field
    for (var i = 0; i < this.suggestfields.length; ++i)
      if (this.suggestfields[i].obj == input)
        return null;

    // Check options
    var url = "";
    var catalog = "";
    var autofocus = false;
    var newtype = "";
    var count = -1;
    var doccount = true;
    var suggestenabled = true;
    var extradata = null;
    if (typeof options == "object")
    {
      url = options.url || "";
      catalog = options.catalog || "";
      autofocus = options.autofocus === true;
      doccount = !(options.document_count === false);
      suggestenabled = !(options.enabled === false);
      newtype = typeof options.type == "string" ? options.type.toLowerCase() : "";
      if (typeof options.count == "number")
        count = options.count;
      if (options.extradata)
        extradata = options.extradata;
    }

    if (!url)
    {
      if (!catalog)
        return null;
      url = this.default_url;
    }

    // Create a field record for this input field
    var fieldid = this.suggestfields.length;
    var newfield = { id: fieldid
                   , obj: input
                   , objpos: toddGetBodyPos(input)
                   , div: this.CreateSuggestionsDiv(fieldid)
                   , showinline: false
                   , showdropdown: false
                   , cur: -1
                   , enabled: suggestenabled

                   , options: []
                   , preventblur: false
                   , preventsubmit: false
                   , url: url
                   , catalog: catalog
                   , timeout: null
                   , xhr: toddCreateRequestObject()

                   , count: count
                   , doccount: ""
                   , type: ""
                   , extradata: extradata
                   };
    newfield.SetCount = function(count)
    {
      // Default to -1 (which returns internal default of 10 results)
      if (typeof count != "number")
        count = -1;

      if (count != newfield.count)
        newfield.count = count;
    }
    newfield.SetDocumentCount = function(doccount)
    {
      // Default to this.doccountmethod
      doccount = doccount ? ConsilioSuggest.doccountmethod : "";

      if (doccount != newfield.doccount)
        newfield.doccount = doccount;
    };
    newfield.SetFieldType = function(type)
    {
      // Default to "dropdown"
      if (type != "inline" && type != "dropdown" && type != "combo")
        type = "dropdown";

      if (type != newfield.type)
      {
        newfield.type = type;
        newfield.showinline = newfield.type == "inline" || newfield.type == "combo";
        newfield.showdropdown = newfield.type == "dropdown" || newfield.type == "combo";
      }
    };
    newfield.SetEnabled = function(enabled)
    {
      if (enabled != newfield.enabled)
      {
        newfield.enabled = enabled;
        if (!newfield.enabled)
        {
          // If timer is running, clear it
          if (newfield.timeout)
            window.clearTimeout(newfield.timeout)
        }
      }
    };

    this.suggestfields.push(newfield);

    // Additional initialization
    newfield.SetDocumentCount(doccount);
    newfield.SetFieldType(newtype);

    // Disable autocomplete on the input element
    input.setAttribute("autocomplete", "off");

    // Set autofocus _after_ turning autocomplete off, otherwise it'll be still enabled in Firefox!
    input.blur();
    if (autofocus)
      this.FocusInput(fieldid);

    // Attach our event handlers to the input field and form
    this.AddEventListener(input, "keydown", "HandleInputKeyDown", fieldid);
    this.AddEventListener(input, "keyup", "HandleInputKeyUp", fieldid);
    this.AddEventListener(input, "keypress", "HandleInputKeyPress", fieldid);
    this.AddEventListener(input, "blur", "HandleInputBlur", fieldid);
    this.AddEventListener(input.form, "submit", "HandleFormSubmit", fieldid);

    return newfield;
  }


////////////////////////////////////////////////////////////////////////////////
//
//  Update to customize

  /** @short Create a div to display suggestions in
      @return The created div
  */
, CreateSuggestionsDiv: function(fieldid)
  {
    var div = document.createElement("div");
    div.className = "whc_suggestionscontainer";
    div.style.display = "none";
    div.style.position = "absolute";
    document.body.appendChild(div);
    return div;
  }

  /** @short Show retrieved suggestions
      @param fieldid The suggest field's id
      @param suggestions The retrieved suggestions
      @cell suggestions.text The suggestion text
      @cell suggestions.count The number of matching documents for the suggestion
      @param div The suggestions div (as returned by CreateSuggestionsDiv)
      @param pos Where the div should be positioned (object with top, left and width fields)
  */
, ShowSuggestionsDiv: function(fieldid, suggestions, div, pos)
  {
    var field = this.suggestfields[fieldid];

    // Remove old options from the suggestions div
    while (div.firstChild)
      div.removeChild(field.div.firstChild);

    field.options.splice(0, field.options.length);

    // Create a table for the options
    var table = div.appendChild(document.createElement("table"));
    table.className = "whc_suggestions";
    var tbody = table.appendChild(document.createElement("tbody"));

    for (var i = 0; i < suggestions.length; ++i)
    {
      var sug = suggestions[i];

      var option = tbody.appendChild(document.createElement("tr"));
      option.appendChild(document.createComment(sug.text));
      field.options.push(option);

      // Attach our event listeners for mouse events on the options
      this.AddEventListener(option, "mouseover", "HandleOptionMouseOver", fieldid);
      this.AddEventListener(option, "mousedown", "HandleOptionMouseDown", fieldid);

      // Create separate span's to highlight currently typed text in suggestions
      var fieldpos = this.GetInputCursorPosition(field.obj);
      var suggestionselected = fieldpos.start + fieldpos.length == field.obj.value.length;
      var text = option.appendChild(document.createElement("td"));
      text.className = "whc_suggestiontext";

      var span = text.appendChild(document.createElement("span"));
      span.appendChild(document.createTextNode(sug.text.substr(0, suggestionselected ? fieldpos.start : field.obj.value.length)));
      span.className = "whc_suggestionmatch";

      span = text.appendChild(document.createElement("span"));
      span.appendChild(document.createTextNode(sug.text.substr(suggestionselected ? fieldpos.start : field.obj.value.length)));
      span.className = "whc_suggestionsuggest";

      // Add suggestion count, if available
      if (sug.count)
      {
        var count = option.appendChild(document.createElement("td"));
        count.appendChild(document.createTextNode(sug.count));
        count.className = "whc_suggestioncount";
      }
    }

    // Position the suggestions div and show it
    div.style.top = pos.top + "px";
    div.style.left = pos.left + "px";
    div.style.width = pos.width + "px";
    div.style.display = "block";
  }

  /** @short Select the option with index option
  */
//ADDME: How to handle field.options? Should that be generated automatically?
//       Or maybe have a function that returns the nth option element?
, SelectSuggestion: function(field, option, setvalue)
  {
    // Deselect currently selected option by removing class name
    if (field.cur >= 0)
      field.options[field.cur].className = "";

    // Select requested option
    if (option < field.options.length)
      field.cur = option;

    // If a valid option was selected, add selection class name
    if (field.cur >= 0)
    {
      field.options[field.cur].className = "whc_suggestionselected";

      // Set the input field value, if requested
      if (setvalue)
        field.obj.value = field.options[field.cur].firstChild.nodeValue;
    }
  }

  /** @short Close the suggestions div
  */
, HideSuggestionsDiv: function(fieldid, div)
  {
    div.style.display = "none";
  }


////////////////////////////////////////////////////////////////////////////////
//
//  Private functions

  // Start a timer to retrieve suggestions after a certain delay (to prevent the suggestions interfering with typing)
, GetSuggestions: function(field)
  {
    // If timer is already running, restart it
    if (field.timeout)
      window.clearTimeout(field.timeout)
    if (field.enabled)
    {
      field.timeout = window.setTimeout(function()
      {
        ConsilioSuggest.RetrieveSuggestions(field.id);
      }, this.requestdelay);
    }
  }

  // Do the actual request for suggestions
, RetrieveSuggestions: function(fieldid)
  {
    var field = this.suggestfields[fieldid];

    // Abort currently running requests
    field.xhr.abort();
    field.xhr.open("post", field.url, true);
    field.xhr.onreadystatechange = function(event)
    {
      ConsilioSuggest.HandleRequestStateChange(fieldid, event);
    };

    var vars = { text: field.obj.value
               , count: field.showdropdown ? field.count : 1
               , doccount: field.doccount
               };
    if (field.catalog)
      vars.catalog = field.catalog;
    if (field.extradata)
      vars.extradata = field.extradata;

    field.xhr.send(JSON.stringify(vars));

    // Reset suggestions retrieval timer
    window.clearTimeout(field.timeout);
    field.timeout = null;
  }

  // Check if an action key was pressed, which shouldn't activate the autocompletion
, IsActionKey: function(code)
  {
    return code == 3 // Enter
        || code == 8 // Backspace
        || code == 13 // Return
        || (code >= 16 && code <= 20) // Shift, Control, Alt, Pause, Caps Lock
        || code == 27 // Escape
        || (code >= 33 && code <= 40) // Page Up, Page Down, End, Home, Left, Up, Right, Down
        || code == 44 // Print Screen
        || code == 46 // Delete
        || code == 91 // Windows
        || code == 93 // Context Menu
        || (code >= 112 && code <= 135) // F1 - F24
        || (code >= 144 && code <= 145) // Num Lock, Scroll Lock
        || code == 224 // Meta
        ;
  }

  // Show the given suggestions for a field
, ShowSuggestions: function(field, suggestions)
  {
    if (suggestions.length > 0)
    {
      // Show the suggestions in a dropdown list
      if (field.showdropdown)
      {
        this.ShowSuggestionsDiv(field.id, suggestions, field.div, { top: field.objpos.top + field.obj.offsetHeight - 1, left: field.objpos.left, width: field.obj.offsetWidth });
        field.cur = -1; // Nothing selected
      }

      // Show the first suggestion inline in the input field
      if (field.showinline)
      {
        var fieldpos = this.GetInputCursorPosition(field.obj);
        var sug = suggestions[0];

        // Set the suggestion as the input's value
        field.obj.value = sug.text;

        // Select the added part
        this.SetInputSelection(field.obj, fieldpos.start, sug.text.length - fieldpos.start);
      }
    }
  }

  // Close the suggestions div
, HideSuggestions: function(field)
  {
    this.HideSuggestionsDiv(field.id, field.div);
    field.cur = -1; // Nothing selected
  }

  // Handle a pressed action key
, HandleActionKey: function(fieldid, code)
  {
    var field = this.suggestfields[fieldid];

    switch (code)
    {
      case 8: // Backspace
      case 46: // Delete
        // Hide the suggestions div, but don't cancel the key event
        this.HideSuggestions(field);
        return false;

      case 13: // Return
        // Not an active suggestion selected, let the key event go through (will submit the form)
        if (field.cur < 0)
          return false;

        // Opera will submit the form anyway, even if we cancel this event, so we don't cancel this event, but prevent form
        // submission by setting this flag
        field.preventsubmit = true;
        // If we're not showing inline suggestions, pressing Return will use the selected suggestion
        if (!field.showinline)
          field.obj.value = field.options[field.cur].firstChild.nodeValue;

        // Hide suggestions, but don't cancel key event, form submit handler will reset preventsubmit
        this.HideSuggestions(field);
        return false;

      case 27: // Escape
        // Hide suggestions, cancel key event
        this.HideSuggestions(field);
        return true;

      case 38: // Up
        // If showing suggestions, select previous suggestion
        if (field.div.style.display != "none")
          this.SelectSuggestion(field, field.cur > 0 ? field.cur - 1 : field.options.length - 1, true);
        return true;

      case 40: // Down
        // If not showing suggestions, activate autocomplete, otherwise select next suggestion
        if (field.div.style.display == "none")
          this.GetSuggestions(field);
        else
          this.SelectSuggestion(field, field.cur < (field.options.length - 1) ? field.cur + 1 : 0, true);
        return true;
    }

    // Not handled navigation key
    return false;
  }

  // Get the selection start and length of an input
, GetInputCursorPosition: function(input)
  {
    if (typeof input.selectionStart != 'undefined')
    {
      return { start: input.selectionStart, length: input.selectionEnd - input.selectionStart };
    }
    else if (document.selection)
    {
      // Get a range for the current selection and check if our input has the selection
      var selrange = document.selection.createRange();
      if (selrange.parentElement() == input)
      {
        var length = selrange.text.length;
        // Collapse to the start of the selection
        selrange.collapse(true);
        // Move the selection end to the end of the input
        selrange.moveEnd('textedit');
        // The starting position is the length of the input value minus the length of the selection
        var start = input.value.length-selrange.text.length;
        return { start: start, length: length };
      }
    }
    // Just return the complete input
    return { start: 0, length: input.value.length };
  }

  // Get the text value of an input field up until the cursor
, GetInputValueToCursor: function(input)
  {
    if (typeof input.selectionStart != 'undefined')
    {
      // Return everything up to the cursor position (start of selection)
      return input.value.substr(0, input.selectionStart);
    }
    else if (document.selection)
    {
      // Get a range for the current selection and check if our input has the selection
      var selrange = document.selection.createRange();
      if (selrange.parentElement() == input)
      {
        // Collapse to the start of the selection
        selrange.collapse(true);
        // Move the selection end to the end of the input
        selrange.moveEnd('textedit');
        // The starting position is the length of the input value minus the length of the selection
        var start = input.value.length-selrange.text.length;
        return input.value.substr(0, start);
      }
    }
    // Just return the input's value
    return input.value;
  }

  // Set the text value of an input field and select length characters, starting at pos
, SetInputSelection: function(input, start, length)
  {
    if (!length)
      length = 0;
    if (typeof input.selectionStart != 'undefined')
    {
      // Set the cursor position to the end of the input
      input.selectionStart = start;
      input.selectionEnd = start+length;
    }
    else if (document.selection)
    {
      // Create a range for the new cursor position
      selrange = input.createTextRange();
      selrange.collapse(true);
      // Set the start at the end of the input
      selrange.moveStart('character', start);
      selrange.moveEnd('character', length);
      // Select the range
      selrange.select();
    }
  }

  // Focus the input field
, FocusInput: function(fieldid)
  {
    var input = this.suggestfields[fieldid].obj;
    input.focus();
    // Set cursor to end of input field
    this.SetInputSelection(input, input.value.length);
  }

  // Handle a key down event on the input field
, HandleInputKeyDown: function(fieldid, event)
  {
    var field = this.suggestfields[fieldid];

    // Abort any running requests
    field.xhr.abort();

    // Cancel the event if it is handled as an action key
    if (this.HandleActionKey(fieldid, event.keyCode))
      return toddDontPropagateEvent(event);

    return true;
  }

  // Handle a key up event on the input field
, HandleInputKeyUp: function(fieldid, event)
  {
    // Only handle non-action key events
    if (!this.IsActionKey(event.keyCode))
    {
      var field = this.suggestfields[fieldid];
      // If the input field was cleared, hide suggestions, otherwise get new suggestions
      if (field.obj.value == "")
        this.HideSuggestions(field);
      else
        this.GetSuggestions(field);
    }
    return true;
  }

  // Handle a blur event on the input field
, HandleInputBlur: function(fieldid, event)
  {
    var field = this.suggestfields[fieldid];

    // The mousedown event on a suggestion will set preventblur (IE still fires the blur event if the mousedown event was
    // canceled, so we'll have to re-focus the input field)
    if (field.preventblur)
    {
      field.preventblur = false;
      this.FocusInput(fieldid);
      return toddDontPropagateEvent(event);
    }

    // Hide the suggestions if the input field was blurred (i.e. by clicking somewhere else or deactivating the browser)
    this.HideSuggestions(field);
  }

  // Handle the input field's form submit
, HandleFormSubmit: function(fieldid, event)
  {
    var field = this.suggestfields[fieldid];

    // Opera will submit the form, even if we cancelled the Enter keydown event
    if (field.preventsubmit)
    {
      field.preventsubmit = false;
      return toddDontPropagateEvent(event);
    }
    return true;
  }

  // Handle a ready state change event on the XMLHttpRequest object
, HandleRequestStateChange: function(fieldid, event)
  {
    var field = this.suggestfields[fieldid];

    // Check if the request was completed
    if (field.xhr.readyState != 4)
      return;

    // Silently ignore server errors
    if (field.xhr.status != 200)
      return;

    // Read the response and show suggestions upon success
    var response = eval('(' + field.xhr.responseText + ')');
    if (response.success)
      this.ShowSuggestions(field, response.suggestions);
    else
      this.HideSuggestions(field);
  }

  // Get the index of the option that was clicked, return -1 if not found
, GetOptionForEvent: function(event)
  {
    // The user clicked a <td>, the option is a <tr> within a <table>
    var tr = event.target;
    while (tr && tr.nodeName.toLowerCase() != "tr")
      tr = tr.parentNode;
    if (!tr)
      return -1;

    // Find out which of the <tr> siblings was clicked
    var i = 0;
      for (var ch = tr.parentNode.firstChild; ch && ch != tr; ch = ch.nextSibling, ++i);
    if (!ch)
      return -1;

    return i;
  }

  // Handle a mouse over event on a suggestion
, HandleOptionMouseOver: function(fieldid, event)
  {
    var field = this.suggestfields[fieldid];

    var option = this.GetOptionForEvent(event);
    if (option < 0 || option >= field.options.length)
      return false;

    // Select the hovered option
    this.SelectSuggestion(field, option, false);
  }

  // Handle a mouse down event on a suggestion
, HandleOptionMouseDown: function(fieldid, event)
  {
    var field = this.suggestfields[fieldid];

    var option = this.GetOptionForEvent(event);
    if (option < 0 || option >= field.options.length)
      return false;

    // Set the input field value to the selected option, hide the options and focus the input
    field.obj.value = field.options[option].firstChild.nodeValue;
    this.HideSuggestions(field);
    this.FocusInput(fieldid);

    // IE will fire the blur event on the input field, even if we're cancelling the mouse event, setting this flag will
    // refocus the input field
    field.preventblur = true;

    return toddDontPropagateEvent(event);
  }

  // Cross-browser method for adding an event listener to a target bound to the global ConsilioSuggest object (for a given
  // suggest field)
, AddEventListener: function(target, type, listener, fieldid)
  {
    if (!target || typeof ConsilioSuggest[listener] == "undefined")
      return;

    if (target.addEventListener)
    {
      // Adding DOM-style event listener (supported by every modern browser)
      target.addEventListener(type, function(event)
      {
        return ConsilioSuggest[listener](fieldid, event);
      }, false);
    }
    else if (target.attachEvent)
    {
      // Old, IE-style attachment of events
      target.attachEvent("on" + type, function(event)
      {
        // Internet Explorer uses global event
        if (!event)
          event = window.event;
        // The event's target is stored in the srcElement attribute
        if (!event.target)
          event.target = event.srcElement;
        return ConsilioSuggest[listener](fieldid, event);
      });
    }
  }
};

