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

Components.utils.import("resource://gre/modules/JSON.jsm");

const FACEBOOK_CID = Components.ID("{81a58e10-4f45-11db-b0de-0800200c9a66}");
const FACEBOOK_CONTRACTID              = "@flock.com/people/facebook;1";
const FACEBOOK_FAVICON                 = "http://www.facebook.com/favicon.ico";
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 FLOCK_RICH_CONTENT_CATEGORY_ENTRY = "flockRichContentHandler";
const XPATH_WALLTEXT = "wallTextXPath";

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://browser/skin/flock/services/common/no_avatar.png";
const FAVES_COOP = "chrome://flock/content/common/load-faves-coop.js";
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 FACEBOOK_SHARE_FLOCK_SUBJECT = "flock.facebook.friendShareFlock.subject";
const FACEBOOK_SHARE_FLOCK_MESSAGE = "flock.facebook.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
// The delay between two refreshes when the sidebar is open (in seconds)
const FACEBOOK_SHORT_INTERVAL = 300; // 5 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 ==========
// ====================================================

var gCompTK;
function getCompTK() {
  if (!gCompTK) {
    gCompTK = CC[FLOCK_SINGLETON_CONTRACTID]
              .getService(CI.flockISingleton)
              .getSingleton("chrome://browser/content/flock/services/common/load-compTK.js")
              .wrappedJSObject;
  }
  return gCompTK;
}

// For use with the scheduler
var gTimers = [];

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

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

function loadSubScript(spec) {
  var loader = CC[JS_SUBSCRIPT_LOADER_CONTRACTID]
               .getService(CI.mozIJSSubScriptLoader);
  var context = {};
  loader.loadSubScript(spec, context);
  return context;
}

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

loadLibraryFromSpec("chrome://browser/content/flock/photo/photoAPI.js");
loadLibraryFromSpec("chrome://flock/content/common/flocksafe.js");

function _getIdentityUrn(aAccountId, aUid) {
  var result = FACEBOOK_IDENTITY_URN_PREFIX
             + aAccountId + ":"
             + aUid;

  return result;
}

// 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();
}

// Appends the Flock breadcrumb to the given text field
function _addBreadcrumb(aField) {
  if (aField) {
    var breadcrumb = CC[FLOCK_RDNDS_CONTRACTID]
                     .getService(CI.flockIRichDNDService)
                     .getBreadcrumb("plain");
    if (breadcrumb) {
      aField.value += breadcrumb;
    }
  }
}


//******************************************
//* Service Class
//******************************************

function flockFacebookService() {
  this.obs = CC[OBSERVER_SERVICE_CONTRACTID].getService(CI.nsIObserverService);
  this.acUtils = CC[FLOCK_ACCOUNT_UTILS_CONTRACTID].
                 getService(CI.flockIAccountUtils);

  this.status = CI.flockIWebService.STATUS_UNKNOWN;
  this.url = "http://www.facebook.com";
  this.api = CC["@flock.com/api/facebook;1"].getService(CI.flockIFacebookAPI).wrappedJSObject;
  this.mIsInitialized = false;
  this.photoList = null;    

  this._ctk = {
    interfaces: [
      "nsISupports",
      "nsIClassInfo",
      "nsISupportsCString",
      "nsIObserver",
      "flockIWebService",
      "flockIAuthenticateNewAccount",
      "flockISocialWebService",
      "flockIManageableWebService",
      "flockIPollingService",
      "flockIMediaWebService",
      "flockIRichContentDropHandler"
    ],
    shortName: "facebook",
    fullName: "Facebook",
    description: FACEBOOK_TITLE,
    domains: gStrings["domains"],
    favicon: FACEBOOK_FAVICON,
    CID: FACEBOOK_CID,
    contractID: FACEBOOK_CONTRACTID,
    accountClass: flockFacebookAccount,
    needPassword: true
  };

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

  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
    }
  };

  this._logger = CC["@flock.com/logger;1"].createInstance(CI.flockILogger);
  this._logger.init("facebookService");
  this._profiler = CC["@flock.com/profiler;1"].getService(CI.flockIProfiler);
  this.init();
}

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(FLOCK_RICH_CONTENT_CATEGORY_ENTRY,
                           CATEGORY_ENTRY_NAME, true);
    return;
  }

  this.faves_coop = CC[FLOCK_SINGLETON_CONTRACTID]
                    .getService(CI.flockISingleton)
                    .getSingleton(FAVES_COOP)
                    .wrappedJSObject;

  this.account_root = this.faves_coop.accounts_root;

  this.fbService = new this.faves_coop.Service(
    "urn:facebook:service", {
      name: "facebook",
      desc: "Facebook"
    });
  this.fbService.serviceId = FACEBOOK_CONTRACTID;

  this.urn = this.fbService.id();

  // Load Web Detective file
  this.webDetective = this.acUtils.useWebDetective("facebook.xml");
  for (var s in gStrings) {
    gStrings[s] = this.webDetective.getString("facebook", s, gStrings[s]);
  }
  this.fbService.domains = gStrings["domains"];
  this.fbService.loginURL = gStrings["userlogin"];

  // 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 also need to reauth the api. We do this by calling
  // login() on the xpcom account obj. However, we have to wait while
  // the Facebook service is finished constructing itself.
  var inst = this;
  var reloginCallback = {
    notify: function reloginCallback_notify(aTimer) {
      inst._logger.debug("reloginCallback.notify()");
      var acctURN = inst.acUtils
                        .getFirstAuthenticatedAccountForService(FACEBOOK_CONTRACTID);

      if (acctURN) {
        var account = inst.getAccount(acctURN);
        if (account) {
          var reloginListener = {
            onSuccess: function reloginListener_onSuccess(aSubject, aTopic) {
              inst._logger.debug(".init(): reloginListener: onSuccess()");
            },
            onError: function reloginListener_onError(aError) {
              inst._logger.debug(".init(): reloginListener: onError()");
              inst.faves_coop.get(acctURN).isAuthenticated = false;
            }
          };
          account.login(reloginListener);
        }
      }
    }
  };
  var reloginTimer = CC["@mozilla.org/timer;1"].createInstance(CI.nsITimer);
  reloginTimer.initWithCallback(reloginCallback, 100, CI.nsITimer.TYPE_ONE_SHOT);

}

/**
 * The following method defined in nsIObserver are provided by component
 * toolkit:
 *
 * @see nsIObserver#observe
 */

/**
 * @see flockIPollingService#refresh
 */
flockFacebookService.prototype.refresh =
function flockFacebookService_refresh(aUrn, aListener) {
  this._logger.debug(".refresh() with aUrn of " + aUrn);
  var refreshItem = this.faves_coop.get(aUrn);

  if (refreshItem instanceof this.faves_coop.Account) {
    this._logger.debug("refreshing an Account");
    if (refreshItem.isAuthenticated && this.api.isLoggedIn) {
      this._refreshAccount(aUrn, aListener);
    } else {
      this._logger.debug("api not auth'd for this account: skipping refresh");
      // If the user is not logged in, return a success without
      // refreshing anything
      aListener.onResult();
    }
  } else if (refreshItem instanceof this.faves_coop.Identity) {
    this._logger.debug("refreshing an Identity");
    aListener.onResult();
  } else {
    throw CR.NS_ERROR_ABORT;
    this._logger.error("can't refresh " + aUrn + " (unsupported type)");
    aListener.onError(null);
  }
}

flockFacebookService.prototype._refreshAccount =
function flockFacebookService_refreshAccount(aUrn, aListener) {
  var inst = this;
  var refreshItem = this.faves_coop.get(aUrn);
  var lastUpdate = refreshItem.lastUpdateDate;
  var peopleHash;
  var numberOfAnswers = 0;

  function onIndividualSuccess() {
    numberOfAnswers++;
    if (numberOfAnswers >= 3) {
      if (inst.acUtils.isPeopleSidebarOpen()) {
        refreshItem.nextRefresh = new Date(Date.now()
                                           + FACEBOOK_SHORT_INTERVAL * 1000);
      }
      if (aListener) {
        aListener.onResult();
      }
    }
  }

  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);
        uid = bag.getProperty("uid");
        peopleHash[uid].media = {
          count: bag.getProperty("count"),
          latest: bag.getProperty("latest")
        }
      }

      // Now that we have all we need, update the RDF
      function myWorker(aShouldYield) {
        var count = 0;
        // ADD or update existing people
        for (uid in peopleHash) {
          count++;
          inst._addPerson(peopleHash[uid], refreshItem);
          if (aShouldYield()) {
            yield;
          }
        }
        // REMOVE locally people removed on the server
        var localEnum = refreshItem.friendsList.children.enumerate();
        while (localEnum.hasMoreElements()) {
          var identity = localEnum.getNext();
          if (!peopleHash[identity.accountId]) {
            inst._logger.info("Friend " + identity.accountId
                              + " has been deleted on the server");
            refreshItem.friendsList.children.remove(identity);
            identity.destroy();
          }
        }
        onIndividualSuccess();
      }
      getCompTK().schedule(gTimers, 0.05, 10, myWorker);
    },
    onError: function mfl_onError(aError) {
      inst._logger.error("Error on getUpdatedMediaFriends");
    }
  }

  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() {
      inst._logger.error("Error on gettingFriends")
    }
  };

  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");
        refreshItem.statusMessage = bag.getProperty("statusMessage");
        refreshItem.lastProfileUpdate = bag.getProperty("lastProfileUpdate");

        onIndividualSuccess();
      }
    },
    onError: function ugil_onError() {
      inst._logger.error("Error on usersGetInfo");
    }
  };

  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
        if ((refreshItem.accountMessages < bag.getProperty("messages")) ||
            (refreshItem.fbPokes < bag.getProperty("pokes")) ||
            (refreshItem.fbFriendRequests < bag.getProperty("friendRequests")) ||
            (refreshItem.fbGroupInvites < bag.getProperty("groupInvites")) ||
            (refreshItem.fbEventInvites < bag.getProperty("eventInvites")))
        {
          inst._lightPeopleIcon();
        }

        // write notifications to RDF
        refreshItem.accountMessages = bag.getProperty("messages");
        refreshItem.fbPokes = bag.getProperty("pokes");
        refreshItem.fbFriendRequests = bag.getProperty("friendRequests");
        refreshItem.fbGroupInvites = bag.getProperty("groupInvites");
        refreshItem.fbEventInvites = bag.getProperty("eventInvites");
      }

      onIndividualSuccess();
    },
    onError: function ngl_onError() {
      inst._logger.error("Error on notificatonsGet");
    }
  };

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

flockFacebookService.prototype._incrementMedia =
function ffs_incrementMedia(aAccountUrn, aUid, aCount, aLatest) {
  var currAcct = this.faves_coop.get(aAccountUrn);

  // Update the count on the identity...
  var identityUrn = _getIdentityUrn(currAcct.accountId, aUid);
  identity = this.faves_coop.get(identityUrn);
  identity.unseenMedia += aCount;

  // + 10 explaination: 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 ((aLatest + 10) >= identity.lastUpdate) {
    identity.lastUpdate = aLatest;
    identity.lastUpdateType = "media";
  }
}


flockFacebookService.prototype._addPerson =
function flockFacebookService_addPerson(aPerson, aCoopAccount) {
  var person = aPerson;

  // We include the current accountId in the identity urn to prevent friend
  // collisions if multiple Facebook accounts have the same friend.
  var identityUrn = _getIdentityUrn(aCoopAccount.accountId,
                                    person.uid);
  var updating = this.faves_coop.Identity.exists(identityUrn);
  var identity;

  var lastProfileUpdate = parseInt(person.profile_update_time, 10);
  var statusTime = parseInt(person.status.time, 10);
  var lastUpdate = 0;
  var lastUpdateType;
  if (statusTime > lastProfileUpdate) {
    lastUpdate = statusTime;
    lastUpdateType = "status";
  } else {
    lastUpdate = lastProfileUpdate;
    lastUpdateType = "profile";
  }
  this._logger.debug("Adding person: " + person.uid + " - " + person.name);

  if (updating) {
    // update data of the identity coop obj here
    identity = this.faves_coop.get(identityUrn);
    if (lastUpdate > identity.lastUpdate) {
      identity.lastUpdate = lastUpdate;
      identity.lastUpdateType = lastUpdateType;
      identity.name = person.name;
      identity.avatar = person.pic_square;
      identity.statusMessage = person.status.message;
    }
  } else {
    identity = new this.faves_coop.Identity(
      identityUrn,
      {
        name: person.name,
        serviceId: FACEBOOK_CONTRACTID,
        accountId: person.uid,
        avatar: person.pic_square,
        statusMessage: person.status.message,
        lastUpdate: lastUpdate,
        lastUpdateType: lastUpdateType
      }
    );
    aCoopAccount.friendsList.children.add(identity);
  }

  if (person.media) {
    this._incrementMedia(aCoopAccount.id(),
                         person.uid,
                         person.media.count,
                         person.media.latest);
  }
}

flockFacebookService.prototype.markAllMediaSeen =
function flockFacebookService_markAllMediaSeen(aIdentityUrn) {
  var identity = this.faves_coop.get(aIdentityUrn);
  identity.unseenMedia = 0;
}

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

flockFacebookService.prototype._lightPeopleIcon =
function flockFacebookService_lightPeopleIcon() {
  this._logger.debug("._lightPeopleIcon()");
  this.obs.notifyObservers(null, "new-people-notification", null);
}

flockFacebookService.prototype._addPhotostream =
function flockFacebookService_addPhotostream(aPhotoPerson, aCoopAccount) {
  var mediaFavesUrn = "urn:media:favorites";
  var query = new queryHelper();
  var person = aPhotoPerson.QueryInterface(CI.nsIPropertyBag);
  query.user = person.getProperty("uid");
  query.username = person.getProperty("name");
  this._logger.info(".addPhotostream('" + query.username + "')");
  var mediaQueryUrn = mediaFavesUrn + ":facebook:" + query.stringVal;
  var mediaQuery = this.faves_coop.get(mediaQueryUrn);
  if (!mediaQuery) {
    mediaQuery = new this.faves_coop.MediaQuery(
      mediaQueryUrn,
      {
        serviceId: FLOCK_PHOTO_API_MANAGER_CONTRACTID,
        service: this.shortName,
        favicon: FACEBOOK_FAVICON,
        isPollable: true,
        query: query.stringVal,
        name: person.getProperty("name")
      }
    );
  }
  mediaQuery.isTransient = aCoopAccount.isTransient;
  var mediaFaves = this.faves_coop.get(mediaFavesUrn);
  if (!mediaFaves) {
    mediaFaves = new this.faves_coop.Folder(mediaFavesUrn);
    this.faves_coop.favorites_root.children.add(mediaFaves);
  }
  mediaFaves.children.addOnce(mediaQuery);
}

/**
 * The following attributes and methods defined in flockIWebService are
 * provided by component toolkit:
 *
 * @see flockIWebService#icon
 * @see flockIWebService#needPassword
 * @see flockIWebService#shortName
 * @see flockIWebService#title
 * @see flockIWebService#urn
 * @see flockIWebService#getAccount
 * @see flockIWebService#getAccounts
 * @see flockIWebService#removeAccount
 */

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

  var pw = this.acUtils.getPassword(this.urn + ":" + aAccountID);
  /// @todo: FIX ME - name shouldn't be email address (pw.user) - DP
  var name = (pw) ? pw.user : aAccountID;

  var accountUrn = "urn:flock:facebook:account:" + aAccountID;
  var account = new this.faves_coop.Account(
    accountUrn, {
      name: name,
      serviceId: FACEBOOK_CONTRACTID,
      service: this.fbService,
      accountId: aAccountID,
      isPollable: false,
      refreshInterval: FACEBOOK_REFRESH_INTERVAL,
      URL: gStrings["userprofile"].replace("%accountid%", aAccountID),
      isTransient: aIsTransient,
      favicon: FACEBOOK_FAVICON
    });

  this.account_root.children.add(account);
  this.USER = account.id();

  var friendsListUrn = accountUrn + ":friends";
  var friendsList = new this.faves_coop.FriendsList(
    friendsListUrn,
    {
      account: account
    });
  account.friendsList = friendsList;

  // Instantiate account component
  var acct = this.getAccount(account.id());
  if (aListener) {
    aListener.onSuccess(acct, "addAccount");
  }
  return acct;
}

// BEGIN flockIAuthenticateNewAccount interface
flockFacebookService.prototype.authenticateNewAccount =
function flockFacebookService_authenticateNewAccount(aListener)
{
  this._logger.info("{flockIAuthenticateNewAccount}.authenticateNewAccount(aListener)");
  var wm = CC["@mozilla.org/appshell/window-mediator;1"]
           .getService(CI.nsIWindowMediator);
  var win = wm.getMostRecentWindow('navigator:browser');
  win.openUILinkIn(this.url, "tab");
}
// END flockIAuthenticateNewAccount interface

/**
 * The following methods defined in flockIManageableWebService are provided by
 * component toolkit:
 *
 * @see flockIManageableWebService#docRepresentsSuccessfulLogin
 * @see flockIManageableWebService#getAccountIDFromDocument
 * @see flockIManageableWebService#getCredentialsFromForm
 * @see flockIManageableWebService#ownsDocument
 * @see flockIManageableWebService#ownsLoginForm
 */

/**
 * @see flockIManageableWebService#updateAccountStatusFromDocument
 */
flockFacebookService.prototype.updateAccountStatusFromDocument =
function flockFacebookService_updateAccountStatusFromDocument(aDocument) {
  this._logger.debug(".updateAccountStatusFromDocument(aDocument)");
  if (this.webDetective.detect("facebook", "loggedout", aDocument, null)) {
    this.acUtils.markAllAccountsAsLoggedOut(FACEBOOK_CONTRACTID);
    this.api.deauthenticate();
  } else if (this.webDetective.detect("facebook",
                                      "loggedin",
                                      aDocument,
                                      null))
  {
    var results = CC[HASH_PROPERTY_BAG_CONTRACTID]
                  .createInstance(CI.nsIWritablePropertyBag2);
    if (this.webDetective.detect("facebook",
                                 "accountinfo",
                                 aDocument,
                                 results))
    {
      var acctId = results.getPropertyAsAString("accountid");
      var acctUrn = this.acUtils.getAccountURNById(this.urn, acctId);
      var acct = this.faves_coop.get(acctUrn);
      if (!acct.isAuthenticated) {
        var inst = this;
        var loginListener = {
          onSuccess: function loginListener_onSuccess() {
            inst._logger.info(".updateAccountStatusFromDocument(): "
                              + "loginListener: onSuccess()");
            acct.isAuthenticated = true;
          },
          onError: function loginListener_onError(aError) {
            inst._logger.info(".updateAccountStatusFromDocument(): "
                              + "loginListener: onError()");
            inst._logger.info(aError ? (aError.errorString) : "No details");
            acct.isAuthenticated = false;
          }
        };
        this.getAccount(acctUrn).login(loginListener);
      }
    }
  }
}




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


/**
 * findByUsername
 * @see flockIMediaWebService#findByUsername
 */
flockFacebookService.prototype.findByUsername =
function flockFacebookService_findByUsername(aListener, aUsername) {
  // need to call this to trigger mediaObserver
  aListener.onError(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(aListener) {
  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.

  aListener.onSuccess(result, "");
}

/**
 * getAlbums
 * @see flockIMediaWebService#getAlbums
 *
 * aUsername is the FB uid
 */
flockFacebookService.prototype.getAlbums =
function flockFacebookService_getAlbums(aListener, aUsername) {
  this._logger.info(".getAlbums(aListener, " + aUsername + ")");
  var myListener = {
    onSuccess: function listener_onSuccess(aEnumResults, aStatus) {
      aListener.onGetAlbumsResult(aEnumResults);
    },
    onError: function listener_onError(aSubject, aTopic, aFlockError) {
      aListener.onError(aFlockError);
    }
  };

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

/**
 * getAuthPerson
 * @see flockIMediaWebService#getAuthPerson
 */
flockFacebookService.prototype.getAuthPerson =
function flockFacebookService_getAuthPerson () {
  try {
    var person = {};
    person.id = this.api.uid;
    // XXX Localize these
    person.fullname = "Unknown";
    person.username = "Unknown";
    person.service = this;
    return person;
  } catch (ex) {
    return null;
  }
}


/**
 * getContacts
 * @see flockIMediaWebService#getContacts
 * @todo Implement this, or handle the error more gracefully.
 * @throws NS_ERROR_NOT_IMPLEMENTED
 */
flockFacebookService.prototype.getContacts =
function flockFacebookService_getContacts(aListener) {
  throw CR.NS_ERROR_NOT_IMPLEMENTED;
}

/**
 * getMostRecentPhotoForList
 * @see flockIMediaWebService#getMostRecentPhotoForList
 * @todo Implement this, or handle the error more gracefully.
 * @throws NS_ERROR_NOT_IMPLEMENTED
 */
flockFacebookService.prototype.getMostRecentPhotoForList =
function flockFacebookService_getMostRecentPhotoForList(aListener,
                                                        aEnumerator) {
  throw CR.NS_ERROR_NOT_IMPLEMENTED;
}

/**
 * getPhoto
 * @see flockIMediaWebService#getPhoto
 * @todo Implement this, or handle the error more gracefully.
 * @throws NS_ERROR_NOT_IMPLEMENTED
 */
flockFacebookService.prototype.getPhoto =
function flockFacebookService_getPhoto(aListener, aPhotoId) {
  throw CR.NS_ERROR_NOT_IMPLEMENTED;
}

/**
 * getPhotoFromRDFNode
 * @see flockIMediaWebService#getPhotoFromRDFNode
 */
flockFacebookService.prototype.getPhotoFromRDFNode =
function flockFacebookService_getPhotoFromRDFNode(aRdfId) {
  var newPhoto = this.api.createPhoto();
  var coopPhoto = this.faves_coop.get(aRdfId);
  newPhoto.webPageUrl = coopPhoto.URL;
  newPhoto.thumbnail = coopPhoto.thumbnail;
  newPhoto.midSizePhoto = coopPhoto.midSizePhoto;
  newPhoto.largeSizePhoto = coopPhoto.largeSizePhoto;
  newPhoto.username = coopPhoto.username;
  newPhoto.userid = coopPhoto.userid;
  newPhoto.title = coopPhoto.name;
  newPhoto.id = coopPhoto.photoid;
  newPhoto.icon = coopPhoto.favicon;
  newPhoto.uploadDate = coopPhoto.datevalue;
  newPhoto.is_public = coopPhoto.is_public;
  newPhoto.is_video = coopPhoto.is_video;

  return newPhoto;
}

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

/**
 * search
 * @see flockIMediaWebService#search
 */
flockFacebookService.prototype.search =
function flockFacebookService_search(aListener, aQueryString, aCount, aPage) {
  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) {
    myarray.reverse();
    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 = [];
    if (inst.photoList) {
      var owners = [];

      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) {
      aListener.onSearchResult(createEnum(photos));
      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;
          }
          photos.sort(function (a,b) {
            return a.uploadDate - b.uploadDate;
          });
          aListener.onSearchResult(createEnum(photos));
        }
      };
      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(aSubject, aTopic, aFlockError) {
      aListener.onError(aFlockError);
    }
  };

  var qry = "SELECT pid, owner, src_small, src_big, src, link, caption, "
          + "created FROM photo ";
  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);

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

    return;
  } else if (aQuery.special) {
    switch (aQuery.special) {
      case "photosFromFriends":
        qry += "WHERE aid IN ("
             + "SELECT aid FROM album WHERE owner IN ("
             + "SELECT uid2 FROM friend WHERE uid1 = "
             + this.api.uid
             + ")) "
             + "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 = "
             + this.api.uid
             + ")) "
             + "AND created > now() - 2592000"; // One month (in seconds)
        break;
      case "photosOfYou":
        qry += "WHERE pid IN ("
             + "SELECT pid FROM photo_tag WHERE subject="
             + this.api.uid
             + ")";
        break;
    }
    this._logger.debug("Performing FQLQuery: " + qry);
    this.api.doFQLQuery(fqlListener, qry);

    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 ]/)) {
      aListener.onSearchResult(createEnum([]));
      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 searchService = CC["@flock.com/lucene/flockLucene;1"]
                      .getService(CI.flockILucene);
  var inst = this;

  var searchListener = {
    onSearchComplete:
    function searchListener_onSearchComplete(aNumResults, aResults) {
      inst._logger.debug("onSearchComplete: " + aNumResults);
      var uidArray = [];
      while (aResults.hasMoreElements()) {
        var result = aResults.getNext().QueryInterface(CI.flockILuceneResult);
        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);
    }
  }

  searchService.search(_normalizeQuery(aQuery.search),
                       "identity",
                       100, searchListener);
}

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

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

/**
 * upload2
 * @see flockIMediaWebService#upload
 */
flockFacebookService.prototype.upload =
function flockFacebookService_upload(aListener, aUpload, aFilename) {
  var params = CC[HASH_PROPERTY_BAG_CONTRACTID]
               .createInstance(CI.nsIWritablePropertyBag2);
  params.setPropertyAsAString("description", aUpload.description);

  this.api.uploadPhotosViaUploader(aListener, aFilename, params, aUpload);
}

/**
 * The following methods defined in flockIMediaWebService are handled by
 * component toolkit:
 *
 * @see flockIMediaWebService#checkIsStreamUrl
 * @see flockIMediaWebService#handlesMediaStream
 * @see flockIMediaWebService#getMediaQueryFromURL
 */

/**
 * 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 = {};
        }
        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 = {};
        }
        aDocument._flock_decorations.mediaArr = mediaArr;
        inst.obs.notifyObservers(aDocument, "media", "media:update");
      }
    };
    this.api.usersGetInfo(myListener, userId, ["name"]);
  }
}


// Checks if the TEXTAREA is drag and droppable
flockFacebookService.prototype._isDnDableTextarea =
function ffs__isDnDableTextarea(aDocument, aXPath, aTextarea) {
  if (aDocument && aXPath && aTextarea) {
    var xpath = this.webDetective.getString("facebook", aXPath, "");
    var results = aDocument.evaluate(xpath, aDocument, null,
                                     CI.nsIDOMXPathResult.ANY_TYPE, null);
    if (results && results.iterateNext() == aTextarea) {
      return true;
    }
  }

  return false;
}


// 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(aXpathField, aTextarea) {
  if (aTextarea && aXpathField && aXpathField == XPATH_WALLTEXT) {
    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
// Handle the rich content drop
flockFacebookService.prototype.handleDrop =
function ffs_handleDrop(aFlavours, aTextarea) {
  this._logger.debug(".handleDrop()");

  var inst = this;
  var dropCallback = function facebook_dropCallback(aFlav) {
    var dataObj = {}, len = {};
    aFlavours.getTransferData(aFlav, dataObj, len);

    // Activate the TEXTAREA if we are dropping on the Wall
    inst._setFocusOnWall(XPATH_WALLTEXT, aTextarea);

    var caretPos = aTextarea.selectionEnd;
    var currentValue = aTextarea.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"))
                      ? ""
                      : " ";
    // Only add a breadcrumb if the insertion point is at the end of
    // the text so that we don't duplicate breadcrumbs
    var breadcrumb = (aTextarea.value.length == caretPos)
                   ? CC[FLOCK_RDNDS_CONTRACTID]
                     .getService(CI.flockIRichDNDService)
                     .getBreadcrumb("plain")
                   : "";

    aTextarea.value = currentValue.substring(0, caretPos)
                    + dataObj.value.QueryInterface(CI.nsISupportsString)
                             .data.replace(/: /, ":\n")
                    + trailingSpace
                    + currentValue.substring(caretPos);

    // Call special Facebook hack to embed content into field
    _initKeyupEvent(aTextarea);

    aTextarea.value += breadcrumb;
  };

  return this._handleTextareaDrop(CATEGORY_ENTRY_NAME, this.fbService.domains,
                                  aTextarea, dropCallback);
}
// END flockIRichContentDropHandler interface


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

function flockFacebookAccount() {
  this.acUtils = CC[FLOCK_ACCOUNT_UTILS_CONTRACTID]
                 .getService(CI.flockIAccountUtils);
  this.service = CC[FACEBOOK_CONTRACTID]
                 .getService(CI.flockIWebService);
  this.api = CC["@flock.com/api/facebook;1"]
             .getService(CI.flockIFacebookAPI)
             .wrappedJSObject;
  this.faves_coop = CC[FLOCK_SINGLETON_CONTRACTID]
                    .getService(CI.flockISingleton)
                    .getSingleton(FAVES_COOP)
                    .wrappedJSObject;
  this.webDetective = CC["@flock.com/web-detective;1"]
                      .getService(CI.flockIWebDetective);
  this._ctk = {
    interfaces: [
      "nsISupports",
      "flockIWebServiceAccount",
      "flockISocialWebServiceAccount",
      "flockIFacebookAccount",
      "flockIMediaWebServiceAccount",
      "flockIMediaUploadAccount"
    ]
  };
  getCompTK().addAllInterfaces(this);

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

// BEGIN flockIWebServiceAccount interface
flockFacebookAccount.prototype.urn = "";
flockFacebookAccount.prototype.username = "";
flockFacebookAccount.prototype.status = "";

flockFacebookAccount.prototype.activate =
function flockFacebookAccount_activate(aListener) {
  this._logger.debug(".activate()");
  var acctCoopObj = this.faves_coop.get(this.urn);

  var inst = this;
  this.api.authenticate({
    onSuccess: function onSuccess() {
      inst._logger.debug(".activate() authenticated: " + inst.api.uid);
      if (aListener) {
        // Get the user's full name before popping the mediabar so the stream
        // name is correct.
        var usersGetInfoListener = {
          onSuccess: function ugil_onSuccess(aEnum, aStatus) {
            aEnum.QueryInterface(CI.nsISimpleEnumerator);
            if (aEnum.hasMoreElements()) {
              var bag = aEnum.getNext();
              bag.QueryInterface(CI.nsIPropertyBag);
              acctCoopObj.avatar = bag.getProperty("avatar");
              acctCoopObj.name = bag.getProperty("name");
              acctCoopObj.nextRefresh = new Date(0);
              acctCoopObj.isPollable = true;
              acctCoopObj.isAuthenticated = true;
              aListener.onSuccess(inst, "accountAuthorized");
            }
          },
          onError: function ugil_onError() {
            inst._logger.error("Error on usersGetInfo");
          }
        };
        inst.api.usersGetInfo(usersGetInfoListener, inst.api.uid, null);
      }
    },
    onError: function onError(aSubject, aTopic, aError) {
      inst._logger.error("error on authentication: " + aError.errorString);
    }
  });
}

flockFacebookAccount.prototype.login =
function flockFacebookAccount_login(aListener) {
  this._logger.debug(".login()");
  var inst = this;
  var myListener = {
    onSuccess: function listener_onSuccess() {
      inst.acUtils.ensureOnlyAuthenticatedAccount(inst.urn);
      // force refresh on login
      var pollerSvc = CC["@flock.com/poller-service;1"]
                      .getService(CI.flockIPollerService);
      pollerSvc.forceRefresh(inst.urn);
      if (aListener) {
        aListener.onSuccess(null, "");
      }
    },
    onError: function listener_onError(aFlockError) {
      if (aListener) {
        aListener.onError(null, null, aFlockError);
      }
    }
  }
  this.api.deauthenticate();
  this.acUtils.markAllAccountsAsLoggedOut(FACEBOOK_CONTRACTID);

  this.api.authenticate(myListener);
}
// END flockIWebServiceAccount interface

// BEGIN flockISocialWebServiceAccount interface
flockFacebookAccount.prototype.hasFriendActions = true;
flockFacebookAccount.prototype.isStatusSupported = true;
flockFacebookAccount.prototype.isStatusEditable  = true;
flockFacebookAccount.prototype.isPostLinkSupported = true;
flockFacebookAccount.prototype.isMyMediaFavoritesSupported = false;

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

  var inst = this;

  var statusListener = {
    onSuccess: function onSuccessHandler(aResult, aTopic) {
      inst.faves_coop.get(inst.urn).statusMessage = aStatusMessage;
      if (aListener) {
        aListener.onSuccess(aResult, aTopic);
      }
    },
    onError: function onErrorHandler(aError) {
      if (aListener) {
        aListener.onError(aError);
      }
    }
  }

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

flockFacebookAccount.prototype.getEditableStatus =
function flockFacebookAccount_getEditableStatus()
{
  this._logger.info(".getEditableStatus()");
  var prefix = this.webDetective.getString("facebook", "statusPrefix", "");
  var statusMessage = this.faves_coop.get(this.urn).statusMessage;
  
  if (prefix && prefix != "") {
    // The status being formatted with a prefix so take
    // it out of the editable status value
    var regex = new RegExp("^" + prefix + " ");
    if (regex.test(statusMessage)) {
      regex = new RegExp(prefix + " ");
      statusMessage = statusMessage.replace(regex,"");
    }
  }
  return 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);
  var prefix = this.webDetective.getString("facebook", "statusPrefix", "");
  // Add the post fix
  var defaultStatus = aStatusMessage
                    + (!aStatusMessage.match(/[.!?]+$/)
                       ? bundle.GetStringFromName("flock.facebook.status.postfix") : "");
  // Is a prefix required to format the status?
  if (prefix && prefix != "") {
    // A prefix is required to format the status
    var regex = new RegExp("^" + prefix + " ");
    if (!regex.test(aStatusMessage)) {
      // Apply the prefix
      return prefix + " " + defaultStatus;
    }
  }
  
  // No prefix required
  return defaultStatus;
}

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,
      URL: inst.webDetective.getString("facebook", aType + "_URL", "")
    });
  }
  var c_acct = this.faves_coop.get(this.urn);
  _addNotie("meMessages", c_acct.accountMessages);
  _addNotie("mePokes", c_acct.fbPokes);
  _addNotie("meFriendRequests", c_acct.fbFriendRequests);
  _addNotie("meGroupRequests", c_acct.fbGroupInvites);
  _addNotie("meEventRequests", c_acct.fbEventInvites);
  return JSON.toString(noties);
}

flockFacebookAccount.prototype.markAllMeNotificationsSeen =
function flockFacebookAccount_markAllMeNotificationsSeen(aType) {
  this._logger.debug(".markAllMeNotificationsSeen('" + aType + "')");
  var c_acct = this.faves_coop.get(this.urn);
  switch (aType) {
    case "meMessages":
      c_acct.accountMessages = 0;
      break;
    case "mePokes":
      c_acct.fbPokes = 0;
      break;
    case "meFriendRequests":
      c_acct.fbFriendRequests = 0;
      break;
    case "meGroupRequests":
      c_acct.fbGroupInvites = 0;
      break;
    case "meEventRequests":
      c_acct.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.faves_coop.get(aFriendURN);
  if (c_friend) {
    var c_acct = this.faves_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)
      });
    }
  }
  return JSON.toString(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.faves_coop.get(aFriendURN);
  if (c_friend) {
    url = this.webDetective.getString("facebook", "friendprofile", "")
                           .replace("%accountid%", c_friend.accountId);
  }

  return url;
}

// END flockISocialWebServiceAccount interface

// BEGIN flockIFacebookAccount interface
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.faves_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.faves_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(FACEBOOK_PROPERTIES);
    var subject = bundle.GetStringFromName(FACEBOOK_SHARE_FLOCK_SUBJECT);
    try {
      var body = bundle.GetStringFromName(FACEBOOK_SHARE_FLOCK_MESSAGE + "0");
      for (var i = 1; ; i++) {
        body += "\n" + bundle.GetStringFromName(FACEBOOK_SHARE_FLOCK_MESSAGE + i);
      }
    } catch (ex) {
      // Ignore -- we've hit the end of our message lines
    }
    // 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.faves_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");
  }
}

// END flockIFacebookAccount interface

//******************************************
//* XPCOM registration
//******************************************

function createModule(aParams) {
  return {
    registerSelf: function registerSelf(aCompMgr,
                                        aFileSpec,
                                        aLocation,
                                        aType) {
      aCompMgr.QueryInterface(CI.nsIComponentRegistrar);
      aCompMgr.registerFactoryLocation(aParams.CID,
                                       aParams.componentName,
                                       aParams.contractID,
                                       aFileSpec,
                                       aLocation,
                                       aType);
      var catMgr = CC[CATEGORY_MANAGER_CONTRACTID].
                   getService(CI.nsICategoryManager);
      if (!aParams.categories) {
        aParams.categories = [];
      }
      for (var i = 0; i < aParams.categories.length; i++) {
        var cat = aParams.categories[i];
        catMgr.addCategoryEntry(cat.category,
                                cat.entry,
                                cat.value,
                                true,
                                true);
      }
    },
    getClassObject: function getClassObject(aCompMgr, aCID, aIID) {
      if (!aCID.equals(aParams.CID)) {
        throw CR.NS_ERROR_NO_INTERFACE;
      }
      if (!aIID.equals(CI.nsIFactory)) {
        throw CR.NS_ERROR_NOT_IMPLEMENTED;
      }
      return { // Factory
        createInstance: function createInstance(aOuter, aIID) {
          if (aOuter != null) {
            throw CR.NS_ERROR_NO_AGGREGATION;
          }
          var comp = new aParams.componentClass();
          if (aParams.implementationFunc) {
            aParams.implementationFunc(comp);
          }
          return comp.QueryInterface(aIID);
        }
      };
    },
    canUnload: function canUnload(aCompMgr) {
      return true;
    }
  };
}

// NS Module entrypoint
function NSGetModule(aCompMgr, aFileSpec) {
  return createModule({
    componentClass: flockFacebookService,
    CID: FACEBOOK_CID,
    contractID: FACEBOOK_CONTRACTID,
    componentName: CATEGORY_COMPONENT_NAME,
    implementationFunc: function implementationFunc(aComp) {
      getCompTK().addAllInterfaces(aComp);
    },
    categories: [
      {
        category: "wsm-startup",
        entry: CATEGORY_COMPONENT_NAME,
        value: FACEBOOK_CONTRACTID
      },
      {
        category: "flockWebService",
        entry: CATEGORY_ENTRY_NAME,
        value: FACEBOOK_CONTRACTID
      },
      {
        category: "flockMediaProvider",
        entry: CATEGORY_ENTRY_NAME,
        value: FACEBOOK_CONTRACTID
      },
      {
        category: FLOCK_RICH_CONTENT_CATEGORY_ENTRY,
        entry: CATEGORY_ENTRY_NAME,
        value: FACEBOOK_CONTRACTID
      }
    ]
  });
}
