var AutoSuggestControl = Function.extend(
  function(poTextbox, poProvider, piTimerTimeout)
  {
    /**
     * Default message when no results come back from a search provider request.
     *
     */
    this.noResultsMessage = 'No results';

    /**
     * Whether the user has aborted the most recent search.
     *
     */
    this.bAborted = false;

    /**
     * Updated when user navigates suggestion list using the keyboard.
     *
     */
    this.lastHighlightedValue = "";
    this.lastHighlightedId = "";

    /**
     * Set true when user exits suggestion list using the keyboard.
     *
     * Take care to call newSearch() to reset this after checking its value in your onblur() or
     * other post-processing code, or just be sure to call newSearch() in your onfocus() code.
     *
     */
    this.wasKeystrokeExit = false;

    /**
     * The currently selected suggestion.
     *
     */
    this.iCursor = -1;

    /**
     * The dropdown list layer.
     *
     */
    this.oLayer = null;

    /**
     * Suggestion provider for the autosuggest feature.
     */
    this.oProvider = poProvider;

    /**
     * The textbox to capture.
     */
    this.oTextbox = poTextbox;

    /**
     * Default watermark text
     */
    this.sWatermarkText = 'Choose Here';

    /**
     * Create a handle to this object
     */
    this.oTextbox.oAutoSuggest = this;

    /**
     * These are used to control the timer that sends the search off
     * to the search provider above.  This is here so that requests
     * aren't getting sent after every keystroke but instead only
     * once, iTimerTimeout seconds after the last keystroke.  Doing
     * it this way increases efficiency in communication both to and
     * from the provider.
     **/
    this.oTimer = null;
    this.iTimerTimeout = piTimerTimeout || 250; /* 1/4 second */

    /* initialize the control */
    this.init();

  }
);

/**
 * Initializes the textbox with event handlers for
 * auto suggest functionality.
 */
AutoSuggestControl.prototype.init = function ()
{
  /* save a reference to this object */
  var oThis = this;

  /* assign the onkeyup event handler */
  this.oTextbox.onkeyup = function (oEvent)
  {
    /* check for the proper location of the event object */
    if(!oEvent)
    {
      oEvent = window.event;

    }
    /* call the handleKeyUp() method with the event object */
    oThis.handleKeyUp(oEvent);
  };

  /* assign onkeydown event handler */
  this.oTextbox.onkeydown = function (oEvent)
  {
    /* check for the proper location of the event object */
    if(!oEvent)
    {
      oEvent = window.event;

    }
    /* call the handleKeyDown() method with the event object */
    return oThis.handleKeyDown(oEvent);
  };

  /* assign onblur event handler (hides suggestions) */
  this.oTextbox.onblur = ( function ()
  {
    var oExistingOnBlur = null;
    if( typeof oThis.oTextbox.onblur == 'function' )
    {
      oExistingOnBlur = oThis.oTextbox.onblur;
    }
    else
    {
      oExistingOnBlur = function() {};
    }

    return function()
    {
      oThis.hideSuggestions();
      oExistingOnBlur();
      oThis.toggleWatermarkText( false );
    }
  } )();

  $(this.oTextbox).bind( 'focus', (function()
  {
    oThis.toggleWatermarkText.call( oThis, true );
  }));
  oThis.toggleWatermarkText( false );

  /* create the suggestions dropdown */
  this.createDropDown();

};

/**
 * Autosuggests one or more suggestions for what the user has typed.
 * If no suggestions are passed in, then no autosuggest occurs.
 *
 * @param poProvider An object representing the SearchProvider
 *                   used by this control
 */
AutoSuggestControl.prototype.setSearchProvider = function (poProvider)
{
  if( typeof poProvider == 'object' )
  {
    this.oProvider = poProvider;
  }
};

/**
 * Traversing up the DOM until we get to the DIV.  Each
 * suggestion is wrapped in a DIV so regardless of what DOMNode
 * is used in the current context (e.g. mouseover, mousedown,
 * etc) we need to make sure we are working with that wrapping
 * DIV.
 */
AutoSuggestControl.prototype.getValidTarget = function( oTarget )
{
  while( oTarget.tagName != 'DIV' )
  {
    oTarget = oTarget.parentNode;

  }
  return oTarget;
};

AutoSuggestControl.prototype.getSuggestedData = function( oTarget )
{
  var retval = { 'id' : null, 'value' : null };

  if(( typeof oTarget != 'undefined' ) && ( oTarget != null ))
  {
    var oTmp = $(this.getValidTarget( oTarget ));
    if( oTmp.length > 0 )
    {
      retval['value'] = oTmp.children('span').html();
      retval['id']    = oTmp.attr( 'id' );
    }
    else
    {
      retval['value'] = oTarget.firstChild.nodeValue;
      retval['id']    = oTarget.id;
    }
  }
  return retval;

};

AutoSuggestControl.prototype.getSuggestedMetaData = function( oTarget )
{
  var retval = {};

  if(( typeof oTarget != 'undefined' ) && ( oTarget != null ))
  {
    var oTmp = $(this.getValidTarget( oTarget ));
    if( oTmp.length > 0 )
    {
      var sMetaName  = '';
      var sMetaValue = '';
      oTmp.children('div').each( function()
                                 {
                                   sMetaName  = $(this).attr( 'class' );
                                   sMetaValue = $(this).html();

                                   retval[sMetaName] = sMetaValue;
                                 });
    }
  }

  return retval;

};

AutoSuggestControl.prototype.getNodePositionForValue = function()
{
  var retval = 0,
      curValue = this.oTextbox.value.toLowerCase(),
      oTmp;

  for(i=0; i < this.oLayer.childNodes.length; i++)
  {
    var oTmp = $(this.oLayer.childNodes[i]);
    var sHtml = oTmp.children('span').html();
    if(( oTmp.length > 0 ) && (sHtml != null && curValue == sHtml.toLowerCase()))
    {
      retval = i;
      break;
    }
  }

  return retval;
}

/**
 * Autosuggests one or more suggestions for what the user has typed.
 * If no suggestions are passed in, then no autosuggest occurs.
 *
 * @param aSuggestions An array of suggestion strings.
 * @param bTypeAhead If the control should provide a type ahead suggestion.
 */
AutoSuggestControl.prototype.autosuggest = function (aSuggestions, bTypeAhead)
{
  /* make sure we haven't aborted */
  if (!this.bAborted)
  {
    /* make sure there's at least one suggestion */
    if(aSuggestions.length > 0)
    {
      if(bTypeAhead)
      {
        this.typeAhead(aSuggestions[0].text);

      }
      this.showSuggestions(aSuggestions);

    }
    else
    {
      this.displayStatusMessage( this.noResultsMessage );
      //this.hideSuggestions();

    }
  }
};

/**
 * Creates the dropdown layer to display multiple suggestions.
 */
AutoSuggestControl.prototype.createDropDown = function ()
{
  var oThis = this;

  /* create the layer and assign styles */
  this.oLayer = document.createElement('div');
  this.oLayer.className = 'suggestions';
  this.oLayer.id = this.oTextbox.id + '_suggestions';
  this.oLayer.style.visibility = 'hidden';
  this.oLayer.style.width = this.oTextbox.offsetWidth + 'px';
  this.oLayer.style.textAlign = 'left';
  this.oLayer.style.zIndex = '2000';

  /**
   * when the user clicks on the a suggestion, get the text (innerHTML)
   * and place it into a textbox
   **/
  this.oLayer.onmousedown =
  this.oLayer.onmouseup   =
  this.oLayer.onmouseover = function (oEvent)
  {
    oEvent  = oEvent || window.event;
    oTarget = oEvent.target || oEvent.srcElement;
    oTarget = oThis.getValidTarget( oTarget );

    /*  We only care of the mouse down was over one of our options */
    if( oEvent.type == 'mousedown' && oTarget.parentNode.id == ( oThis.oTextbox.id + '_suggestions' ) )
    {

      if( 'AutoSuggestStatusMessage' != oTarget.id )
      {
        var oSuggestedData  = oThis.getSuggestedData( oTarget );
        var oMetadata       = oThis.getSuggestedMetaData( oTarget );

        if(oThis.oTextbox.updateHandler)
        {
          oThis.oTextbox.updateHandler.handleUpdate( oSuggestedData.id, oSuggestedData.value, false, oMetadata );
        }
        else
        {
          oThis.oTextbox.value = oSuggestedData.value;
        }
      }
      oThis.hideSuggestions();

      /**
       * Now we need to reposition the cursor
       **/
      var i=0;
      for(i=0; i < oTarget.parentNode.childNodes.length; i++)
      {
        if(oTarget.parentNode.childNodes[i].firstChild.nodeValue == oTarget.firstChild.nodeValue)
        {
          oThis.iCursor = i;
          break;

        }
      }
    }
    else if(oEvent.type == 'mouseover')
    {
      if( 'AutoSuggestStatusMessage' != oTarget.id )
      {
        oThis.highlightSuggestion(oTarget);

      }
    }
    else
    {
      /*oThis.oTextbox.focus();*/

    }
  };
  document.body.appendChild(this.oLayer);

};

/**
 * Gets the left coordinate of the textbox.
 *
 * @return The left coordinate of the textbox in pixels.
 */
AutoSuggestControl.prototype.getLeft = function ()
{
  var oNode = this.oTextbox;
  var iLeft = 0;

  while(( oNode ) && (oNode.tagName != 'BODY'))
  {
    iLeft += oNode.offsetLeft;
    oNode = oNode.offsetParent;

  }
  return iLeft;

};

/**
 * Gets the top coordinate of the textbox.
 *
 * @return The top coordinate of the textbox in pixels.
 */
AutoSuggestControl.prototype.getTop = function ()
{
  var oNode = this.oTextbox;
  var iTop = 0;

  while(( oNode ) && (oNode.tagName != 'BODY'))
  {
    iTop += oNode.offsetTop;
    oNode = oNode.offsetParent;

  }
  return iTop;

};

/**
 * Handles three keydown events.
 *
 * @param oEvent The event object for the keydown event.
 */
AutoSuggestControl.prototype.handleKeyDown = function (oEvent)
{
  var bReturnValue;
  switch(oEvent.keyCode)
  {
    case 38: /* up arrow */
      /**
       * Only show if there is actually something to show and
       * make sure there is something to scroll through
       **/
      if((0 < this.oLayer.childNodes.length) && ('hidden' == this.oLayer.style.visibility))
      {
        this.oLayer.style.visibility = 'visible';
        this.oLayer.style.overflow = 'auto';


      }
      else
      {
        this.previousSuggestion();

      }
      bReturnValue = true;
      break;

    case 40: /* down arrow */
      /**
       * Only show if there is actually something to show and
       * make sure there is something to scroll through
       **/
      if((0 < this.oLayer.childNodes.length) && ('hidden' == this.oLayer.style.visibility))
      {
        this.oLayer.style.overflow = 'auto';
        this.oLayer.style.visibility = 'visible';

      }
      else
      {
        this.nextSuggestion();

      }
      bReturnValue = true;
      break;

    case 13: /* enter */
    case 9: /* tab and shift-tab */

      this.wasKeystrokeExit = true;
      if(this.oTextbox.updateHandler)
      {
        var bContinueProcessing = true;
        if( this.oLayer.childNodes.length > 0 )
        {
          if( this.iCursor === -1 )
          {
            /**
             * Hitting enter or tab/alt-tab selects the first one by default
             * if the user hasn't already started arrowing up or down.
             *
             * @author jcboget (3/31/2010)
             * Fix implemented for DEFECT8303
             */
            this.iCursor = this.getNodePositionForValue();//0;
          }
          if( typeof oTarget == 'undefined' )
          {
            oTarget = this.getValidTarget( this.oLayer.childNodes[this.iCursor] );
          }

          if( this.iCursor >= 0 )
          {
            var oNode           = this.oLayer.childNodes[this.iCursor];
            var oSuggestedData  = this.getSuggestedData( oNode );
            var oMetadata       = this.getSuggestedMetaData( oTarget );

            if( 'AutoSuggestStatusMessage' != oSuggestedData.id )
            {
              this.oTextbox.updateHandler.handleUpdate( oSuggestedData.id, oSuggestedData.value, false, oMetadata );
            }
            bContinueProcessing = false;
          }
        }

        if( bContinueProcessing  )
        {
          var trimmedPickerValue = $(this.oTextbox).val().replace(/^\s*|\s*$/, "");
          if (trimmedPickerValue != "" )
          {
            this.oTextbox.updateHandler.handleUpdate(null, trimmedPickerValue, true, oMetadata );
          }
          else
          {
            this.oTextbox.updateHandler.handleUpdate(null, '', true, oMetadata );
          }
        }
      }
      else
      {
        var oSuggestedData = this.getSuggestedData( oTarget );
        if( 'AutoSuggestStatusMessage' != oSuggestedData.id )
        {
          $(this.oTextbox).val(oSuggestedData.value);
        }
      }
      this.hideSuggestions();
      if (oEvent.keyCode === 13)
      {
        bReturnValue = false;
      }
      else
      {
        bReturnValue = true;
      }
      break;
    }

    return bReturnValue;
  };

/**
 * Handles keyup events.
 *
 * @param oEvent The event object for the keyup event.
 */
AutoSuggestControl.prototype.handleKeyUp = function (oEvent)
{
  var iKeyCode = oEvent.keyCode;

  if(this.oTimer)
  {
    clearTimeout(this.oTimer);

  }
  /* for backspace (8) and delete (46), shows suggestions without typeahead */
  if(iKeyCode == 8 || iKeyCode == 46)
  {
    /* make sure not to interfere with non-character keys */
    oThis = this;
    this.oTimer = setTimeout('oThis.displayStatusMessage(); oThis.oProvider.requestSuggestions(oThis, false);', this.iTimerTimeout);

  }
  else if(iKeyCode < 32 || (iKeyCode >= 33 && iKeyCode < 46) || (iKeyCode >= 112 && iKeyCode <= 123))
  {
    /* ignore */
  }
  else
  {
    if( this.oTextbox.value.replace(/^\s+/, '').replace(/\s+$/, '') != '' )
    {
      /* request suggestions from the suggestion provider, again without typeahead (why not?) */
      oThis = this;
      this.oTimer = setTimeout('oThis.displayStatusMessage(); oThis.oProvider.requestSuggestions(oThis, false);', this.iTimerTimeout);
      this.iCursor = -1; /* Reset the cursor to work with new result set */

    }
  }
};

AutoSuggestControl.prototype.displayStatusMessage = function( statusMsg )
{
  /* make sure we haven't aborted */
  if (!this.bAborted)
  {
    if( '' != this.oTextbox.value )
    {
      //if the width hasn't been set, reset it
      //  when the object is initialized offsetWidth
      //  will be 0 if the object display is none
      if ( parseInt( this.oLayer.style.width ) == 0 )
      {
       this.oLayer.style.width = this.oTextbox.offsetWidth + 'px';
      }
      this.oLayer.innerHTML = '';  /* clear contents of the layer */

      oDiv = document.createElement('div');
      oDiv.id = 'AutoSuggestStatusMessage';

      if( !statusMsg )
      {
        var oImg = document.createElement('IMG');
        oImg.src = '/images/loading.gif';

        oDiv.appendChild( oImg );
        oDiv.appendChild(document.createTextNode( 'Searching' ));

      }
      else
      {
        oDiv.appendChild(document.createTextNode( statusMsg ));
        $(oDiv).addClass('ui-state-highlight').attr('title','New Tag');

      }
      var oOffset = $( this.oTextbox ).offset();
      var sTop = (parseInt(oOffset.top) + parseInt($(this.oTextbox).height()) + 6);
      this.oLayer.appendChild(oDiv);
      this.oLayer.style.left = oOffset.left + 'px';
      this.oLayer.style.top = sTop + 'px';
      this.oLayer.style.overflow = 'auto';
      this.oLayer.style.visibility = 'visible';

    }
  }

};

AutoSuggestControl.prototype.setWatermarkText = function( sWatermarkText )
{
  this.sWatermarkText = sWatermarkText;
  this.oTextbox.value = '';
  this.toggleWatermarkText( false );
};

AutoSuggestControl.prototype.toggleWatermarkText = function( bGotFocus )
{
  if( this.sWatermarkText != '' )
  {
    if( bGotFocus === true )
    {
      if(this.oTextbox.value == this.sWatermarkText)
      {
        this.oTextbox.value = '';
        this.oTextbox.style.color = '#000000';
      }
    }
    else
    {
      if(this.oTextbox.value == '')
      {
        this.oTextbox.value = this.sWatermarkText;
        this.oTextbox.style.color = '#999999';
      }
    }
  }
};

/**
 * Aborts the current search.
 */
AutoSuggestControl.prototype.abortSearch = function()
{
  this.bAborted = true;
  this.hideSuggestions();

};

/**
 * Changes the "No Results" message to a value other than the default.
 */
AutoSuggestControl.prototype.setNoResultsMessage = function(newMessage)
{
  this.noResultsMessage = newMessage;

};

/**
 * Permits a new search after a call to abortSearch().
 */
AutoSuggestControl.prototype.newSearch = function()
{
  this.bAborted = false;
  this.wasKeystrokeExit = false;
  this.lastHighlightedValue = "";
  this.lastHighlightedId = 0;

};

/**
 * Hides the suggestion dropdown.
 */
AutoSuggestControl.prototype.hideSuggestions = function ()
{
  this.oLayer.style.overflow = 'hidden';
  this.oLayer.style.visibility = 'hidden';
};

/**
 * Highlights the given node in the suggestions dropdown.
 *
 * @param oSuggestionNode The node representing a suggestion in the dropdown.
 */
AutoSuggestControl.prototype.highlightSuggestion = function (oSuggestionNode)
{
  while( oSuggestionNode.tagName != 'DIV' )
  {
    oSuggestionNode = oSuggestionNode.parentNode;

  }
  var i=0;
  for(i=0; i < this.oLayer.childNodes.length; i++)
  {
    var oNode = this.oLayer.childNodes[i];
    if(oNode == oSuggestionNode)
    {
      $(oNode).addClass( 'ui-state-active' );

    }
    else
    {
      $(oNode).removeClass( 'ui-state-active' );

    }
  }
};

/**
 * Highlights the next suggestion in the dropdown and
 * places the suggestion into the textbox.
 *
 */
AutoSuggestControl.prototype.nextSuggestion = function ()
{
  var cSuggestionNodes = this.oLayer.childNodes;

  if(cSuggestionNodes.length > 0 && this.iCursor < cSuggestionNodes.length-1)
  {
    var oNode = cSuggestionNodes[++this.iCursor];
    var oSuggestedData = this.getSuggestedData( oNode );

    this.oLayer.scrollTop = oNode.offsetTop;
    if( 'AutoSuggestStatusMessage' != oSuggestedData.id )
    {
      this.highlightSuggestion(oNode);
      this.oTextbox.value = oSuggestedData.value;
      this.lastHighlightedValue = oSuggestedData.value;
      this.lastHighlightedId = oNode.id;
    }
  }
};

/**
 * Highlights the previous suggestion in the dropdown and
 * places the suggestion into the textbox.
 *
 * TODO: ALSO NEEDS TO PUT THE ID OF THE NEXT SUGGESTION INTO THE APPROPRIATE FIELD ON THE PAGE, IF ANY.
 *
 */
AutoSuggestControl.prototype.previousSuggestion = function ()
{
  var cSuggestionNodes = this.oLayer.childNodes;

  if(cSuggestionNodes.length > 0 && this.iCursor > 0)
  {
    var oNode = cSuggestionNodes[--this.iCursor];
    var oSuggestedData = this.getSuggestedData( oNode );

    this.oLayer.scrollTop = oNode.offsetTop;
    if( 'AutoSuggestStatusMessage' != oSuggestedData.id )
    {
      this.highlightSuggestion(oNode);
      this.oTextbox.value = oSuggestedData.value;
      this.lastHighlightedValue = oSuggestedData.value;
      this.lastHighlightedId = oNode.id;

    }
  }
};

/**
 * Selects a range of text in the textbox.
 *
 * @param iStart The start index (base 0) of the selection.
 * @param iLength The number of characters to select.
 */
AutoSuggestControl.prototype.selectRange = function (iStart, iLength)
{
  if(this.oTextbox.createTextRange)
  {
    /* use text ranges for Internet Explorer */
    var oRange = this.oTextbox.createTextRange();
    oRange.moveStart('character', iStart);
    oRange.moveEnd('character', iLength - this.oTextbox.value.length);
    oRange.select();

  }
  else if(this.oTextbox.setSelectionRange)
  {
    /* use setSelectionRange() for Mozilla */
    this.oTextbox.setSelectionRange(iStart, iLength);

  }
  /* set focus back to the textbox */
  this.oTextbox.focus();

};

/**
 * Builds the suggestion layer contents, moves it into position,
 * and displays the layer.
 *
 * @param aSuggestions An array of suggestions for the control.
 */
AutoSuggestControl.prototype.showSuggestions = function (aSuggestions)
{
  /* make sure we haven't aborted */
  if (!this.bAborted)
  {
    var oDiv = null;

    var iLayerWidth       = 0;
    this.oLayer.innerHTML = '';  /* clear contents of the layer */
    var i=0;
    for(i=0; i < aSuggestions.length; i++)
    {
      oDiv = document.createElement('div');
      oDiv.id = aSuggestions[i].id;
      oDiv.innerHTML = aSuggestions[i].text;
      oDiv.style.whiteSpace = 'nowrap';
      oDiv.style.cursor = 'pointer';

      this.oLayer.appendChild(oDiv);

      if( oDiv.scrollWidth > iLayerWidth )
      {
        iLayerWidth = oDiv.scrollWidth;

      }
    }
    var oOffset = $( this.oTextbox ).offset();
    var sTop = (parseInt(oOffset.top) + parseInt($(this.oTextbox).height()) + 6);
    this.oLayer.style.left = oOffset.left + 'px';
    this.oLayer.style.top = sTop + 'px';
    this.oLayer.style.overflow = 'auto';
    this.oLayer.style.visibility = 'visible';
//      this.oLayer.style.width = iLayerWidth + 'px';//this.oTextbox.width + 'px';
    this.oLayer.style.width = 'auto';
    this.oLayer.style.overflowX = 'auto';
    this.oLayer.style.overflowY = 'visible';
    this.oLayer.style.minWidth = iLayerWidth + 'px';
    this.oLayer.style.zIndex = '2000';

  }
};

/**
 * Inserts a suggestion into the textbox, highlighting the
 * suggested part of the text.
 *
 * @param sSuggestion The suggestion for the textbox.
 */
AutoSuggestControl.prototype.typeAhead = function (sSuggestion)
{
  /* check for support of typeahead functionality */
  if(this.oTextbox.createTextRange || this.oTextbox.setSelectionRange)
  {
    var iLen = this.oTextbox.value.length;
    this.oTextbox.value = sSuggestion;
    this.selectRange(iLen, sSuggestion.length);

  }
};

