
function DualListBox(options) {
  this.totalInItems = 0;

  this.listBoxVariableName = options.listBoxVariableName;
  this.itemType = options.itemType;
  this.moveItemCallback = options.moveItemCallback;
  this.createFindUrlCallback = options.createFindUrlCallback;
  this.createMoreUrlCallback = options.createMoreUrlCallback;
  this.maxNumberIncludedItems = options.maxNumberIncludedItems;
  this.noSelectionsMessage = options.noSelectionsMessage;
  
  this.excludedListElement = this.listBoxVariableName + "_excludedList";
  this.includedListElement = this.listBoxVariableName + "_includedList";
  if(this.includedListElement.length > 0) {
    this.totalInItems = this.includedListElement.length;
  }
  this.excludedFindElement = this.listBoxVariableName + "_excludedFindBox";
  this.includedFindElement = this.listBoxVariableName + "_includedFindBox";
  this.maxedOutMessageAreaElement = this.listBoxVariableName + "_maxedOutMessageArea";
  this.overMaxMessageAreaElement = this.listBoxVariableName + "_overMaxMessageArea";
  this.noSelectionsMessageAreaElement = this.listBoxVariableName + "_noSelectionsMessageArea";

  this.excludedListItems = new Array();
  this.includedListItems = new Array();

  this.hasMoreExcludedItems = false;
  this.currentExcludedOffset = 0;
  this.hasMoreIncludedItems = false;
  this.currentIncludedOffset = 0;

  if (this.createFindUrlCallback) {
    var dualListBox = this;
    new DelayedKeyupObserver(this.excludedFindElement, 500, function(value) {
      dualListBox.populateAndRenderListFromAjax(false, dualListBox.createFindUrlCallback(value, false)); });
    disableSubmitOnEnter(this.excludedFindElement);
    new DelayedKeyupObserver(this.includedFindElement, 500, function(value) {
      dualListBox.populateAndRenderListFromAjax(true, dualListBox.createFindUrlCallback(value, true)); });
    disableSubmitOnEnter(this.includedFindElement);
  }
}

DualListBox.prototype.populateAndRenderList = function(isIncludedList, jsonText) {
  var jsonObject = gatherAjax.JSON2Object(jsonText);
  var itemList = ListBoxItem.convertList(jsonObject.list, this.getItemConstructor());
  if (isIncludedList) {
    this.includedListItems = itemList;
    this.hasMoreIncludedItems = jsonObject.more;
    this.currentIncludedOffset = jsonObject.offset;
    this.totalInItems = itemList.length;
  } else {
    this.excludedListItems = itemList;
    this.hasMoreExcludedItems = jsonObject.more;
    this.currentExcludedOffset = jsonObject.offset;
  }
  this.renderList(isIncludedList);
}

DualListBox.prototype.populateAndRenderListFromAjax = function(isIncludedList, ajaxUrl, append) {
  var dualListBox = this;
  gatherAjax.Request({
    url: ajaxUrl,
    options: {
      method: "get",
      onException: function(object, e) {
        alert(e.message);
      },
      onSuccess: function(data) {
        var jsonObject = gatherAjax.JSON2Object(data.responseText);
        if (jsonObject.success) {
          var itemList = ListBoxItem.convertList(jsonObject.list, dualListBox.getItemConstructor());
          if (isIncludedList) {
            dualListBox.includedListItems = append ? dualListBox.includedListItems.concat(itemList) : itemList;
            dualListBox.hasMoreIncludedItems = jsonObject.more;
            dualListBox.currentIncludedOffset = jsonObject.offset;
          } else {
            dualListBox.excludedListItems = append ? dualListBox.excludedListItems.concat(itemList) : itemList;
            dualListBox.hasMoreExcludedItems = jsonObject.more;
            dualListBox.currentExcludedOffset = jsonObject.offset;
          }
          if (append) {
            dualListBox.renderList(isIncludedList, true, itemList);
          } else {
            dualListBox.renderList(isIncludedList, false);
          }
        } else {
          alert(jsonObject.message);
        }
      }
    }
  });
}

DualListBox.prototype.getItemConstructor = function() {
  if (this.itemType == 'member') {
    return MemberListBoxItem;
  } else if (this.itemType == 'group') {
    return GroupListBoxItem;
  } else if (this.itemType == 'friendSet') {
    return FriendSetListBoxItem;
  } else {
    alert("Unknown item type for DualListBox");
  }
}

/*
  If append is true, must pass in a listToAppend, and this is what is rendered and added to the HTML.  Otherwise,
  just renders the current included or excluded list (depending on the value of isIncludedList).
 */
DualListBox.prototype.renderList = function(isIncludedList, append, listToAppend) {
  var listElement = isIncludedList ? this.includedListElement : this.excludedListElement;
  var itemList = append ? listToAppend : (isIncludedList ? this.includedListItems : this.excludedListItems);
  var hasMore = isIncludedList ? this.hasMoreIncludedItems : this.hasMoreExcludedItems;
  var offset = isIncludedList ? this.currentIncludedOffset : this.currentExcludedOffset;
  var listElementContents = "";

  for (var i = 0; i < itemList.length; i++) {
    var li = "";
    var item = itemList[i];
    li += "<li onclick='" + this.listBoxVariableName + ".moveItem(this, " + !isIncludedList + ")'"
    li += " id=" + this.elementIdFor(item);
    if (i % 2) {
      li += " class='even'"
    }
    li += ">" + item.render() + "</li>\n";
    listElementContents += li;
  }

  if (append) {
    $(listElement).down(".more").remove();
    $(listElement).insert(listElementContents);
  } else {
    $(listElement).update(listElementContents);
  }

  if (hasMore && this.createMoreUrlCallback) {
    var url = this.createMoreUrlCallback(offset, isIncludedList);
    $(listElement).insert("<li class='more'><a href='" + url + "' onclick='" + this.listBoxVariableName
            + ".populateAndRenderListFromAjax(" + isIncludedList + ", this.href, true); return false'>more...</a></li>\n");
  }
}

DualListBox.prototype.moveItem = function(li, include) {
  var elementId = li.id;
  var itemId = elementId.substring(this.listBoxVariableName.length + 1);

  // need to check whether the target list filter includes it, but for now assume it does
  var sourceListItems, sourceListElement, targetListItems, targetListElement, targetFindBox;
  if (include) {
    sourceListItems = this.excludedListItems;
    sourceListElement = this.excludedListElement;
    targetListItems = this.includedListItems;
    targetListElement = this.includedListElement;
    targetFindBox = this.includedFindElement;
  } else {
    sourceListItems = this.includedListItems;
    sourceListElement = this.includedListElement;
    targetListItems = this.excludedListItems;
    targetListElement = this.excludedListElement;
    targetFindBox = this.excludedFindElement;
  }

  // NEW STUFF
  var cancelMove = false;

  if (this.maxNumberIncludedItems) {
	  if (include) {
	    var currentNumItemsInList = this.totalInItems; //this.includedListItems.length; $(targetListElement).select('li').length;

	    if (currentNumItemsInList >= this.maxNumberIncludedItems) {
	        cancelMove = true;
	        if(currentNumItemsInList == this.maxNumberIncludedItems) {
	            $(this.maxedOutMessageAreaElement).style.display = '';
	            $(this.overMaxMessageAreaElement).style.display = 'none';
	        } else {
	            $(this.overMaxMessageAreaElement).style.display = '';
	            $(this.maxedOutMessageAreaElement).style.display = 'none';
	        }
	    } else {
	        if ((currentNumItemsInList+1) == this.maxNumberIncludedItems) {
	            $(this.maxedOutMessageAreaElement).style.display = '';
	            $(this.overMaxMessageAreaElement).style.display = 'none';
	        }
	    }
	  } else {
	    var currentNumItemsInList = this.totalInItems; //$(sourceListElement).select('li').length;

        if ((currentNumItemsInList-1) < this.maxNumberIncludedItems) {
            $(this.maxedOutMessageAreaElement).style.display = 'none';
            $(this.overMaxMessageAreaElement).style.display = 'none';
        }
	  }
  }
  
  if (this.noSelectionsMessage) {
      if (!include) {
        var currentNumItemsInList = this.totalInItems; //this.includedListItems.length; $(targetListElement).select('li').length;

        if (currentNumItemsInList <= 1) {
            $(this.noSelectionsMessageAreaElement).style.display = '';
        } else {
            $(this.noSelectionsMessageAreaElement).style.display = 'none';
        }
      } else {
          $(this.noSelectionsMessageAreaElement).style.display = 'none';
      }
  }

  if (!cancelMove) {
      if (include) {
        this.totalInItems++;
      } else {
        this.totalInItems--;
      }

	  // remove from source list (& HTML list), restripe odd/even rows
	  var item = DualListBox.removeItemFromList(itemId, sourceListItems);
	  $(li).remove();
	  DualListBox.restripeItems(sourceListElement);

	  this.doAddItemToList(item, targetFindBox, targetListItems, targetListElement, include);

	  // call callback method
	  if (this.moveItemCallback) {
	    this.moveItemCallback(itemId, include);
	  }
   }
}

DualListBox.prototype.addNewItemToIncludedList = function(item) {
  this.doAddItemToList(item, this.includedFindElement, this.includedListItems, this.includedListElement, true);
}

DualListBox.prototype.doAddItemToList = function(item, targetFindBox, targetListItems, targetListElement, include) {
  // only add to other list if it matches find criteria (TODO: and/or paging?)
  var findValue;
  if (this.createFindUrlCallback) {
    findValue = $F($(targetFindBox));
  } else {
    findValue = "";
  }
  if (findValue == "" || item.comparableValue().toLowerCase().startsWith(findValue.toLowerCase())) {
    // add to target list (& HTML list), resort & rerender
    targetListItems[targetListItems.length] = item;
    targetListItems.sort(targetListItems[0].compare);
    this.renderList(include);

    // scroll to added item
    scrollToSubElement(targetListElement, this.elementIdFor(item));

    // highlight item that moved
    new Effect.Highlight(this.elementIdFor(item), { startcolor: "#f3901c" });
  } else {
    // highlight the whole list box, since the actual item is not viewable
    new Effect.Highlight(targetListElement, { startcolor: "#f3901c" });
  }
}

DualListBox.removeItemFromList = function(itemId, array) {
  for (var i = 0; i < array.length; i++) {
    if (array[i].id == itemId) {
      var item = array[i];
      array.splice(i, 1);
      return item;
    }
  }
}

DualListBox.restripeItems = function(listElement) {
  var liArray = $(listElement).childElements();
  for (var i = 0; i < liArray.length; i++) {
    var li = liArray[i];
    if (i % 2) {
      $(li).addClassName('even');
    } else {
      $(li).removeClassName('even');
    }
  }
}

DualListBox.prototype.serializeIds = function(isIncludedList) {
  var items = isIncludedList ? this.includedListItems : this.excludedListItems;
  var result = "";
  for (var i = 0; i < items.length; i++) {
    if (i > 0) { result += ","; }
    result += items[i].id;
  }
  return result;
}

DualListBox.prototype.elementIdFor = function(listBoxItem) {
  return this.listBoxVariableName + "_" + listBoxItem.id;
}

/* Base class ListBoxItem --------------------------------------------------------------------------------------------*/
function ListBoxItem(id) {
  this.id = id;
};

ListBoxItem.convertList = function(jsonList, itemConstructor) {
  var items = new Array();
  for (var i = 0; i < jsonList.length; i++) {
    items[i] = new itemConstructor(jsonList[i]);
  }
  return items;
}

/*
 * Should be overridden.  The display depends on what kind of item you're listing.  Returns an HTML snippet that will
 * be placed inside a LI element.
 */
ListBoxItem.prototype.render = function() {
  return this.id;
};

ListBoxItem.prototype.comparableValue = function() {
  return this.id;
}

ListBoxItem.prototype.compare = function(a, b) {
  return a.comparableValue() - b.comparableValue();
}

/* MemberListBoxItem -------------------------------------------------------------------------------------------------*/
function MemberListBoxItem(parameters) {
  this.id = parameters.id;
  this.fullName = parameters.fullName;
  this.username = parameters.username;
  this.tagline = parameters.tagline;
  this.iconUrl = parameters.iconUrl;
}
MemberListBoxItem.prototype = new ListBoxItem(0);
MemberListBoxItem.constructor = MemberListBoxItem;

MemberListBoxItem.prototype.render = function() {
  var html = "<div>";
  html += "<strong>" + this.fullName + "</strong>";
  html += ", " + this.username + "<br/>";
  html += this.tagline;
  html += "</div><div class='cb'></div>";
  return html;
}

MemberListBoxItem.prototype.comparableValue = function() {
  return this.fullName.toLowerCase();
}

MemberListBoxItem.prototype.compare = function(a, b) {
  return a.comparableValue() > b.comparableValue() ? 1 : a.comparableValue() < b.comparableValue() ? -1 : 0;
}

/* GroupListBoxItem -------------------------------------------------------------------------------------------------*/
function GroupListBoxItem(parameters) {
  this.id = parameters.id;
  this.name = parameters.name;
  this.isPrivate = parameters.isPrivate;
  this.isModerated = parameters.isModerated;
  this.summary = parameters.summary;
  this.iconUrl = parameters.iconUrl;
}
GroupListBoxItem.prototype = new ListBoxItem(0);
GroupListBoxItem.constructor = GroupListBoxItem;

GroupListBoxItem.prototype.render = function() {
  var html = "<div>";
  html += "<strong>" + this.name + "</strong>";
  if (this.isPrivate || this.isModerated) {
    html += " (";
    if (this.isPrivate) {
      html += "private";
    }
    if (this.isPrivate && this.isModerated) {
      html += ", ";
    }
    if (this.isModerated) {
      html += "moderated";
    }
    html += ")";
  }
  html += "<br/>"
  html += this.summary;
  html += "</div>";
  return html;
}

GroupListBoxItem.prototype.comparableValue = function() {
  return this.name.toLowerCase();
}

GroupListBoxItem.prototype.compare = function(a, b) {
  return a.comparableValue() > b.comparableValue() ? 1 : a.comparableValue() < b.comparableValue() ? -1 : 0;
}

/* FriendSetListBoxItem -------------------------------------------------------------------------------------------------*/
function FriendSetListBoxItem(parameters) {
  this.id = parameters.id;
  this.name = parameters.name;
  this.numConnections = parameters.numConnections;
}
FriendSetListBoxItem.prototype = new ListBoxItem(0);
FriendSetListBoxItem.constructor = FriendSetListBoxItem;

FriendSetListBoxItem.prototype.render = function() {
  return this.name + " (" + this.numConnections + ")";
}

FriendSetListBoxItem.prototype.comparableValue = function() {
  return this.numConnections;
}

FriendSetListBoxItem.prototype.compare = function(a, b) {
  return b.comparableValue() - a.comparableValue();
}

/* Utility -----------------------------------------------------------------------------------------------------------*/
function DelayedKeyupObserver(element, millis, callback) {
  this.element = $(element);
  this.millis = millis;
  this.callback = callback;
  Event.observe(this.element, "keyup", this.onKeyup.bind(this));
}

DelayedKeyupObserver.prototype.onKeyup = function() {
  if (this.timer) {
    clearTimeout(this.timer);
  }
  var observer = this;
  this.timer = setTimeout(function() {
    observer.callback($F(observer.element));
  }, observer.millis);
}

function scrollToSubElement(container, subElement) {
  container = $(container);
  subElement = $(subElement);
  var container_y = container.cumulativeOffset().top;
  var subElement_y = subElement.cumulativeOffset().top;
  container.scrollTop = subElement_y - container_y;
  return false;
}

function disableSubmitOnEnter(element) {
  // this needs to be keypress, won't work with keyup or keydown
  Event.observe(element, "keypress", function(event) {
    if (Event.KEY_RETURN == (event.keyCode || event.which)) {
      Event.stop(event);
    }
  });
}
