var NameSelector = Class.create();
NameSelector.prototype = {
  
  autoCompleter: null,
  initialize: function(options) {
    this.setOptions(options);
    this.inputElement = this.findField(this.options.inputField);
    this.targetElement = this.findField(this.options.targetField);
    this.namesListElement = this.options.fixedNamesList ? $(this.options.fixedNamesList.elementId) : null;
    // If an init Field element is specified, we use it for the initialization of the name blocks
    // If not, then we use the targetElement for initialization
    this.initElement = this.findField(this.options.initField);

    this._prepareTokenMap();
    if ( this.inputElement ) { 
      var choicesLength = $H(this.options.tokenMap).keys().length;
      // We only show the popup if there is more than one item to show, and that item isn't already on the list
      if ( this.namesListElement && (this.options.fixedNamesList.autoPopup) && 
        !(this.fixedIds().length == choicesLength ) ) {
        this.displayNameList();
      } 
      // Only invoke the autocompleter if required
      if ( $(this.options.choiceListId) ){
        this.autoCompleter = new Autocompleter.Local(this.inputElement, this.options.choiceListId, $H(this.options.tokenMap).keys(), 
          {
            tokens: ",", frequency: 0.05, 
            choices: 20,
            afterUpdateElement: function(element){ this.addFrom(this.inputElement)}.bind(this),
            onShow: function(event) { this.displayAutoCompleteList(event) }.bind(this),
            onBlur: function(event){
                if ( !this.autoCompleter.active ) {
                this.addFrom(this.inputElement); 
                this.resetInputField();
                this._clearMessage();
                return true; } 
              }.bind(this),
            
            // Return true if we did something final
            activeKeyEventHandler: function(event,autcompleter) {
              switch (String.fromCharCode(event.which)) {
                case ',':
                case ';':
                  if ( this.options.allowNewValues) {
                    this.addFrom(this.inputElement); 
                  }
                  return true;    
              }
            }.bind(this),
            inactiveKeyEventHandler: function(event) { 
              this._clearMessage();
              switch(event.keyCode) {
                case Event.KEY_BACKSPACE:
                  this._clearError();
                  return this.manageDelete(this.inputElement); 
                case Event.KEY_RETURN:
                  this.addFrom(this.inputElement);
                  Event.stop(event);
                  return false;
                case Event.KEY_TAB: 
                  if (this.inputElement.value.match(/\S/)) {
                    this.addFrom(this.inputElement);
                    Event.stop(event);
                  }
              }
              switch (String.fromCharCode(event.which)) {
                case ' ':
                  if ( ! this.inputElement.value.match(/\S/) ) {
                    this.resetInputField();
                    this.displayAllChoices();
                    return true;
                  }
                  return false;
                case ',':
                case ';':
                  this.addFrom(this.inputElement); 
                  Event.stop(event);
                  return true;
              }
            }.bind(this)
          }
        );
      }
      this.checkDisableEntry();
    };
    this.initBlocks()
  },
  
  setOptions: function(options)  {
    this.options = {
      formId: 'mainForm',
      initField: null,                    // The field name that includes the initial settings - these are not deletable
      targetField: '',                    // Where to put the IDs of the selected items
      inputField: '',                   // The name of the input text field that we are sensitive to
      allowNewValues: true,                   // If set to true, then we do allow new values to be typed in
      choiceListId: null,                     // This is where the autocomplete choice list is displayed
      maxSelections: null,                    // Set to an integer specifying the maximum number of items that can be selected
      fixedNamesList: null,                   // Hash containing instructions for handling the fixed names list
      messageId: null,                        // The name of the div that will hold the message if an error occurs
      allowGroupExpansion: true,              // If set, we allow the expansion of groups
      tokenMap: {},                           // Not really an option (this is where we keep our tokens)
      tokenSeparator: ',',                    // This is used to separate the tokens in the init and target fields
      nameBlock: true,                         // if set to false, we don't create name blocks, but rather just fill in the value 
      alternativesToNew: null                // If set, this is called to get alternatives for new items (to catch errors, for e.g)
    }
    Object.extend(this.options, options || {});
  },
  
  // The field can be an element, a string, or a field name.  If it's a field name, 
  // then we try to find it with reference to the indicated formId
  findField: function(field) {
    if ( $(field) ) 
      return $(field)
    else if ( this.options.formId && typeof formHandler != 'undefined' ) {
      return formHandler.findElementByName(this.options.formId, field)
    }
  },
  
  // If the token map is an array then I map it to the requested hash 
  _prepareTokenMap: function() {
    if (this.isArray(this.options.tokenMap)) {
      if ( this.isString(this.options.tokenMap[0]) ) {
        var newTokenMap = {};
        this.options.tokenMap.each( function(t) { newTokenMap[t] = { id: t, name: t}})
      } else {
        // Assume it's already in { id: something, name:something } format
        var newTokenMap = {};
        this.options.tokenMap.each( function(t) { newTokenMap[t] = t})
      }
      this.options.tokenMap = newTokenMap;
    }
  },
  
  // The list of IDs that cannot be changed 
  fixedIds: function() {
    return this.initElement ? this._split(this.initElement.value) : [];
  },
  
  displayAllChoices: function() {
    var ret       = []; // returned values
    var choices = $H(this.options.tokenMap).keys().sort();
    for (var i = 0; i < choices.length; i++) { 
      ret.push("<li>" + choices[i] + "</li>");
    }
    this.autoCompleter.activate();
    this.autoCompleter.updateChoices("<ul>" + ret.join('') + "</ul>")
  },
  
  // Display the available options a-priori, without autocompletion
  // Places this inside the namesListElement
  // options should be set up as a hash fixedNamesList 
  // elementId: ID where the list should be displayed
  // columns: number of columns to use in displaying the names (1 or 2, probably not more)
  // namesOnly: if you want to omit the emails - just show the names
  displayNameList: function(type) {
    var target = this.namesListElement;
    if (  target) {
      if ( !this.namesListCreated || type ) {
        var choices = $H(this.options.tokenMap).keys().sort();
        if ( $('nameSelectorDisplayNameList') ) Element.remove($('nameSelectorDisplayNameList'));
        
        var tbl = document.createElement('table');
        tbl.className = 'j_fixedNameList';
        tbl.id = 'nameSelectorDisplayNameList';
        tbl.setAttribute("cellspacing", "2");
        tbl.setAttribute("cellpadding", "1");
        tbl.setAttribute("width", "100%");
        var columns = this.options.fixedNamesList.columns || 2;
        tbl.appendChild(this._createHeadingRow("Select name(s) below:",columns));
        var width = parseInt(100/columns) + "%";
        for ( var i = 0; i < choices.length; ) {
          var row = document.createElement("tr");
          for ( var col = 0 ; col < columns && i < choices.length ; i++ )  {
            if ( !type || this.options.tokenMap[choices[i]].type == type ) {
              var nametd = document.createElement("td");
              nametd.className = "j_name";
              nametd.width = width;
              nametd.id = 'nl_'+ $H(this.options.tokenMap)[choices[i]].id;
              nametd.innerHTML = this.options.fixedNamesList.namesOnly ? this.options.tokenMap[choices[i]].name : choices[i]  
              nametd.onclick = function(event) { this.toggle(Event.element(event).innerHTML,this.options.fixedNamesList.namesOnly);Event.stop(event) }.bind(this);
              row.appendChild(nametd);
              col++;
            }
          }
          tbl.appendChild(row);
        }
        target.appendChild(tbl);
        this.namesListCreated = true;
      }
      Element.show(target);
    }
  },
  
  namesListCreated: false,
  
  toggleDisplayNameList: function() {
    if ( this.namesListElement && Element.visible(this.namesListElement) ) {
      Element.hide(this.namesListElement);
    } else {
      this.displayNameList();
    }
  },
  
  setNamesListSelected: function(id,on) {
    if ( $('nl_'+id) ) {
      if ( on  ) $('nl_'+id).addClassName('selected')
      else $('nl_'+id).removeClassName('selected')
    }
  },

  // I have to set the opacity manually because it's probably left to the last value  
  displayAutoCompleteList: function() {
    Element.setOpacity(this.options.choiceListId,1);
    Element.show(this.options.choiceListId);
  },
  
  tokenMap: function() {
    return this.options.tokenMap
  },
      
  // Assuming that the names are in json format in some ID, parse them
  // Assumed format: array of hash, each indexed by the full display name and providing the full_name and the ID
  // If this is slow, in the future, we can change the indexing
  parse: function(id) {
    this.options.tokenMap = eval($(id).innerHTML);
    // console.log(this.options.tokenMap.inspect)
  },
  
  displayName: function(id,name,noDelete) {
    if ( !name ) {
      if ( this.options.allowNewValues )
        name = id;
      else
        return
    }

    if ( this.options.nameBlock )
      this.createNameBlock(id,name,noDelete);
    else {
      // Add the name to the list
      if ( noDelete ) 
        this.createSimpleNameBlock(id,name)
      else
        this.addToInput(name);
    }
  },
  
  addToInput: function(name) {
    var v = this.inputElement.value;
    // console.log("v= "+v);
    var n;
    if ( n = v.lastIndexOf(this.options.tokenSeparator) ) 
      v = v.substring(0,n+1) + name;
    else 
      v = name;
    // console.log("after save= "+v);
    this.inputElement.value = v;
  },
  
  createSimpleNameBlock: function (id,name) {
    var e = document.createElement('div');
    e.className = 'j_name';
    e.innerHTML = name;
    this.inputElement.parentNode.insertBefore(this,this.inputElement);    
  },
  
  // this method creates the little block in the UI assocaited with the names or groups
  createNameBlock: function (id,name,noDelete) {
    if ( this._isBlank(id) ) return;
    var key = this.keyForId(id);
    
    var tbl = document.createElement('table');    
    tbl.className = 'j_nameBlock';
    tbl.id = this._uniqueBlockId(id);
    tbl.setAttribute("cellspacing", "0");
    tbl.setAttribute("cellpadding", "0");
    var tblBody = document.createElement("tbody");
    var row = document.createElement("tr");
    var nametd = document.createElement("td");
    nametd.className = "j_name";
    nametd.innerHTML = name;
    nametd.onclick = function(event) { this.editName(event,id); Event.stop(event) }.bind(this);
    
    row.appendChild(nametd);
        
    if ( !key )  { 
      tbl.className += " j_new";
      // if the options are set, then query and see if there are variations
      if ( this.options.alternativesToNew ) {
        new Ajax.Request(this.options.alternativesToNew, 
          { 
            parameters: {name: id}, 
            evalJSON: true,
            asynchronous: false,
            onSuccess: function(transport) {
              var alternatives = eval(transport.responseText);
              if ( alternatives ) {
                this.setAlternatives(id,alternatives);
                var listtd = document.createElement("td");
                listtd.className = "j_icon"
                var limage = document.createElement("img");
                limage.setAttribute("src","/images/list_out.png" );
                limage.setAttribute("height","13" );
                limage.setAttribute("width","13" );
                listtd.appendChild(limage);
                listtd.onclick = function(event) { this.showAlternatives(event,id); Event.stop(event) }.bind(this);
                row.appendChild(listtd);
              }
            }.bind(this)
          }
        ) // Ajax.Request
      }
      
    }
    // Create a button that allows us to expand the item
    if ( this._isKeyGroup(key) ) {
      var listtd = document.createElement("td");
      listtd.className = "j_icon"
      var limage = document.createElement("img");
      limage.setAttribute("src","/images/list_out.png" );
      limage.setAttribute("height","13" );
      limage.setAttribute("width","13" );
      listtd.appendChild(limage);
      listtd.onclick = function(event) { this.showGroupMembers(event,id); Event.stop(event) }.bind(this);
      row.appendChild(listtd);
    }
    // If the id is numeric, then we assume it's a group and not an email
    // and we allow it to be expanded

    if ( this.options.allowGroupExpansion && this._isKeyGroup(key) ) {
      var expandtd = document.createElement("td");
      expandtd.className = "j_icon"
      var eimage = document.createElement("img");
      eimage.setAttribute("src","/images/expand_out.png" );
      eimage.setAttribute("height","13" );
      eimage.setAttribute("width","13" );
      expandtd.appendChild(eimage);
      expandtd.onclick = function(event) { this.expandGroupMembers(id);Event.stop(event) }.bind(this);
      row.appendChild(expandtd);
    }
    if ( ! noDelete && ! this._isKeyGroup(key) ) {
      var deletetd = document.createElement("td");
      deletetd.className = "j_icon"
      var dimage = document.createElement("img");
      dimage.setAttribute("src","/images/x.gif" );
      deletetd.appendChild(dimage);
      deletetd.onclick = function(event) { this.remove(id);Event.stop(event) }.bind(this);
      row.appendChild(deletetd);
    }
    tblBody.appendChild(row);
    tbl.appendChild(tblBody);
    this.inputElement.parentNode.insertBefore(tbl,this.inputElement);
    if ( this.namesListCreated )
      this.setNamesListSelected(id,true);
  },
  
  _removeNameBlock: function(id) {
    Element.remove(this._uniqueBlockId(id)); 
    if ( this.namesListCreated )
      this.setNamesListSelected(id,false);
  },
  
  // the block is clicked, so remove it and put the contents back into the editing area
  editName: function(event,id) {
    this.remove(id);
    this.inputElement.value = this.nameForId(id) || id ;
    this.inputElement.show();
    this.inputElement.focus();
  },
  
  // Find the token entry for the indicated ID
  entryForId: function(id) {
    var result = null;
    $H(this.options.tokenMap).each( function( pair) {
      if ( id == pair.value.id ) {result = pair; throw $break;}
    } )
    return result;
  },

  // Find the token entry for the indicated name
  entryForName: function(name) {
    var result = null;
    $H(this.options.tokenMap).each( function( pair) {
      if ( name == pair.value.name ) {result = pair; throw $break;}
    } )
    return result;    
  },
  
  keyForId: function(id) {
    var result = this.entryForId(id);
    if ( result ) result = result[0];
    return result;
  },
  
  keyForName: function(name) {
    var result = this.entryForName(name);
    if ( result ) result = result[0];
    return result;    
  },
  
  // given a name, return the appropriate ID
  idForName: function(name) {
    var result = this.entryForName(id)
    if ( result ) result = result[1].id
    return result;    
  },
  
  // Find the name for the indicated ID
  nameForId: function(id) {
    var result = this.entryForId(id)
    if ( result ) result = result[1].name
    return result;
  },
    
  focusOnInput: function() {
    if ( this.inputElement) this.inputElement.focus();
  },
  
  // Initialize the blocks based upon the values in the initElement and targetElement fields
  // We use the init field value and the target value because we have a scenario where the init
  // field values are non-deletable, but there also some deletable values in the targetelement
  // for example, if there was a problem with the form
  initBlocks: function() {
    var toList;
    if ( this.initElement ) {
      var ids = this._split(this.initElement.value);
      for ( var i = 0; i < ids.length ; i++ ) {
        // console.log(this.options.tokenMap[ids[i]].name);
        if ( ids[i].match(/\S/) ) this.displayName(ids[i],this.nameForId(ids[i]), true)
      }
    }
    if ( this.targetElement ) {
      var ids = this._split(this.targetElement.value);
      for ( var i = 0; i < ids.length ; i++ ) {
        // console.log('Initializing target value '+ids[i] + ': '+this.nameForId(ids[i]));
        this.displayName(ids[i],this.nameForId(ids[i]),false)
      }
    }
  },
  
  _isBlank: function(val) {
    return (!val || !val.match(/\S/) );
  },
  
  _split: function(eval) {
    var values = [];
    if ( eval.match(/\S/)  )  {
      values = this.options.tokenSeparator ? eval.split(this.options.tokenSeparator) : [eval];
    }
    return values
  },
  
  // Return the full list of entries in both the init element and the targetElement
  _fullListArray: function() {
    value = this._split(this.targetElement.value) ;
    if ( this.initElement && this.initElement != this.targetElement )
      value = this._split(this.initElement.value).concat(value);
    return value;
  },

  // Create the heading row for our table-based style of input
  _createHeadingRow: function(heading,columns) {
    var row = document.createElement("tr");
    var td = document.createElement("td");
    td.setAttribute('colspan',columns);
    var span = document.createElement("span");
    span.className = 'j_heading';
    span.innerHTML = heading;
    td.appendChild(span);
    row.appendChild(td);
    return row;
  },
  
  alternativesLists: {},
  
  setAlternatives: function(id,alternatives) {
    var key = this.keyForId(id);
    if (  alternatives && alternatives.length > 0 ) {
      var tbl = document.createElement('table');
      tbl.className = 'j_alternativesList';
      tbl.setAttribute("cellspacing", "1");
      tbl.setAttribute("cellpadding", "0");
      tbl.setAttribute("width", "100%");
      var columns = 1;
      var width = parseInt(100/columns) + "%";
      // Create the heading
      // tbl.appendChild(this._createHeadingRow("Alternatives",columns));
      for ( var i = 0; i < alternatives.length; ) {
        var row = document.createElement("tr");
        for ( var col = 0 ; col < columns && i < alternatives.length ; col++, i++ )  {
          var nametd = document.createElement("td");
          nametd.className = "j_name";
          nametd.width = width;
          nametd.innerHTML = this.keyForId(alternatives[i]) || alternatives[i];
          row.appendChild(nametd);
          var name = alternatives[i];
          // console.log("adding "+alternatives[i]);
          // nametd.observe('click',function(event) { console.log(event.srcElement.innerText);this.replace(id,event.srcElement.innerText); }.bind(this),false);
          // WARNING - won't work w/ that POS IE
          nametd.onclick = function(e) { this.replace(id,e.currentTarget.innerHTML); }.bind(this);
        }
        tbl.appendChild(row);
      }
      this.alternativesLists[id] = tbl;
      document.body.appendChild(tbl);
    }
    return tbl;    
  },
  
  showAlternatives: function(e,id) {
    var list = this.alternativesLists[id]
    windowHandler.showHere(list,e,{ offset: { xpos: 5,ypos: 5}, placement: {xpos: 'left', ypos: 'top'}, removeOnClick: true })    
  },
  
  
  groupMembersLists: {},
  // Expand the group by showing the members
  // Input value is the gorup id
  groupMembersList: function(id) {
    var key = this.keyForId(id);
    var groupMembers = this.options.tokenMap[key].members;

    if (  !this.groupMembersLists[id] ) {
      var tbl = document.createElement('table');
      tbl.className = 'j_groupMembersList';
      tbl.setAttribute("cellspacing", "1");
      tbl.setAttribute("cellpadding", "0");
      tbl.setAttribute("width", "100%");
      var columns = 1;
      var width = parseInt(100/columns) + "%";
      // Create the heading
      tbl.appendChild(this._createHeadingRow("Other members of " + this.nameForId(id) + ": ",columns));
      for ( var i = 0; i < groupMembers.length; ) {
        var row = document.createElement("tr");
        for ( var col = 0 ; col < columns && i < groupMembers.length ; col++, i++ )  {
          var nametd = document.createElement("td");
          nametd.className = "j_name";
          nametd.width = width;
          nametd.innerHTML = this.keyForId(groupMembers[i]) || groupMembers[i];
          row.appendChild(nametd);
        }
        tbl.appendChild(row);
      }
      this.groupMembersLists[id] = tbl;
      document.body.appendChild(tbl);
    }
    return this.groupMembersLists[id];
  },
  
  showGroupMembers: function(e,id) {
    var list = this.groupMembersList(id);
    windowHandler.showHere(list,e,{ offset: { xpos: 5,ypos: 5}, placement: {xpos: 'left', ypos: 'top'}, removeOnClick: true })    
  },
  
  // Expand the group into its members, and remove the group if it's already in the to list
  expandGroupMembers: function(id) {
    var key = this.keyForId(id);
    var groupMembers = this.options.tokenMap[key].members;
    // Delete the existing group
    this.remove(id);
    for ( var i = 0; i < groupMembers.length; i++ ) {
      this.add(this.keyForId(groupMembers[i]))
    }
  },
  
  // Ajust the available autocomplete options given the names that are in the to list already
  getUpdatedChoices: function() {
    var ids = this._fullListArray();
    var tmpMap = $H(Object.clone(this.options.tokenMap));
    for ( var i = 0; i < ids.length ; i++ ) {
      tmpMap.remove(this.keyForId(ids[i]));
    }
    return $H(tmpMap).keys();
  },
  
  // This adds the name to the internal list - it does not do anything to the display list
  _addToList: function(id) {
    if (this.targetElement.value.match(/\S/) )
      this.targetElement.value += ','+id;
    else
      this.targetElement.value = id;
  },

  // Return true if the list includes the indicated id for the item
  listIncludes: function(id) {
    return (this.targetElement && new RegExp("(^|,)"+id+"($|,)","i").exec(this.targetElement.value) ) ||
      (this.initElement && new RegExp("(^|,)"+id+"($|,)","i").exec(this.initElement.value) || this.targetGroupIncludes(id)) 
  },

  // Determine if the target group already contains the associated with the given ID
  targetGroupIncludes: function(id) {
    var groups = this._findGroups();
    for ( i = 0 ; i < groups.length; i++ ) {
      var key = this.keyForId(groups[i]);
      var groupMembers = this.options.tokenMap[key].members;
      for ( var i = 0; i < groupMembers.length; i++ ) {
        if ( groupMembers[i] == id ) return true;
      }
    }
    return false;
  },
  
  // Find the groups that are in the target or init elements
  _findGroups: function() {
    var groups = []
    var ids = this._fullListArray();
    for ( i = 0 ; i < ids.length ; i++ ) {
      if ( this._isGroup(ids[i]) )
        groups.push(ids[i])
    }
    return groups
  },
  
  // We determine that this is a group
  _isGroup: function(id) {
    if (this._isBlank(id)) return false;
    return this._isKeyGroup(this.keyForId(id))
  },
  
  // We determine that this is a group
  _isKeyGroup: function(key) {
    if (this._isBlank(key)) return false;
    return this.options.tokenMap[key] && this.options.tokenMap[key].members
  },
  
  // this should only be used internally - 
  _removeFromList: function(id) {
    var vals = this._split(this.targetElement.value);
    var removed = false;
    for ( var i = 0 ; i < vals.length; i++ ) {
      if ( vals[i] == id ) {
        vals.splice(i,1) ;
        removed = true;
        break;
      }
    }
    this.targetElement.value = vals.join(',');
    return removed;
    
    // console.log(this.targetElement.value);
  },
  
  _uniqueBlockId: function(id) {
    return this.targetElement.id + '-' + id;
  },
  
  // if the value is already present in the to field then do nothing and clear the input field
  // else if the value is a named token, then fill it.
  // else if it doesn't have a named token but is a valid email then add it.
  // else if it is not a valid email then leave it in the input field colored red.
  add: function(value) {
    if (this._isBlank(value) ) return;
    
    var map;
    var errorMessage;
    map = this.options.tokenMap[value]
    this.inputElement.removeClassName("j_error");
    // if the value is a token and already present in the to field then dont add and clear the input field
    var idIncluded = map && this.listIncludes(map.id);
    if ( idIncluded ) {
      this._noteMessage("'" + map.name + "' has already been included")
      this.resetInputField();
      return false;
    // if the value is a token and not present in the to field then add to the to field
    } else if ( this.options.tokenMap && map && !idIncluded ) {
      // console.log(map.name);
      this.displayName(map.id,map.name);
      this._addToList(map.id);
      this.checkDisableEntry();
      return true;
    } else if ( !this.options.allowNewValues ) {
      this._noteMessage("Sorry - please select a valid value");
      $(this.inputElement).addClassName("j_error");
      return false;
    } else if ( (typeof this.validateEntries == 'function') && (errorMessage = this.validateEntries(value)) ) {
      this._noteMessage(errorMessage);
      $(this.inputElement).addClassName("j_error");
      return false
    // else if it doesn't have a named token but is a valid id then add it.
    } else {
      this.displayName(value, value);
      this._addToList(value);
      this.resetInputField();
      this.checkDisableEntry();
      return true;
    }
    // console.log("Not a valid email.");
  },
  
  // toggle the status of the indicated value
  // Value is either a key, or a name
  toggle: function(value,valueIsName) {
    if ( valueIsName ) 
      value = this.keyForName(value)
    map = this.options.tokenMap[value]
    if ( this.listIncludes(map.id) ) 
      this.remove(map.id)
    else 
      this.add(value)
  },
  
  _clear: function(id) {
    var el = $(id);
    el.value = '';
    // console.log("Resetting the input field "+el.id);
    // The following causes Safari to misbehave
    // el.focus();
  },
  
  _clearError: function() {
    this.inputElement.removeClassName('j_error');
  },
  
  messageElement: null,
  
  // Return the element that should be used for messages
  _getMessageElement: function() {
    var m;
    if ( this.messageElement )
      m= this.messageElement
    else if ( this.options.messageId )
      m = $(this.options.messageId);
    else {
      var m = document.createElement('div')
      m.className = 'j_nameMessage';
      this.inputElement.parentNode.insertBefore(m,this.inputElement);
    }
    this.messageElement = m;
    return m;
  },
  
  // Present the indicated message
  _noteMessage: function(msg) {
    var msgElementId = this._getMessageElement();
    if ( msgElementId ) {
      msgElementId.innerHTML = msg;
      Element.show(msgElementId);
    }
  },
  
  _clearMessage: function() {
    var msgElementId = this._getMessageElement();
    if ( msgElementId ) {
      msgElementId.innerHTML = '';
      Element.hide(msgElementId);
    }
  },
  
  resetInputField: function() {
    this._clear(this.inputElement);
  },
  
  // Add from an input element - currently we return the list if it's not recognized
  addFrom: function(id) {
    // console.log("Calling addFrom for "+id.name);
    var el = $(id);
    var newEls = [];
    if ( el.value ) {
      var els = this._split(el.value);
      for ( var i = 0 ; i < els.length; i++ ) {
        if ( ! this.add(els[i]) ) 
          newEls.push(els[i]);
      }
    }
    el.value = newEls.join(', ');
  },
  
  // remove from both the display and the to List
  remove: function(id) {
    // console.log("Called remove with id "+id)
    this.inputElement.focus();
    this._clearMessage();
    if ( id ) {
      if ( this._removeFromList(id) ) {
        this._removeNameBlock(id);
        this.checkDisableEntry();
      }
    }
    this.inputElement.focus();
  },

  replace: function(id,new_id) {
    this.remove(id);
    this.add(new_id);
  },
  
  // Clear the existing values
  clearExistingValues: function() {
    if ( this.targetElement && this.targetElement.value) {
      var ids = this._split(this.targetElement.value);
      for ( var i = 0 ; i < ids.length; i++ ) {
        this.remove(ids[i])
      }
    }
  },
  
  _removeLast: function(){
    this.remove(this._split(this.targetElement.value).last());
  },
  
  // If a delete key has been pressed
  // Returns true if a previous item was deleted
  // Returns false if there was some text in the element value and so this was never exectued
  manageDelete: function(element) {
    var val = element.value;
    if ( val.match(/^\s*$/) ) {
      // console.log("Got a delete key in field"+element.name+" value "+val);
      this._removeLast();
      return true;
    } else 
      return false    
  },
  
  // Check if there is a maxSelections limit, and if so, disable the entry
  checkDisableEntry: function() {
    var vals = this._fullListArray();
    if (this.options.maxSelections && vals.length >= this.options.maxSelections ) {
      // If I hide the input field w/o first blurring it, it causes the cursor to disappear on firefox
      this.inputElement.blur();
      this.inputElement.hide();
    } else
      this.inputElement.show();
    return status;
  },
  
  // ==========
  // = Generic Utility =
  // ==========
  isArray: function(obj) {
    return obj instanceof Array ;
    // Following doesn't work in Safari
    // if (obj.constructor.toString().indexOf('Array') == -1)
    //   return false;
    // else
    //   return true;
  },
  
  isString: function(obj) {
    return typeof obj == "string" ;    
  }
  
  
}
