// vim: ts=2 sw=2 expandtab cindent
//
// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2009
// http://flock.com
//
// This file may be used under the terms of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// END FLOCK GPL

const CC = Components.classes;
const CI = Components.interfaces;
const CR = Components.results;
const CU = Components.utils;

CU.import("resource:///modules/FlockMediaUtils.jsm");
CU.import("resource:///modules/FlockCastUtils.jsm");
CU.import("resource:///modules/FlockURLUtils.jsm");

const FLOCK_SNOWMAN_URL = "chrome://flock/skin/people/no_avatar.png";

const DEMO_VIDEO_URL = "http://www.flock.com/tutorials/people/";
const FLOCK_API_URL = "http://developer.flock.com/";

const FLOCK_RICH_CONTENT_FLAVOUR = "application/x-flock-rich-content";
const FLOCK_UNICODE_FLAVOUR = "text/unicode";
const FLOCK_SHELFICON_FLAVOUR = "flock/shelficon";
const TWITTER_CONTRACT_ID = "@flock.com/people/twitter;1";

// Constants for prefs
const PEOPLE_PREFERRED_SERVICES_LIST_PREF = "flock.people.preferredServiceList";

// constants for peopleSidebarDeck
const DECK_SHOWING_DISCOVERY_PANEL = 0;
const DECK_SHOWING_NO_FRIENDS_PANEL = 1;
const DECK_SHOWING_FRIENDS_LIST = 2;

const LAST_CHANGE_TIMER_INTERVAL = 60 * 1000;

const PEOPLE_STR_BUNDLE = "people/people";
const FRIEND_FILTER_METRICS_INTERVAL = 300;

const AVATAR_MAX_SIZE = 50;
const OOBE_DIALOG = "chrome://flock/content/oobe/oobeDialog.xul";
const OOBE_ENABLED = "flock.oobe.enabled";

const SORT_BY_RECENCY = 1;
const SORT_BY_NAME = 2;

CU.import("resource:///modules/FlockScheduler.jsm");
CU.import("resource:///modules/FlockStringBundleHelpers.jsm");
CU.import("resource:///modules/FlockPrefsUtils.jsm");
CU.import("resource:///modules/FlockDateFormatter.jsm");
CU.import("resource:///modules/FlockXMLUtils.jsm");

var gLoaded = false;
var gFriendListController;
var gPeopleSidebarController;
var gCurrentItem = null;
var gStatusChangePanelDragDropHandler = null;
var gStatusEntryDragDropHandler = null;
var _coop = CC["@flock.com/singleton;1"]
            .getService(CI.flockISingleton)
            .getSingleton("chrome://flock/content/common/load-faves-coop.js")
            .wrappedJSObject;

var _logger = CC["@flock.com/logger;1"].createInstance(CI.flockILogger);
_logger.init("peopleSidebar");

var _metrics = CC["@flock.com/metrics-service;1"]
               .getService(CI.flockIMetricsService);

// Associative array of accountURN => flockISocialAccount objects
var gAccounts = [];

var _wm = CC["@mozilla.org/appshell/window-mediator;1"]
          .getService(CI.nsIWindowMediator);

var _rdfSvc = CC["@mozilla.org/rdf/rdf-service;1"]
              .getService(CI.nsIRDFService);

var _ds = _rdfSvc.GetDataSource("rdf:flock-favorites");

var _acUtils = CC["@flock.com/account-utils;1"]
               .getService(CI.flockIAccountUtils);
var _sb = CC["@mozilla.org/intl/stringbundle;1"]
          .getService(CI.nsIStringBundleService)
          .createBundle("chrome://flock/locale/people/people.properties");

var gAccountObserver = {
  onAccountAuthenticated: function acctObs_onAccountAuthenticated(aAccount) {
    _logger.debug("onAccountAuthenticated(" + aAccount.urn + ")");
    var serviceId = aAccount.getParam("serviceId");
    var psTabs = document.getElementById("peopleServiceTabs");
    psTabs.refresh();

    // Select the service tab for the newly authenticated account making sure
    // that we do not interrupt a status change action
    if (psTabs.selectedMecard) {
      psTabs.selectedMecard.setupFlockcastElementsOnStatusChangePopup();

      if (!psTabs.selectedMecard.isChangingStatus ||
          !psTabs.selectedMecard.isAuthenticated) 
      {
        psTabs.selectTabByServiceId(serviceId);
      }
    } else {
      psTabs.selectTabByServiceId(serviceId);
    }

    gPeopleSidebarController.refreshDiscoveryPanel();
    psTabs.toggleRefreshingIndicatorByServiceId(serviceId, true);
    refreshSelectedAccount();
    gFriendListController._updateAccount(aAccount.getParam("friendsList"),
                                         true);
    if (gPeopleSidebarController.isServiceDisplayed(serviceId)) {
      doLayout(false);
    }
  },
  onAccountRemoved: function acctObs_onAccountRemoved(aAccount) {
    _logger.debug("onAccountRemoved(" + aAccount.urn + ")");
    var psTabs = document.getElementById("peopleServiceTabs");
    psTabs.refresh();
    // Update flockcast elements
    var selectedMecard = psTabs.selectedMecard;
    if (selectedMecard) {
      psTabs.selectedMecard.setupFlockcastElementsOnStatusChangePopup();
    }
    gPeopleSidebarController.refreshDiscoveryPanel();
  },
  onAccountUnauthenticated: function acctObs_onAccountUnauthenticated(aAccount) {
    _logger.debug("onAccountUNAuthenticated(" + aAccount.urn + ")");
    var serviceId = aAccount.getParam("serviceId");
    this.onAccountNoLongerRefreshing(aAccount);
    var psTabs = document.getElementById("peopleServiceTabs");
    psTabs.markServiceAsLoggedOut(serviceId);
    psTabs.refresh();
    // Update flockcast elements
    var selectedMecard = psTabs.selectedMecard;
    if (selectedMecard) {
      psTabs.selectedMecard.setupFlockcastElementsOnStatusChangePopup();
    }
    gFriendListController._updateAccount(aAccount.getParam("friendsList"),
                                         false);
    gPeopleSidebarController.refreshDiscoveryPanel();
    if (gPeopleSidebarController.isServiceDisplayed(serviceId)) {
      doLayout(false);
    }
  },
  onAccountRefreshing: function acctObs_onAccountRefreshing(aAccount) {
    _logger.debug("onAccountRefreshing(" + aAccount.urn + ")");
    var serviceId = aAccount.getParam("serviceId");
    var psTabs = document.getElementById("peopleServiceTabs");
    if (psTabs && psTabs.toggleRefreshingIndicatorByServiceId) {
      psTabs.toggleRefreshingIndicatorByServiceId(serviceId, true);
    }
    if (gPeopleSidebarController.isServiceDisplayed(serviceId)) {
      doLayout(false);
    }
  },
  onAccountNoLongerRefreshing: function acctObs_onAccountNoLongerRefreshing(aAccount) {
    _logger.debug("onAccountNoLongerRefreshing(" + aAccount.urn + ")");
    var serviceId = aAccount.getParam("serviceId");
    var psTabs = document.getElementById("peopleServiceTabs");
    if (psTabs &&
        psTabs.toggleRefreshingIndicatorByServiceId &&
        psTabs.refresh)
    {
      psTabs.toggleRefreshingIndicatorByServiceId(serviceId, false);
      psTabs.refresh();
    }
    var filterText = document.getElementById("filterBox").value;
    if (filterText != "") {
      gFriendListController.filter(filterText);
    }

    if (gPeopleSidebarController.isServiceDisplayed(serviceId)) {
      // The account has completed refreshing so we call doLayout with a check
      // for no friends
      doLayout(true);
    }

    if (serviceId == TWITTER_CONTRACT_ID) {
      refreshActivityList(serviceId);
    }
  }
};

function FriendListController() {
  this.listDeck = document.getElementById("listDeck");
  this.friendList = document.getElementById("friendList");
  this.order = 1;
  this.sorted = true;
  this.friendIDMap = {};
  this.schedulerTimers = [];
  this.filterNumResults = 0;
  this.filterLastFiltered = 0;
  this.filterLastFilterText = "";
  var inst = this;
  this.peopleObservor = {
    onFriendRemove: function peepObs_onFriendRemove(aFriendUrn) {
      inst._removeFriend(aFriendUrn);
    },
    onFriendUpdate: function peepObs_onFriendUpdate(aFriendUrn) {
      var coopObj = _coop.get(aFriendUrn);
      var update = function onFriendUpdate_update() {
        inst._updateFriend(coopObj);
        inst.filter(inst.filterLastFilterText);
      };

      // Wait a moment before updating the friend. We do this so that the coop
      // properties have had time to be created.
      setTimeout(update, 100);
    },
    onInsertFriend: function peepObs_onInsertFriend(aFriendUrn) {
      var coopObj = _coop.get(aFriendUrn);
      inst._insertFriend(coopObj);
      inst.filter(inst.filterLastFilterText);
    },
    onNewActivity: function peepObs_onNewActivity(aActivityUrn) {
    }
  };
  this.lastChangeTimer = CC["@mozilla.org/timer;1"].createInstance(CI.nsITimer);
  this.lastChangeTimerListener = {
    notify: function FriendListController_notify(aTimer) {
      _logger.debug("FriendListController: lastChangeTimerListener");
      function updateTimes(aShouldYield) {
        var nodeList = inst.friendList.childNodes;
        for (var c = 0; c < nodeList.length; c++) {
          var item = nodeList.item(c);
          if (item && item.updateTime) {
            item.updateTime();
          }
          if (aShouldYield()) {
            yield;
          }
        }
      }
      FlockScheduler.schedule(inst.schedulerTimers, 0.05, 10, updateTimes);
    }
  }
}

function filterCommand(aFilterText) {
  gFriendListController.filter(aFilterText);
  var pst = document.getElementById("peopleServiceTabs");
  if (CC[pst.selectedServiceId]) {
    refreshActivityList(pst.selectedServiceId);
  }
}

function filterFriendCard(aFriendCard, aFilterText) {
  if (!aFilterText) {
    aFilterText = gFriendListController.filterLastFilterText;
  }
  // Case insensitive search on 'displayName' and 'tagline' properties
  var filterRE = new RegExp(aFilterText, "i");
  if (aFriendCard.displayName.match(filterRE) ||
      aFriendCard.tagline.match(filterRE))
  {
    aFriendCard.setAttribute("class", "match");
    return true;
  }
  aFriendCard.removeAttribute("class");
  return false;
}

FriendListController.prototype.filter =
function FriendListController_filter(aFilterText) {
  _logger.debug("filter");
  if (aFilterText) {
    this.filterNumResults = 0;
    for (var friendId in this.friendIDMap) {
      var friendCard = this.friendIDMap[friendId];
      if (filterFriendCard(friendCard, aFilterText)) {
        this.filterNumResults++;
      }
    }
    this.filterLastFilterText = aFilterText;
    this.filterLastFiltered = Date.now();
    this.listDeck.setAttribute("filtered", "true");
  } else {
    this.clearFilter();
  }
};

FriendListController.prototype.init =
function FriendListController_init() {
  var acctsExist = false;
  var accounts = _coop.accounts_root.children.enumerate();
  while (accounts.hasMoreElements()) {
    var account = accounts.getNext();
    var friendsList = account.friendsList;
    if (friendsList) {
      acctsExist = true;
      if (account.isAuthenticated) {
        this._updateAccount(friendsList, true);
      }
    }
  }

  this.lastChangeTimer.initWithCallback(this.lastChangeTimerListener,
                                        LAST_CHANGE_TIMER_INTERVAL,
                                        CI.nsITimer.TYPE_REPEATING_SLACK);
  var p = CC["@flock.com/people-service;1"].getService(CI.flockIPeopleService);
  p.addObserver(this.peopleObservor);
  return acctsExist;
};

FriendListController.prototype.uninit =
function FriendListController_uninit() {
  this.lastChangeTimer.cancel();
  this.lastChangeTimer = null;
  for (var c = this.schedulerTimers.length - 1; c >= 0; c--) {
    FlockScheduler.cancel(this.schedulerTimers, c);
  }
  var p = CC["@flock.com/people-service;1"].getService(CI.flockIPeopleService);
  p.removeObserver(this.peopleObservor);
}
FriendListController.prototype._compare =
function FriendListController__compare(friendCard, refCard) {
  if (this.order == 1) {
    var friendLastUpdate = Number(friendCard.getAttribute("lastupdate"));
    var refLastUpdate = Number(refCard.getAttribute("lastupdate"));
    if (friendLastUpdate != refLastUpdate) {
      return friendLastUpdate > refLastUpdate;
    }
  } else if (this.order != 2) {
    _logger.error("FriendListController.compare invalid order: " + this.order);
    return false;
  }
  var friendName = friendCard.getAttribute("displayName").toLowerCase();
  var refName = refCard.getAttribute("displayName").toLowerCase();
  return friendName <= refName;
}

FriendListController.prototype._findPosition =
function FriendListController__findPosition(friendCard) {
  var position = 0;
  for (var c = 0; c < this.friendList.childNodes.length; c++) {
    var refCard = this.friendList.childNodes.item(c);
    if (this._compare(friendCard, refCard)) {
      break;
    }
    position = c + 1;
  }
  return position;
}

FriendListController.prototype._findPositionSorted =
function FriendListController__findPositionSorted(friendCard) {
  var low = 0;
  var high = this.friendList.childNodes.length - 1;
  var mid, refCard;
  while (low < high) {
    mid = Math.floor((low + high) / 2);
    refCard = this.friendList.childNodes.item(mid);
    if (this._compare(friendCard, refCard)) {
      high = mid;
    } else {
      low = mid + 1;
    }
  }
  if (low == high) {
    refCard = this.friendList.childNodes.item(low);
    if (!this._compare(friendCard, refCard)) {
      low++;
    }
  }
  return low;
}

FriendListController.prototype._insertFriend =
function FriendListController__insertFriend(friend) {
  _logger.debug("FriendListController: _insertFriend");
  var account = friend.getParent().account;
  if (!account) {
    return;
  }

  // check for existing friend card element
  var friendCard = this.friendIDMap[friend.id()];
  if (friendCard) {
    return;
  }

  friendCard = document.createElement("friendcard");

  friendCard.setAttribute("accountURN", account.id());
  friendCard.setAttribute("accountId", friend.accountId);
  friendCard.setAttribute("displayName", (friend.screenName)
                                           ? friend.screenName
                                           : friend.name);
  friendCard.setAttribute("avatar", friend.avatar);
  friendCard.setAttribute("avatarcommand", "");
  friendCard.setAttribute("lastupdate", friend.lastUpdate);
  friendCard.setAttribute("lastupdatetype", friend.lastUpdateType);
  friendCard.setAttribute("quote", friend.statusMessage);
  friendCard.setAttribute("quoteUrl", friend.statusMessageUrl);
  friendCard.setAttribute("serviceId", friend.serviceId);
  friendCard.setAttribute("unseenMedia", friend.unseenMedia);
  friendCard.setAttribute("uri", friend.id());
  friendCard.setAttribute("id", friend.id());

  friendCard.setAttribute("ondragdrop",
                          "this.removeAttribute('dropzoneengaged');"
                          + "nsDragAndDrop.drop(event, personcardDNDObserver, true);"
                          + "event.stopPropagation();");
  friendCard.setAttribute("ondragover",
                          "this.setAttribute('dropzoneengaged', 'true');"
                          + "nsDragAndDrop.dragOver(event, personcardDNDObserver);"
                          + "event.stopPropagation();");
  friendCard.setAttribute("ondragenter",
                          "this.setAttribute('dropzoneengaged', 'true');"
                          + "nsDragAndDrop.dragEnter(event, personcardDNDObserver);"
                          + "event.stopPropagation();");
  friendCard.setAttribute("ondragexit",
                          "this.removeAttribute('dropzoneengaged');"
                          + "nsDragAndDrop.dragExit(event, personcardDNDObserver);"
                          + "event.stopPropagation();");

  var position;
  if (this.sorted) {
    position = this._findPositionSorted(friendCard);
  } else {
    position = this._findPosition(friendCard);
  }
  if (position == this.friendList.childNodes.length) {
    this.friendList.appendChild(friendCard);
  } else {
    var refCard = this.friendList.childNodes.item(position);
    this.friendList.insertBefore(friendCard, refCard);
  }

  friendCard.displayName = (friend.screenName) ? friend.screenName : friend.name;
  friendCard.tooltipName = friend.name;
  this.friendIDMap[friend.id()] = friendCard;
}

FriendListController.prototype._removeFriend =
function FriendListController__removeFriend(friendURI) {
  _logger.debug("FriendListController: _removeFriend");
  var friendCard = this.friendIDMap[friendURI];
  if (friendCard) {
    this.friendList.removeChild(friendCard);
    delete this.friendIDMap[friendURI];
  }
}

FriendListController.prototype._updateFriend =
function FriendListController__updateFriend(identity) {
  _logger.debug("FriendListController: _updateFriend");
  this._removeFriend(identity.id());
  this._insertFriend(identity);
}

FriendListController.prototype._updateAccount =
function FriendListController__updateAccount(aFriendsList, aIsAuthenticated) {
  _logger.debug("FriendListController: _updateAccount");
  if (aFriendsList) {
    var inst = this;
    function updateAccountContent(aShouldYield) {
      // check that the account is not being destroyed.
      var friendEnum = aFriendsList.children.enumerate();
      if (aIsAuthenticated) {
        while (friendEnum.hasMoreElements()) {
          var friend = friendEnum.getNext();
          inst._insertFriend(friend);
          if (aShouldYield()) {
            yield;
          }
        }
      } else {
        while (friendEnum.hasMoreElements()) {
          var friend = friendEnum.getNext();
          inst._removeFriend(friend.id());
          if (aShouldYield()) {
            yield;
          }
        }
      }
    }
    FlockScheduler.schedule(this.schedulerTimers, 0.1, 50, updateAccountContent);
  }
}

FriendListController.prototype.sort =
function FriendListController_sort(order) {
  _logger.debug("FriendListController: sort");
  if (this.order == order) {
    return;
  }
  this.order = order;
  this.sorted = false;
  var inst = this;
  function sortContent(aShouldYield) {
    for (var c = 0; c < inst.friendList.childNodes.length; c++) {
      var friendCard = inst.friendList.childNodes.item(c);
      var newPosition = inst._findPosition(friendCard);
      if (newPosition != c) {
        if (newPosition == inst.friendList.childNodes.length) {
          inst.friendList.appendChild(friendCard);
        } else {
          var refCard = inst.friendList.childNodes.item(newPosition);
          inst.friendList.insertBefore(friendCard, refCard);
        }
      }
      if (aShouldYield()) {
        yield;
      }
    }
    inst.sorted = true;
  }
  FlockScheduler.schedule(this.schedulerTimers, 0.25, 80, sortContent);
}

FriendListController.prototype.clearFilter =
function FriendListController_clearFilter() {
  _logger.debug("FriendListController: clearFilter");
  this.listDeck.removeAttribute("filtered");
  this.filterLastFilterText = "";
}


function setStreamView(aServiceId, aView) {
  _logger.debug("setStreamView('" + aServiceId + "', '" + aView + "')");
  var listDeck = document.getElementById("listDeck");
  if (!aView) {
    aView = "friends";
  }
  if (aServiceId == TWITTER_CONTRACT_ID && aView != "friends") {
    // Show activityList
    refreshActivityList(aServiceId, aView);
    listDeck.setAttribute("selectedIndex", "1");
  } else {
    // Show friendList
    listDeck.setAttribute("selectedIndex", "0");
  }
  if (aServiceId == TWITTER_CONTRACT_ID) {
    var svc = CC[aServiceId].getService(CI.flockISocialWebService);
    var accountURN = svc.getAuthenticatedAccount();
    if (!accountURN) {
      return;
    }
    var psTabs = document.getElementById("peopleServiceTabs");
    var mecard = document.getAnonymousElementByAttribute(
                   psTabs, "id", "mecard:" + aServiceId);
    var friendsBtn = document.getAnonymousElementByAttribute(
                       mecard, "anonid", "friends-button-container");
    var repliesBtn = document.getAnonymousElementByAttribute(
                       mecard, "anonid", "replies-button-container");
    var dmessagesBtn = document.getAnonymousElementByAttribute(
                       mecard, "anonid", "dmessages-button-container");
    var c_acct = _coop.get(accountURN);
    switch (aView) {
      case "friends":
        friendsBtn.setAttribute("selected", "true");
        repliesBtn.removeAttribute("selected");
        dmessagesBtn.removeAttribute("selected");
        break;
      case "reply":
        friendsBtn.removeAttribute("selected");
        repliesBtn.setAttribute("selected", "true");
        dmessagesBtn.removeAttribute("selected");
        c_acct.set("replyCount", "0", true);
        mecard.refreshNotifications();
        break;
      case "dmessage":
        friendsBtn.removeAttribute("selected");
        repliesBtn.removeAttribute("selected");
        dmessagesBtn.setAttribute("selected", "true");
        c_acct.set("dmessageCount", "0", true);
        mecard.refreshNotifications();
        break;
    }
    psTabs.lastViewSelected = aView;
  }
}

function refreshActivityList(aServiceId, aView) {
  if (!aView) {
    var psTabs = document.getElementById("peopleServiceTabs");
    var mecard = document.getAnonymousElementByAttribute(
                   psTabs, "id", "mecard:" + aServiceId);
    if (!mecard || !mecard.hasAttribute("latestStream")) {
      return;
    }
    aView = mecard.getAttribute("latestStream");
  }
  if (aServiceId == TWITTER_CONTRACT_ID && aView != "friends") {
    var activityList = document.getElementById("activityList");
    while (activityList.firstChild) {
      activityList.removeChild(activityList.firstChild);
    }

    var svc = CC[aServiceId].getService(CI.flockISocialWebService);
    var accountURN = svc.getAuthenticatedAccount();
    var coopAcct = _coop.get(accountURN);
    if (!coopAcct || !coopAcct.otherActivityLog) {
      return;
    }
    var accountId = coopAcct.accountId;
    var itemEnum = coopAcct.otherActivityLog.children.enumerateBackwards();
    while (itemEnum.hasMoreElements()) {
      var activity = itemEnum.getNext();
      if (activity.updateType != aView) {
        continue;
      }
      var friendcard = document.createElement("friendcard");
      var identity = (activity.identity) ? activity.identity : activity;
      friendcard.setAttribute("accountURN", accountURN);
      friendcard.setAttribute("accountId", accountId);
      friendcard.setAttribute("displayName", (identity.screenName)
                                               ? identity.screenName
                                               : identity.name);
      friendcard.setAttribute("avatar", identity.avatar);
      friendcard.setAttribute("avatarcommand", "");
      var activityDate = (new Date(activity.updateDate)).getTime();
      if (activityDate > 0) {
        activityDate = Math.floor(activityDate / 1000);
      }
      friendcard.setAttribute("lastupdate", activityDate);
      friendcard.setAttribute("lastupdatetype", activity.updateType);
      friendcard.setAttribute("quote", activity.updateValue);
      friendcard.setAttribute("quoteUrl", (aView != "dmessage")
                                            ? activity.updateURL
                                            : "");
      friendcard.setAttribute("serviceId", aServiceId);
      friendcard.setAttribute("uri", identity.id());
      let statusId = activity.updateURL.split("/").pop();
      friendcard.setAttribute("id", svc.shortName + ":" + statusId);

      friendcard.setAttribute("ondragdrop",
                              "this.removeAttribute('dropzoneengaged');"
                              + "nsDragAndDrop.drop(event, personcardDNDObserver, true);"
                              + "event.stopPropagation();");
      friendcard.setAttribute("ondragover",
                              "this.setAttribute('dropzoneengaged', 'true');"
                              + "nsDragAndDrop.dragOver(event, personcardDNDObserver);"
                              + "event.stopPropagation();");
      friendcard.setAttribute("ondragenter",
                              "this.setAttribute('dropzoneengaged', 'true');"
                              + "nsDragAndDrop.dragEnter(event, personcardDNDObserver);"
                              + "event.stopPropagation();");
      friendcard.setAttribute("ondragexit",
                              "this.removeAttribute('dropzoneengaged');"
                              + "nsDragAndDrop.dragExit(event, personcardDNDObserver);"
                              + "event.stopPropagation();");

      activityList.appendChild(friendcard);
      filterFriendCard(friendcard);
    }
  }
}

function PeopleSidebarController() {
  this._coop = CC["@flock.com/singleton;1"]
               .getService(CI.flockISingleton)
               .getSingleton("chrome://flock/content/common/load-faves-coop.js")
               .wrappedJSObject;
  this._logger = CC["@flock.com/logger;1"]
                 .createInstance(CI.flockILogger);
  this._logger.init("PeopleSidebarController");
  this._serviceTabs = document.getElementById("peopleServiceTabs");
  this._obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);
  this._Obsrvr = {};
}

PeopleSidebarController.prototype.init =
function PeopleSidebarController_init() {
  var inst = this;
  this._Obsrvr.observe = 
    function PeopleSidebarController_Obsrvr(aSubject, aTopic, aString) {
      inst._logger.debug("PeopleSidebarController_Obsrvr");
      var psTabs = document.getElementById("peopleServiceTabs");
      switch (aTopic) {
        case "flock-acct-refresh":
          // User info retrieved so refresh the appropriate service tab.
          if (aString && aString == "user-info") {
            var account = aSubject.QueryInterface(CI.flockISocialAccount);
            if (account) {
              if (psTabs && psTabs.refreshTabByServiceId) {
                psTabs.refreshTabByServiceId(account.getParam("serviceId"));
              }
            }
          }
          break;
        case "flock-sidebar-toggled":
          if (aSubject.getAttribute("id") == "flockPeopleSidebarBroadcaster" &&
              aString == "close")
          {
            // The sidebar has been switched so close the status change panel.
            document.getElementById("statusChangePanel").hidePopup();
          }
          break;
        case "flock-people-tabs-built":
          // Case: Myworld Twitter topic or reply button clicked when people
          //       sidebar closed.
          // prePopStatusText is assigned by the Myworld Twitter widget
          // before the sidebar is opened. Use this text to prepopulate
          // the mecard status textarea.
          // flockAction is assigned by the Myworld Twitter widget
          // if the reply button was clicked (format: 'replyId:<some id>').
          var peopleSBBroadcaster =
            top.document.getElementById("flockPeopleSidebarBroadcaster");
          var text = peopleSBBroadcaster.getAttribute("prePopStatusText");
          if (text) {
            peopleSBBroadcaster.removeAttribute("prePopStatusText");
            psTabs.lastServiceIdSelected = "@flock.com/people/twitter;1";
            psTabs.selectTabByServiceId("@flock.com/people/twitter;1");
            var flockAction = peopleSBBroadcaster.getAttribute("flockAction");
            if (!flockAction) {
              flockAction = "newTopic";
            }
            flockAction = flockAction.split(":");
            switch (flockAction[0]) {
              case "replyId":
                // This is a reply
                peopleSBBroadcaster.removeAttribute("flockAction");
                psTabs.selectedMecard.engageStatusChange(null, text, false);
                psTabs.selectedMecard
                      .setAttribute("inReplyTo", "/" + flockAction[1]);
                break;

              default:
                // This is a topic
                psTabs.selectedMecard
                      .engageStatusChange(null, " " + text, false, "start");
            }
          }
          break;
        case "flock-people-trigger-status-change":
          // Case: Myworld Twitter topic or reply button clicked when people
          //       sidebar open.
          // Prepopulate the mecard status textarea.
          // aString = status text
          // flockAction is assigned by the Myworld Twitter widget
          // if the reply button was clicked (format: 'replyId:<some id>').
          psTabs.selectTabByServiceId("@flock.com/people/twitter;1");
          var peopleSBBroadcaster =
            top.document.getElementById("flockPeopleSidebarBroadcaster");
          var flockAction = peopleSBBroadcaster.getAttribute("flockAction");
          if (!flockAction) {
            flockAction = "newTopic";
          }
          flockAction = flockAction.split(":");
          switch (flockAction[0]) {
            case "replyId":
              // This is a reply
              peopleSBBroadcaster.removeAttribute("flockAction");
              psTabs.selectedMecard.engageStatusChange(null, aString, false);
              psTabs.selectedMecard
                    .setAttribute("inReplyTo", "/" + flockAction[1]);
              break;

            case "dnd":
              peopleSBBroadcaster.removeAttribute("flockAction");
              psTabs.selectedMecard.engageStatusChange(null, aString, false);
              break;

            default:
              // This is a topic
              psTabs.selectedMecard
                    .engageStatusChange(null, " " + aString, false, "start");
          }
          break;
        case "flock-process-state":
          if (aString == "background") {
            psTabs.selectedMecard.disengageStatusChange();
          }
          break;
      }
    }

    this._obs.addObserver(this._Obsrvr, "flock-acct-refresh", false);
    this._obs.addObserver(this._Obsrvr, "flock-sidebar-toggled", false);
    this._obs.addObserver(this._Obsrvr, "flock-people-tabs-built", false);
    this._obs.addObserver(this._Obsrvr,
                          "flock-people-trigger-status-change", false);
    this._obs.addObserver(this._Obsrvr, "flock-process-state", false);
};


PeopleSidebarController.prototype.uninit =
function PeopleSidebarController_uninit() {
  this._obs.removeObserver(this._Obsrvr, "flock-acct-refresh");
  this._obs.removeObserver(this._Obsrvr, "flock-sidebar-toggled");
  this._obs.removeObserver(this._Obsrvr, "flock-people-tabs-built", false);
  this._obs.removeObserver(this._Obsrvr,
                          "flock-people-trigger-status-change", false);
  this._obs.removeObserver(this._Obsrvr, "flock-process-state", false);
}

PeopleSidebarController.prototype.openUserGuidePage =
function PeopleSidebarController_openUserGuidePage(aSource, aPageName) {
  _metrics.report("HelpClick", { source: aSource, dest: aPageName });
  var ver = Components.classes["@mozilla.org/xre/app-info;1"]
                      .getService(Components.interfaces.nsIXULAppInfo)
                      .version;
  window.open("http://help.flock.com/user-guide/" + ver + "/" + aPageName,
              "FlockHelp",
              "width=555,height=545,resizable,scrollbars");
};

// Determine whether a given service is being displayed or not. A service is
// displayed if the All card is selected or the service is selected.
PeopleSidebarController.prototype.isServiceDisplayed =
function PeopleSidebarController_isServiceDisplayed(aServiceId) {
  this._logger.debug("isServiceDisplayed");
  if (this._serviceTabs.isAllCardSelected ||
      (this._serviceTabs.selectedServiceId == aServiceId))
  {
      return true;
  }
  return false;
};

// Display the oobe people tutorial.
PeopleSidebarController.prototype.openTutorial =
function PeopleSidebarController_openTutorial() {
  this._logger.debug("openTutorial");
  var windowTitle =
    flockGetString(PEOPLE_STR_BUNDLE,
                   "flock.people.sidebar.discovery.getStarted.tutorial.windowTitle");

  window.openDialog(OOBE_DIALOG,
                    windowTitle,
                    "resizable = 0, modal, centerscreen",
                    1);
};

// Click handler for the service discovery buttons 
PeopleSidebarController.prototype.discoveryLinkClicked =
function PeopleSidebarController_discoveryLinkClicked(aEvent) {
  // Suppress mouse right-click
  if (aEvent.button == 2) {
    return;
  }
  this._logger.debug("discoveryLinkClicked()");
  var serviceId = aEvent.target.getAttribute("serviceId");
  var svc = CC[serviceId].getService(CI.flockIWebService);
  // Metrics reporting.
  Components.classes['@flock.com/metrics-service;1']
            .getService(Components.interfaces.flockIMetricsService)
            .report("PeopleSidebar-DiscoverService", svc.shortName);
  // Remember the account.
  rememberAccount(svc.urn);
  // Display the login page for the service.
  loadURI(svc.loginURL, aEvent);
};

// refreshDiscoveryPanel builds a panel of service buttons that when clicked
// display the login page for the service. Only those services that do not have
// configured accounts are displayed.
PeopleSidebarController.prototype.refreshDiscoveryPanel =
function PeopleSidebarController_refreshDiscoveryPanel() {
  this._logger.debug("refreshDiscoveryPanel");
  // Refresh available services for discovery
  var peopleAccountsExist = false;

  // discoveryButtonServiceList will contain the list of services for which we
  // will build a service discovery button on the discovery panel.
  var discoveryButtonServiceList = [];

  // Get the pref that governs the set of preferred services.
  var preferredServicesPref =
    FlockPrefsUtils.getCharPref(PEOPLE_PREFERRED_SERVICES_LIST_PREF);
  if (preferredServicesPref) {
    discoveryButtonServiceList = preferredServicesPref.split("|");
  }

  for each(var svc in this.getServicesWithConfiguredAccounts()) {
    var pos = discoveryButtonServiceList.indexOf(svc.shortName);
    if (pos != -1) {
      discoveryButtonServiceList.splice(pos, 1);
    }
    // We know that services with configured accounts exist.
    peopleAccountsExist = true;
  }

  var serviceBox = document.getElementById("serviceDiscoveryButtonsPanel");
  var svcChild = serviceBox.firstChild;

  // Clear the current content.
  while (svcChild) {
    serviceBox.removeChild(svcChild);
    svcChild = serviceBox.firstChild;
  }
  // Create the service buttons.
  for each (var svcShortName in discoveryButtonServiceList) {
    this._logger.debug("svcShortName = " + svcShortName);
    var svc = _acUtils.getServiceFromShortname(svcShortName);
    // This checks for disabled services, ie flock.service.<name>.enabled=false
    // This works because you cannot instantiate a service, svc will be null.
    if (svc instanceof CI.flockIWebService) {
      var serviceButton = document.createElement("serviceDiscoveryButton");
      serviceBox.appendChild(serviceButton);
      serviceButton.serviceId = svc.contractId;
      serviceButton.setAttribute("svcShortName", svcShortName);
      serviceButton.setAttribute("onclick",
                                 "gPeopleSidebarController.discoveryLinkClicked(event);");
      var svcLnkTtiptext =
        getStringFromBundle(_sb,
                            "flock.people.sidebar.discovery.serviceLogin.tooltiptext",
                            [svc.title]);
      serviceButton.setAttribute("tooltiptext", svcLnkTtiptext);
      var logInLnkLabelText =
        getStringFromBundle(_sb, "flock.people.sidebar.discovery.serviceLogin");
      serviceButton.setAttribute("linkText", logInLnkLabelText);
    }
  }

  // Set the visibility of the various message panels.
  var serviceDiscoveryButtonsPanel =
    document.getElementById("serviceDiscoveryButtonsPanel");
  serviceDiscoveryButtonsPanel.setAttribute("hidden", "true");
  var otherServicesMessage = document.getElementById("otherServicesMessage");
  otherServicesMessage.setAttribute("hidden", "true");
  var gettingStartedMessage = document.getElementById("gettingStartedMessage");
  gettingStartedMessage.setAttribute("hidden", "true");
  var nothingToShowMessage = document.getElementById("nothingToShowMessage");

  if (serviceBox.firstChild) {
    nothingToShowMessage.setAttribute("hidden", "true");
    // Show the service discovery buttons panel.
    serviceDiscoveryButtonsPanel.setAttribute("hidden", "false");
    if (peopleAccountsExist) {
      // There are undiscovered accounts but the user has done this before.
      otherServicesMessage.setAttribute("hidden", "false");
    } else {
      // The user has never configured a people account so show the
      // "Getting Started" message.
      gettingStartedMessage.setAttribute("hidden", "false");
    }
  } else {
    // No service discovery buttons to show.
    nothingToShowMessage.setAttribute("hidden", "false");
  }
};

PeopleSidebarController.prototype.getServicesWithConfiguredAccounts =
function PeopleSidebarController_getServicesWithConfiguredAccounts() {
  this._logger.debug("getServicesWithConfiguredAccounts");
  // Build a list of services for which there are configured accounts
  var servicesList = [];
  var accounts = this._coop.accounts_root.children.enumerate();
  while (accounts.hasMoreElements()) {
    var account = accounts.getNext();
    if (!account.friendsList) {
      continue;
    }

    // The XPCOM service associated with this account doesn't
    // currently exist, so skip it.
    if (!CC[account.serviceId]) {
      continue;
    }

    var service = CC[account.serviceId].getService(CI.flockIWebService);
    // We only care about flockISocialWebServiceAccount
    if (!(service instanceof CI.flockISocialWebService)) {
      continue;
    }

    // We only need one service status bar per service
    if (servicesList.indexOf(service) == -1) {
      this._logger.debug("Adding to service list: " + account.serviceId );
      servicesList.push(service);
    }
  }

  servicesList.sort(this.compareService);
  return servicesList;
};

PeopleSidebarController.prototype.compareService =
function PeopleSidebarController_compareService(aService1,
                                                aService2)
{

  var account1 = aService1.getAuthenticatedAccount();
  var account2 = aService2.getAuthenticatedAccount();

  var isLoggedInAccount1 = account1 ? true : false;
  var isLoggedInAccount2 = account2 ? true : false;

  // If the authenticated state is the same then order alphabetical
  if (isLoggedInAccount1 == isLoggedInAccount2) {

    var serviceName1 = aService1.title;

    var serviceName2 = aService2.title;

    if (serviceName1 > serviceName2) {
      return 1;
    }

    if (serviceName1 < serviceName2) {
      return -1;
    }

    return 0;
  }


  if (isLoggedInAccount1) {
    return -1;
  } else {
    return 1;
  }
};

PeopleSidebarController.prototype.filterOnBlur =
function PeopleSidebarController_filterOnBlur() {
  var filter = document.getElementById("filterBox");
  // Report metrics if we haven't reported in a while
  if ((filter.value != "") &&
      (Date.now() - gFriendListController.filterLastFiltered) > 
       FRIEND_FILTER_METRICS_INTERVAL)
  {
    var pst = document.getElementById("peopleServiceTabs");

    var tabName = "all";
    if (pst.selectedServiceId) {
      tabName = CC[pst.selectedServiceId]
                .getService(CI.flockISocialWebService).shortName;
    }

    var filterSize = filter.value.length;
    _metrics.report("PeopleSidebar-FriendsFiltered",
                    [{results: gFriendListController.filterNumResults,
                     size: filterSize,
                     selectedService: tabName}]);
  }
};

PeopleSidebarController.prototype.sortFriendList =
function PeopleSidebarController_sortFriendList() {
  var sortBy = document.getElementById("peopleSidebar")
                       .getAttribute("sortmode");
  gFriendListController.sort((sortBy == "recency") ? SORT_BY_RECENCY
                                                   : SORT_BY_NAME);
  document.getElementById("friendList").selectedIndex = 0;
};

PeopleSidebarController.prototype.sortFriends =
function PeopleSidebarController_sortFriends(aElement) {
  // Set the 'sort' mode and persist the state in localstore.rdf
  var sortBy = (aElement.id == "sortByRecency") ? "recency" : "name";
  document.getElementById("peopleSidebar").setAttribute("sortmode", sortBy);
  this.sortFriendList();
  _metrics.report("PeopleSidebar-FriendsSorted",
                  { sort: sortBy, source: aElement.id });
};

PeopleSidebarController.prototype.toggleView =
function PeopleSidebarController_toggleView(aElement) {
  // Set the 'view' mode and persist the state in localstore.rdf
  var view = (aElement.id == "fullView") ? "full" : "compact";
  document.getElementById("peopleSidebar").setAttribute("viewmode", view);
  _metrics.report("PeopleSidebar-Options-SwitchDisplayMode",
                  { mode: view, source: aElement.id });
};

function performActionSpec(aActionSpec, aFriendURN, aAccountURN) {
  _logger.debug("performActionSpec");
  _logger.debug("aActionSpec = " + aActionSpec);
  _logger.debug("aFriendURN = " + aFriendURN);
  // The action spec has four possible values:
  //  "newtab:<url>" - open a new tab with the specified URL
  //  "newwindow:<url>" - open a new window with the specified URL
  //  "mediaquery:<query>" - open the media bar with the specified stream
  //  "mediabar:???" TODO - undocumented.
  //  "method:<methodselector>:<methodAction>" - QI the service for the
  //      flockISocialAccount interface and then call the method indicated
  //      by the selector. The action is passed untouched to the called method.

  // Parse type and action out of the spec
  var colonIndex = aActionSpec.indexOf(":");
  if (colonIndex == -1) {
    _logger.debug("Invalid action spec: " + aActionSpec);
    return;
  }
  var specType = aActionSpec.substring(0, colonIndex);
  var specAction = aActionSpec.substring(colonIndex + 1);

  // Handle each scenario
  switch (specType) {
    case "newtab":
    case "newwindow":
      // Some actions may use the standard %currentURL% substring, for which we
      // should substitute the URL of the current page.
      var currentURL = _wm.getMostRecentWindow("navigator:browser")
                          .document.getElementById("content")
                          .contentDocument.URL;

      var where = specType.substring(3);
      loadURIIn(specAction.replace("%currentURL%", currentURL), where);
      break;

    case "mediaquery":
      // Load results of an arbitrary query in the Media Bar.
      var identity = _coop.get(aFriendURN);
      var srvName = identity.getParent().account.service.name;
      var query = new QueryHelper(specAction);
      query.user = identity.accountId;
      query.username = identity.name;
      var queryStr = query.stringVal;
      _logger.debug("Opening media bar with query: '" + queryStr + "'");
      _openMediaBar(srvName, queryStr, identity.name);
      break;

    case "method":
      // Split out the method selector and action.
      var methodSelector;
      var methodAction;
      var actionArray = specAction.split(":");
      if (actionArray.length == 1) {
        methodSelector = specAction;
        methodAction = "n/a";
      } else {
        methodSelector = actionArray[0];
        methodAction = actionArray[1];
      }

      // Determine which service this friend is from and get the
      // flockISocialAccount interface on the associated account.
      var identity = _coop.get(aFriendURN);
      var serviceId = (methodSelector == "twitter")
                        ? TWITTER_CONTRACT_ID : identity.serviceId;
      var srvAcct = CC[serviceId].getService(CI.flockIWebService)
                                 .getAccount(aAccountURN)
                                 .QueryInterface(CI.flockISocialAccount);

      // Execute the appropriate method.
      _logger.debug("Performing action: " + methodSelector);
      switch (methodSelector) {
        case "shareFlock":
          srvAcct.shareFlock(aFriendURN);
          break;
        case "shareWithFriend":
          srvAcct.shareWithFriend(aFriendURN, null);
          break;
        case "friendAction":
          srvAcct.friendAction(methodAction, aFriendURN, null);
          break;
        case "twitter":
          var psTabs = document.getElementById("peopleServiceTabs");
          if (psTabs.isAllCardSelected) {
            // Select the Twitter tab if we are viewing all services
            psTabs.selectTabByServiceId(serviceId);
            // Make sure the friends view is selected
            setStreamView(serviceId, "friends");
            // Scroll to the friendcard in question
            var friendsList = document.getElementById("friendList");
            if (friendsList && friendsList.selectedIndex != -1) {
              friendsList.scrollToIndex(friendsList.selectedIndex);
            }
          }
          // Get the selected friendcard
          var friendCard = null;
          // Check which list is being viewed
          var listDeck = document.getElementById("listDeck");
          if (listDeck && listDeck.getAttribute("selectedIndex") == "0") {
            var friendsList = document.getElementById("friendList");
            if (friendsList && friendsList.selectedIndex != -1) {
              friendCard = friendsList.selectedItem;
            }
          } else {
            var activityList = document.getElementById("activityList");
            if (activityList && activityList.selectedIndex != -1) {
              friendCard = activityList.selectedItem;
            }
          }
          if (friendCard) {
            var meCard = document.getElementById("mecard:" + serviceId);
            switch (methodAction) {
              case "retweet":
                var statusText;
                // Check if this is already a re-tweet
                if (friendCard.tagline.match(/^rt\W*@/i)) {
                  // Preserve the original re-tweet
                  statusText = friendCard.tagline;
                } else {
                  // Append re-tweet info
                  statusText = "RT @" + friendCard.displayName
                             + " " + friendCard.tagline;
                }
                meCard.engageStatusChange(null, statusText, false);
                meCard.setAttribute("inReplyTo", friendCard.taglineUrl);
                break;
              case "reply":
                meCard.engageStatusChange(null,
                                          "@" + friendCard.displayName + " ",
                                          false);
                meCard.setAttribute("inReplyTo", friendCard.taglineUrl);
                break;
              case "directMessage":
                meCard.engageStatusChange(null,
                                          "d " + friendCard.displayName + " ",
                                          true);
                meCard.setAttribute("inReplyTo", friendCard.taglineUrl);
                break;
              default:
                _logger.error("Invalid Twitter action: '" + methodAction +"'");
                break;
            }
          }
          break;
        default:
          // Unrecognized action.
          _logger.error("Invalid action: '" + specAction
                        + "' -- Fix in a web detective file");
          break;
      }
      break;

      case "mediabar":
        colonIndex = specAction.indexOf(":");
        if (colonIndex == -1) {
          _logger.debug("Invalid method action: " + specAction);
          return;
        }
        var queryType = specAction.substring(0, colonIndex);
        var identity = _coop.get(aFriendURN);
        var srvName = identity.getParent().account.service.name;
        if (!queryType) {
          var query = new QueryHelper();
          query.user = identity.accountId;
          query.username = identity.name;
          query = query.stringVal;
        } else {
          query = queryType + ":" + identity.accountId;
        }
        _openMediaBar(srvName, query, identity.name);
        break;

    default:
      _logger.debug("Unknown specType: " + specType);
      return;
  }
}

// Assemble the tag line tooltip for a mecard or friendcard
function assembleTooltip(aNode) {
  _logger.debug("assembleTooltip");
  var tooltipDisplayNameBroadcaster = document.getElementById("tooltipDisplayNameBroadcaster");
  tooltipDisplayNameBroadcaster.setAttribute("value", aNode.displayName);

  var tooltipTaglineBroadcaster = document.getElementById("tooltipTaglineBroadcaster");
  tooltipTaglineBroadcaster.setAttribute("value", aNode.tagline);

  // Set the height of the tooltip popup (for some reason the tooltip popup
  // does not expand to fit the contents without doing this)
  var tooltip = document.getElementById("taglineTooltip");
  var container = document.getElementById("ttipOuterContainer");
  tooltip.height = container.boxObject.height + 10;
}

function getCurrentTabURL() {
  _logger.debug("getCurrentTabURL()")
  var win = _wm.getMostRecentWindow("navigator:browser");
  var result = null;
  if (win) {
    var gBrowser = win.getBrowser();
    result = gBrowser.currentURI.spec;
  }
  _logger.debug("result = " + result);
  return result;
}

function toggleObservers(aToggleOn) {
  _logger.debug("toggleObservers(" + aToggleOn + ")");
  var accountService = CC["@flock.com/account-service;1"]
                       .getService(CI.flockIAccountService);
  if (aToggleOn) {
    accountService.addObserver(gAccountObserver);
  } else {
    accountService.removeObserver(gAccountObserver);
  }
}

// Auto-remember account when we click a service for discovery
function rememberAccount(aSvcURN) {
  _logger.debug("rememberAccount: " + aSvcURN);
  var wsMgr = CC["@flock.com/webservice-manager;1"]
              .getService(CI.flockIWebServiceManager);
  wsMgr.autoKeepAccountForService(aSvcURN);
}

// Load a url in a new tab or window
function loadURIIn(aUri, aWhere) {
  _logger.debug("loadURIIn('" + aUri + "', '"+ aWhere +"')");
  var win = _wm.getMostRecentWindow("navigator:browser");
  if (win) {
    win.openUILinkIn(aUri, aWhere);
  }
}

function hoverURI(aLinkNode, aEvent) {
  function cb_setStatus(aStatus) {
    var statusbar = window.top.document.getElementById("statusbar-display");
    statusbar.label = aStatus;
  }
  if (aLinkNode) {
    FlockURLUtils.hoverLink(aLinkNode, cb_setStatus);
  } else {
    cb_setStatus("");
  }
}

function onKeyup(aEvent) {
  if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN &&
      aEvent.originalTarget.selectedItem)
  {
    aEvent.originalTarget.selectedItem.loadProfile(aEvent);
  }
}

function loadURI(aUri, aEvent) {
  // Suppress mouse right-click
  if (aEvent.button == 2) {
    return;
  }
  var win = _wm.getMostRecentWindow("navigator:browser");
  win.FlockTabs.openURL(aUri, aEvent);
}

function logout(aNode, aEvent) {
  _logger.debug("logout()");
  var serviceId = aNode.getAttribute("serviceId");
  CC[serviceId].getService(CI.flockIWebService).logout();
}

// Get a reference to a mecard button for a given service in the People Sidebar
function getServiceButton(aServiceId, aAnonId) {
  if (aServiceId && aAnonId) {
    let psTabs = document.getElementById("peopleServiceTabs");
    let mecard = document.getAnonymousElementByAttribute(
                   psTabs, "id", "mecard:" + aServiceId);
    return document.getAnonymousElementByAttribute(mecard, "anonid", aAnonId);
  }
  return null;
}

// Display a media item in the media bar or reply from twitter
function onFirstButton(aNode, aEvent) {
  _logger.debug("onFirstButton()");
  var name = aNode.getAttribute("displayName");
  _logger.debug("name: " + name);
  var accountId = aNode.getAttribute("accountId");
  _logger.debug("accountId: " + accountId);

  var serviceId = aNode.getAttribute("serviceId");
  if (serviceId == TWITTER_CONTRACT_ID) {
    var dmBtn = getServiceButton(serviceId, "dmessages-button-container");
    var actionSpec = "method:twitter:"
                   + (dmBtn && dmBtn.hasAttribute("selected") ? "directMessage"
                                                              : "reply");
    performActionSpec(actionSpec,
                      aNode.getAttribute("uri"),
                      aNode.getAttribute("accountURN"));
    return;
  }
  var svc = CC[serviceId].getService(CI.flockISocialWebService);
  svc.markAllMediaSeen(aNode.id);

  var query = new QueryHelper();
  query.user = accountId;
  query.username = name;

  _metrics.report("MediaBar-Open", "FriendCard");
  _openMediaBar(svc.shortName, query.stringVal, name);

  // Pop message bar
  var favItem = _coop.MediaQuery.find({query: query.stringVal, isPollable: true});
  if (favItem.length == 0) {
    var msg = flockGetString("favorites/favorites",
                             "flock.favs.media.discovery");
    var nBox = getNotificationBox();
    nBox.appendUniqueNotification(msg,
                                  "media-discovery-info",
                                  "chrome://flock/skin/favorites/media-icon.png",
                                  nBox.FLOCK_PRIORITY_LOW,
                                  null,
                                  null);
  }
}

function getNotificationBox() {
  var wm = CC["@mozilla.org/appshell/window-mediator;1"]
           .getService(CI.nsIWindowMediator);
  var win = wm.getMostRecentWindow("navigator:browser");
  if (win) {
    var gBrowser = win.getBrowser();
    var notificationBox = gBrowser.getNotificationBox();

    return notificationBox;
  }

  return null;
}

function _openMediaBar(aShortName, aStringValue, aName)
{
  var win = _wm.getMostRecentWindow("navigator:browser");
  if (win) {
    win.FlockTopbar.selectById("flock_MediabarTopbarBroadcaster", true);
    win.mediabar.loadQuery(aShortName, aStringValue, aName);
  }
}

function serviceTabClicked() {
  _logger.debug("serviceTabClicked");
  if (!gLoaded) {
    return;
  }

  var pst = document.getElementById("peopleServiceTabs");

  // Keep track of the tab selected so that we can select if the user opens
  // and closes the sidebar
  pst.lastServiceIdSelected = pst.selectedServiceId;

  // Report which tab was selected to the metrics service
  var tabName = "all";
  if (pst.selectedServiceId != null) {
    tabName = CC[pst.selectedServiceId].getService(CI.flockISocialWebService)
              .shortName;
  }
  _metrics.report("PeopleSidebar-ServiceClick", tabName);

  updateFriendListFilter();
  doLayout(true);
}

function updateFriendListFilter() {
  // Set the currentServiceId attribute on the friendlist as this will be
  // referenced in the css so that only friends appropriate to the selected
  // tab are shown
  var pst = document.getElementById("peopleServiceTabs");
  var friendsList = document.getElementById("friendList");
  if (pst.selectedServiceId && CC[pst.selectedServiceId]) {
    friendsList.setAttribute("currentServiceId", pst.selectedServiceId);
  } else {
    friendsList.removeAttribute("currentServiceId");
  }

  // Set the friend list view
  var view = (pst.selectedServiceId == TWITTER_CONTRACT_ID)
           ? pst.lastViewSelected : null;
  if (view) {
    pst.selectedMecard.setAttribute("latestStream", view);
  }
  setStreamView(pst.selectedServiceId, view);
}

function setFriendViewState(aId) {
  document.getElementById(aId).setAttribute("checked", true);
}

function onLoad() {
  _logger.debug("onLoad start");

  var sidebarHelp = window.top.document.getElementById("sidebar-helpbutton");
  sidebarHelp.hidden = false;

  gFriendListController = new FriendListController();
  var acctsExist = gFriendListController.init();

  gPeopleSidebarController = new PeopleSidebarController();
  gPeopleSidebarController.init();

  _metrics.report("PeopleSidebar-Opened", null);
  // Get the persisting sortmode and viewmode
  var ps = document.getElementById("peopleSidebar");
  setFriendViewState((ps.getAttribute("sortmode") == "recency") ? "sortByRecency"
                                                                : "sortByName");
  gPeopleSidebarController.sortFriendList();
  setFriendViewState((ps.getAttribute("viewmode") == "full") ? "fullView"
                                                             : "compactView");
  var psTabs = document.getElementById("peopleServiceTabs");

  // Only build tabs if there are accounts to be shown
  if (acctsExist && psTabs && psTabs.refresh) {
    // Accounts exist, build the tabs
    psTabs.refresh();
  }
  toggleObservers(true);

  if (psTabs) {
    var psbBroadcaster = top.document.getElementById("flockPeopleSidebarBroadcaster");
    if (psbBroadcaster.hasAttribute("newAccountId")) {
      // Select the service tab for a newly added account
      psTabs.selectTabByAccountId(psbBroadcaster.getAttribute("newAccountId"));
      psbBroadcaster.removeAttribute("newAccountId");
      // the account will be refreshed on authentication.
    } else if (psTabs.lastServiceIdSelected) {
      // Select the last selected service tab
      psTabs.selectTabByServiceId(psTabs.lastServiceIdSelected);
      setTimeout("refreshSelectedAccount();", 2000);
    }
  }
  updateFriendListFilter();

  gPeopleSidebarController.refreshDiscoveryPanel();
  doLayout(false);
  //setTimeout("gFriendListController.init();", 100);
  gLoaded = true;
  _logger.debug("onLoad end");
}

function refreshSelectedAccount() {
  var psTabs = document.getElementById("peopleServiceTabs");
  if (psTabs.selectedAccount) {
    CC["@flock.com/poller-service;1"]
    .getService(CI.flockIPollerService)
    .forceRefresh(psTabs.selectedAccount);
  }
}

function refreshNoFriendsPanel(aAuthenticatedAccountsWithNoFriends) {
  _logger.debug("refreshNoFriendsPanel start");

  var serviceTabs = document.getElementById("peopleServiceTabs");

  var noFriendsMsgBody =
    document.getElementById("noFriendsMessageBodyContainer");

  // Remove existing content from the panel.
  while (noFriendsMsgBody.hasChildNodes()) {
    noFriendsMsgBody.removeChild(noFriendsMsgBody.lastChild);
  }

  var appName = brandGetString("brand", "brandShortName");
  var msg;
  if (serviceTabs.isAllCardSelected) {
    msg = flockGetString(PEOPLE_STR_BUNDLE,
                         "flock.people.sidebar.noFriendsMessage.multiService.part1",
                         [appName]);
  } else {
    msg = flockGetString(PEOPLE_STR_BUNDLE,
                         "flock.people.sidebar.noFriendsMessage.singleService.part1");
  }

  var div = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
  div.appendChild(document.createTextNode(msg));
  div.setAttribute("class", "no-friends-message-body");
  noFriendsMsgBody.appendChild(div);

  var linksContainer = document.createElement("vbox");
  linksContainer.setAttribute("class", "service-link-container");
  noFriendsMsgBody.appendChild(linksContainer);

  // Generate the links.
  for each (var account in aAuthenticatedAccountsWithNoFriends) {
    account = account.QueryInterface(CI.flockISocialAccount);
    var service = account.getService().QueryInterface(CI.flockIWebService);
    var addFriendsURL = account.getInviteFriendsURL();

    _logger.debug("addFriendsURL = " + addFriendsURL);

    if (serviceTabs.isAllCardSelected) {
      var hbox = document.createElement("hbox");
      hbox.setAttribute("class", "service-entry");
      var favicon = document.createElement("image");
      favicon.setAttribute("src", service.icon);
      hbox.appendChild(favicon);

      var label = document.createElement("label");

      label.setAttribute("value",
        flockGetString(PEOPLE_STR_BUNDLE,
                       "flock.people.sidebar.noFriendsMessage.addFriendLink.multiService",
                       [service.title]));
      label.setAttribute("onclick",
                         "loadURI('" + addFriendsURL + "', event)");
      label.setAttribute("class", "service-entry-link");
      hbox.appendChild(label);
      linksContainer.appendChild(hbox);
    } else {
      var div = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
      var spanPart1 = document.createElementNS("http://www.w3.org/1999/xhtml", "span");
      spanPart1.appendChild(
        document.createTextNode(
          flockGetString(PEOPLE_STR_BUNDLE,
                         "flock.people.sidebar.noFriendsMessage.addFriendLink.singleService.part1",
                         [service.title]) + " "));
      spanPart1.setAttribute("onclick",
                             "loadURI('" + addFriendsURL + "', event)");
      spanPart1.setAttribute("class", "service-entry-link");

      div.appendChild(spanPart1);

      var spanPart2 = document.createElementNS("http://www.w3.org/1999/xhtml", "span");
      spanPart2.appendChild(
        document.createTextNode(
          flockGetString(PEOPLE_STR_BUNDLE,
                         "flock.people.sidebar.noFriendsMessage.addFriendLink.singleService.part2",
                          [appName])));
      div.appendChild(spanPart2);
      linksContainer.appendChild(div);
    }
  }

  _logger.debug("refreshNoFriendsPanel end");
}

// doLayout determines which of the following is displayed:
// 1. Discovery panel - no logged in accounts
// 2. No friends panel - account(s) logged in but no friends to display
// 3. Friend list - there are friends to display
function doLayout(aCheckForNoFriends) {
  _logger.debug("doLayout");
  var sidebarDeck = document.getElementById("peopleSidebarDeck");
  var serviceTabs = document.getElementById("peopleServiceTabs");
  var authenticatedAccountsWithNoFriends = [];
  var authenticatedAccounts = 0;
  var services = gPeopleSidebarController.getServicesWithConfiguredAccounts();

  // Handle the ALL card selection.
  if (serviceTabs.isAllCardSelected) {
    for each (var svc in services) {
      var accountUrn = svc.getAuthenticatedAccount();

      _logger.debug("Checking account: " + accountUrn);

      if (!accountUrn) {
        continue;
      }
      authenticatedAccounts++;

      if (!aCheckForNoFriends) {
        continue;
      }
      var account = svc.getAccount(accountUrn);

      // We can only determine that an account has no friends once we know it
      // has completed refreshing.
      if (account.getParam("isRefreshing")) {
        _logger.debug("Account is still refreshing so skipping: " + account.urn);
        continue;
      }

      var friendsEnum = account.QueryInterface(CI.flockISocialAccount)
                               .enumerateFriends();
      if (!friendsEnum.hasMoreElements()) {
        authenticatedAccountsWithNoFriends.push(account);
      }
    }
  } else if (services.length && serviceTabs.selectedServiceId) {
    // Handle the case where a specific service/account is selected.
    var svc = CC[serviceTabs.selectedServiceId]
              .getService(CI.flockILoginWebService);
    var accountUrn = svc.getAuthenticatedAccount();
    _logger.debug("Checking account: " + accountUrn);
    if (accountUrn) {
      _logger.debug("Account is logged in.");
      authenticatedAccounts++;
      if (aCheckForNoFriends) {
        // If an account is still refreshing it is premature to determine whether
        // it has friends or not
        var account = svc.getAccount(accountUrn);
        if (!account.getParam("isRefreshing")) {
          var friendsEnum = account.QueryInterface(CI.flockISocialAccount)
                                   .enumerateFriends();
          if (!friendsEnum.hasMoreElements()) {
            _logger.debug("Account has no friends.");
            authenticatedAccountsWithNoFriends.push(account);
          }
        }
      }
    } else {
      _logger.debug("Account is logged out.");
    }
  }

  _logger.debug("authenticatedAccounts = " + authenticatedAccounts);
  if (authenticatedAccounts == 0) {
    _logger.debug("All accounts logged out so showing discovery panel");
    sidebarDeck.selectedIndex = DECK_SHOWING_DISCOVERY_PANEL;

    // Show OOBE Tutorial link? Suppress also if flash isn't present.
    if (!FlockPrefsUtils.getBoolPref(OOBE_ENABLED) ||
        !detectFlash())
    {
      // Don't show it.
      document.getElementById("tutorialLinkID")
              .setAttribute("class", "discovery-item-hidden");
    }
    return;
  }

  _logger.debug("authenticatedAccountsWithNoFriends.length = "
                + authenticatedAccountsWithNoFriends.length);
  if (aCheckForNoFriends &&
      authenticatedAccountsWithNoFriends.length == authenticatedAccounts)
  {
    _logger.debug("Showing no friends panel.");
    refreshNoFriendsPanel(authenticatedAccountsWithNoFriends);
    sidebarDeck.selectedIndex = DECK_SHOWING_NO_FRIENDS_PANEL;
    return;
  }

  _logger.debug("Showing the friends list");
  sidebarDeck.selectedIndex = DECK_SHOWING_FRIENDS_LIST;
  return;
}

function getFormattedTime(aInput) {
  return FlockDateFormatter.getFormattedTime(new Date(aInput * 1000));
}

function unLoad() {
  _logger.debug("unLoad");

  var sidebarHelp = window.top.document.getElementById("sidebar-helpbutton");
  sidebarHelp.hidden = true;

  if (gPeopleSidebarController) {
    gPeopleSidebarController.uninit();
    gPeopleSidebarController = null;
  }

  if (gFriendListController) {
    toggleObservers(false);
    gFriendListController.uninit();
    _metrics.report("PeopleSidebar-Close", null);
    gFriendListController = null;
  }
}

// localizeElement is a helper function that takes a xul element and sets the
// value of a specified attribute to a localized resource string. If aServiceId
// is provided then the function will pull the string from the string bundle
// for the service otherwise it used the default string bundle for the people
// sidebar (people.properties)
// aElement: target element
// aAttributeName: attribute that will be set
// aStringName: resource string name
// aServiceId: service id used for a service specific resource
function localizeElement(aElement,
                         aAttributeName,
                         aStringName,
                         aServiceId)
{
  var localizedString = "";
  try {
    // Get the service specific string if there is one
    localizedString = CC[aServiceId].getService(CI.flockIWebService)
                                    .getStringBundle()
                                    .GetStringFromName(aStringName);
    aElement.setAttribute(aAttributeName, localizedString);
  } catch (ex) {
    // Try from the people bundle.
    try {
      localizedString = _sb.GetStringFromName(aStringName);
      aElement.setAttribute(aAttributeName, localizedString);
    } catch (ex) {
      // String doesn't exist in either bundle. Nothing we
      // can do.
    }
  }
}

// linkifyDescription is a helper function that sets up a description
// element such that a portion of the display value is a link
// aNode: the description node that we are setting up
// aDisplayText: the display text
// aLinkedText: the portion of display text that will be a link
// aClickedAction: the handler for the click of the link
function linkifyDescription(aNode,
                            aDisplayText,
                            aLinkedText,
                            aClickedAction)
{
  // Remove existing node content.
  while (aNode.lastChild) {
    aNode.removeChild(aNode.lastChild);
  }

  var pos = aDisplayText.indexOf(aLinkedText);

  // We break aDisplayText into 3 parts.
  var prependToLinkText = aDisplayText.substring(0, pos);
  var appendToLinkText =
    aDisplayText.substring(pos + aLinkedText.length);

  if (prependToLinkText) {
    aNode.appendChild(document.createTextNode(prependToLinkText));
  }

  var link =
    document.createElementNS("http://www.w3.org/1999/xhtml", "a");
  link.setAttribute("onmouseup", aClickedAction);
  link.setAttribute("href", "return false;");
  link.setAttribute("onclick", "return false;");
  link.onmouseover = function clearStatusBarText() {
    window.top.document.getElementById("statusbar-display").label = "";
  };
  link.innerHTML = aLinkedText;

  aNode.appendChild(link);
  aNode.appendChild(document.createTextNode(appendToLinkText));
}

// This is the DND observer for the Post Link button
var mecardPostLinkDNDObserver = {
  canDrop: function mc_canDrop(aEvent, aDragSession) {
    // Cannot drop if dragged from outside browser or text
    var node = aDragSession.sourceNode;
    if (!node) {
      return false;
    }

    var isTwitter = false;
    var meCard = document.getBindingParent(aEvent.target);
    if (meCard) {
      isTwitter = (meCard.serviceId == TWITTER_CONTRACT_ID);
    }
    var text_node = 3;
    var linkDragged = true;
    if ((node.nodeType == text_node) && !isTwitter) {
      var top = _wm.getMostRecentWindow("navigator:browser");
      var selection = top.getSelection();
      if (selection.toString().length > 0) {
        linkDragged = false;
      }
    }
    return linkDragged;
  },

  canHandleMultipleItems: false,

  onDragOver: function mc_onDragOver(aEvent, aFlavor, aSession) {
    _logger.debug("mecardPostLinkDNDObserver.onDragOver()");
  },

  onDragEnter: function mc_onDragEnter(aEvent, aSession) {
    _logger.debug("mecardPostLinkDNDObserver.onDragEnter()");
  },

  onDragExit: function mc_onDragExit(aEvent, aSession) {
    _logger.debug("mecardPostLinkDNDObserver.onDragExit()");
  },

  onDrop: function mc_onDrop(aEvent, aDropData, aSession) {
    _logger.debug("mecardPostLinkDNDObserver.onDrop()");
    aEvent.stopPropagation();

    // aEvent.target is the "Post Link" button
    var meCard = document.getBindingParent(aEvent.target);

    // Get account and service info
    var serviceId = meCard.serviceId;
    var accountURN = meCard.c_account.id();

    try {
      // Get transferable from session
      var richSvc = CC["@flock.com/rich-dnd-service;1"]
                    .getService(CI.flockIRichDNDService);
      var transferable = richSvc.getRichSelection(aSession,
                                                  aDropData.flavour.contentType,
                                                  aDropData.data);

      // Don't continue if we don't have any data
      if (!transferable) {
        _logger.debug("No transferable! Exiting.");
        return;
      }

      if (serviceId == TWITTER_CONTRACT_ID) {
        var flavors = [FLOCK_RICH_CONTENT_FLAVOUR,
                       "text/unicode",
                       "text/x-moz-url"];
        var message = richSvc.getMessageFromTransferable(transferable,
                                                         flavors.length,
                                                         flavors);
        if (message && message.body) {
          top.document.getElementById("flockPeopleSidebarBroadcaster")
                      .setAttribute("flockAction", "dnd");
          CC["@mozilla.org/observer-service;1"]
            .getService(CI.nsIObserverService)
            .notifyObservers(null,
                             "flock-people-trigger-status-change",
                             message.body);
        }
      } else {
        // Perform service-specific post-link action
        CC[serviceId].getService(CI.flockIWebService)
                     .getAccount(accountURN)
                     .QueryInterface(CI.flockISocialAccount)
                     .postLink(transferable);
      }
    } catch (ex) {
      // While we work on getRichSelection() ...
      _logger.debug("KABOOM = " + ex);
    }
  },

  getSupportedFlavours: function mc_getSupportedFlavours() {
    _logger.debug("mecardPostLinkDNDObserver.getSupportedFlavours()");
    var flavors = new FlavourSet();
    flavors.appendFlavour(FLOCK_RICH_CONTENT_FLAVOUR);
    flavors.appendFlavour("text/x-moz-url");
    flavors.appendFlavour("text/unicode");
    flavors.appendFlavour("text/html");
    return flavors;
  }
}

var personcardDNDObserver = {
  canDrop: function pc_canDrop(aEvent, aDragSession) {
    // Cannot drop if dragged from outside browser
    return (aDragSession.sourceNode != null);
  },

  canHandleMultipleItems: false,

  onDragOver: function pc_onDragOver(aEvent, aFlavor, aSession) {
    _logger.debug("personcardDNDObserver.onDragOver()");
  },

  onDragEnter: function pc_onDragEnter(aEvent, aSession) {
    _logger.debug("personcardDNDObserver.onDragEnter()");
  },

  onDragExit: function pc_onDragExit(aEvent, aSession) {
    _logger.debug("personcardDNDObserver.onDragExit()");
  },

  onDrop: function pc_onDrop(aEvent, aDropData, aSession) {
    _logger.debug("personcardDNDObserver.onDrop()");
    _metrics.report("PeopleSidebar-FriendCardDnD",
                    aDropData.flavour.contentType);
    aEvent.stopPropagation();

    try {
      // Get transferable from session
      var richSvc = CC["@flock.com/rich-dnd-service;1"]
                    .getService(CI.flockIRichDNDService);
      var transferable = richSvc.getRichSelection(aSession,
                                                  aDropData.flavour.contentType,
                                                  aDropData.data);
      if (!transferable) {
        return;
      }

      var serviceId = aEvent.target.getAttribute("serviceId");
      if (serviceId == TWITTER_CONTRACT_ID) {
        top.document.getElementById("flockPeopleSidebarBroadcaster")
                    .setAttribute("flockAction", "dnd");
        var flavors = [FLOCK_RICH_CONTENT_FLAVOUR,
                       "text/unicode",
                       "text/x-moz-url"];
        var message = richSvc.getMessageFromTransferable(transferable,
                                                         flavors.length,
                                                         flavors);
        var psTabs = document.getElementById("peopleServiceTabs");
        var prefix = (psTabs.lastViewSelected == "dmessage") ? "d " : "@";
        var statusText =
          prefix + aEvent.target.displayName + " " + message.body;
        CC["@mozilla.org/observer-service;1"]
          .getService(CI.nsIObserverService)
          .notifyObservers(null,
                           "flock-people-trigger-status-change",
                           statusText);
      } else { 
        // We know that aEvent.target is a friendcard
        var friendURN = aEvent.target.id;
        var identity = _coop.get(friendURN);
        var accountURN = identity.getParent().account.id();

        // Perform service-specific sharing action
        CC[serviceId].getService(CI.flockIWebService)
                     .getAccount(accountURN)
                     .QueryInterface(CI.flockISocialAccount)
                     .shareWithFriend(friendURN, transferable);
      }

      // Select the friend item
      document.getElementById("friendList").selectedItem = aEvent.target;
    } catch (ex) {
      // While we work on getRichSelection() ...
      _logger.debug("KABOOM = " + ex);
    }
  },

  getSupportedFlavours: function pc_getSupportedFlavours() {
    _logger.debug("personcardDNDObserver.getSupportedFlavours()");
    var flavors = new FlavourSet();
    flavors.appendFlavour(FLOCK_RICH_CONTENT_FLAVOUR);
    flavors.appendFlavour("text/x-moz-url");
    flavors.appendFlavour("text/unicode");
    flavors.appendFlavour("text/html");
    return flavors;
  }
}

var statusEntryDNDObserver = {
  canDrop: function se_canDrop(aEvent, aDragSession) {
    // Cannot drop if dragged from outside browser
    return (aDragSession.sourceNode != null);
  },

  canHandleMultipleItems: false,
  lastHandledDropEventTimestamp: null,

  onDragOver: function se_onDragOver(aEvent, aFlavor, aSession) {
    _logger.debug("statusEntryDNDObserver.onDragOver()");
  },

  onDragEnter: function se_onDragEnter(aEvent, aSession) {
    _logger.debug("statusEntryDNDObserver.onDragEnter()");
  },

  onDragExit: function se_onDragExit(aEvent, aSession) {
    _logger.debug("statusEntryDNDObserver.onDragExit()");
  },

  onDrop: function se_onDrop(aEvent, aDropData, aSession) {
    _logger.debug("statusEntryDNDObserver.onDrop()");
    aEvent.stopPropagation();
    var textbox = document.getElementById("statusEntry");

    try {
      // Get transferable from session
      var richSvc = CC["@flock.com/rich-dnd-service;1"]
                    .getService(CI.flockIRichDNDService);
      var transferable = richSvc.getRichSelection(aSession,
                                                  aDropData.flavour.contentType,
                                                  aDropData.data);
      if (!transferable) {
        return;
      }

      _metrics.report("PeopleSidebar-StatusEntryDnD",
                      aDropData.flavour.contentType);

      var flavors = ["text/unicode"];
      var message = richSvc.getMessageFromTransferable(transferable,
                                                       flavors.length,
                                                       flavors);
      // If the content we are dropping is a url, we need to ensure it doesn't
      // get malformed
      var content = " " + message.body + " ";

      // Sometimes we get the same drop event come in multiple times so we keep
      // track of the most recent one to ensure that we can ignore any event
      // that has already been handled
      if (statusEntryDNDObserver.lastHandledDropEventTimestamp == aEvent.timeStamp) {
        // Ignored event
        return;
      }
      this.lastHandledDropEventTimestamp = aEvent.timeStamp;
 
      // If the user has selected a particular cursor position in the status
      // message, insert the media item url at that position
      if (textbox.selectionStart == textbox.selectionEnd) {
        textbox.value = textbox.value.substring(0, textbox.selectionStart)
                      + content
                      + textbox.value.substring(textbox.selectionStart);
      } else {
        // If no cursor position has been selected, just add it to the end of
        // the status message
        textbox.value += content;
      }
      textbox.value = textbox.value.replace(/^\s+|\s+$/g, "");
    } catch (ex) {
      // While we work on getRichSelection() ...
      _logger.debug("KABOOM = " + ex);
    }
  },

  getSupportedFlavours: function se_getSupportedFlavours() {
    _logger.debug("statusEntryDNDObserver.getSupportedFlavours()");
    var flavors = new FlavourSet();
    flavors.appendFlavour(FLOCK_RICH_CONTENT_FLAVOUR);
    flavors.appendFlavour("text/x-moz-url");
    flavors.appendFlavour("text/unicode");
    flavors.appendFlavour("text/html");
    return flavors;
  }
};

window.addEventListener("load", onLoad, false);
window.addEventListener("unload", unLoad, false);

/** Drag and drop code cloned from /shelf/content/shelf.js and modified */
const NS_XHTML = "http://www.w3.org/1999/xhtml";
const MARK_SELECTION_START = "\u200B\u200B\u200B\u200B\u200B";
const MARK_SELECTION_END = "\u200C\u200C\u200C\u200C\u200C";

function flock_getMarkedUpSelection(aSession) {
  var topWindow = _wm.getMostRecentWindow("navigator:browser");
  var data = "";
  var node = aSession.sourceNode;
  var serializer = new XMLSerializer();
  var parser = new DOMParser();

  var flavourset = new FlavourSet();
  flavourset.appendFlavour("text/html");
  flavourset.appendFlavour("text/unicode");
  flavourset.appendFlavour("text/x-moz-url");

  // For dragging from outside the browser (not supported)
  if (!node) {
    return null;
  }

  var contentEl = topWindow.document.getElementById("content");
  var webPageUrl = contentEl.contentDocument.URL;
  var webPageTitle = contentEl.contentTitle;

  if (webPageTitle.length == 0) {
    webPageTitle = location;
  }
  if (webPageUrl && webPageUrl.match(/^chrome:\/\/.+/)) {
    webPageUrl = "";
  }

  var contentDetector = new nsContentDetector();
  contentDetector.setTarget(node);

  // Images
  if (contentDetector.onImage) {
    var imageTitle = node.alt;
    imageURL = contentDetector.imageURL;
    // Don't insert a stupid image containing Javascript
    if (imageURL.match("javascript:")) {
      return null;
    }
    // Bug 3756, when the image alone is displayed in the browser
    if (imageURL == webPageUrl) {
      imageTitle = imageTitle.split("/").pop();
    }

    var content = "<a href='" + webPageUrl + "' title='" + imageTitle + "'>"
                + "<img border='0' alt='" + imageTitle + "' src='" + imageURL
                + "'/></a>";
    return {
      type: "image",
      title: imageTitle,
      URL: imageURL,
      content: content
    };
  }

  _logger.debug("node.nodeName: " + node.nodeName);
  switch (node.nodeName.toLowerCase()) {
    // Big hack. This is for when somebody drags the favicon onto the shelf.
    // 'image' is actually a XUL image (the favicon), while HTML images are
    // 'img'
    case "image":
      _logger.debug("node.src = " + node.getAttribute("src"));
      return {
        type: "link",
        title: webPageTitle,
        URL: webPageUrl,
        content: "<a href='" + webPageUrl + "'>" + webPageTitle + "</a>"
      };

    // Mediabar photos
    case "photo":
      header = node.title;
      header = header.replace(/%20/g, " ");
      label = node.src;
      webPageUrl = node.webPageUrl;
      return {
        type: "image",
        title: header,
        URL: webPageUrl,
        content: node.buildHTML()
      };

    // Browser tabs
    case "tab":
    case "xul:tab":
      var loc = window.top.content.document.location.toString();
      return {
        type: "link",
        title: node.label,
        URL: loc,
        content: "<a href='" + loc + "'/>"+ node.label +"</a>"
      };

    case "shelficon":
    case "richtreefolder":
      return { dragfrom: node.id };

    default:
      // Handle selections
      var selection = getSelection();

      if (selection.toString().length > 0) {
        var selection_formatted = select_src(selection);
        _logger.debug("selection_formatted.html: " + selection_formatted.html);
        data = selection_formatted.html;

        header = webPageTitle;
        label = selection;
        return {
          type: "document",
          title: webPageTitle,
          URL: webPageUrl,
          content: data
        };
      }

      if (node.nodeName.toString().toLowerCase() == "#text") {
        node = node.parentNode;
      }

      // If it is an img, make sure the src attribute is the full path
      if (node.src) {
        node.setAttribute("src", node.src);
        label = node.src;
        header = node.src;
        if (node.getAttribute("alt")) {
          header = node.getAttribute("alt");
        }
        return {
          type: "image",
          title: header,
          URL: node.src,
          content: "<a href='" + webPageUrl + "' title='" + header + "'>" +
                   "<img border='0' alt='" + header + "' src='" + node.src + "'/></a>"
        };
      }

      // If it is a link make sure the href attribute is the full path
      if (node.href) {
        node.setAttribute("href", node.href);
        label = node.href;
        header = node.textContent;
        return {
          type: "link",
          title: label,
          URL: node.href,
          content: serializer.serializeToString(node)
        };
      }
      break;
  }

  // sourceNode didn't work, let's try with dragData
  var dragData = nsTransferable.get(flavourset,
                                    nsDragAndDrop.getDragData,
                                    true)
                               .first.first;
  _logger.debug ("Flavour: " + dragData.flavour.contentType);

  switch (dragData.flavour.contentType) {
    case "text/unicode":
      return {
        type: "document",
        title: dragData.data,
        URL: "",
        content: dragData.data
      };

    case "text/html":
      return {
        type: "document",
        title: dragData.data,
        URL: "",
        content: dragData.data
      };
  }

  // Couldn't find anything
  _logger.debug("Couldn't find anything!");
  return null;
}

function getSelection() {
  var focusedWindow = document.commandDispatcher.focusedWindow;
  var winWrapper = new XPCNativeWrapper(focusedWindow,
                                        "document",
                                        "getSelection()");
  return winWrapper.getSelection();
}

function select_src(aSelection) {
  var range = aSelection.getRangeAt(0);
  var ancestorContainer = range.commonAncestorContainer;
  var doc = ancestorContainer.ownerDocument;

  var startContainer = range.startContainer;
  var endContainer = range.endContainer;
  var startOffset = range.startOffset;
  var endOffset = range.endOffset;

  // let the ancestor be an element
  if (ancestorContainer.nodeType == Node.TEXT_NODE ||
      ancestorContainer.nodeType == Node.CDATA_SECTION_NODE)
  {
    ancestorContainer = ancestorContainer.parentNode;
  }

  // for selectAll, let's use the entire document, including <html>...</html>
  // @see DocumentViewerImpl::SelectAll() for how selectAll is implemented
  try {
    if (ancestorContainer == doc.body) {
      ancestorContainer = doc.documentElement;
    }
  } catch (ex) {
  }

  // each path is a "child sequence" (a.k.a. "tumbler") that
  // descends from the ancestor down to the boundary point
  var startPath = getPath(ancestorContainer, startContainer);
  var endPath = getPath(ancestorContainer, endContainer);

  // clone the fragment of interest and reset everything to be relative to it
  // note: it is with the clone that we operate from now on
  ancestorContainer = ancestorContainer.cloneNode(true);
  startContainer = ancestorContainer;
  endContainer = ancestorContainer;
  var i;
  for (i = startPath ? startPath.length-1 : -1; i >= 0; i--) {
    startContainer = startContainer.childNodes.item(startPath[i]);
  }
  for (i = endPath ? endPath.length-1 : -1; i >= 0; i--) {
    endContainer = endContainer.childNodes.item(endPath[i]);
  }

  // add special markers to record the extent of the selection
  // note: |startOffset| and |endOffset| are interpreted either as offsets in
  // the text data or as child indices (see the Range spec)
  // (here, munging the end point first to keep the start point safe...)
  var tmpNode;
  if (endContainer.nodeType == Node.TEXT_NODE ||
      endContainer.nodeType == Node.CDATA_SECTION_NODE)
  {
    // Do some tweaks to try to avoid the view-source output to look like
    // ...<tag>]... or ...]</tag>... (where ']' marks the end of selection).
    // To get a neat output, the idea here is to remap the end point from:
    // 1. ...<tag>]...   to   ...]<tag>...
    // 2. ...]</tag>...  to   ...</tag>]...
    if ( (endOffset > 0 && endOffset < endContainer.data.length) ||
         !endContainer.parentNode ||
         !endContainer.parentNode.parentNode )
    {
      endContainer.insertData(endOffset, MARK_SELECTION_END);
    } else {
      tmpNode = doc.createTextNode(MARK_SELECTION_END);
      endContainer = endContainer.parentNode;
      if (endOffset == 0) {
        endContainer.parentNode.insertBefore(tmpNode, endContainer);
      } else {
        endContainer.parentNode.insertBefore(tmpNode,
                                             endContainer.nextSibling);
      }
    }
  } else {
    tmpNode = doc.createTextNode(MARK_SELECTION_END);
    endContainer.insertBefore(tmpNode,
                              endContainer.childNodes.item(endOffset));
  }

  if (startContainer.nodeType == Node.TEXT_NODE ||
      startContainer.nodeType == Node.CDATA_SECTION_NODE)
  {
    // Do some tweaks to try to avoid the view-source output to look like
    // ...<tag>[... or ...[</tag>... (where '[' marks the start of selection).
    // To get a neat output, the idea here is to remap the start point from:
    // 1. ...<tag>[...   to   ...[<tag>...
    // 2. ...[</tag>...  to   ...</tag>[...
    if ((startOffset > 0 && startOffset < startContainer.data.length) ||
        !startContainer.parentNode || !startContainer.parentNode.parentNode ||
        startContainer != startContainer.parentNode.lastChild)
    {
      startContainer.insertData(startOffset, MARK_SELECTION_START);
    } else {
      tmpNode = doc.createTextNode(MARK_SELECTION_START);
      startContainer = startContainer.parentNode;
      if (startOffset == 0) {
        startContainer.parentNode.insertBefore(tmpNode, startContainer);
      } else {
        startContainer.parentNode.insertBefore(tmpNode,
                                               startContainer.nextSibling);
      }
    }
  } else {
    tmpNode = doc.createTextNode(MARK_SELECTION_START);
    startContainer.insertBefore(tmpNode,
                                startContainer.childNodes.item(startOffset));
  }

  // Make relative links absolute
  makeRelativeLinksAbsolute(doc.location, ancestorContainer);

  // now extract and display the syntax highlighted source
  tmpNode = doc.createElementNS(NS_XHTML, "div");
  tmpNode.appendChild(ancestorContainer);

  var html = tmpNode.innerHTML;
  var txt = tmpNode.textContent;
  // Because there are five characters in that const.
  var html_start = html.indexOf(MARK_SELECTION_START) + 5;
  var html_end = html.indexOf(MARK_SELECTION_END);
  html = html.substring(html_start, html_end);
  // Because there are five characters in that const.
  var txt_start = txt.indexOf(MARK_SELECTION_START) + 5;
  var txt_end = txt.indexOf(MARK_SELECTION_END);
  txt = txt.substring(txt_start, txt_end);
  _logger.debug("START: " + html_start + "\n" +
                "END: " + html_end + "\n" +
                "VAL: --" + html + "--");
  _logger.debug("TXT: " + txt);
  _logger.debug("HTML: " + html);
  return { html: html, txt: txt };
}

// XXX daryl: Also ripped from viewPartialSource.js. Probably a good
// idea to keep this here, in case it changes in the core source.
function getPath(aAncestor, aNode) {
  var n = aNode;
  var p = n.parentNode;
  if (n == aAncestor || !p) {
    return null;
  }
  var path = [];
  if (!path) {
    return null;
  }
  do {
    for (var i = 0; i < p.childNodes.length; i++) {
      if (p.childNodes.item(i) == n) {
        path.push(i);
        break;
      }
    }
    n = p;
    p = n.parentNode;
  } while (n != aAncestor && p);
  return path;
}

function makeRelativeLinksAbsolute(aBaseUrl, aDOMNode) {
  // This is a recursive method: bail if no tag name
  if (!aDOMNode.tagName) {
    return;
  }

  switch (aDOMNode.tagName.toLowerCase()) {
    case "img":
      var url = aDOMNode.getAttribute("src");
      if (!url.match("http")) {
        aDOMNode.setAttribute("src", makeURLAbsolute(aBaseUrl, url));
      }
      break;

    case "a":
      var url = aDOMNode.getAttribute("href");
      if (!url.match("http")) {
        aDOMNode.setAttribute("href", makeURLAbsolute(aBaseUrl, url));
      }
      break;

    default:
      break
  }

  if (aDOMNode.hasChildNodes()) {
    var kids = aDOMNode.childNodes;
    for (var i = 0; i < kids.length; i++) {
      makeRelativeLinksAbsolute(aBaseUrl, kids[i]);
    }
  }
}

// From browser.js, required for mozextras.js
function makeURLAbsolute(aBase, aUrl) {
  try {
    var ios = CC["@mozilla.org/network/io-service;1"]
              .getService(CI.nsIIOService);
    var baseURI = ios.newURI(aBase, null, null);

    return ios.newURI(baseURI.resolve(aUrl), null, null).spec;
  } catch (ex) {
    // Special URLs throw exceptions, in this case we just return it "as is"
    return aUrl;
  }
}
