// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2008
// 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/FlockXPCOMUtils.jsm");
FlockXPCOMUtils.debug = false;

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

const FACEBOOK_CID = Components.ID("{81a58e10-4f45-11db-b0de-0800200c9a66}");
const FACEBOOK_CONTRACTID              = "@flock.com/people/facebook;1";
const FACEBOOK_FAVICON = "chrome://flock/content/services/facebook/favicon.png";
const FACEBOOK_TITLE                   = "Facebook Web Service";
const SERVICE_ENABLED_PREF             = "flock.service.facebook.enabled";
const CATEGORY_COMPONENT_NAME          = "Facebook JS Component";
const CATEGORY_ENTRY_NAME              = "facebook";
const MODULE_NAME = "facebook";
const CLASS_SHORT_NAME = "facebook";
const CLASS_NAME = "Facebook";

const FLOCK_ACCOUNT_UTILS_CONTRACTID   = "@flock.com/account-utils;1";
const FLOCK_ERROR_CONTRACTID           = "@flock.com/error;1";
const FLOCK_PHOTO_ACCOUNT_CONTRACTID   = "@flock.com/photo-account;1";
const FLOCK_PHOTO_ALBUM_CONTRACTID     = "@flock.com/photo-album;1";
const FLOCK_PHOTO_API_MANAGER_CONTRACTID = "@flock.com/photo-api-manager;1?";
const FLOCK_PHOTO_CONTRACTID           = "@flock.com/photo;1";
const FLOCK_RDNDS_CONTRACTID           = "@flock.com/rich-dnd-service;1";
const FLOCK_SINGLETON_CONTRACTID       = "@flock.com/singleton;1";
const CATEGORY_MANAGER_CONTRACTID      = "@mozilla.org/categorymanager;1";
const HASH_PROPERTY_BAG_CONTRACTID     = "@mozilla.org/hash-property-bag;1";
const JS_SUBSCRIPT_LOADER_CONTRACTID   = "@mozilla.org/moz/jssubscript-loader;1";
const OBSERVER_SERVICE_CONTRACTID      = "@mozilla.org/observer-service;1";

const FLOCK_SNOWMAN_URL = "chrome://flock/skin/services/common/no_avatar.png";
const FAVES_COOP = "chrome://flock/content/common/load-faves-coop.js";
const SERVICES_PROPERTIES_FILE = "chrome://flock/locale/services/services.properties";
const FACEBOOK_PROPERTIES = "chrome://flock/locale/services/facebook.properties";
const PEOPLE_PROPERTIES = "chrome://flock/locale/people/people.properties";

const FACEBOOK_IDENTITY_URN_PREFIX = "urn:flock:identity:facebook:";

const SERVICES_SHARE_FLOCK_SUBJECT = "flock.friendShareFlock.subject";
const SERVICES_SHARE_FLOCK_MESSAGE = "flock.friendShareFlock.message";

// The first time, only get photos not older than one week
const MEDIA_INITIAL_FETCH = 7 * 24 * 3600;
// The delay between two refreshes when the sidebar is closed (in seconds)
const FACEBOOK_REFRESH_INTERVAL = 1800; // 30 minutes

// CONSTANTS FOR PHOTO UPLOADING
// XXX This is a rough guess.  Facebook allows a max of 60 pictures per
//     personal account, at a max file size of 5MB each.  Once there are API
//     calls to actually get the space remaining, we should replace this.
const FACEBOOK_MAX_PHOTO_SPACE = 60 * 5 * 1024 * 1024;
const FACEBOOK_USED_PHOTO_SPACE = 0;
// Facebook has a maximum file size of 5MB
const FACEBOOK_MAX_FILE_SIZE = 5 * 1024 * 1024;

const CHAR_CODE_SPACE = " ".charCodeAt(0);

// ====================================================
// ========== BEGIN General helper functions ==========
// ====================================================

// String defaults... may be updated later through Web Detective
var gStrings = {
  "domains": "facebook.com",
  "userprofile": "http://www.facebook.com/profile.php?id=%accountid%"
};

var _wm = null;

function loadLibraryFromSpec(aSpec) {
  var loader = CC[JS_SUBSCRIPT_LOADER_CONTRACTID]
               .getService(CI.mozIJSSubScriptLoader);
  loader.loadSubScript(aSpec);
}

// A special hack to trigger embedding rich content into a Facebook field.
// Added as a general helper function as it is used in both FacebookService
// and FacebookAccount classes.
function _initKeyupEvent(aTarget) {
  if (!aTarget) {
    return;
  }

  var doc = aTarget.ownerDocument;
  if (!(doc instanceof CI.nsIDOMHTMLDocument)) {
    return;
  }

  var docView = doc.QueryInterface(CI.nsIDOMDocumentView).defaultView;
  if (!(docView instanceof CI.nsIDOMAbstractView)) {
    return;
  }

  // add an extra space as the first of two parts to simulate the keyup event
  aTarget.value += " ";

  // Send a key-up event to the field: this triggers a Facebook
  // process which scrapes the field for a link which, if found,
  // will embed the rich content into the field.
  var eventObj = doc.createEvent("KeyEvents");
  eventObj.initKeyEvent("keyup", true, true, docView, false, false, false,
                        false, CI.nsIDOMKeyEvent.DOM_VK_SPACE, CHAR_CODE_SPACE);
  aTarget.dispatchEvent(eventObj);

  // Remove the space to get back to the original value
  var val = aTarget.value;
  aTarget.value = val.substring(0, val.length - 1);

  // Give focus to the target
  aTarget.focus();
}

function _dispatchMouseEvent(aArea) {
  if (aArea) {
    var doc = aArea.ownerDocument;
    var docView = doc.QueryInterface(CI.nsIDOMDocumentView).defaultView;
    var e = doc.createEvent("mouseevents");
    e.initMouseEvent("click", true, true, docView, 0, 1, 1, 1, 1,
                     false, false, false, false, 0, null);
    aArea.dispatchEvent(e);
  } else {
    inst._logger.error("dispatchMouseEvent: aArea is undefined")
  }
}

// Appends the Flock breadcrumb to the given text field
function _addBreadcrumb(aField) {
  if (aField) {
    // We'll only add a breadcrumb if there isn't one already present
    var breadcrumb = "";
    aField.focus();
    var richDnDSvc = CC[FLOCK_RDNDS_CONTRACTID]
                     .getService(CI.flockIRichDNDService);
    if (!richDnDSvc.hasBreadcrumb(aField)) {
      breadcrumb = richDnDSvc.getBreadcrumb("plain");
    }
    aField.value += breadcrumb;
  }
}

/*************************************************************************
 * Component: flockFacebookService
 *************************************************************************/

function flockFacebookService() {
  _wm = CC["@mozilla.org/appshell/window-mediator;1"]
            .getService(CI.nsIWindowMediator);
  loadLibraryFromSpec("chrome://flock/content/photo/photoAPI.js");
  this.obs = CC[OBSERVER_SERVICE_CONTRACTID].getService(CI.nsIObserverService);
  this.ppUtils = CC["@flock.com/people-utils;1"]
                 .getService(CI.flockIPeopleUtils);
  this.ppSvc = CC["@flock.com/people-service;1"]
               .getService(CI.flockIPeopleService);

  this.api = CC["@flock.com/api/facebook;1"].getService(CI.flockIFacebookAPI).wrappedJSObject;
  this.mIsInitialized = false;
  this.photoList = null;

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(FACEBOOK_PROPERTIES);

  this._accountClassCtor = flockFacebookAccount;

  this._channels = {
    "special:photosFromFriends": {
      title: bundle.GetStringFromName("flock.facebook.title.friends"),
      supportsSearch: false
    },
    "special:photosOfFriends": {
      title: bundle.GetStringFromName("flock.facebook.title.recent"),
      supportsSearch: false
    },
      "special:photosOfYou": {
      title: bundle.GetStringFromName("flock.facebook.title.you"),
      supportsSearch: false
    }
  };
  FlockSvcUtils.getLogger(this).init("facebookService");
  this._profiler = CC["@flock.com/profiler;1"].getService(CI.flockIProfiler);

  FlockSvcUtils.flockIAuthenticateNewAccount
               .addDefaultMethod(this, "authenticateNewAccount");

  var ws = FlockSvcUtils.flockIWebService;
  ws.addDefaultMethod(this, "url");
  ws.addDefaultMethod(this, "getStringBundle");
  ws.addDefaultMethod(this, "isHidden");

  var lws = FlockSvcUtils.flockILoginWebService;
  lws.addDefaultMethod(this, "loginURL");
  lws.addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  lws.addDefaultMethod(this, "getAccount");
  lws.addDefaultMethod(this, "getAccountIDFromDocument");
  lws.addDefaultMethod(this, "getAccounts");
  lws.addDefaultMethod(this, "getAuthenticatedAccount");
  lws.addDefaultMethod(this, "getCredentialsFromForm");
  lws.addDefaultMethod(this, "getSessionValue");
  lws.addDefaultMethod(this, "ownsDocument");
  lws.addDefaultMethod(this, "ownsLoginForm");
  lws.addDefaultMethod(this, "removeAccount");

  var sws = FlockSvcUtils.flockISocialWebService;
  sws.addDefaultMethod(this, "markAllMediaSeen");

  var mws = FlockSvcUtils.flockIMediaWebService;
  mws.addDefaultMethod(this, "getChannel");
  mws.addDefaultMethod(this, "enumerateChannels");
  mws.addDefaultMethod(this, "getMediaItemFormatter");
  mws.addDefaultMethod(this, "getIconForQuery");

  var muws = FlockSvcUtils.flockIMediaUploadWebService;
  muws.addDefaultMethod(this, "getAlbumsForUpload");

  FlockSvcUtils.flockIRichContentDropHandler
               .addDefaultMethod(this, "_handleTextareaDrop");

  this._init();
}

/*************************************************************************
 * flockFacebookService: XPCOM Component Creation
 *************************************************************************/

flockFacebookService.prototype = new FlockXPCOMUtils.genericComponent(
  FACEBOOK_TITLE,
  FACEBOOK_CID,
  FACEBOOK_CONTRACTID,
  flockFacebookService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.nsISupports,
    CI.nsIClassInfo,
    CI.nsISupportsCString,
    CI.nsIObserver,
    CI.flockIWebService,
    CI.flockIAuthenticateNewAccount,
    CI.flockISocialWebService,
    CI.flockILoginWebService,
    CI.flockIPollingService,
    CI.flockIMediaWebService,
    CI.flockIMediaUploadWebService,
    CI.flockIRichContentDropHandler
  ]
);

// FlockXPCOMUtils.genericModule() categories
flockFacebookService.prototype._xpcom_categories = [
  { category: "wsm-startup" },
  { category: "flockMediaProvider", entry: CATEGORY_ENTRY_NAME },
  { category: "flockRichContentHandler", entry: CATEGORY_ENTRY_NAME },
  { category: "flockWebService", entry: CATEGORY_ENTRY_NAME }
];

/*************************************************************************
 * flockFacebookService: flockIWebService Implementation
 *************************************************************************/

// readonly attribute AString contractId;
// ALMOST duplicated by nsIClassInfo::contractID
// with different case: contractId != contractID
flockFacebookService.prototype.contractId = FACEBOOK_CONTRACTID;

// readonly attribute AString icon;
flockFacebookService.prototype.icon = FACEBOOK_FAVICON;

// readonly attribute boolean needPassword;
flockFacebookService.prototype.needPassword = true;

// readonly attribute AString shortName;
flockFacebookService.prototype.shortName = CLASS_SHORT_NAME;

// readonly attribute AString title;
flockFacebookService.prototype.title = CLASS_NAME;

// readonly attribute AString urn;
flockFacebookService.prototype.urn = "urn:" + CLASS_SHORT_NAME + ":service";

/**************************************************************************
 * flockFacebookService: private members
 **************************************************************************/

flockFacebookService.prototype._init =
function flockFacebookService__init() {
  // Prevent re-entry
  if (this.mIsInitialized) {
    return;
  }
  this.mIsInitialized = true;
  this._logger.info(".init()");

  // Determine whether this service has been disabled, and unregister if so.
  this.prefService = CC["@mozilla.org/preferences-service;1"]
                     .getService(CI.nsIPrefBranch);
  if (this.prefService.getPrefType(SERVICE_ENABLED_PREF) &&
       !this.prefService.getBoolPref(SERVICE_ENABLED_PREF)) {
    this._logger.info(SERVICE_ENABLED_PREF + " is FALSE! Not initializing.");
    var cm = CC[CATEGORY_MANAGER_CONTRACTID].getService(CI.nsICategoryManager);
    cm.deleteCategoryEntry("wsm-startup", CATEGORY_COMPONENT_NAME, true);
    cm.deleteCategoryEntry("flockWebService", CATEGORY_ENTRY_NAME, true);
    cm.deleteCategoryEntry("flockMediaProvider", CATEGORY_ENTRY_NAME, true);
    cm.deleteCategoryEntry("flockRichContentHandler",
                           CATEGORY_ENTRY_NAME, true);
    return;
  }

  // getWD() also adds the "_acUtils" property.
  FlockSvcUtils.getWD(this);
  for (var s in gStrings) {
    gStrings[s] = this._WebDetective.getString("facebook", s, gStrings[s]);
  }
  this.domains = gStrings["domains"];

  FlockSvcUtils.getCoopService(this);

  // Initialize member that specifies path for localized string bundle
  this._stringBundlePath = FACEBOOK_PROPERTIES;

  // Update auth states
  try {
    if (this._WebDetective.detectCookies("facebook", "loggedout", null)) {
      this._acUtils.markAllAccountsAsLoggedOut(FACEBOOK_CONTRACTID);
      this.logout();
    }
  } catch (ex) {
    this._logger.error("ERROR updating auth states for Facebook: " + ex);
  }
  // On browser restart if a Facebook coop account is still marked as
  // authenticated we will need to reauth the api.
}

/**
 * @see flockIPollingService#refresh
 */
flockFacebookService.prototype.refresh =
function flockFacebookService_refresh(aUrn, aPollingListener) {
  this._logger.debug(".refresh() with aUrn of " + aUrn);
  var account = this.getAccount(aUrn);

  if (!account) {
    this._logger.error("can't refresh " + aUrn + ": not a valid account");
    aPollingListener.onError(null);
  }

  if (account.isAuthenticated()) {
    var inst = this;
    var flockListener = {
      onSuccess: function flockListener_onSuccess(aSubject, aTopic) {
        inst._refreshAccount(account, aPollingListener);
      },
      onError: function flockListener_onError(aFlockError, aTopic) {
        inst._acUtils.markAllAccountsAsLoggedOut(FACEBOOK_CONTRACTID);
        aPollingListener.onError(aFlockError);
      }
    };
    this.api.authenticate(flockListener);
  } else {
    this._logger.debug("account is not logged in - skipping refresh");
    // If the user is not logged in, return a success without
    // refreshing anything
    aPollingListener.onResult();
  }

}

flockFacebookService.prototype._refreshAccount =
function flockFacebookService_refreshAccount(aAccount, aPollingListener) {
  var inst = this;
  var refreshItem = aAccount.coopObj;
  var lastUpdate = refreshItem.lastUpdateDate;
  var peopleHash;

  var pplRefreshListener = CC["@flock.com/people-refresh-listener;1"]
                           .createInstance(CI.flockIPeopleRefreshListener);
  pplRefreshListener.init(3, refreshItem.id(), aPollingListener, 0);

  var mediaFriendsListener = {
    onSuccess: function mfl_onSuccess(aEnum, aStatus) {
      inst._logger.info("Successful getUpdatedMediaFriends");
      aEnum.QueryInterface(CI.nsISimpleEnumerator);

      // Get the media information, put it in the peopleHash
      while (aEnum.hasMoreElements()) {
        var bag = aEnum.getNext();
        bag.QueryInterface(CI.nsIPropertyBag);
        var uid = bag.getProperty("uid");
        peopleHash[uid].media = {
          count: bag.getProperty("count"),
          latest: bag.getProperty("latest")
        }
      }

      // Now that we have all we need, update the local datastore
      function myWorker(aShouldYield) {
        if (aAccount.isAuthenticated()) {
          // REMOVE locally people removed on the server
          inst._logger.debug("Checking if friends were removed on the server...");
          var localFriends = aAccount.enumerateFriends();
          while (localFriends.hasMoreElements()) {
            var person = localFriends.getNext();
            if (!peopleHash[person.accountId]) {
              inst._logger.info(person.name
                                 + " removed on the server,"
                                 + " let's remove locally");
              inst.ppUtils.removePerson(aAccount, person);
            } else {
              peopleHash[person.accountId].unseenMedia = person.unseenMedia;
            }
          }
          var count = 0;
          // ADD or update existing people
          for each (var person in peopleHash) {
            count++;
            inst._addPerson(aAccount, person);
            if (aShouldYield()) {
              yield;
              if (!aAccount.isAuthenticated()) {
                // Account has just been deleted or logged out
                break;
              }
            }
          }
        }
        pplRefreshListener.onSuccess(aAccount, "mediaFriendsListener");
      }
      FlockScheduler.schedule(null, 0.05, 10, myWorker);
    },
    onError: function mfl_onError(aFlockError, aTopic) {
      inst._logger.error("Error on getUpdatedMediaFriends");
      pplRefreshListener.onError(aFlockError, "mediaFriendsListener");
    }
  }

  var friendsGetListener = {
    onSuccess: function fgl_onSuccess(aResult, aStatus) {
      inst._logger.info("Success for friendsGetListener");

      peopleHash = aResult.wrappedJS;

      refreshItem.lastUpdateDate = new Date();
      var lastUpdateStr;
      if (lastUpdate) {
        // Convert milliseconds to seconds then to a string
        lastUpdateStr = new String(parseInt(lastUpdate.getTime() / 1000));
      } else {
        lastUpdateStr = new String(parseInt(Date.now() / 1000)
                                   - MEDIA_INITIAL_FETCH);
      }
      inst.api.getUpdatedMediaFriends(mediaFriendsListener, lastUpdateStr);
    },
    onError: function fgl_onError(aFlockError, aTopic) {
      inst._logger.error("Error on gettingFriends")
      pplRefreshListener.onError(aFlockError, "friendsGetListener");
    }
  };

  var usersGetInfoListener = {
    onSuccess: function ugil_onSuccess(aEnum, aStatus) {
      inst._logger.info("Success for usersGetInfo");
      aEnum.QueryInterface(CI.nsISimpleEnumerator);
      if (aEnum.hasMoreElements()) {
        var bag = aEnum.getNext();
        bag.QueryInterface(CI.nsIPropertyBag);
        refreshItem.avatar = bag.getProperty("avatar");
        // We get the full name each refresh since it's possible for the
        // user's full name to change.
        refreshItem.name = bag.getProperty("name");
        var statusMessage = bag.getProperty("statusMessage");
        var lastStatusMessageUpdateDate = bag.getProperty("lastStatusMessageUpdateDate");
        if (statusMessage != "") {
          refreshItem.statusMessage = flockXMLEncode(statusMessage);
          refreshItem.lastStatusMessageUpdateDate = lastStatusMessageUpdateDate;
        } else {
          refreshItem.statusMessage = null;
          refreshItem.lastStatusMessageUpdateDate = 0;
        }
        inst.obs.notifyObservers(aAccount.coopObj.resource(),
                                 "flock-acct-refresh",
                                 "user-info");
        pplRefreshListener.onSuccess(aAccount, "usersGetInfoListener");
      }
    },
    onError: function ugil_onError(aFlockError, aTopic) {
      inst._logger.error("Error on usersGetInfo");
      pplRefreshListener.onError(aFlockError, "usersGetInfoListener");
    }
  };

  var notificationsGetListener = {
    onSuccess: function ngl_onSuccess(aEnum, aStatus) {
      inst._logger.info("Success for notificatonsGet");
      aEnum.QueryInterface(CI.nsISimpleEnumerator);
      if (aEnum.hasMoreElements()) {
        var bag = aEnum.getNext();
        bag.QueryInterface(CI.nsIPropertyBag);

        // if any of the Me notifications have increased in count,
        // light the people button
        var c_params = aAccount.getAllCustomParams();
        if ((refreshItem.accountMessages < bag.getProperty("messages")) ||
            (c_params.getProperty("fbPokes") < bag.getProperty("pokes")) ||
            (c_params.getProperty("fbFriendRequests") <
              bag.getProperty("friendRequests")) ||
            (c_params.getProperty("fbGroupInvites") <
              bag.getProperty("groupInvites")) ||
            (c_params.getProperty("fbEventInvites") <
              bag.getProperty("eventInvites")))
        {
          inst.ppSvc.togglePeopleIcon(true);
        }

        // write notifications to RDF
        aAccount.setCustomParam("fbPokes", bag.getProperty("pokes"));
        refreshItem.accountMessages = bag.getProperty("messages");
        aAccount.setCustomParam("fbFriendRequests",
                                bag.getProperty("friendRequests"));
        aAccount.setCustomParam("fbGroupInvites",
                                bag.getProperty("groupInvites"));
        aAccount.setCustomParam("fbEventInvites",
                                bag.getProperty("eventInvites"));
      }
      inst.obs.notifyObservers(aAccount.coopObj.resource(),
                               "flock-acct-refresh",
                               "user-info");
      pplRefreshListener.onSuccess(aAccount, "notificationsGetListener");
    },
    onError: function ngl_onError(aFlockError, aTopic) {
      inst._logger.error("Error on notificatonsGet");
      pplRefreshListener.onError(aFlockError, "notificationsGetListener");
    }
  };

  this.api.usersGetInfo(usersGetInfoListener,
                        this.api.uid,
                        ["name,pic_square,status"]);
  this.api.friendsGet(friendsGetListener);
  this.api.notificationsGet(notificationsGetListener);
}


flockFacebookService.prototype._addPerson =
function flockFacebookService_addPerson(aAccount, aPerson) {
  // Make sure null update times aren't missed
  var lastProfileUpdate = 0;
  if (aPerson.profile_update_time) {
    lastProfileUpdate = parseInt(aPerson.profile_update_time, 10);
  }

  // Make sure null status times aren't missed
  var statusTime = 0;
  if (aPerson.status.time) {
    statusTime = parseInt(aPerson.status.time, 10);
  }

  var lastUpdate = 0;
  var lastUpdateType;

  if (statusTime > lastProfileUpdate) {
    lastUpdate = statusTime;
    lastUpdateType = "status";
  } else {
    lastUpdate = lastProfileUpdate;
    lastUpdateType = "profile";
  }

  var person = CC["@flock.com/person;1"]
               .createInstance(CI.flockIPerson);
  person.accountId = aPerson.uid;
  person.lastUpdate = lastUpdate;
  person.lastUpdateType = lastUpdateType;
  person.name = aPerson.name;
  person.avatar = aPerson.pic_square;
  person.unseenMedia = aPerson.unseenMedia;
  person.statusMessage = aPerson.status.message
                       ? flockXMLEncode(aPerson.status.message)
                       : "";
  person.profileURL = gStrings["userprofile"].replace("%accountid%",
                                                      aPerson.uid);

  this.ppUtils.addPerson(aAccount, person);

  if (aPerson.media) {
    this.ppUtils.incrementMedia(aAccount,
                                aPerson.uid,
                                aPerson.media.count,
                                aPerson.media.latest);
  }
}

// Facebook's HTML sets a maxlength of 160 on the status entry textbox.
flockFacebookService.prototype.maxStatusLength = 160;

/**
 * @see flockIWebService#addAccount
 */
flockFacebookService.prototype.addAccount =
function flockFacebookService_addAccount(aAccountID,
                                             aIsTransient,
                                             aFlockListener) {
  this._logger.debug(".addAccount('"
                     + aAccountID + "', "
                     + aIsTransient + "...)");

  var pw = this._acUtils.getPassword(this.urn + ":" + aAccountID);
  /// @todo: FIX ME - name shouldn't be email address (pw.username) - DP
  var name = (pw) ? pw.username : aAccountID;
  var url = gStrings["userprofile"].replace("%accountid%", aAccountID);
  var accountUrn = this._acUtils.createAccount(this,
                                              aAccountID,
                                              name,
                                              url,
                                              aIsTransient);
  var coopAccount = this._coop.get(accountUrn);
  coopAccount.refreshInterval = FACEBOOK_REFRESH_INTERVAL;
  var list = CC["@mozilla.org/hash-property-bag;1"]
             .createInstance(CI.nsIWritablePropertyBag2);
  list.setPropertyAsInt32("fbPokes", 0);
  list.setPropertyAsInt32("fbGroupInvites", 0);
  list.setPropertyAsInt32("fbEventInvites", 0);
  list.setPropertyAsInt32("fbFriendRequests", 0);
  this._acUtils.addCustomParamsToAccount(list, accountUrn);
  // Instantiate account component
  var acct = this.getAccount(accountUrn);
  if (aFlockListener) {
    aFlockListener.onSuccess(acct, "addAccount");
  }
  return acct;
}

// BEGIN flockIAuthenticateNewAccount interface

// DEFAULT: void authenticateNewAccount();

// END flockIAuthenticateNewAccount interface

/**
 * @see flockILoginWebService#updateAccountStatusFromDocument
 */
flockFacebookService.prototype.updateAccountStatusFromDocument =
function flockFacebookService_updateAccountStatusFromDocument(aDocument, 
                                                              aAcctURN,
                                                              aFlockListener)
{
  this._logger.debug("updateAccountStatusFromDocument('" + aAcctURN + "')");
  if (aAcctURN) {
    // We know we're already logged in to this account
    var account = this.getAccount(aAcctURN);
    if (!account.isAuthenticated()) {
      account.login(aFlockListener);
    }
  } else if (this._WebDetective
                 .detect("facebook", "loggedout", aDocument, null))
  {
    this._acUtils.markAllAccountsAsLoggedOut(FACEBOOK_CONTRACTID);
    this.api.deauthenticate();
  }
}

/**
 * migrateAccount
 * @see flockIMediaWebService#migrateAccount
 */
flockFacebookService.prototype.migrateAccount =
function flockFacebookService_migrateAccount( aId, aUsername ) {
  this.init();
  var token = "";
  this.addAccount( aId, false, null, aUsername, token);
}

/**
 * createAlbum
 * @see flockIMediaWebService#createAlbum
 */
flockFacebookService.prototype.createAlbum =
function flockFacebookService_createAlbum(aFlockListener, aAlbumName) {
  // trim white space from front and end of string
  var newTitle = aAlbumName.replace(/^\s+|\s+$/g, "");
  if (newTitle) {
    this.api.createAlbum(aFlockListener, newTitle);
  } else {
    var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
    error.errorCode = CI.flockIError.PHOTOSERVICE_EMPTY_ALBUMNAME;
    aFlockListener.onError(error, null);
  }
}


/**
 * findByUsername
 * @see flockIMediaWebService#findByUsername
 */
flockFacebookService.prototype.findByUsername =
function flockFacebookService_findByUsername(aFlockListener, aUsername) {
  // need to call this to trigger mediaObserver
  aFlockListener.onError(null, null);
}

/**
 * getAccountStatus
 *
 * This currently returns hardcoded values so the uploader works.  This
 * should be replaced by actual calls to the Facebok API, or with values
 * screen-scraped by Web Detective.
 *
 * @see flockIMediaWebService#getAccountStatus
 * @todo Use the Facebook API or Web Detective to correctly obtain this
 *       information.
 * @todo Localize the hardcoded strings.
 */
flockFacebookService.prototype.getAccountStatus =
function flockFacebookService_getAccountStatus(aFlockListener) {
  var result = CC["@mozilla.org/hash-property-bag;1"]
               .createInstance(CI.nsIWritablePropertyBag2);

  result.setPropertyAsAString("maxSpace", FACEBOOK_MAX_PHOTO_SPACE);
  result.setPropertyAsAString("usedSpace", FACEBOOK_USED_PHOTO_SPACE);
  result.setPropertyAsAString("maxFileSize", FACEBOOK_MAX_FILE_SIZE);
  // This is a key, not something displayed to the user. Don't l10n this!
  result.setPropertyAsAString("usageUnits", "bytes");
  result.setPropertyAsBool("isPremium", true); // Facebook has no "premium" level.

  aFlockListener.onSuccess(result, "");
}

/**
 * getAlbums
 * @see flockIMediaWebService#getAlbums
 *
 * aUsername is the FB uid
 */
flockFacebookService.prototype.getAlbums =
function flockFacebookService_getAlbums(aFlockListener, aUsername) {
  this._logger.info(".getAlbums(aFlockListener, " + aUsername + ")");

  this.api.getAlbums(aFlockListener, aUsername);
}


/**
 * logout
 * @see flockIWebService#logout
 */
flockFacebookService.prototype.logout =
function flockFacebookService_logout() {
  this._logger.debug(".logout()");
  this.api.deauthenticate();
  this._acUtils.removeCookies(this._WebDetective.getSessionCookies("facebook"));
}

/**
 * search
 * @see flockIMediaWebService#search
 */
flockFacebookService.prototype.search =
function flockFacebookService_search(aFlockListener,
                                     aQueryString,
                                     aCount,
                                     aPage,
                                     aRequestId)
{
  this._logger.debug("Attempting to run search query: " + aQueryString);
  var inst = this;
  if (aPage > 1) {
    // Implement a pseudo-pagination since Facebook does not support pagination
    getPhotosDetails(aCount, aPage);
    return;
  }

  var aQuery = new queryHelper(aQueryString);

  function createEnum(myarray) {
    return {
      QueryInterface: function (iid) {
        if (iid.equals(Components.interfaces.nsISimpleEnumerator)) {
          return this;
        }
        throw Components.results.NS_ERROR_NO_INTERFACE;
      },
      hasMoreElements: function() {
        return (myarray.length>0);
      },
      getNext: function() {
        return myarray.shift();
      }
    }
  }

  function getPhotosDetails(aCount, aPage) {
    var photos = [];
    var owners = [];
    if (inst.photoList) {

      var sbs = CC["@mozilla.org/intl/stringbundle;1"].
                getService(CI.nsIStringBundleService);
      var bundleUrl = "chrome://flock/locale/photo/mediabar.properties";
      var bundle = sbs.createBundle(bundleUrl);

      var pageEnd = aCount * aPage;
      if (pageEnd > inst.photoList.length) {
        pageEnd = inst.photoList.length;
      }
      for (var i = aCount * (aPage - 1); i < pageEnd; i++) {
        var photo = inst.photoList[i];
        var id = photo.getElementsByTagName('pid')[0]
                      .firstChild
                      .nodeValue;
        var title;
        if (photo.getElementsByTagName('caption')[0].firstChild) {
          title = photo.getElementsByTagName('caption')[0]
                       .firstChild
                       .nodeValue;
        } else {
          title = bundle.GetStringFromName("flock.media.tooltip.untitled");
        }
        var owner = photo.getElementsByTagName('owner')[0]
                         .firstChild.nodeValue;
        var date_upload = photo.getElementsByTagName('created')[0]
                               .firstChild
                               .nodeValue;
        var square_url = photo.getElementsByTagName('src')[0]
                              .firstChild
                              .nodeValue;
        var small_url = photo.getElementsByTagName('src_small')[0]
                             .firstChild
                             .nodeValue;
        var big_url = photo.getElementsByTagName('src_big')[0]
                           .firstChild
                           .nodeValue;
        var page_url = photo.getElementsByTagName('link')[0]
                            .firstChild
                            .nodeValue;
        var newPhoto = inst.api.createPhoto();
        newPhoto.webPageUrl = page_url;
        newPhoto.thumbnail = small_url;
        newPhoto.midSizePhoto = square_url;
        newPhoto.largeSizePhoto = big_url;
        newPhoto.userid = owner;
        newPhoto.title = title;
        newPhoto.id = id;
        newPhoto.uploadDate = parseInt(date_upload) * 1000;
        photos.push(newPhoto);
        owners.push(owner);
      }
      if (pageEnd == inst.photoList.length) {
        inst.photoList = null;
      }
    }

    // Exit this whole thing early if we got nothing back.
    if (photos.length == 0) {
      aFlockListener.onSuccess(createEnum(photos), aRequestId);
      return;
    }

    // We sort the array to ease de-dupeing by placing duplicate numbers
    // next to each other.
    owners = owners.sort();
    var ownerList = "";
    var prev = 0;
    for each (var owner in owners) {
      if (owner == prev) {
        continue;
      }
      if (ownerList != "") {
        ownerList += ",";
      }
      ownerList += owner;
      prev = owner;
    }

    if (ownerList != "") {
      var detailsListener = {
        onSuccess: function listener_onSuccess(aEnum) {
          var ownersObject = {};
          aEnum = aEnum.QueryInterface(CI.nsISimpleEnumerator);
          while (aEnum.hasMoreElements()) {
            var person = aEnum.getNext();
            person = person.QueryInterface(CI.nsIWritablePropertyBag2);
            ownersObject[person.getProperty("uid")] = person;
          }

          for (var j = 0; j < photos.length; j++) {
            var owner = ownersObject[photos[j].userid];
            owner = owner.QueryInterface(CI.nsIWritablePropertyBag2);
            photos[j].username = owner.getProperty("name");
            var avatar = owner.getProperty("avatar");
            if (avatar == "") {
              avatar = FLOCK_SNOWMAN_URL;
            }
            photos[j].icon = avatar;
          }
          aFlockListener.onSuccess(createEnum(photos), aRequestId);
        }
      };
      inst.api.usersGetInfo(detailsListener,
                            ownerList,
                            ["name,pic_square"]);
    }
  }

  var fqlListener = {
    onSuccess: function listener_onSuccess(aXml, aStatus) {
      inst.photoList = aXml.getElementsByTagName("photo");
      getPhotosDetails(aCount, 1);
    },
    onError: function listener_onError(aFlockError, aTopic) {
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  var qry = "SELECT pid, owner, src_small, src_big, src, link, caption, "
          + "created FROM photo ";
  var orderby = " ORDER BY created DESC";
  if (aQuery.user) {
    // Query to get all photos by person with name defined in aQuery.search.
    // Could be more than one person's sets of photos.
    if (aQuery.album) {
      qry += "WHERE aid IN (" + aQuery.album + ")";
    } else {
      qry += "WHERE aid IN ("
           + "SELECT aid FROM album WHERE owner = '"
           + aQuery.user
           + "')";
    }
    this._logger.debug("Performing FQLQuery: " + qry);
    this.api.doFQLQuery(fqlListener, qry + orderby);

    return;
  } else if (aQuery.searchunique) {
    // Query to get all photos tagged with uid specified in aQuery.searchunique
    qry += "WHERE pid IN ("
         + "SELECT pid FROM photo_tag WHERE subject = '"
         + aQuery.searchunique
         + "')";
    this._logger.debug("Performing FQLQuery: " + qry);
    this.api.doFQLQuery(fqlListener, qry + orderby);

    return;
  } else if (aQuery.special) {
    var acct = this.getAuthenticatedAccount();
    if (!acct) {
      var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
      error.errorCode = CI.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN;
      aFlockListener.onError(error, aRequestId);
    } else {
      switch (aQuery.special) {
        case "photosFromFriends":
          qry += "WHERE aid IN ("
               + "SELECT aid FROM album WHERE owner IN ("
               + "SELECT uid2 FROM friend WHERE uid1 = "
               + acct.coopObj.accountId
               + ")) "
               + "AND created > now() - 2592000"; // One month (in seconds)
          break;
        case "photosOfFriends":
          qry += "WHERE pid IN ("
               + "SELECT pid FROM photo_tag WHERE subject IN ("
               + "SELECT uid2 FROM friend WHERE uid1 = "
               + acct.coopObj.accountId
               + ")) "
               + "AND created > now() - 2592000"; // One month (in seconds)
          break;
        case "photosOfYou":
          qry += "WHERE pid IN ("
               + "SELECT pid FROM photo_tag WHERE subject="
               + acct.coopObj.accountId
               + ")";
          break;
      }
      this._logger.debug("Performing FQLQuery: " + qry);
      this.api.doFQLQuery(fqlListener, qry + orderby);
    }

    return;
  } else {
    // if user enters a search with illegal characters
    // (any non alpha-num or space)
    // then return right away with no results
    if (aQuery.search.match(/[^a-zA-Z0-9 ]/)) {
      aFlockListener.onSuccess(createEnum([]), aRequestId);
      return;
    }
  }

  function _normalizeQuery(input) {
    var output;
    var in_quotes = false;

    // strip leading spaces
    while (input.length && input[0] == " ") {
      // chop off the first character
      input = input.substr(1);
    }

    // Convert to lowercase for the wildcard search
    input = input.toLowerCase();

    // add leading plus
    output = "+";
    while (input.length) {
      if (input[0] == "\"") {
        if (in_quotes) {
          in_quotes = false;
        } else {
          in_quotes = true;
        }
      }

      // escape special chars
      if ("&|!(){}[]^~*?:\\".indexOf(input[0]) >= 0) {
        output = output + "\\";
      }

      output = output + input[0];

      if (!in_quotes && input[0] == " " && input.length > 1) {
          // put a plus before every word
          output = output + "+";
      }

      input = input.substr(1);
    }

    // remove trailing spaces
    var removed_spaces = false;
    while (output.length > 0 && output[output.length - 1] == " ") {
      output = output.substr(0, output.length - 1);
      removed_spaces = true;
    }

    // if we're in a quoted string we should close that
    if (in_quotes) {
      output = output + "\"";
    }

    // if theres an incomplete word
    if ((!removed_spaces) && (output[output.length - 1] != "\"")) {
      // search for partial matches
      output = output + "*";
    }

    return output;
  }

  // Query to get all photos tagged with name defined in aQuery.search.
  // We use Lucene to get partial matches.
  var inst = this;

  function searchCallback(aNumResults, aResults) {
    inst._logger.debug("searchCallback: " + aNumResults);

    var uidArray = [];
    while (aResults.hasMoreElements()) {
      var result = aResults.getNext();
      var uid = result.URI.split(":").pop();
      uidArray.push(uid);
    }

    var uidArrayStr = "(" + uidArray.join(",") + ")";
    var fullnameQuery = "(SELECT uid "
                      + "FROM user "
                      + "WHERE name = '"
                      + aQuery.search
                      + "')";
    qry += "WHERE pid IN ("
           + "SELECT pid FROM photo_tag "
           + "WHERE subject IN " + fullnameQuery + " "
           + "OR subject IN " + uidArrayStr
           + ")";

    inst._logger.debug("Performing FQLQuery: " + qry);
    inst.api.doFQLQuery(fqlListener, qry);
  }

  var searchService = CC["@flock.com/lucene;1"].getService(CI.flockILucene);
  searchService.search(_normalizeQuery(aQuery.search),
                       "identity",
                       100,
                       searchCallback);
}

/**
 * supportsUsers
 * @see flockIMediaWebService#supportsUsers
 */
flockFacebookService.prototype.supportsUsers = true;

/**
 * supportsFeature
 * @see flockIMediaWebService#supportsFeature
 */
flockFacebookService.prototype.supportsFeature =
function flockFacebookService_supportsFeature(aFeature) {
  var supports = {
    albumCreation: true,
    contacts: true,
    description: true,
    filename: false,
    privacy: false,
    tags: false,
    notes: true,
    title: false
  };
  return supports[aFeature] ? supports[aFeature] : false;
}

/**
 * supportsSearch
 * @see flockIMediaWebService#supportsSearch
 */
flockFacebookService.prototype.supportsSearch =
function flockFacebookService_supportsSearch(aQueryString) {
  return false;
}

/**
 * upload2
 * @see flockIMediaWebService#upload
 */
// flockIMediaItemFormatter getMediaItemFormatter();
flockFacebookService.prototype.getMediaItemFormatter =
function pbSvc_getMediaItemFormatter() {
  return this.api.flockMediaItemFormatter;
};

flockFacebookService.prototype.upload =
function flockFacebookService_upload(aUploadListener, aUpload, aFilename) {
  var params = CC[HASH_PROPERTY_BAG_CONTRACTID]
               .createInstance(CI.nsIWritablePropertyBag2);
  var description = aUpload.description;
  var prefService = CC["@mozilla.org/preferences-service;1"]
                    .getService(CI.nsIPrefService);
  var prefBranch = prefService.getBranch("flock.photo.uploader.");
  if (prefBranch.getPrefType("breadcrumb.enabled") &&
      prefBranch.getBoolPref("breadcrumb.enabled"))
  {
    description += flockGetString("services/services",
                                  "flock.uploader.breadcrumb");
  }

  params.setPropertyAsAString("description", description);
  this.api.uploadPhotosViaUploader(aUploadListener, aFilename, params, aUpload);
};

/**
 * decorateForMedia
 * @see flockIMediaWebService#decorateForMedia
 */
flockFacebookService.prototype.decorateForMedia =
function flockFacebookService_decorateForMedia(aDocument) {
  this._logger.debug(".decorateForMedia(aDocument)");
  var results = CC[HASH_PROPERTY_BAG_CONTRACTID]
                .createInstance(CI.nsIWritablePropertyBag2);
  var inst = this;
  if (this._WebDetective.detect("facebook", "person", aDocument, results) ||
      this._WebDetective.detect("facebook", "photo", aDocument, results)) {
    var userId = results.getPropertyAsAString("userid");
    var myListener = {
      onSuccess: function listener_onSuccess(aEnum) {
        aEnum = aEnum.QueryInterface(CI.nsISimpleEnumerator);
        var person = aEnum.getNext()
                          .QueryInterface(CI.nsIWritablePropertyBag2);
        var username;
        try {
          username = person.getProperty("name");
        } catch (ex) {
          username = userId;
        }

        var mediaArr = [];
        var media = {
          name: username,
          query: "user:" + userId + "|username:" + username,
          label: username,
          favicon: inst.icon,
          service: inst.shortName
        }
        mediaArr.push(media);
        if (!aDocument._flock_decorations) {
          aDocument._flock_decorations = {};
        }

        if (aDocument._flock_decorations.mediaArr) {
          aDocument._flock_decorations.mediaArr =
            aDocument._flock_decorations.mediaArr.concat(mediaArr);
        } else {
          aDocument._flock_decorations.mediaArr = mediaArr;
        }

        inst.obs.notifyObservers(aDocument, "media", "media:update");
      }
    };
    this.api.usersGetInfo(myListener, userId, ["name"]);
  } else if (this._WebDetective.detect("facebook",
                                       "tagged",
                                       aDocument,
                                       results))
  {
    var userId = results.getPropertyAsAString("userid");
    var myListener = {
      onSuccess: function listener_onSuccess(aEnum) {
        aEnum = aEnum.QueryInterface(CI.nsISimpleEnumerator);
        var person = aEnum.getNext()
                          .QueryInterface(CI.nsIWritablePropertyBag2);
        var username;
        try {
          username = person.getProperty("name");
        } catch (ex) {
          username = userId;
        }

        var label = flockGetString("photo/mediabar",
                                   "flock.media.simpleviewing.searchunique",
                                   [username]);

        var mediaArr = [];
        var media = {
          name: username,
          query: "searchunique:" + userId + "|username:" + username,
          label: label,
          favicon: inst.icon,
          service: inst.shortName
        }
        mediaArr.push(media);
        if (!aDocument._flock_decorations) {
          aDocument._flock_decorations = {};
        }

        if (aDocument._flock_decorations.mediaArr) {
          aDocument._flock_decorations.mediaArr =
            aDocument._flock_decorations.mediaArr.concat(mediaArr);
        } else {
          aDocument._flock_decorations.mediaArr = mediaArr;
        }

        inst.obs.notifyObservers(aDocument, "media", "media:update");
      }
    };
    this.api.usersGetInfo(myListener, userId, ["name"]);
  }
};

// Set the focus on the Wall TEXTAREA by sending a focus event.
// This action is only required to activate the field if the user has not yet
// typed anything in the Wall space.
flockFacebookService.prototype._setFocusOnWall =
function ffs__setFocusOnWall(aTextarea) {
  if (aTextarea) {
    var doc = aTextarea.ownerDocument;
    if (doc instanceof CI.nsIDOMHTMLDocument) {
      var placeholder = this._WebDetective.getString("facebook",
                                                     "wallPlaceholder",
                                                     "");

      // If the placeholder text is displaying, trigger the focus event to
      // activate the TEXTAREA
      if (aTextarea.value == aTextarea.getAttribute(placeholder)) {
        var eventObj = doc.createEvent("Events");
        eventObj.initEvent("focus", true, true);
        aTextarea.dispatchEvent(eventObj);
      }
    }
  }
};


// BEGIN flockIRichContentDropHandler Interface

// boolean handleDrop(in nsITransferable aFlavours,
//                    in nsIDOMHTMLElement aTargetElement);
flockFacebookService.prototype.handleDrop =
function ffs_handleDrop(aFlavours, aTargetElement) {
  // Handle the rich content drop
  this._logger.debug(".handleDrop()");
  var inst = this;
  // Handle textarea drops
  var dropCallback = function facebook_dropCallback(aXPath, aFlav) {
    // Get the content from the transferable
    var dataObj = {};
    var len = {};
    aFlavours.getTransferData(aFlav, dataObj, len);
    var content = dataObj.value.QueryInterface(CI.nsISupportsString).data;
    inst._logger.debug("facebook_dropCallback: content = " + content);
    // Get the message data from the content
    var contentParts = content.split(": ");
    var message = {
      title: "",
      link: ""
    };
    if (contentParts.length > 1) {
      message.link = contentParts.pop();
      message.title = contentParts.join(": ");
    } else {
      message.link = content;
    }
    // Determine where we're dropping the content
    var doc = aTargetElement.ownerDocument;
    if (aXPath == "newWallTextXPath") {
      // Dropped on New Facebook "Write" pane of Publisher: move the link to the
      // "Share Link" pane of the Publisher so that the link will be attached
      // to the message
      if (message.link) {
        var writeLinkListener = {
          onSuccess: function writeLinkListener_onSuccess(aLinkArea) {
            // Add User's post from their wall to the comment area
            aLinkArea.value = aTargetElement.value;
            _addBreadcrumb(aLinkArea);
          },
          onError: function writeLinkListener_onError(aFlockError) {
            // No error should be returned
            inst._logger.error("writeLinkListener: Error occured while trying "
                               + "to set up link area");
          }
        };
        inst._setUpShareLink(message.link, doc, writeLinkListener, true);
        // Link will be dropped in listener, so we're done
        return;
      }
    } else if (aXPath == "newLinkTextXPath") {
      // Dropped on New Facebook "Share Link" pane of Publisher: we just want
      // to drop the link here and not the title
      if (message.link) {
        var shareLinkListener = {
          onSuccess: function shareLinkListener_onSuccess(aLinkArea) {
            // Add breadcrumb to link comment
            _addBreadcrumb(aLinkArea);
          },
          onError: function shareLinkListener_onError(aFlockError) {
            // No error should be returned
            inst._logger.error("shareLinkListener: Error occured while trying "
                               + "to set up link area");
          }
        };
        inst._setUpShareLink(message.link, doc, shareLinkListener, false);
        // Link will be dropped in listener, so we're done
        return;
      }      
    } else if (aXPath == "newWriteNoteXPath") {
      // Dropped on (a) the Body field of the New Facebook "Write Note" pane or
      // (b) the Body field of the "Write a Note" editor: move the title from
      // the dropped content to the Title field when appropriate. 
      var title = inst._getHtmlElement("newWriteNoteTitleXPath", doc);
      if (title) {
        if (title.value == "") {
          // The user hasn't entered a title, so put the content title into the
          // Title field
          title.value = message.title;
          // Remove the title from the content dropped onto the body
          content = message.link;
        } else if (title.value == message.title) {
          // The user entered the content title into the Title field, so remove
          // it from the content dropped onto the body
          content = message.link;
        }
      }
    } else if (aXPath == "wallTextXPath") {
      // Dropped on Old Facebook wall: make sure it has focus before dropping,
      // to remove the placeholder text ("Write something...")
      inst._setFocusOnWall(aTargetElement);
    }
    // Drop the content onto the element
    inst._dropMessage(aTargetElement, content);
  };
  return this._handleAreaDrop(aTargetElement, dropCallback);
};

flockFacebookService.prototype._handleAreaDrop =
function ffs__handleAreaDrop(aElement, aCallback) {
  const WD_XPATH = 0;
  const WD_FLAVOUR = 1;
  var doc = aElement.ownerDocument;
  if (doc instanceof CI.nsIDOMHTMLDocument) {
    var domainsArray = this.domains.split(",");
    for each (var domain in domainsArray) {
      if (this._WebDetective.testDomain(doc.URL, this.domains)) {
        // Retrieve the specific fields from Web Detective to which
        // we cannot DnD
        var fields = this._WebDetective.getString(this.shortName,
                                                  "avoidDnDXPathFields", null);
        if (fields) {
          fields = fields.split(";");
          for each (var avoidDnD in fields) {
            var xPath = this._WebDetective.getString(this.shortName,
                                                     avoidDnD,
                                                     null);
            if (xPath) {
              var results = doc.evaluate(xPath, doc, null,
                                         CI.nsIDOMXPathResult.ANY_TYPE,
                                         null);
              if (results) {
                var node = results.iterateNext();
                while (node) {
                  if (node == aElement) {
                    // The matching field does not accept rich content, bail
                    return true;
                  }
                  node = results.iterateNext();
                }
              }
            }
          }
        }

        // Retrieve the specific fields from Web Detective to which
        // we can DnD
        var pairs = [];
        fields = this._WebDetective.getString(this.shortName,
                                              "dndXPathFields",
                                              null);
        if (fields) {
          fields = fields.split(";");
          for each (var xpfFields in fields) {
            pairs.push(xpfFields);
          }
        }

        // Go through the list of DnD fields to find a match
        for each (var xpfPair in pairs) {
          var xpf = xpfPair.split(",");
          var xPath = this._WebDetective.getString(this.shortName,
                                                   xpf[WD_XPATH],
                                                   null);
          if (xPath) {
            var results = doc.evaluate(xPath, doc, null,
                                       CI.nsIDOMXPathResult.ANY_TYPE,
                                       null);
            if (results) {
              var node = results.iterateNext();
              while (node) {
                if (node == aElement) {
                  // Let the service perform the drop via the callback
                  aCallback(xpf[WD_XPATH], xpf[WD_FLAVOUR]);
                  return true;
                }
                node = results.iterateNext();
              }
            }
          }
        }
      }
    }
  }
  return false;
};

// Put a link into the Share Link pane of the Publisher, hit the Preview button,
// and wait for the comment area to appear
flockFacebookService.prototype._setUpShareLink =
function ffs_setUpShareLink(aNewLink, aDoc, aFlockListener, aClickTab) {
  // Do we need to go to the pane?
  if (aClickTab) {
    // Click on the Share Link tab
    var linkArea = this._getHtmlElement("newLinkXPathLink", aDoc);
    linkArea.QueryInterface(CI.nsIDOMElement);
    _dispatchMouseEvent(linkArea);
  }
  // Put the new link into the input field
  var inputArea = this._getHtmlElement("newLinkTextXPath", aDoc);
  inputArea.value = aNewLink;
  _dispatchMouseEvent(inputArea);
  // Click the Preview button
  var preview = this._getHtmlElement("newShareLinkPreviewButton", aDoc);
  _dispatchMouseEvent(preview);
  // Wait for comment area to appear
  var inst = this;
  var timer = CC["@mozilla.org/timer;1"].createInstance(CI.nsITimer);
  timer.initWithCallback(
    {
      notify: function setPreviewTimer_callback(aTimer) {
        var commentArea = inst._getHtmlElement("newShareLinkCommentArea", aDoc);
        _dispatchMouseEvent(commentArea);
        aFlockListener.onSuccess(commentArea);
      }
    },
    1000,
    CI.nsITimer.TYPE_ONE_SHOT);
};

flockFacebookService.prototype._getHtmlElement =
function ffs__getHtmlElement(aWebDicXPath, aDoc) {
  var textAreaXPATH = this._WebDetective.getString("facebook",
                                                   aWebDicXPath,
                                                   "");
  var area = aDoc.evaluate(textAreaXPATH, aDoc, null,
                           CI.nsIDOMXPathResult.ANY_TYPE, null);
  return area ? area.iterateNext() : null;
};

flockFacebookService.prototype._dropMessage =
function ffs__dropMessage(aTargetElement, aMsg) {
  var caretPos = aTargetElement.selectionEnd;
  var currentValue = aTargetElement.value;

  // Add a trailing space so that Facebook scrapes for a valid,
  // unmodified url
  var nextChar = currentValue.charAt(caretPos);
  var trailingSpace = ((nextChar == "") ||
                       (nextChar == " ") ||
                       (nextChar == "\n"))
                    ? ""
                    : " ";
  aTargetElement.value = currentValue.substring(0, caretPos)
                       + aMsg.replace(/: /, ":\n")
                       + trailingSpace
                       + currentValue.substring(caretPos);
  _addBreadcrumb(aTargetElement);
};

// END flockIRichContentDropHandler interface


//**************************************
//* Facebook Account implementation
//**************************************

function flockFacebookAccount() {
  FlockSvcUtils.getLogger(this).init("facebookAccount");
  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);

  this.service = CC[FACEBOOK_CONTRACTID]
                 .getService(CI.flockIWebService);
  this.api = CC["@flock.com/api/facebook;1"]
             .getService(CI.flockIFacebookAPI)
             .wrappedJSObject;
  this._WebDetective = CC["@flock.com/web-detective;1"]
                      .getService(CI.flockIWebDetective);

  var wsa = FlockSvcUtils.flockIWebServiceAccount;
  wsa.addDefaultMethod(this, "getAllCustomParams");
  wsa.addDefaultMethod(this, "getCustomParam");
  wsa.addDefaultMethod(this, "getParam");
  wsa.addDefaultMethod(this, "getService");
  wsa.addDefaultMethod(this, "isAuthenticated");
  wsa.addDefaultMethod(this, "logout");
  wsa.addDefaultMethod(this, "keep");
  wsa.addDefaultMethod(this, "setCustomParam");
  wsa.addDefaultMethod(this, "setParam");

  var sa = FlockSvcUtils.flockISocialAccount;
  sa.addDefaultMethod(this, "enumerateFriends");
  sa.addDefaultMethod(this, "getFriendCount");
  sa.addDefaultMethod(this, "getInviteFriendsURL");
  sa.addDefaultMethod(this, "getFormattedFriendUpdateType");
  sa.addDefaultMethod(this, "formatFriendActivityForDisplay");
}

// Use genericComponent() for the goodness it provides (QI, nsIClassInfo, etc),
// but do NOT add this component to the list of constructors passed to
// FlockXPCOMUtils.genericModule().
flockFacebookAccount.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " Account",
  "",
  "",
  flockFacebookAccount,
  0,
  [
    CI.flockIWebServiceAccount,
    CI.flockISocialAccount,
    CI.flockIFacebookAccount,
    CI.flockIMediaAccount,
    CI.flockIMediaUploadAccount
  ]
);

/*************************************************************************
 * flockFacebookAccount: flockIWebServiceAccount Implementation
 *************************************************************************/

flockFacebookAccount.prototype.urn = "";
flockFacebookAccount.prototype.username = "";
flockFacebookAccount.prototype.status = "";

flockFacebookAccount.prototype.login =
function flockFacebookAccount_login(aFlockListener) {
  this._logger.debug(".login()");
  this._acUtils.ensureOnlyAuthenticatedAccount(this.urn);
  // force refresh on login
  var pollerSvc = CC["@flock.com/poller-service;1"]
                  .getService(CI.flockIPollerService);
  pollerSvc.forceRefresh(this.urn);
  if (aFlockListener) {
    aFlockListener.onSuccess(this, "");
  }
}

/*************************************************************************
 * flockFacebookAccount: flockISocialAccount Implementation
 *************************************************************************/

flockFacebookAccount.prototype.hasFriendActions = true;
flockFacebookAccount.prototype.isMeStatusSupported = true;
flockFacebookAccount.prototype.isFriendStatusSupported = true;
flockFacebookAccount.prototype.isStatusEditable  = true;
flockFacebookAccount.prototype.isPostLinkSupported = true;
flockFacebookAccount.prototype.isMyMediaFavoritesSupported = false;

flockFacebookAccount.prototype.setStatus =
function flockFacebookAccount_setStatus(aStatusMessage, aFlockListener)
{
  this._logger.info(".setStatus('" + aStatusMessage + "')");

  var inst = this;

  var statusListener = {
    onSuccess: function onSuccessHandler(aResult, aTopic) {
      inst._coop.get(inst.urn).statusMessage = aStatusMessage;

      // Save the current time in seconds
      var now = new Date().getTime();
      now = Math.round(now / 1000);
      inst._coop.get(inst.urn).lastStatusMessageUpdateDate = now;

      if (aFlockListener) {
        aFlockListener.onSuccess(aResult, aTopic);
      }
    },
    onError: function onErrorHandler(aFlockError, aTopic) {
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, aTopic);
      }
    }
  }

  this.api.setStatus(aStatusMessage, statusListener);
}

flockFacebookAccount.prototype.getEditableStatus =
function flockFacebookAccount_getEditableStatus()
{
  this._logger.info(".getEditableStatus()");
  var statusMessage = this._coop.get(this.urn).statusMessage;
  return (statusMessage) ? flockXMLDecode(statusMessage) : "";
}

flockFacebookAccount.prototype.formatStatusForDisplay =
function flockFacebookAccount_formatStatusForDisplay(aStatusMessage)
{
  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(FACEBOOK_PROPERTIES);
  return flockXMLDecode(aStatusMessage) +
         (!aStatusMessage.match(/[.!?]+$/) 
          ? bundle.GetStringFromName("flock.facebook.status.postfix") : "");
}

flockFacebookAccount.prototype.getMeNotifications =
function flockFacebookAccount_getMeNotifications()
{
  this._logger.info(".getMeNotifications()");

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(FACEBOOK_PROPERTIES);

  var noties = [];
  var inst = this;
  function _addNotie(aType, aCount) {
    var stringName = "flock.facebook.noties."
                   + aType + "."
                   + ((parseInt(aCount) <= 0) ? "none" : "some");
    noties.push({
      class: aType,
      tooltip: bundle.GetStringFromName(stringName),
      metricsName: aType,
      count: (aCount) ? aCount : 0,
      URL: inst._WebDetective.getString("facebook", aType + "_URL", "")
    });
  }
  var c_acct = this._coop.get(this.urn);

  var c_params = this.getAllCustomParams();
  _addNotie("meMessages", c_acct.accountMessages);
  _addNotie("mePokes", c_params.getProperty("fbPokes"));
  _addNotie("meFriendRequests", c_params.getProperty("fbFriendRequests"));
  _addNotie("meGroupRequests", c_params.getProperty("fbGroupInvites"));
  _addNotie("meEventRequests", c_params.getProperty("fbEventInvites"));
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(noties);
}

flockFacebookAccount.prototype.markAllMeNotificationsSeen =
function flockFacebookAccount_markAllMeNotificationsSeen(aType) {
  this._logger.debug(".markAllMeNotificationsSeen('" + aType + "')");
  var c_acct = this._coop.get(this.urn);
  switch (aType) {
    case "meMessages":
      c_acct.accountMessages = 0;
      break;
    case "mePokes":
      this.setCustomParam("fbPokes", 0);
      break;
    case "meFriendRequests":
      this.setCustomParam("fbFriendRequests", 0);
      break;
    case "meGroupRequests":
      this.setCustomParam("fbGroupInvites", 0);
      break;
    case "meEventRequests":
      this.setCustomParam("fbEventInvites", 0);
      break;
    default:
      break;
  }
}

flockFacebookAccount.prototype.getFriendActions =
function flockFacebookAccount_getFriendActions(aFriendURN)
{
  this._logger.info(".getFriendActions('" + aFriendURN + "')");

  var actionNames = ["friendPoke",
                     "friendMessage",
                     "friendWallPost",
                     "friendShareLink",
                     "friendViewProfile",
                     "friendGiveGift",
                     "friendShareFlock"];

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(FACEBOOK_PROPERTIES);

  var actions = [];
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    var c_acct = this._coop.get(this.urn);
    for each (var i in actionNames) {
      actions.push({
        label: bundle.GetStringFromName("flock.facebook.actions." + i),
        class: i,
        spec: this._WebDetective.getString("facebook", i, "")
                  .replace("%accountid%", c_acct.accountId)
                  .replace("%friendid%", c_friend.accountId)
      });
    }
  }
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(actions);
}

flockFacebookAccount.prototype.getSharingAction =
function flockFacebookAccount_getSharingAction(aFriendURN, aTransferable)
{
  this._logger.info(".getSharingAction('" + aFriendURN + "'," +
                                      "'" + aTransferable + "')");

  // We want to take advantage of Facebook's "Share Link" feature so we try to
  // incorporate it for every object we share.
  return "method:flockIFacebookAccount:sendMessageWithSharedLink";
}

flockFacebookAccount.prototype.getPostLinkAction =
function flockFacebookAccount_getPostLinkAction(aTransferable)
{
  // We need to add the breadcrumb so we'll handle this in the postLink method
  return "method:flockIFacebookAccount:postLink";
}

flockFacebookAccount.prototype.getProfileURLForFriend = 
function flockFacebookAccount_getProfileURLForFriend(aFriendURN)
{
  this._logger.info(".getProfileURLForFriend('" + aFriendURN + "')");

  var url = "";
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    url = this._WebDetective.getString("facebook", "friendprofile", "")
                            .replace("%accountid%", c_friend.accountId);
  }

  return url;
}

/*************************************************************************
 * flockFacebookAccount: flockIFacebookAccount Implementation
 *************************************************************************/

flockFacebookAccount.prototype.sendMessageWithSharedLink =
function flockFacebookAccount_sendMessageWithSharedLink(aFriendURN,
                                                        aTransferable)
{
  this._logger.info(".sendMessageWithSharedLink('" + aFriendURN + "'," +
                                               "'" + aTransferable + "')");

  var msgURL = "";

  if (!aTransferable) {
    // Was not a DND but a programmatical call
    // Special case requires us to get the active browser tab and then use
    // that as the sharing URL
    var win = _wm.getMostRecentWindow("navigator:browser");
    if (win) {
      var currentURL = win.gBrowser.currentURI.spec;
      var subject = win.gBrowser.contentTitle;
      var body = currentURL; 
      var c_friend = this._coop.get(aFriendURN);

      msgURL = this._buildSendMessageURL(c_friend.accountId, subject, body);
    }
  } else {
    msgURL = this._getSendMessageURLFromTransferable(aFriendURN,
                                                     aTransferable);
  }

  if (msgURL) {
    this._loadNewTabWithCallback(msgURL, this._sharedLinkCallback);
  }
}

flockFacebookAccount.prototype.shareFlock =
function flockFacebookAccount_shareFlock(aFriendURN)
{
  this._logger.info(".shareFlock('" + aFriendURN + "')");

  // Get friend
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    // Get "Share Flock" message text
    var sbs = CC["@mozilla.org/intl/stringbundle;1"]
              .getService(CI.nsIStringBundleService);
    var bundle = sbs.createBundle(SERVICES_PROPERTIES_FILE);
    var subject = bundle.GetStringFromName(SERVICES_SHARE_FLOCK_SUBJECT);
    try {
      var body = bundle.GetStringFromName(SERVICES_SHARE_FLOCK_MESSAGE + "0");
      for (var i = 1; ; i++) {
        body += "\n" + bundle.GetStringFromName(SERVICES_SHARE_FLOCK_MESSAGE + i);
      }
    } catch (ex) {
      // Ignore -- we've hit the end of our message lines
    }
    body = body.replace("%SERVICE", CLASS_NAME);
    // Build the URL and send it to the friend
    var msgURL = this._buildSendMessageURL(c_friend.accountId, subject, body);
    this._loadNewTabWithCallback(msgURL, this._shareFlockCallback);
  }
}

flockFacebookAccount.prototype.postLink =
function flockFacebookAccount_postLink(aTransferable)
{
  var url;

  if (aTransferable) {
    // Something was dropped onto the "Post Link" button: get the URL from the
    // transferable

    // This flavor set will ignore a text selection, which is OK
    var flavors = ["text/x-flock-media",
                   "text/x-moz-url"];
    var message = CC[FLOCK_RDNDS_CONTRACTID]
                  .getService(CI.flockIRichDNDService)
                  .getMessageFromTransferable(aTransferable,
                                              flavors.length,
                                              flavors);
    url = message.body;
  } else {
    // The "Post Link" button was clicked: get the current tab's URL
    var win = _wm.getMostRecentWindow("navigator:browser");
    if (win) {
      url = win.gBrowser.currentURI.spec;
    }
  }

  if (url) {
    // Post it onto this user's profile
    var postURL = this._WebDetective.getString("facebook", "postLink", "")
                                    .replace("%url%", encodeURIComponent(url));
    if (postURL) {
      this._loadNewTabWithCallback(postURL, this._postLinkCallback);
    }
  }
}

flockFacebookAccount.prototype._getSendMessageURLFromTransferable =
function flockFacebookAccount__getSendMessageURLFromTransferable(aFriendURN,
                                                                 aTransferable)
{
  this._logger.info("._getSendMessageURLFromTransferable('" + aFriendURN + "'," +
                                                        "'" + aTransferable + "')");

  var messageURL = "";
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    // Flavors we want to support, in order of preference
    var flavors = ["text/x-flock-media",
                   "text/x-moz-url",
                   "text/unicode",
                   "text/html"];

    var message = CC[FLOCK_RDNDS_CONTRACTID]
                  .getService(CI.flockIRichDNDService)
                  .getMessageFromTransferable(aTransferable,
                                              flavors.length,
                                              flavors);
    if (message.body) {
      messageURL = this._buildSendMessageURL(c_friend.accountId,
                                             message.subject,
                                             message.body);
    }
  }

  return messageURL;
}

flockFacebookAccount.prototype._buildSendMessageURL =
function flockFacebookAccount__buildSendMessageURL(aFriendId, aSubject, aBody) {
  return this._WebDetective.getString("facebook", "sendMessage", "")
             .replace("%friendid%", aFriendId)
             .replace("%subject%", encodeURIComponent(aSubject))
             .replace("%message%", encodeURIComponent(aBody));
}

// This opens a new tab and loads aURL into it. After the tab has loaded, the
// aCallback method is called.
flockFacebookAccount.prototype._loadNewTabWithCallback =
function flockFacebookAccount__loadNewTabWithCallback(aUrl, aCallback)
{
  var win = _wm.getMostRecentWindow("navigator:browser");
  if (win) {
    var browser = win.getBrowser();
    var newTab = browser.loadOneTab(aUrl, null, null, null, false, false);

    var obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);

    var observer = {
      observe: function loadNewTabWithCallback_observer(aContent,
                                                        aTopic,
                                                        aOwnsWeak)
      {
        var contentWindow = newTab.linkedBrowser.docShell
                                  .QueryInterface(CI.nsIInterfaceRequestor)
                                  .getInterface(CI.nsIDOMWindow);

        if (contentWindow == aContent) {
          obs.removeObserver(this, aTopic);

          // Callback?
          if (aCallback) {
            aCallback(contentWindow);
          }
        }
      }
    }

    obs.addObserver(observer, "EndDocumentLoad", false);
  }
}

// Callbacks for _loadNewTabWithCallback

// Adds the breadcrumb to the post link comment field.
flockFacebookAccount.prototype._postLinkCallback =
function flockFacebookAccount__postLinkCallback(aWindow) {
  // TEXTAREA field on post link page is called "sharer_popup_message"
  var formField = aWindow.document.getElementById("sharer_popup_message");
  if (formField) {
    // Add breadcrumb
    _addBreadcrumb(formField);

    // Put the cursor at the top of the text area, above breadcrumb
    formField.setSelectionRange(0, 0);
  }
}

// Embeds the shared link into the content of the message and then adds the
// breadcrumb.
flockFacebookAccount.prototype._sharedLinkCallback =
function flockFacebookAccount__sharedLinkCallback(aWindow) {
  // TEXTAREA field on send message page is called "message"
  var formField = aWindow.document.getElementById("message");
  if (formField) {
    // Call special Facebook hack to embed content into field
    _initKeyupEvent(formField);

    // Add breadcrumb
    _addBreadcrumb(formField);
  }
}

// Embeds the Flock link into the content of the message, cleans up the text,
// and then moves the cursor to the top of the message.
flockFacebookAccount.prototype._shareFlockCallback =
function flockFacebookAccount__shareFlockCallback(aWindow) {
  // TEXTAREA field on send message page is called "message"
  var formField = aWindow.document.getElementById("message");
  if (formField) {
    // Call special Facebook hack to embed content into field
    _initKeyupEvent(formField);

    // Facebook converts newlines on the command line to breaks in the message,
    // which looks bad, so convert them back to newlines in the message.
    formField.value = formField.value.replace("<br />", "\n", "gi");
  }
}

/*************************************************************************
 * XPCOM Support - Module Construction
 *************************************************************************/

// Create array of components.
var componentsArray = [flockFacebookService];

// Generate a module for XPCOM to find.
var NSGetModule = FlockXPCOMUtils.generateNSGetModule(MODULE_NAME,
                                                      componentsArray);
