// 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/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");
CU.import("resource:///modules/FlockMediaUtils.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%",
  "maxStatusLength" : "420"
};

var _wm = null;

// 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);
  this.obs = CC[OBSERVER_SERVICE_CONTRACTID].getService(CI.nsIObserverService);

  this.ppSvc = CC["@flock.com/people-service;1"]
               .getService(CI.flockIPeopleService);

  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, "tooltip");
  lws.addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  lws.addDefaultMethod(this, "getAccount");
  lws.addDefaultMethod(this, "getAccountIDFromDocument");
  lws.addDefaultMethod(this, "getAccountUrns");
  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");
  sws.addDefaultMethod(this, "parseStatus");

  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"];
  this.maxStatusLength = parseInt(gStrings["maxStatusLength"]);
  FlockSvcUtils.initService(this, true);
  this.api = CC["@flock.com/api/facebook;1"].getService(CI.flockIFacebookAPI).wrappedJSObject;

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

/**
 * @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(inst);
        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 identity = localFriends.getNext();
            if (!peopleHash[identity.accountId]) {
              inst._logger.info(identity.name
                                 + " removed on the server,"
                                 + " let's remove locally");
              identity.destroy();
            } else {
              peopleHash[identity.accountId].unseenMedia = identity.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, "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, "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;
  // We ignore clear status type changes
  if (aPerson.status.time && aPerson.status.message != "") {
    statusTime = parseInt(aPerson.status.time, 10);
  }

  var lastUpdate = 0;

  if (statusTime > lastProfileUpdate) {
    lastUpdate = statusTime;
    aPerson.lastUpdateType = "status";
  } else {
    lastUpdate = lastProfileUpdate;
    aPerson.lastUpdateType = "profile";
  }
  if (aPerson.media) {
    // + 10 explanation: when the friend uploads a new photo, Facebook
    // updates the profile update time a few seconds after the photo upload.
    // We want that to show as a "media" update, not as a "profile" update
    if ((aPerson.media.latest + 10) >= lastUpdate) {
      lastUpdate = new Date(aPerson.media.latest);
      aPerson.lastUpdateType = "media";
    }
  }

  aPerson.accountId = aPerson.uid;
  aPerson.avatar = aPerson.pic_square;
  aPerson.statusMessage = aPerson.status.message
                        ? flockXMLEncode(aPerson.status.message)
                        : "";
  aPerson.URL = gStrings["userprofile"].replace("%accountid%", aPerson.uid);

  // Set the status id
  var statusMessageUrl = null;
  if (aPerson.status.status_id) {
    var url = this._WebDetective
                  .getString(CLASS_SHORT_NAME, "statusCommentURL", "");
    statusMessageUrl = url.replace("%friendid%", aPerson.uid)
                          .replace("%storyid%", aPerson.status.status_id);
  }
  aPerson.statusMessageUrl = statusMessageUrl;
  var coopIdentity = aAccount.coopObj.addPerson(aPerson, lastUpdate);
  if (aPerson.media) {
    coopIdentity.unseenMedia += aPerson.media.count;
  }
}

/**
 * @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);
  // Instantiate account component
  var account = this.getAccount(accountUrn);
  account.setParam("service", this._coopObj);
  account.setParam("refreshInterval", FACEBOOK_REFRESH_INTERVAL);
  account.setCustomParam("fbPokes", 0);
  account.setCustomParam("fbGroupInvites", 0);
  account.setCustomParam("fbEventInvites", 0);
  account.setCustomParam("fbFriendRequests", 0);
  if (aFlockListener) {
    aFlockListener.onSuccess(account, "addAccount");
  }
  return account;
}

// BEGIN flockIAuthenticateNewAccount interface

// DEFAULT: void authenticateNewAccount();

// END flockIAuthenticateNewAccount interface

/**
 * @see flockILoginWebService#updateAccountStatusFromDocument
 */
flockFacebookService.prototype.updateAccountStatusFromDocument =
function flockFacebookService_updateAccountStatusFromDocument(aDocument, 
                                                              aAccount,
                                                              aFlockListener)
{
  if (aAccount) {
    // We know we're already logged in to this account
    if (!aAccount.isAuthenticated()) {
      aAccount.login(aFlockListener);
    }
  } else if (this._WebDetective
                 .detect("facebook", "loggedout", aDocument, null))
  {
    this._acUtils.markAllAccountsAsLoggedOut(this);
    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, aAcctId, 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.markAllAccountsAsLoggedOut(this);
  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);
        },
        onError: aFlockListener.onError
      };
      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();
    var c_acct = acct ? inst._coop.Account.get(acct) : null;
    if (!c_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 = "
               + c_acct.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 = "
               + c_acct.accountId
               + ")) "
               + "AND created > now() - 2592000"; // One month (in seconds)
          break;
        case "photosOfYou":
          qry += "WHERE pid IN ("
               + "SELECT pid FROM photo_tag WHERE subject="
               + c_acct.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 += "\n" + 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, aAccount) {
  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) {
        var username = userId;

        aEnum = aEnum.QueryInterface(CI.nsISimpleEnumerator);
        if (aEnum.hasMoreElements()) {
          var person = aEnum.getNext()
                            .QueryInterface(CI.nsIWritablePropertyBag2);
          try {
            username = person.getProperty("name");
          } catch (ex) {
            // Use the userId if we can't find the name
          }
        }

        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");
      },
      onError: function listener_onError(aFlockError, aTopic) {
        inst._logger.error("usersGetInfo: Error occured");
      }
    };
    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) {
        var username = userId;

        aEnum = aEnum.QueryInterface(CI.nsISimpleEnumerator);
        if (aEnum.hasMoreElements()) {
          var person = aEnum.getNext()
                            .QueryInterface(CI.nsIWritablePropertyBag2);
          try {
            username = person.getProperty("name");
          } catch (ex) {
            // Use the userId if we can't find the name
          }
        }

        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");
      },
      onError: function listener_onError(aFlockError, aTopic) {
        inst._logger.error("usersGetInfo: Error occured");
      }
    };
    this.api.usersGetInfo(myListener, userId, ["name"]);
  }
};

// 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) {
    inst._logger.debug("facebook_dropCallback('" + aXPath + "')");
    // 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;
    // March 2009: Yet another change to the Publisher. We'll leave the other
    // cases in here for now, in case they revert this change at some point.
    if (aXPath == "publisherTextXPath") {
      // Dropped on "Write something" textarea of Publisher. If possible, move
      // the link to the "Share Link" pane of the Publisher so that the link
      // will be attached to the message.
      if (message.link) {
        // Make sure text area has focus, to expose "Add" buttons if necessary
        aTargetElement.focus();
        // Is Link tab open?
        var link = inst._getHtmlElement("publisherLinkXPath", doc);
        if (link) {
          // Found it, so drop onto Link
          inst._doPublisherLink(message, doc, aTargetElement, link);
          return;
        } else {
          // Can we click on the Link button to open the Link tab?
          var button = inst._getHtmlElement("publisherLinkButtonXPath", doc);
          if (button) {
            // Click on the Link button
            _dispatchMouseEvent(button);
            // Wait for the Link Tab to load             
            var timer = CC["@mozilla.org/timer;1"].createInstance(CI.nsITimer);
            timer.initWithCallback(
              {
                tries: 0,
                notify: function fs_clickLinkCallback(aTimer) {
                  this.tries++;
                  if (this.tries > 20) {
                    // Didn't find it so drop full content onto the wall
                    inst._dropMessage(aTargetElement, content);
                    timer.cancel();
                  } else {
                    var link = inst._getHtmlElement("publisherLinkXPath", doc);
                    if (link) {
                      // Found it, so drop onto Link
                      inst._doPublisherLink(message, doc, aTargetElement, link);
                      timer.cancel();
                    }
                  }
                }
              },
              250,
              CI.nsITimer.TYPE_REPEATING_SLACK);
            return;
          }
        }
      }
    } else if (aXPath == "publisherLinkXPath") {
      // Dropped on Link field in the Link pane of Publisher.
      if (message.link) {
        var wall = inst._getHtmlElement("publisherTextXPath", doc);
        inst._doPublisherLink(message, doc, wall, aTargetElement);
        return;
      }
    } else 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;
        }
      }
    }
    // 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 Attach button,
// and drop breadcrumb on the wall.
flockFacebookService.prototype._doPublisherLink =
function ffs__doPublisherLink(aMessage, aDoc, aWall, aLink) {
  // Write bread crumb onto wall
  _addBreadcrumb(aWall);

  // Drop message link into the Link field
  aLink.value = aMessage.link;

  // Click on the Attach button
  var attach = this._getHtmlElement("publisherLinkAttachXPath", aDoc);
  _dispatchMouseEvent(attach);
};

// 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 xpath = this._WebDetective.getString("facebook", aWebDicXPath, "");
  var results = aDoc.evaluate(xpath, aDoc, null,
                              CI.nsIDOMXPathResult.ANY_TYPE, null);
  return results ? results.iterateNext() : null;
};

flockFacebookService.prototype._dropMessage =
function ffs__dropMessage(aTargetElement, aMsg) {
  // Make sure the textarea has focus. This will remove any placeholders from
  // the text before we start manipulating it.
  aTargetElement.focus();

  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.flockIMediaAccount,
    CI.flockIMediaUploadAccount
  ]
);

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

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

flockFacebookAccount.prototype.login =
function flockFacebookAccount_login(aFlockListener) {
  this._logger.debug(".login()");
  this._acUtils.ensureOnlyAuthenticatedAccount(this);
  // Force refresh on login is done by the people sidebar
  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.coopObj.statusMessage;
  return (statusMessage) ? flockXMLDecode(statusMessage) : "";
}

flockFacebookAccount.prototype.formatStatusForDisplay =
function flockFacebookAccount_formatStatusForDisplay(aStatusMessage)
{
  if (aStatusMessage && aStatusMessage.length > 0) {
    return flockXMLDecode(aStatusMessage);
  }
  return aStatusMessage;
}

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.coopObj;

  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.coopObj;
  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.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);
  }
};

// void friendAction(in AString aAction, in AString aFriendURN, in AString..
flockFacebookAccount.prototype.friendAction =
function flockFacebookAccount_friendAction(aAction, aFriendURN, aActivityURN) {
  switch (aAction) {
    case "statusComment":
      this._statusComment(aFriendURN, aActivityURN);
      break;
    default:
      this._logger.debug("Invalid friendAction: '" + aAction + "'");
      break;
  }
};

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

  var actionNames = ["friendStatusComment",
                     "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) {
    if (!c_friend.statusMessageUrl) {
      // Remove the "Status Comment" action from the actions menu
      actionNames.splice(actionNames.indexOf("friendStatusComment"), 1);
    }
    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%", this.coopObj.accountId)
                  .replace("%friendid%", c_friend.accountId)
      });
    }
  }
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(actions);
}

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

  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.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.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.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 acct = this;
  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)
      {
        obs.removeObserver(this, aTopic);
        if (newTab.linkedBrowser.docShell) {
          var contentWindow = newTab.linkedBrowser.docShell
                                    .QueryInterface(CI.nsIInterfaceRequestor)
                                    .getInterface(CI.nsIDOMWindow);

          if (contentWindow == aContent) {

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

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

// Opens a friend's profile page with their status highlighted, and then runs
// the callback method
flockFacebookAccount.prototype._statusComment =
function flockFacebookAccount__statusComment(aFriendURN, aActivityURN) {
  this._logger.info("._statusComment('" + aFriendURN + "')");
  var updateURL;
  if (aActivityURN) {
    updateURL = this._coop.FriendActivity.get(aActivityURN).updateURL;
  } else {
    updateURL = this._coop.Identity.get(aFriendURN).statusMessageUrl;
  }
  if (updateURL) {
    // Open the friend's profile with the status highlighted
    this._loadNewTabWithCallback(updateURL, this._statusCommentCallback);
  }
};

// Callbacks for _loadNewTabWithCallback

// Adds the breadcrumb to the post link comment field.
flockFacebookAccount.prototype._postLinkCallback =
function flockFacebookAccount__postLinkCallback(aAcct, 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(aAcct, 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(aAcct, 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");
  }
};

// Opens the comment field for the highlighted status and gives it focus
flockFacebookAccount.prototype._statusCommentCallback =
function flockFacebookAccount__statusCommentCallback(aAcct, aWindow) {
  // Find the comment button
  var results = FlockSvcUtils.newResults();
  if (aAcct._WebDetective.detect(CLASS_SHORT_NAME,
                                 "statusCommentLink",
                                 aWindow.document,
                                 results))
  {
    var link = results.getPropertyAsInterface("comment_link", CI.nsIDOMNode);
    if (link) {
      // Simulate a click on the comment link to reveal the comment field
      var evt = aWindow.document.createEvent("MouseEvents");
      evt.initMouseEvent("click", true, true, aWindow, 0, 0, 0, 0, 0,
                         false, false, false, false, 0, null);
      link.dispatchEvent(evt);

      // Make sure the comment field has focus
      if (aAcct._WebDetective.detect(CLASS_SHORT_NAME,
                                     "statusComment",
                                     aWindow.document,
                                     results))
      {
        var comment = results.getPropertyAsInterface("comment", CI.nsIDOMNode);
        if (comment) {
          comment.focus();
        }
      }
    }
  }
};

/*************************************************************************
 * 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);
