/* Movielens extensions to scriptaculous
 * 
 * These provide auto-complete functionality for the movie linking
 * and the tagging features.  They are based on and depend on the 
 * scriptaculous library.
 */

var ML = { Movie: {}, Tag: {} };

ML.Movie.QuickPick = {};
ML.Movie.QuickPick.Autocompleter = Class.create();
// This is a subclass of Ajax.Autocompleter.
Object.extend(Object.extend(ML.Movie.QuickPick.Autocompleter.prototype, 
                            Ajax.Autocompleter.prototype), {
  updateElement: function(selectedElement) 
  {
    var movieId = selectedElement.getAttribute("movieId");
    var title = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
    if(movieId != null)
      addElement(movieId, title);
    this.element.value = '';
  }
});

ML.Movie.ForumLinker = {};
ML.Movie.ForumLinker.Autocompleter = Class.create();
// This is a subclass of Ajax.Autocompleter.
Object.extend(Object.extend(ML.Movie.ForumLinker.Autocompleter.prototype, 
                            Ajax.Autocompleter.prototype), {
  // Selecting a movie from the auto-complete dropdown causes some 
  // specially-formatted text to appear in another textarea on the page
  // as well as *deletes* the contents of the auto-complete text box.
  updateElement: function(selectedElement) 
  {
    var movieId = selectedElement.getAttribute("movieId");
    var title = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
    if(movieId != null)
    {
      this.movielink(movieId, title);
    }
    this.element.value = '';
  },

  // note, this code is dependent on the mvnforum js files being present in the browser
  movielink: function(id, text) {
    var txtarea = document.mvnform.message;
        regexpr = new RegExp(" \\(\\d\\d\\d\\d\\)");
        text = text.replace(regexpr,"");
    var ml = "[movie id=" + id + "]" + text + "[/movie]";

    // insert text appropriately
    var selection = getSelection(txtarea);  // mvncode.js
    var inserttext = "";
    if (selection.length > 0) {
        ml = "[movie id=" + id + "]" + selection + "[/movie]";
    }
    insertString(txtarea, ml);
  }

});

ML.Tag.Autocompleter = Class.create();
// This is a subclass of Ajax.Autocompleter.
Object.extend(Object.extend(ML.Tag.Autocompleter.prototype, 
                            Ajax.Autocompleter.prototype), {

  initialize: function(element, update, url, options) {
    Ajax.Autocompleter.prototype.initialize.call(
      this, element, update, url, options);
    this.options.movieId = this.options.movieId || null;
    this.options.autoToken = this.options.autoToken || '';
    this.options.onEnter = this.options.onEnter || null;
    this.options.logUrl = options.logUrl || null;
    this.options.logParams = options.logParams || '';

    // Register a mouse click handler since it can trigger changes in the
    // dropdown menu's contents (the caret might be moved to a different
    // token in the string)
    Event.observe(this.element, "click", this.onMouseClick.bindAsEventListener(this));
    
    // This needs to maintain the last known caret position in the text 
    // field since some browsers (e.g. MSHTML-based ones) apparently 
    // will not remember where the caret was.  This is not always up
    // to date (in fact, it is usually one event behind), but is close 
    // enough when it matters.
    this.lastCaretPosition = 0;
    
    // If applicable, add a movieId parameter to the URL so that the 
    // auto-complete listener knows what movie is being tagged.
    if(this.options.movieId)
    {
      var movieIdParam = 'movieId=' + this.options.movieId;
      if(this.options.defaultParams)
        this.options.defaultParams += '&' + movieIdParam;
      else
        this.options.defaultParams = movieIdParam;
      if(this.options.logParams)
        this.options.logParams += '&' + movieIdParam;
      else
        this.options.logParams = movieIdParam;
    }
  },

  // This is an improved version of updateElement that does completion of the
  // correct token rather than just tacking on the selected item to the
  // end of the text box.  Also, it can optionally add a delimiter afer the
  // completed token and place the caret in the appropriate place so that
  // the user can immediately start typing a new thing after hitting enter
  // or clicking an choice from the dropdown.
  updateElement: function(selectedElement)
  {
    // find the text of the selected item and optionally tack on a delimiter
    var selection = 
          Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
          
    var selectedValue = selection;
    if(this.options.autoToken)
      selectedValue += this.options.autoToken;
    
    // find the text that is to be replaced and some position info about it
    var caretPos  = this.getCaretPosition();
    var startPos  = this.getPreviousDelimiterPosition(caretPos);
    var endPos    = this.getNextDelimiterPosition(caretPos);
    var origValue = this.getToken();

    // compute the parts of the string not affected by the auto-completion
    var prefix = "";
    var postfix = this.element.value.substr(endPos);
    if (startPos != -1)
      prefix = this.element.value.substr(0, startPos + 1) + " ";

    // build the new auto-completed string
    this.element.value = prefix + selectedValue + postfix;

    // set the caret to the right place (immediately after the auto-completed
    // token or immediately after the automatically-added delimiter, whichever
    // applies based on the options)
    var newCaretPos = this.getNextDelimiterPosition(startPos + 1);
    if(this.options.autoToken) newCaretPos += this.options.autoToken.length;
    this.setCaretPosition(newCaretPos);

    // restore focus to the text box
    this.element.focus();
    
    // log the autocompletion
    if (this.options.logUrl != null) {
      var params = this.options.logParams || "";
      params = "tag=" + encodeURIComponent(selection) + "&" + params;
      var options = 
      { 
        method:     'get', 
        parameters: params, 
        onComplete: function(request) {}
      };
      var req = new Ajax.Request(this.options.logUrl, options);
    }
  },
  
  selectEntry: function() {
    // call super's selectEntry 
    Ajax.Autocompleter.prototype.selectEntry.call(this);

    // trigger the keypress observer event if minChars says it should be
    if(this.options.minChars < 1)
    {
      // there's a long delay to allow someone to double-tap the enter key
      // to both select the first suggestion and trigger the onEnter event 
      // (typically some sort of submit/save action) instead of accidentally 
      // choosing the first suggestion twice
      this.hasFocus = true;
      if(this.observer) clearTimeout(this.observer);
      this.observer = 
        setTimeout(this.onObserverEvent.bind(this), 1000);
    }
  },
  
  // since a click in the textbox can move the caret, handle it similarly 
  // to a keypress
  onMouseClick: function(event)
  {
    this.changed = true;
    this.hasFocus = true;
    
    this.updateLastCaretPosition();
    
    if(this.observer) clearTimeout(this.observer);
    this.observer = 
      setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
  },

  onKeyPress: function(event) 
  {
    // Capture enter events for onEnter handler when the dropdown is inactive 
    // or the dropdown has zero items
    if(this.options.onEnter 
       && event.keyCode == Event.KEY_RETURN
       && (!this.active || this.entryCount == 0))
    {
      if(this.observer) clearTimeout(this.observer);
      this.options.onEnter(this);
      // prevent enter key from causing form to be submitted
      Event.stop(event);
    }
    else
    {
      // Special-case the left and right arrow keys -- the parent handler
      // doesn't allow the auto-complete dropdown to change when left or right
      // is pressed since it never would change without the "smarter" tokenized
      // auto-complete.
      if(event.keyCode == Event.KEY_LEFT || event.keyCode == Event.KEY_RIGHT)
      {
        this.changed = true;
        this.hasFocus = true;

        if(this.observer) clearTimeout(this.observer);
        this.observer = 
          setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
      }
      else
      {
        // Everything else goes to the autocompleter keypress handler
        Ajax.Autocompleter.prototype.onKeyPress.call(this, event);
        // ...but unconditionally stop event propagation if it was the up or 
        // down key, since Firefox on some platforms normally responds to 
        // these keystrokes by moving the caret around (which is bad, but
        // not fatally bad in the absence of tokenized auto-complete).
        if(event.keyCode == Event.KEY_UP || event.keyCode == Event.KEY_DOWN)
          Event.stop(event);
      }
      
      this.updateLastCaretPosition();
    }
  },

  // returns the token that the caret is currently on
  getToken: function() 
  {
    var caretPos = this.getCaretPosition();
    var startPos = this.getPreviousDelimiterPosition(caretPos);
    var endPos   = this.getNextDelimiterPosition(caretPos);
    var token = this.element.value.substr(startPos + 1, endPos - startPos - 1);
    return token.replace(/^\s+/,'').replace(/\s+$/,'');
  },

  // returns the zero-indexed position of the closest delimiter to the left of
  // the caret position
  getPreviousDelimiterPosition: function(position)
  {
    // boundary condition -- caret is at the beginning of the string
    if(position <= 0)
      return -1;
    
    // this is equivalent to Autocompleter.base's findLastToken applied to
    // the part of the string up to position

    var str = this.element.value.substr(0, position);
    var lastTokenPos = -1;
    for (var i=0; i<this.options.tokens.length; i++) {
      var thisTokenPos = str.lastIndexOf(this.options.tokens[i]);
      if (thisTokenPos > lastTokenPos)
        lastTokenPos = thisTokenPos;
    }
    return lastTokenPos;
  },

  // returns the zero-indexed position of the closest delimiter to the right of
  // the caret position
  getNextDelimiterPosition: function(position)
  {
    // boundary condition -- caret is at the end of the string
    if(position >= this.element.value.length) 
      return position;

    // find the first delimiter in the part of the string after position
    // (this is pretty much the opposite of findLastToken)

    var str = this.element.value.substr(position);
    var nextTokenPos = str.length;
    for (var i=0; i<this.options.tokens.length; i++) {
      var thisTokenPos = str.indexOf(this.options.tokens[i]);
      if (thisTokenPos > -1 && thisTokenPos < nextTokenPos)
        nextTokenPos = thisTokenPos;
    }
    return position + nextTokenPos;
  },

  /*
   * The caret methods are adopted from functions appearing in zichun's
   * auto-complete control, which is published under the Creative Commons
   * license.  The original source can be found at:
   *
   *    http://www.codeproject.com/jscript/jsactb.asp
   */

  // Returns the caret position; in the case that a range of characters
  // is selected, this returns the position of the *start* of the range.
  getCaretPosition: function()
  {
    // TODO: test under KHTML/WebCore
    
    if(typeof this.element.selectionStart != "undefined")
      // Opera and Gecko-based browsers support this
      return this.element.selectionStart;
    else if(document.selection && document.selection.createRange)
    {
      // This is apparently a hackaround for MSHTML-based browsers

      // if the text field has lost focus, this can only return the last 
      // known position since MSHTML-based browsers don't remember position
      if(!this.hasFocus)
        return this.lastCaretPosition;

      var docRange = document.selection.createRange();
      var range = this.element.createTextRange();
      range.setEndPoint("EndToStart", docRange);
      var position = range.text.length;
      if(position > this.element.value.length)
        return -1;
      return position;
    }
    // no support for finding caret position?
    return this.element.value.length;
  },

  setCaretPosition: function(position)
  {
    // TODO: test under KHTML/WebCore
    
    // This is not compatible with Opera and is a known issue to their devs:
    //   http://my.opera.com/community/forums/topic.dml?id=105795&page=1

    this.element.focus();
    if(this.element.setSelectionRange)
      // Gecko-based browsers
      this.element.setSelectionRange(position, position);
    else if(this.element.createTextRange)
    {
      // MSHTML-based browsers
      newRange = this.element.createTextRange();		
      newRange.moveStart('character', position);
      newRange.collapse();
      newRange.select();
    }
    this.lastCaretPosition = position;
  },
  
  updateLastCaretPosition: function()
  {
    this.lastCaretPosition = this.getCaretPosition();
  }
});
