/*global FlockXPCOMUtils, FlockSvcUtils, FlockPrefsUtils, FlockPhotoUploader, FlockScheduler, FlockCryptoHash, QueryHelper, XML, flockGetString */
/*global BeboAccount, BeboAPI */
// 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/FlockSvcUtils.jsm");
CU.import("resource:///modules/FlockPrefsUtils.jsm");
CU.import("resource:///modules/FlockScheduler.jsm");
CU.import("resource:///modules/FlockCryptoHash.jsm");
CU.import("resource:///modules/FlockMediaUtils.jsm");
CU.import("resource:///modules/FlockStringBundleHelpers.jsm");
CU.import("resource:///modules/FlockPhotoUploader.jsm");

const FLOCK_ERROR_CONTRACTID = "@flock.com/error;1";
const FLOCK_PHOTO_ALBUM_CONTRACTID = "@flock.com/photo-album;1";
const FLOCK_RDNDS_CONTRACTID = "@flock.com/rich-dnd-service;1";

const BEBO_CID = Components.ID("{d6449a86-2ae6-4b7d-96e7-e8e596960225}");
const BEBO_CONTRACTID = "@flock.com/webservice/bebo;1";

const SERVICE_ENABLED_PREF = "flock.service.bebo.enabled";
const BEBO_API_CID = Components.ID("{225b7e45-da20-410a-8206-c112ab1e3187}");
const BEBO_API_CONTRACTID = "@flock.com/webservice/api/bebo;1";
const BEBO_FAVICON = "chrome://flock/content/services/bebo/favicon.png";
const BEBO_PROPERTIES = "chrome://flock/locale/services/bebo.properties";
const SERVICES_PROPERTIES_FILE = "chrome://flock/locale/services/services.properties";
const SERVICES_SHARE_FLOCK_MESSAGE = "flock.friendShareFlock.message";

const BEBO_API_KEY = "QJ4m8oq1PRV38w0Tsij7DC49XWbUM4QF4mC7";
const BEBO_API_SECRET = "JpamLzF5ZkKQWPFiw5lFxiBWyPJ5JwIJHjsy";
const BEBO_API_VERSION = "1.0";

const MODULE_NAME = "Bebo";
const CLASS_NAME = "Flock Bebo Service";
const CLASS_TITLE = "Bebo";
const CLASS_SHORT_NAME = "bebo";

// The delay between refreshes for the poller, in seconds.
const REFRESH_INTERVAL = 1800;  // 30 minutes

const XMLHTTPREQUEST_READYSTATE_COMPLETED = 4;
const BEBO_MAX_STATUS_LENGTH = 140;


/*************************************************************************
 * Global Functions and Data
 *************************************************************************/

// The Bebo API object
var gApi = null;

// String defaults... may be updated later through Web Detective.
var gStrings = {
  "domains": "bebo.com",
  "homePageURL": "http://www.bebo.com/",
  "loginURL": "http://www.bebo.com/SignIn.jsp",
  "profileURL": "http://www.bebo.com/Profile.jsp?MemberId=%userId%",
  "endpoint": "http://api.bebo.com/restserver.php",
  "authTokenEndpoint": "http://www.bebo.com",
  "userApprovalCall": "/c/auth_token/add",
  "errorResponse": "error_response",
  "errorCode": "error_code",
  "errorMessage": "error_msg",
  "dataFields": "name,pic_square,status,profile_update_time",
  "topPhotosUid": "7819682324",
  "setStatusURL": "http://www.bebo.com/Saying.jsp",
  "statusTokenScrapeURL": "http://www.bebo.com/c/home/index.html",
  "deleteStatusURL": "http://www.bebo.com/SayingDelete.jsp",
  "statusIdScrapeURL": "http://www.bebo.com/Profile.jsp"
};

// Helper to create an nsISimpleEnumerator over an array.
function createEnum(aEnumArray) {
  return {
    QueryInterface: function se_QueryInterface(iid) {
      if (iid.equals(CI.nsISimpleEnumerator)) {
        return this;
      }
      throw CR.NS_ERROR_NO_INTERFACE;
    },
    hasMoreElements: function se_hasMoreElements() {
      return aEnumArray.length > 0;
    },
    getNext: function se_getNext() {
      return aEnumArray.shift();
    }
  };
}

// Helper to create an nsISimpleEnumerator over a NodeList.
function createNodeListEnum(aNodeList) {
  return {
    QueryInterface: function se_QueryInterface(iid) {
      if (iid.equals(CI.nsISimpleEnumerator)) {
        return this;
      }
      throw CR.NS_ERROR_NO_INTERFACE;
    },
    hasMoreElements: function se_hasMoreElements() {
      return aNodeList.length > this.current;
    },
    getNext: function se_getNext() {
      var node = aNodeList.item(this.current);
      this.current++;
      return node;
    },
    current: 0
  };
}

// Helper function to parse XML
function getNodeList(aNode, aTagName) {
  if (aNode) {
    return aNode.getElementsByTagName(aTagName);
  }

  return null;
}

// Helper function to parse XML
function getNode(aNode, aTagName) {
  var list = getNodeList(aNode, aTagName);
  if (list) {
    return list.item(0);
  }

  return null;
}

// Helper function to parse XML
function getNodeName(aNode) {
  if (aNode) {
    return aNode.nodeName;
  }

  return "";
}

// Helper function to parse XML
function getNodeValue(aNode, aTagName) {
  var node = (aTagName && aTagName.length > 0) ? getNode(aNode, aTagName)
                                               : aNode;
  if (node && node.firstChild) {
    return node.firstChild.nodeValue;
  }

  return "";
}

// Helper function to retrieve the last update time in seconds instead
// of milliseconds
function getLastUpdateInSeconds(aUserNode, aField) {
  return Math.round(getNodeValue(aUserNode, aField) / 1000);
}

// Formatter for Album items in media bar.
var flockAlbumItemFormatter = {
  canBuildTooltip: true,

  buildTooltip: function faif_buildTooltip(aMediaItem) {
    var sbs = CC["@mozilla.org/intl/stringbundle;1"]
              .getService(CI.nsIStringBundleService);
    var bundle = sbs.createBundle(BEBO_PROPERTIES);
    var titleText = bundle.GetStringFromName(
                           "flock.bebo.mediabar.tooltip.album.title");
    var labelText = bundle.GetStringFromName(
                           "flock.bebo.mediabar.tooltip.album.label");
    var xml;
    /*e4x[*/
    default xml namespace =
      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
      xml =
      <vbox>
        <hbox>
          <image src={aMediaItem.midSizePhoto} style="margin-bottom: 2px;" />
          <spacer flex="1" />
        </hbox>
        <hbox>
          <vbox>
            <image src={aMediaItem.icon} />
            <spacer flex="1" />
          </vbox>
          <vbox anonid="ttInfoContainer">
            <label anonid="ttTitleTxt">{titleText} {aMediaItem.title}</label>
            <vbox anonid="ttUserTxt">
              <label>{labelText} {aMediaItem.comment_count}</label>
            </vbox>
          </vbox>
        </hbox>
      </vbox>;
    /*]e4x*/
    return xml;
  }
};

var flockBandItemFormatter = {
  canBuildTooltip: true,

  buildTooltip: function fbif_buildTooltip(aMediaItem) {
    var sbs = CC["@mozilla.org/intl/stringbundle;1"]
              .getService(CI.nsIStringBundleService);
    var bundle = sbs.createBundle(BEBO_PROPERTIES);
    var titleText = bundle.GetStringFromName(
                           "flock.bebo.mediabar.tooltip.band.title");
    var xml;
    /*e4x[*/
    default xml namespace =
      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
      xml =
      <vbox>
        <hbox>
          <image src={aMediaItem.largeSizePhoto} style="margin-bottom: 2px;" />
          <spacer flex="1" />
        </hbox>
        <hbox>
          <vbox>
            <image src={aMediaItem.icon} />
            <spacer flex="1" />
          </vbox>
          <vbox anonid="ttInfoContainer">
            <label anonid="ttTitleTxt">{titleText} {aMediaItem.title}</label>
            <vbox anonid="ttUserTxt">
              <label></label>
            </vbox>
          </vbox>
        </hbox>
      </vbox>;
    /*]e4x*/
    return xml;
  }
};


/*************************************************************************
 * Component: Flock Bebo Service
 *************************************************************************/

function BeboService() {
  var profiler = CC["@flock.com/profiler;1"].getService(CI.flockIProfiler);
  var evtID = profiler.profileEventStart("bebo-init");
  FlockSvcUtils.getLogger(this).debug(".init()");

  if (FlockPrefsUtils.getBoolPref(SERVICE_ENABLED_PREF) === false) {
    this._logger.info("Pref " + SERVICE_ENABLED_PREF
                              + " set to FALSE... not initializing.");
    this.deleteCategories();
    return;
  }

  // These are used by FlockSvcUtils.
  this._accountClassCtor = BeboAccount;
  this._stringBundlePath = BEBO_PROPERTIES;

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

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

  var lws = FlockSvcUtils.flockILoginWebService;
  lws.addDefaultMethod(this, "loginURL");
  lws.addDefaultMethod(this, "tooltip");
  lws.addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  lws.addDefaultMethod(this, "getAccountIDFromDocument");
  lws.addDefaultMethod(this, "getAccountUrns");
  lws.addDefaultMethod(this, "getAccount");
  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 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");

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

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

  // Add observer service
  this._obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);
  this._ppSvc = CC["@flock.com/people-service;1"]
                .getService(CI.flockIPeopleService);

  FlockSvcUtils.getWD(this);

  for (var s in gStrings) {
    gStrings[s] = this._WebDetective.getString(CLASS_SHORT_NAME,
                                               s,
                                               gStrings[s]);
  }
  var authenticatedUrn = FlockSvcUtils.initService(this, true);

  // The channels returned by enumerateChannels
  var bundle = this.getStringBundle();
  this._channels = {
    "special:topPhotos": {
      title: bundle.GetStringFromName("flock.bebo.title.topphotos"),
      supportsSearch: false
    }
  };

  // Cached media items for media bar, to support pagination
  this._miList = [];

  // Instantiate the API
  gApi = new BeboAPI();

  // Update auth
  if (authenticatedUrn) {
    var acct = this._coop.Account.get(authenticatedUrn);
    var results = FlockSvcUtils.newResults();
    if (this._WebDetective
            .detectCookies(CLASS_SHORT_NAME, "loggedincookies", results) &&
        results.getPropertyAsAString("password") != "autoturnedoff")
    {
      gApi.setAuthedAccount(this.getAccount(authenticatedUrn));
    } else {
      // User must have closed the browser without logging out, and had not
      // checked "Keep me logged in for two weeks".
      this._logger.debug("Apparently not logged in, so de-authenticating.");
      acct.isAuthenticated =  false;
    }
  }

  profiler.profileEventEnd(evtID, "");
}

/*************************************************************************
 * BeboService: XPCOM Component Creation
 *************************************************************************/

BeboService.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME,
  BEBO_CID,
  BEBO_CONTRACTID,
  BeboService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.flockIWebService,
    CI.flockIAuthenticateNewAccount,
    CI.flockILoginWebService,
    CI.flockIMediaWebService,
    // BUG #15944: Disable photo uploader pending resolution from Bebo.
    //CI.flockIMediaUploadWebService,
    CI.flockIMediaEmbedWebService,
    CI.flockISocialWebService,
    CI.flockIRichContentDropHandler,
    CI.flockIPollingService
  ]
);

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


/*************************************************************************
 * BeboService: flockIWebService implementation
 *************************************************************************/

// readonly attribute AString title;
BeboService.prototype.__defineGetter__("title",
function BeboService_gettitle() {
  return CLASS_TITLE;
});

// readonly attribute AString shortName;
BeboService.prototype.__defineGetter__("shortName",
function BeboService_getshortName() {
  return CLASS_SHORT_NAME;
});

// readonly attribute AString icon;
BeboService.prototype.__defineGetter__("icon",
function BeboService_geticon() {
  return BEBO_FAVICON;
});

// DEFAULT: readonly attribute AString url;

// readonly attribute AString contractId;
BeboService.prototype.__defineGetter__("contractId",
function BeboService_getcontractId() {
  return BEBO_CONTRACTID;
});

// readonly attribute AString urn;
BeboService.prototype.__defineGetter__("urn",
function BeboService_geturn() {
  return "urn:" + CLASS_SHORT_NAME + ":service";
});

// DEFAULT: nsIStringBundle getStringBundle();
// DEFAULT: boolean isHidden();

/*************************************************************************
 * BeboService: flockIAuthenticateNewAccount implementation
 *************************************************************************/

// DEFAULT: void authenticateNewAccount();

/*************************************************************************
 * BeboService: flockILoginWebService implementation
 *************************************************************************/

// readonly attribute AString domains;
BeboService.prototype.__defineGetter__("domains",
function BeboService_getdomains() {
  return gStrings["domains"];
});

// readonly attribute boolean needPassword;
BeboService.prototype.__defineGetter__("needPassword",
function BeboService_getneedPassword() {
  return true;
});

// DEFAULT: readonly attribute AString loginURL;
// DEFAULT: readonly attribute AString tooltip;

// flockIWebServiceAccount addAccount(in AString aAccountId, ...
BeboService.prototype.addAccount =
function BeboService_addAccount(aAccountId, aIsTransient, aFlockListener) {
  this._logger.debug("addAccount('" + aAccountId + "', "
                     + aIsTransient + ", aFlockListener)");
  if (!aAccountId) {
    if (aFlockListener) {
      var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
      // XXX See bug 10749 - flockIError cleanup
      error.errorCode = 9999;
      error.errorString = "No Account ID provided";
      aFlockListener.onError(error, arguments.callee.name);
    }
    return null;
  }

  var pw = this._acUtils.getPassword(this.urn + ":" + aAccountId);
  var name = (pw) ? pw.username : aAccountId;
  var url = this._makeProfileUrl("profileURL", 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", REFRESH_INTERVAL);
  account.setCustomParam("beboPokes", 0);
  account.setCustomParam("beboFriendRequests", 0);
  account.setCustomParam("beboGroupRequests", 0);
  account.setCustomParam("beboEventRequests", 0);
  account.setCustomParam("beboStatusToken", "");
  if (aFlockListener) {
    aFlockListener.onSuccess(account, "addAccount");
  }
  return account;
};

// DEFAULT: boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument ...
// DEFAULT: AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: nsIStringEnumerator getAccountUrns();
// DEFAULT: flockIWebServiceAccount getAccount(in AString aUrn);
// DEFAULT: AString getAuthenticatedAccount();
// DEFAULT: flockILoginInfo getCredentialsFromForm(in nsIDOMHTMLFormElement ...
// DEFAULT: AString getSessionValue();

// void logout();
BeboService.prototype.logout =
function BeboService_logout() {
  this._logger.debug("logout()");
  this._acUtils.markAllAccountsAsLoggedOut(this);
  this._acUtils.removeCookies(this._WebDetective
                                  .getSessionCookies(this.shortName));
  gApi.deauthenticate();
};

// DEFAULT: boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: void removeAccount(in AString aUrn);

// void updateAccountStatusFromDocument(in nsIDOMHTMLDocument aDocument,
//                                      in flockIWebServiceAccount aAccount,
//                                      in flockIListener aFlockListener);
BeboService.prototype.updateAccountStatusFromDocument =
function BeboService_updateAccountStatusFromDocument(aDocument,
                                                     aAccount,
                                                     aFlockListener)
{
  this._logger.debug("updateAccountStatusFromDocument(): Mm, what's up, Doc?");
  if (aAccount) {
    // We know we're already logged in to this account
    gApi.setAuthedAccount(aAccount);
    if (!this._acUtils.ensureOnlyAuthenticatedAccount(aAccount)) {
      aAccount.login(aFlockListener);
    }
    this._logger.debug("updateAccountStatusFromDocument() - logged IN");
  } else if (this._WebDetective
                 .detect(CLASS_SHORT_NAME, "loggedout", aDocument, null))
  {
    // We're logged out (of all accounts)
    this._logger.debug("updateAccountStatusFromDocument() - logged OUT");
    this._acUtils.markAllAccountsAsLoggedOut(this);
    gApi.deauthenticate();
  }
};

/*************************************************************************
 * BeboService: flockIMediaWebService implementation
 *************************************************************************/

// void decorateForMedia(in nsIDOMHTMLDocument aDocument, ...
BeboService.prototype.decorateForMedia =
function BeboService_decorateForMedia(aDocument, aAccount) {
  this._logger.debug("decorateForMedia('" + aDocument.URL + "')");
  var results = FlockSvcUtils.newResults();
  var mediaArr = [];

  if (this._WebDetective.detect(CLASS_SHORT_NAME,
                                "person",
                                aDocument,
                                results))
  {
    this._logger.debug("detected person");
    var username = results.getPropertyAsAString("username");
    var media = {
      name: username,
      query: "user:" + results.getPropertyAsAString("userid")
                     + "|username:" + username,
      label: username,
      favicon: this.icon,
      service: this.shortName
    };
    mediaArr.push(media);
  }

  if (mediaArr.length > 0) {
    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;
    }

    this._obs.notifyObservers(aDocument, "media", "media:update");
  }
};

// void findByUsername(in flockIListener aFlockListener, ...
BeboService.prototype.findByUsername =
function BeboService_findByUsername(aFlockListener, aUsername) {
  this._logger.debug("findByUsername(..., '" + aUsername + "')");

  var userObj = {};
  var inst = this;
  var findByUsernameListener = {
    onSuccess: function fbuL_onSuccess(aSubject, aTopic) {
      inst._logger.debug("findByUsername() Success");
      var user;
      var username;
      var userList = aSubject.getElementsByTagName('user');
      for (var i=0; i<userList.length; i++) {
        user = userList[i];
        username = getNodeValue(user, "name");
        if (username === aUsername) {
          userObj.accountId = getNodeValue(user, "uid");
          userObj.name = username;
          userObj.avatar = getNodeValue(user, "pic_square");
          userObj.totalMedia = -1;
          break;
        }
      }
      if (userObj.name) {
        aFlockListener.onSuccess({ wrappedJSObject: { newUserObj: userObj } },
                                 aTopic);
      } else {
        aFlockListener.onError(null, aTopic);
      }
    },
    onError: function fbuL_onError(aFlockError, aTopic) {
      inst._logger.debug("findByUsername() error: " + aFlockError.errorString);
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  var un = aUsername; // TODO: XXX Do we need to scrub this?
  var params = {};
  params.query = "select name,uid,pic_square from user where name='"+ un + "'";

  gApi.call("socialNetwork.snql.query", params, null,
            CI.flockIWebServiceAPI.GET, findByUsernameListener);
};

// void getAlbums(in flockIListener aFlockListener, in AString ...
BeboService.prototype.getAlbums =
function BeboService_getAlbums(aFlockListener, aUsername) {
  this._logger.debug("getAlbums('" + aUsername + "')");

  var inst = this;
  var getAlbumsListener = {
    onSuccess: function gAL_onSuccess(aSubject, aTopic) {
      inst._logger.debug("getAlbums() Success");
      var album;
      var albums = [];
      var albumList = aSubject.getElementsByTagName('album');
      var conv = CC["@mozilla.org/intl/utf8converterservice;1"]
                 .getService(CI.nsIUTF8ConverterService);
      for (var i = 0; i < albumList.length; i++) {
        album = albumList[i];
        var newAlbum = CC[FLOCK_PHOTO_ALBUM_CONTRACTID]
                       .createInstance(CI.flockIPhotoAlbum);
        newAlbum.id = getNodeValue(album, "aid");
        try {
          var title = getNodeValue(album, "name");
          newAlbum.title = conv.convertStringToUTF8(title, "utf-8", false);
        } catch (ex) {
          inst._logger.debug("getAlbums - conversion error: " + ex);
        }
        newAlbum.url = getNodeValue(album, "link");
        albums.push(newAlbum);
      }
      aFlockListener.onSuccess(createEnum(albums), aTopic);
    },
    onError: function gAL_onError(aFlockError, aTopic) {
      inst._logger.debug("getAlbums() error: " + aFlockError.errorString);
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  var params = {};
  params.uid = aUsername;
  params.aids = "";

  gApi.call("socialNetwork.photos.getAlbums", params, null,
            CI.flockIWebServiceAPI.GET, getAlbumsListener);
};

// DEFAULT: flockIMediaChannel getChannel(in AString AChannelId);
// DEFAULT: nsISimpleEnumerator enumerateChannels();
// DEFAULT: flockIMediaItemFormatter getMediaItemFormatter();

// void migrateAccount(in AString aId, in AString aUsername);
BeboService.prototype.migrateAccount =
function BeboService_migrateAccount(aId, aUsername) {
  this._logger.debug("migrateAccount('" + aId + "', '" + aUsername + "')");
};

// void search(in flockIListener aFlockListener, in AString aQuery, ...
BeboService.prototype.search =
function BeboService_search(aFlockListener, aQuery, aCount, aPage, aRequestId) {
  this._logger.debug("search('" + aQuery + "', " + aCount
                                + ", " + aPage + ", '" + aRequestId + "')");

  var query = new QueryHelper(aQuery);
  var foundValidQuery = false;

  // If the search string only contains whitespace, then return empty result
  var querySearch = query.getKey("search");
  if (querySearch && (querySearch.replace(/\s/g, "") === "")) {
    aFlockListener.onSuccess(createEnum([]), aRequestId);
    return;
  }

  // Process according to search type.
  if (query.special) {
    switch (query.special) {
      case "topPhotos":
        // This is a predefined channel query.
        this._logger.debug("Setting up query: Channel: Top Photos albums");
        if (aPage > 1) {
          this._srReturnNextPage(aFlockListener, aCount, aPage, aRequestId);
        } else {
          this._miList = [];
          this._srchGetChannel(aFlockListener,
                               query,
                               aCount,
                               aPage,
                               aRequestId);
        }
        foundValidQuery = true;
        break;
    }
  } else if (query.search) {
    // This is an arbitrary text search query, not supported by Bebo.
    this._logger.debug("Setting up query: Search: not supported");
    this._miList = [];
    aFlockListener.onSuccess(createEnum([]), aRequestId);
    foundValidQuery = true;
  } else if (query.user && query.album) {
    // This is a query for the photos in a specific album.
    this._logger.debug("Setting up query: User: Photos in album");
    if (aPage > 1) {
      this._srReturnNextPage(aFlockListener, aCount, aPage, aRequestId);
    } else {
      this._miList = [];
      this._srchGetAlbumPhotos(aFlockListener,
                               query,
                               aCount,
                               aPage,
                               aRequestId);
    }
    foundValidQuery = true;
  } else if (query.user) {
    if (query.userdefined === "bands") {
      // This is a search for bands the user is a fan of.
      // eg: "user:7938118552|username:Flocky Flockr|userdefined:bands"
      this._logger.debug("Setting up query: User: Bands");
      if (aPage > 1) {
        this._srReturnNextPage(aFlockListener, aCount, aPage, aRequestId);
      } else {
        this._miList = [];
        this._srchGetBands(aFlockListener, query, aCount, aPage, aRequestId);
      }
      foundValidQuery = true;
    } else {
      // This is a query for all photos for a user.
      this._logger.debug("Setting up query: User: Photos");
      if (aPage > 1) {
        this._srReturnNextPage(aFlockListener, aCount, aPage, aRequestId);
      } else {
        this._miList = [];
        this._srchGetPhotos(aFlockListener, query, aCount, aPage, aRequestId);
      }
      foundValidQuery = true;
    }
  }

  if (!foundValidQuery) {
    // No valid query found; call onError().
    this._logger.error("Setting up query: Unsupported type: " + aQuery);
    var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
    error.errorCode = CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR;
    aFlockListener.onError(error, aRequestId);
  }
};

// boolean supportsSearch(in AString aQuery);
BeboService.prototype.supportsSearch =
function BeboService_supportsSearch(aQuery) {
  this._logger.debug("supportsSearch('" + aQuery + "')");
  return false;
};

// DEFAULT: AString getIconForQuery(in AString aQuery);

// readonly attribute boolean supportsUsers;
BeboService.prototype.__defineGetter__("supportsUsers",
function BeboService_getsupportsUsers() {
  return true;
});


/*************************************************************************
 * BeboService: flockIMediaUploadWebService implementation
 *************************************************************************/

// void getAccountStatus(in flockIListener aFlockListener);
BeboService.prototype.getAccountStatus =
function BeboService_getAccountStatus(aFlockListener, aAlbumName) {
  this._logger.debug("getAccountStatus()");

  var acctStatus = FlockSvcUtils.newResults();

  // TODO: XXX Look for these in the API, and retrieve them.
  // Update: There doesn't seem to be any way of determining usage or quota.

  // This is a key, not something displayed to the user. Don't l10n this!
  acctStatus.setPropertyAsAString("usageUnits", "bytes");
  acctStatus.setPropertyAsAString("maxSpace", 100 * 5 * 1024 * 1024); // bogus.
  acctStatus.setPropertyAsAString("usedSpace", 0);
  acctStatus.setPropertyAsBool("isPremium", true); // Bebo has no "pro" level.

  aFlockListener.onSuccess(acctStatus, "");
};

// DEFAULT: void getAlbumsForUpload(in flockIListener aFlockListener, ...

// void createAlbum(in flockIListener aFlockListener, in AString aAccountId, ...
BeboService.prototype.createAlbum =
function BeboService_createAlbum(aFlockListener, aAccountId, aAlbumName) {
  this._logger.debug("createAlbum('" + aAccountId + "', '" + aAlbumName + "')");

  var inst = this;
  var createAlbumListener = {
    onSuccess: function cAL_onSuccess(aSubject, aTopic) {
      inst._logger.debug("createAlbum() Success");
      var album = getNode(aSubject, "album");
      var newAlbum = CC[FLOCK_PHOTO_ALBUM_CONTRACTID]
                     .createInstance(CI.flockIPhotoAlbum);
      newAlbum.id = getNodeValue(album, "aid");
      newAlbum.title = aAlbumName;
      newAlbum.url = getNodeValue(album, "link");
      aFlockListener.onSuccess(newAlbum, aTopic);
    },
    onError: function cAL_onError(aFlockError, aTopic) {
      inst._logger.debug("createAlbum() error: " + aFlockError.errorString);
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  var params = {};
  params.name = aAlbumName;
  params.description = "";

  gApi.call("socialNetwork.photos.createAlbum", params, null,
            CI.flockIWebServiceAPI.GET, createAlbumListener);
};

// boolean supportsFeature(in AString aFeature);
BeboService.prototype.supportsFeature =
function BeboService_supportsFeature(aFeature) {
  this._logger.debug("supportsFeature('" + aFeature + "')");
  switch (aFeature) {
    case 'albumCreation':
    case 'title':
      return true;
    case 'description':
    case 'fileName':
    case 'tags':
    case 'notes':
    case 'privacy':
     return false;
    default:
     return false;
  }
};

// void upload(in flockIPhotoUploadAPIListener aUploadListener, ...
BeboService.prototype.upload =
function BeboService_upload(aUploadListener, aUpload, aFilename) {
  this._logger.debug("upload('" + aFilename + "')");

  var caption = aUpload.title;
  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"))
  {
    caption += " - " + flockGetString("services/services",
                                      "flock.uploader.breadcrumb");
  }

  if (aFilename.match(/(jpeg$|jpg$)/i)) {
    aUpload.mimetype = "image/jpeg";
  } else if (aFilename.match(/png$/i)) {
    aUpload.mimetype = "image/png";
  } else if (aFilename.match(/gif$/i)) {
    aUpload.mimetype = "image/gif";
  } else if (aFilename.match(/bmp$/i)) {
    aUpload.mimetype = "image/bmp";
  } else {
    var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
    error.errorCode = CI.flockIError.PHOTOSERVICE_UNSUPPORTED_IMAGE_FORMAT;
    aUploadListener.onError(error);
  }

  var params = gApi.getBasicParams("socialNetwork.photos.upload");
  params.aid = aUpload.album;
  params.caption = caption;
  // Because the uploader is going to make the API call for us, we need to
  // explicitly add the Bebo signature ourselves.
  params.sig = gApi.generateSignature(params, BEBO_API_SECRET);

  this._uploadPhotos(aUploadListener, aFilename, params, aUpload);
};


/*************************************************************************
 * BeboService: flockIMediaEmbedWebService implementation
 *************************************************************************/

// boolean checkIsStreamUrl(in AString aUrl);
BeboService.prototype.checkIsStreamUrl =
function BeboService_checkIsStreamUrl(aUrl) {
  this._logger.debug("checkIsStreamUrl('" + aUrl + "')");
  var b = this._WebDetective.detectNoDOM(CLASS_SHORT_NAME, "isStreamUrl", "",
                                         aUrl, null);
  this._logger.debug("Checking if url is a bebo stream: "
                     + (b ? "YES: " : "NO: ")
                     + aUrl);
  return b;
};

// void getMediaQueryFromURL(in AString aUrl, in flockIListener aFlockListener);
BeboService.prototype.getMediaQueryFromURL =
function BeboService_getMediaQueryFromURL(aUrl, aFlockListener) {
  this._logger.debug("getMediaQueryFromURL('" + aUrl + "')");

  // Extract the MemberId from the jpg filename
  var userid = aUrl.match(/\/(\d{10})[A-Za-z0-9]+\.jpg$/);
  var username = "Bebo user";
  var query = "user:" + userid[1] + "|username:" + username;
  this._logger.debug("MediaQuery is: '" + query + "'");

  var results = FlockSvcUtils.newResults();
  results.setPropertyAsAString("query", query);
  results.setPropertyAsAString("title", username);
  aFlockListener.onSuccess(results, "query");
};

// boolean getSharingContent(in nsIDOMHTMLElement aSrc, ...
BeboService.prototype.getSharingContent =
function BeboService_getSharingContent(aSrc, aProp) {
  this._logger.debug("getSharingContent()");
  if (aSrc && aSrc instanceof CI.nsIDOMHTMLImageElement) {
    aProp.setPropertyAsAString("url", aSrc.src);
    aProp.setPropertyAsAString("title", "");  // aSrc.alt?
    return true;
  }
  return false;
};


/*************************************************************************
 * BeboService: flockISocialWebService implementation
 *************************************************************************/

// void markAllMediaSeen(in AString aPersonUrn);
BeboService.prototype.markAllMediaSeen =
function BeboService_markAllMediaSeen(aPersonUrn) {
  this._logger.debug("markAllMediaSeen('" + aPersonUrn + "')");
  var identity = this._coop.get(aPersonUrn);
  identity.unseenMedia = 0;
  identity.lastSeenUploadedMedia = identity.lastUploadedMedia;
};

// readonly attribute long maxStatusLength;
BeboService.prototype.__defineGetter__("maxStatusLength",
function BeboService_getmaxStatusLength() {
  return BEBO_MAX_STATUS_LENGTH;
});


/*************************************************************************
 * BeboService: flockIRichContentDropHandler implementation
 *************************************************************************/

// boolean handleDrop(in nsITransferable aFlavours, ...
BeboService.prototype.handleDrop =
function BeboService_handleDrop(aFlavours, aTargetElement) {
  this._logger.debug("handleDrop(...)");

  // Handle textarea drops
  if (aTargetElement instanceof CI.nsIDOMHTMLTextAreaElement) {
    var dropCallback = function bebo_dropCallback(aFlav) {
      var data = {};
      var len = {};
      aFlavours.getTransferData(aFlav, data, len);
      var caretPos = aTargetElement.selectionEnd;
      var currentValue = aTargetElement.value;

      // Add a trailing space so that we don't mangle the url
      var nextChar = currentValue.charAt(caretPos);
      var trailingSpace = ((nextChar === "") ||
                           (nextChar === " ") ||
                           (nextChar === "\n"))
                        ? ""
                        : " ";

      // We'll only add a breadcrumb if there isn't one already present
      var breadcrumb = "";
      var richDnDSvc = CC[FLOCK_RDNDS_CONTRACTID]
                       .getService(CI.flockIRichDNDService);
      if (!richDnDSvc.hasBreadcrumb(aTargetElement)) {
        breadcrumb = richDnDSvc.getBreadcrumb("plain");
      }

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

    return this._handleTextareaDrop(CLASS_SHORT_NAME,
                                    this._coopObj.domains,
                                    aTargetElement,
                                    dropCallback);
  }

  // Default handling otherwise
  return false;
};


/*************************************************************************
 * BeboService: flockIPollingService implementation
 *************************************************************************/

// void refresh(in AString aUrn, in flockIPollingListener aPollingListener);
BeboService.prototype.refresh =
function BeboService_refresh(aUrn, aPollingListener) {
  this._logger.debug("refresh('" + aUrn + "')");

  var account = this.getAccount(aUrn);
  if (account.isAuthenticated()) {
    var inst = this;
    var flockListener = {
      onSuccess: function flockListener_onSuccess(aSubject, aTopic) {
        var pplRefreshListener = CC["@flock.com/people-refresh-listener;1"]
                                 .createInstance(CI.flockIPeopleRefreshListener);
        pplRefreshListener.init(4, aUrn, aPollingListener, 0);
        inst._refreshAccount(account, pplRefreshListener);
      },
      onError: function flockListener_onError(aFlockError, aTopic) {
        aPollingListener.onError(aFlockError);
      }
    };
    gApi.authenticate(flockListener);
  } else {
    aPollingListener.onResult();
  }
};


/*************************************************************************
 * BeboService: Private Data and Functions
 *************************************************************************/

// Helper function to insert a user id in an URL from web detective.
BeboService.prototype._makeProfileUrl =
function BeboService__makeProfileUrl(aUrlType, aUserId) {
  this._logger.debug("_makeProfileUrl(" + aUrlType + ", " + aUserId + ")");
  return this._WebDetective.getString(CLASS_SHORT_NAME, aUrlType, null)
             .replace("%userId%", aUserId);
};

BeboService.prototype._refreshAccount =
function BeboService__refreshAccount(aAccount, aPplRefreshListener) {
  this._logger.debug("_refreshAccount('" + aAccount.urn + "', ...)");

  var inst = this;

  var userListener = {
    onSuccess: function userListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("userListener_onSuccess(..., '" + aTopic + "')");

      inst._updateAccount(aAccount, aSubject);
      aPplRefreshListener.onSuccess(null, arguments.callee.name);
    },

    onError: function userListener_onError(aFlockError, aTopic) {
      inst._logger.debug("userListener_onError(..., '" + aTopic + "')");
      aPplRefreshListener.onError(aFlockError, aTopic);
    }
  };

  var lastUploadListener = {
    onSuccess: function lastUploadListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("lastUploadListener_onSuccess(..., '" + aTopic + "')");
      var node = getNode(aSubject, "snql_query_response");
      if (node) {
        var photoList = getNodeList(node, "photo");
        if (photoList.length) {
          var uid = getNodeValue(photoList[0], "owner");
          var pid = getNodeValue(photoList[0], "pid");
          var time = getLastUpdateInSeconds(photoList[0], "created");
          var accountUid = gApi.getSessionUid();
          var friendUrn = "urn:flock:identity:bebo:" + accountUid + ":" + uid;
          var account = inst.getAccount(friendUrn);
          if (account) {
            if (account.coopObj.lastSeenUploadedMedia != pid) {
              // Store the most recent PID for use in markAllMediaSeen(),
              // and light the media icon (set "unseenMedia" non-zero).
              inst._logger.debug("setting beboLastPid on URN: '"
                                 + account.urn + "'");
              account.coopObj.lastUploadedMedia = pid;
              account.coopObj.unseenMedia = 1;
              // Don't clobber the last update unless this update is newer.
              var latest = parseInt(account.getParam("lastUpdate"), 10);
              if (time > latest) {
                inst._updateAccountParam(account, "lastUpdateType", "media");
                inst._updateAccountParam(account, "lastUpdate", time);
              }
            }
          }
        }
      }
      // else: This friend has no media.
    },

    onError: function lastUploadListener_onError(aFlockError, aTopic) {
      inst._logger.debug("lastUploadListener_onError(..., '" + aTopic + "')");
      // Do not call aFlockListener.onError.
    }
  };

  var albumListener = {
    onSuccess: function albumListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("albumListener_onSuccess(..., '" + aTopic + "')");
      var node = getNode(aSubject, "snql_query_response");
      if (node) {
        var aidList = [];
        var albumList = getNodeList(node, "album");
        for (let i = 0; i < albumList.length; i++) {
          aidList.push(getNodeValue(albumList[i], "aid"));
        }
        if (aidList.length) {
          var params = {};
          params.query = "SELECT owner,pid,created FROM photo WHERE aid IN ("
                        + aidList.join(",")
                        + ") ORDER BY created DESC LIMIT 1";
          inst._logger.debug("SNQL query is: '" + params.query + "'");
          gApi.call("socialNetwork.snql.query", params, null,
                    CI.flockIWebServiceAPI.GET, lastUploadListener);
        }
      }
      // else: This friend has no albums to inspect for new media.
    },

    onError: function albumListener_onError(aFlockError, aTopic) {
      inst._logger.debug("albumListener_onError(..., '" + aTopic + "')");
      // Do not call aFlockListener.onError.
    }
  };

  var friendsListener = {
    onSuccess: function friendsListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("friendsListener_onSuccess(..., '" + aTopic + "')");

      var friends =[];
      var friendData = createNodeListEnum(aSubject);

      function myWorker(aShouldYield) {
        inst._logger.debug(arguments.callee.name + ": begin");

        // Add or update existing people
        while (friendData.hasMoreElements()) {
          var userNode = friendData.getNext();
          var uid = inst._updateAccount(aAccount, userNode);

          // Check for new media.
          var params = {};
          params.query = "select aid from album where owner = " + uid;
          inst._logger.debug("SNQL query is: '" + params.query + "'");
          gApi.call("socialNetwork.snql.query", params, null,
                    CI.flockIWebServiceAPI.GET, albumListener);

          // Temporarily store this value so we can check for friends removed
          // in the code below
          friends[uid] = uid;

          if (aShouldYield()) {
            yield;
            if (!aAccount.isAuthenticated()) {
              // Account has just been deleted or logged out
              break;
            }
          }
        }

        if (aAccount.isAuthenticated()) {
          // Reflect removed friends from the server, locally
          var localFriends = aAccount.enumerateFriends();
          while (localFriends.hasMoreElements()) {
            var identity = localFriends.getNext();
            if (!friends[identity.accountId]) {
              inst._logger.debug("Friend " + identity.accountId
                                + " has been deleted on the server");
              identity.destroy();
            }
          }
        }

        inst._logger.debug(arguments.callee.name
                           + ": now calling People Refresh Listener");
        aPplRefreshListener.onSuccess(null, arguments.callee.name);
        inst._logger.debug(arguments.callee.name + ": finish");
      }

      FlockScheduler.schedule(null, 0.05, 10, myWorker);
    },

    onError: function friendsListener_onError(aFlockError, aTopic) {
      inst._logger.debug("friendsListener_onError(..., '" + aTopic + "')");
      aPplRefreshListener.onError(aFlockError, aTopic);
    }
  };

  var notieListener = {
    onSuccess: function notieListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("notieListener_onSuccess(..., '" + aTopic + "')");

      aSubject.QueryInterface(CI.nsISimpleEnumerator);
      if (aSubject.hasMoreElements()) {
        var bag = aSubject.getNext();
        bag.QueryInterface(CI.nsIPropertyBag2);

        // if any of the Me notifications have increased in count,
        // light the people button
        if ((aAccount.coopObj.accountMessages <
              bag.getPropertyAsInt32("messages")) ||
            (aAccount.getCustomParam("beboPokes") <
              bag.getPropertyAsInt32("pokes")) ||
            (aAccount.getCustomParam("beboFriendRequests") <
              bag.getPropertyAsInt32("friendRequests")) ||
            (aAccount.getCustomParam("beboGroupRequests") <
              bag.getPropertyAsInt32("groupRequests")) ||
            (aAccount.getCustomParam("beboEventRequests") <
              bag.getPropertyAsInt32("eventRequests")))
        {
          inst._ppSvc.togglePeopleIcon(true);
        }

        // write notifications to RDF
        aAccount.coopObj.accountMessages = bag.getPropertyAsInt32("messages");
        aAccount.setCustomParam("beboPokes", bag.getPropertyAsInt32("pokes"));
        aAccount.setCustomParam("beboFriendRequests",
                                bag.getPropertyAsInt32("friendRequests"));
        aAccount.setCustomParam("beboGroupRequests",
                                bag.getPropertyAsInt32("groupRequests"));
        aAccount.setCustomParam("beboEventRequests",
                                bag.getPropertyAsInt32("eventRequests"));
      }
      inst._obs.notifyObservers(aAccount, "flock-acct-refresh", "user-info");

      aPplRefreshListener.onSuccess(null, arguments.callee.name);
    },

    onError: function notieListener_onError(aFlockError, aTopic) {
      inst._logger.debug("notieListener_onError(..., '" + aTopic + "')");
      aPplRefreshListener.onError(aFlockError, aTopic);
    }
  };

  var statusTokenListener = {
    onSuccess: function stListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("stListener_onSuccess(..., '" + aTopic + "')");
      // Save the status token in a custom account param
      aAccount.setCustomParam("beboStatusToken", aSubject);
      aPplRefreshListener.onSuccess(null, arguments.callee.name);
    },
    onError: function stListener_onError(aFlockError, aTopic) {
      inst._logger.debug("stListener_onError(..., '" + aTopic + "')");
      aPplRefreshListener.onError(aFlockError, aTopic);
    }
  };

  this._getUserData(userListener);
  this._getFriendData(friendsListener);
  this._getNotieData(notieListener);
  this._getStatusToken(statusTokenListener);
};

// Returns an array of friend data
BeboService.prototype._getFriendData =
function BeboService__getFriendData(aFlockListener) {
  this._logger.debug("_getFriendData(...)");

  var inst = this;
  var dataListener = {
    onSuccess: function dataListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("dataListener_onSuccess(..., '" + aTopic + "')");
      var node = getNode(aSubject, "users_getInfo_response");
      if (node) {
        var nodes = getNodeList(node, "user");
        // TODO XXX: Will 'nodes' leak memory?
        aFlockListener.onSuccess(nodes, arguments.callee.name);
      } else {
        inst._logger.debug("Uh oh, we couldn't get the friend data");
        aFlockListener.onError(null, arguments.callee.name);
      }
    },

    onError: function dataListener_onError(aFlockError, aTopic) {
      inst._logger.debug("dataListener_onError(..., '" + aTopic + "')");
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  var uidListener = {
    onSuccess: function uidListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("uidListener_onSuccess(..., '" + aTopic + "')");
      var node = getNode(aSubject, "friends_get_response");
      if (node) {
        // Add the logged in user's friends
        var uids = "";
        var nodes = createNodeListEnum(getNodeList(node, "uid"));
        // TODO XXX: Will 'nodes' leak memory?
        while (nodes.hasMoreElements()) {
          // This list must be separated by commas with NO spaces
          if (uids.length > 0) {
            uids += ",";
          }
          node = nodes.getNext();
          uids += getNodeValue(node);
        }

        // Get the name, avatar, status and when the profile was last updated
        var params = {};
        params.uids = uids;
        params.fields = gStrings["dataFields"];

        gApi.call("socialNetwork.users.getInfo", params, null,
                  CI.flockIWebServiceAPI.GET, dataListener);
      } else {
        inst._logger.debug("Uh oh, we couldn't get the list of friends");
        aFlockListener.onError(null, arguments.callee.name);
      }
    },

    onError: function uidListener_onError(aFlockError, aTopic) {
      inst._logger.debug("uidListener_onError(..., '" + aTopic + "')");
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  gApi.call("socialNetwork.friends.get", null, null,
            CI.flockIWebServiceAPI.GET, uidListener);
};

// Returns notification data
BeboService.prototype._getNotieData =
function BeboService__getNotieData(aFlockListener) {
  this._logger.debug("_getNotieData(...)");

  var inst = this;
  var notieDataListener = {
    onSuccess: function notieDataListener_onSuccess(aXml, aTopic) {
      var notifications = [];
      var meNotifications = FlockSvcUtils.newResults();
      meNotifications.setPropertyAsAString("messages",
        aXml.getElementsByTagName("messages")[0]
            .getElementsByTagName("unread")[0]
            .childNodes[0]
            .nodeValue);
      meNotifications.setPropertyAsAString("pokes",
        aXml.getElementsByTagName("pokes")[0]
            .getElementsByTagName("unread")[0]
            .firstChild
            .nodeValue);
      meNotifications.setPropertyAsAString("friendRequests",
        aXml.getElementsByTagName("friend_requests")[0]
            .getElementsByTagName("uid")
            .length);
      meNotifications.setPropertyAsAString("groupRequests",
        aXml.getElementsByTagName("group_invites")[0]
            .getElementsByTagName("gid")
            .length);
      meNotifications.setPropertyAsAString("eventRequests",
        aXml.getElementsByTagName("event_invites")[0]
            .getElementsByTagName("eid")
            .length);
      notifications.push(meNotifications);

      aFlockListener.onSuccess(createEnum(notifications), aTopic);
    },
    onError: function notieDataListener_onError(aFlockError, aTopic) {
      inst._logger.debug("notieDataListener:" + aFlockError.errorString);
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  gApi.call("socialNetwork.notifications.get", null, null,
            CI.flockIWebServiceAPI.GET, notieDataListener);
};

// Returns the user data
BeboService.prototype._getUserData =
function BeboService__getUserData(aFlockListener) {
  this._logger.debug("_getUserData(...)");

  var inst = this;
  var userDataListener = {
    onSuccess: function userDataListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("userDataListener_onSuccess(..., '" + aTopic + "')");
      var node = getNode(aSubject, "users_getInfo_response");
      if (node) {
        node = getNode(node, "user");
        aFlockListener.onSuccess(node, arguments.callee.name);
      } else {
        inst._logger.debug("Uh oh, we couldn't get the user data");
        aFlockListener.onError(null, arguments.callee.name);
      }
    },

    onError: function userDataListener_onError(aFlockError, aTopic) {
      inst._logger.debug("userDataListener_onError(..., '" + aTopic + "')");
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  // Get the name, avatar, status and when the profile was last updated
  var params = {};
  params.uids = gApi.getSessionUid();
  params.fields = gStrings["dataFields"];

  gApi.call("socialNetwork.users.getInfo", params, null,
            CI.flockIWebServiceAPI.GET, userDataListener);
};

// Scrape user homepage for the token required to set status. This token is
// not available thru the API and is reset every login session.
BeboService.prototype._getStatusToken =
function BeboService__getStatusToken(aFlockListener) {
  var inst = this;
  var req = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
            .createInstance(CI.nsIXMLHttpRequest);

  req.onreadystatechange = function getStatusToken_onreadystatechange(aEvent) {
    if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      if (Math.floor(req.status / 100) == 2) {
        var results = FlockSvcUtils.newResults();
        if (inst._WebDetective.detectNoDOM(CLASS_SHORT_NAME,
                                           "statustoken",
                                           null,
                                           req.responseText,
                                           results))
        {
          var token = results.getPropertyAsAString("token");
          aFlockListener.onSuccess(token, arguments.callee.name);
        }
      } else {
        var error = gApi.getError(CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR);
        aFlockListener.onError(error, arguments.callee.name);
      }
    }
  };

  req.open("GET", gStrings["statusTokenScrapeURL"]);
  req.send(null);
};

// Retrieve all items for a predefined media channel
BeboService.prototype._srchGetChannel =
function BeboService__srchGetChannel(aFlockListener,
                                     aQueryHelper,
                                     aCount,
                                     aPage,
                                     aRequestId)
{
  var inst = this;
  var searchListener = {
    onSuccess: function search_onSuccess(aSubject, aTopic) {
      inst._logger.debug("search_onSuccess()");
      inst._srAlbumsToArray(aSubject,
                            aFlockListener,
                            aCount,
                            aPage,
                            aRequestId);
    },
    onError: function search_onError(aFlockError, aTopic) {
      inst._logger.debug("search_onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  var params = {};
  params.query = "SELECT aid,cover_pid,owner,name,created,modified,"
               + "description,location,link,size FROM album WHERE owner="
               + gStrings["topPhotosUid"];
  this._logger.debug("SNQL query is: '" + params.query + "'");

  gApi.call("socialNetwork.snql.query", params, null,
             CI.flockIWebServiceAPI.GET, searchListener);
};

// Retrieve all photos for a user
BeboService.prototype._srchGetPhotos =
function BeboService__srchGetPhotos(aFlockListener,
                                    aQueryHelper,
                                    aCount,
                                    aPage,
                                    aRequestId)
{
  var inst = this;
  var getPhotosListener = {
    onSuccess: function getPhotos_onSuccess(aSubject, aTopic) {
      inst._logger.debug("getPhotos_onSuccess()");
      inst._srPhotosToArray(aSubject,
                            aFlockListener,
                            aQueryHelper,
                            aCount,
                            aPage,
                            aRequestId);
    },
    onError: function getPhotos_onError(aFlockError, aTopic) {
      inst._logger.debug("getPhotos_onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  var getAlbumsListener = {
    onSuccess: function getAlbums_onSuccess(aSubject, aTopic) {
      inst._logger.debug("getAlbums_onSuccess()");
      var aidList = [];
      var albumList = aSubject.getElementsByTagName("album");
      for (let i = 0; i < albumList.length; i++) {
        aidList.push(getNodeValue(albumList[i], "aid"));
      }
      if (aidList.length) {
        var params2 = {};
        params2.query = "SELECT aid,caption,created,link,owner,pid,src,"
                      + "src_big,src_small FROM photo WHERE aid IN ("
                      + aidList.join(",")
                      + ")"
                      // Pagination breaks sort, and returns "random" pictures
                      //+ " LIMIT " + aCount
                      //+ " OFFSET " + aCount * (aPage - 1)
                      + " ORDER BY created DESC";
        gApi.call("socialNetwork.snql.query", params2, null,
                  CI.flockIWebServiceAPI.GET, getPhotosListener);
      } else {
        aFlockListener.onSuccess(createEnum([]), aRequestId);
      }
    },
    onError: function getAlbums_onError(aFlockError, aTopic) {
      inst._logger.debug("getAlbums_onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  var params = {};
  params.query = "SELECT aid FROM album WHERE owner=" + aQueryHelper.user;
  this._logger.debug("SNQL query is: '" + params.query + "'");

  gApi.call("socialNetwork.snql.query", params, null,
            CI.flockIWebServiceAPI.GET, getAlbumsListener);
};

// Retrieve all photos in an album for a user
BeboService.prototype._srchGetAlbumPhotos =
function BeboService__srchGetAlbumPhotos(aFlockListener,
                                         aQueryHelper,
                                         aCount,
                                         aPage,
                                         aRequestId)
{
  var inst = this;
  var searchListener = {
    onSuccess: function search_onSuccess(aSubject, aTopic) {
      inst._logger.debug("search_onSuccess()");
      inst._srPhotosToArray(aSubject,
                            aFlockListener,
                            aQueryHelper,
                            aCount,
                            aPage,
                            aRequestId);
    },
    onError: function search_onError(aFlockError, aTopic) {
      inst._logger.debug("search_onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  var params = {};
  params.query = "SELECT aid FROM album WHERE owner=" + aQueryHelper.user;
  params.query = "SELECT aid,caption,created,link,owner,pid,src,"
                + "src_big,src_small FROM photo WHERE aid="
                + aQueryHelper.album
                // Pagination breaks sort
                //+ " LIMIT " + aCount
                //+ " OFFSET " + aCount * (aPage - 1)
                + " ORDER BY created DESC";
  this._logger.debug("SNQL query is: '" + params.query + "'");

  gApi.call("socialNetwork.snql.query", params, null,
            CI.flockIWebServiceAPI.GET, searchListener);
};

// Retrieve all bands for a user
BeboService.prototype._srchGetBands =
function BeboService__srchGetBands(aFlockListener,
                                   aQueryHelper,
                                   aCount,
                                   aPage,
                                   aRequestId)
{
  var inst = this;
  var searchListener = {
    onSuccess: function search_onSuccess(aSubject, aTopic) {
      inst._logger.debug("search_onSuccess()");
      inst._srBandsToArray(aSubject, aFlockListener, aCount, aPage, aRequestId);
    },
    onError: function search_onError(aFlockError, aTopic) {
      inst._logger.debug("search_onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  var params = {};
  params.query = "SELECT bid FROM band_fan WHERE uid=" + aQueryHelper.user;

  gApi.call("socialNetwork.snql.query", params, null,
            CI.flockIWebServiceAPI.GET, searchListener);
};

// Read albumlist XML and convert to array of flockIMediaItem
BeboService.prototype._srAlbumsToArray =
function BeboService__srAlbumsToArray(aXML,
                                      aFlockListener,
                                      aCount,
                                      aPage,
                                      aRequestId)
{
  var miList = [];  // To be returned; order matters, matches Bebo page.
  var miHash = {};  // Hashed by cover_pid for quick access
  var pidList = []; // For retrieving all cover pictures at once
  var album;

  // Enumerate and process albums.
  var albumList = aXML.getElementsByTagName("album");
  for (let i = 0; i < albumList.length; i++) {
    album = albumList[i];
    var mediaItem = CC["@flock.com/photo;1"]
                    .createInstance(CI.flockIMediaItem);
    mediaItem.init(CLASS_SHORT_NAME, flockAlbumItemFormatter);

    mediaItem.id = getNodeValue(album, "aid");
    mediaItem.title = getNodeValue(album, "name");
    mediaItem.userid = getNodeValue(album, "owner");
    mediaItem.username = getNodeValue(album, "name");
    mediaItem.description = getNodeValue(album, "description");
    mediaItem.webPageUrl = getNodeValue(album, "link");
    mediaItem.uploadDate = parseInt(getNodeValue(album, "created"), 10);
    mediaItem.lastUpdate = parseInt(getNodeValue(album, "modified "), 10);
    mediaItem.comment_count = getNodeValue(album, "size");
    mediaItem.icon = this.icon;
    miList.push(mediaItem);
    // Keep a reference to the mediaItem hashed by cover_pid.
    miHash[getNodeValue(album, "cover_pid")] = miList[miList.length - 1];
    // We also need to keep a list of the pids to retrieve.
    pidList.push(getNodeValue(album, "cover_pid"));
  }

  // Get the cover photos, all at once.
  var inst = this;
  var coverListener = {
    onSuccess: function cover_onSuccess(aSubject, aTopic) {
      inst._logger.debug("cover_onSuccess()");
      var photo;
      var pid;
      var item;
      // Enumerate the returned photos, and update the mediaItems.
      var photoList = aSubject.getElementsByTagName("photo");
      for (let i = 0; i < photoList.length; i++) {
        photo = photoList[i];
        pid = getNodeValue(photo, "pid");
        inst._logger.debug("  Processing pid: " + pid);
        item = miHash[pid];
        item.thumbnail = getNodeValue(photo, "src_small");
        item.midSizePhoto = getNodeValue(photo, "src");
        item.largeSizePhoto = getNodeValue(photo, "src_big");
      }
      inst._miList = miList;
      inst._srReturnNextPage(aFlockListener, aCount, aPage, aRequestId);
    },
    onError: function cover_onError(aFlockError, aTopic) {
      inst._logger.debug("cover_onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  if (pidList.length) {
    var params = {};
    params.pids = pidList.join(",");

    gApi.call("socialNetwork.photos.get", params, null,
              CI.flockIWebServiceAPI.GET, coverListener);
  } else {
    aFlockListener.onSuccess(createEnum([]), aRequestId);
  }
};

// Read bandIdlist XML and convert to array of flockIMediaItem
BeboService.prototype._srBandsToArray =
function BeboService__srBandsToArray(aXML,
                                     aFlockListener,
                                     aCount,
                                     aPage,
                                     aRequestId)
{
  var miList = [];  // To be returned; order matters, matches Bebo page.
  var miHash = {};  // Hashed by bid for quick access
  var bidList = []; // For retrieving all bands at once
  var bandId;

  // Enumerate and process bandIds.
  var bandIdList = aXML.getElementsByTagName("band_fan");
  for (let i = 0; i < bandIdList.length; i++) {
    bandId = bandIdList[i];
    var mediaItem = CC["@flock.com/photo;1"]
                    .createInstance(CI.flockIMediaItem);
    mediaItem.init(CLASS_SHORT_NAME, flockBandItemFormatter);

    mediaItem.id = getNodeValue(bandId, "bid");
    mediaItem.userid = mediaItem.id;
    mediaItem.webPageUrl = this._makeProfileUrl("profileURL", mediaItem.id);
    mediaItem.icon = this.icon;
    miList.push(mediaItem);
    // Keep a reference to the mediaItem hashed by bid.
    miHash[getNodeValue(bandId, "bid")] = miList[miList.length - 1];
    // We also need to keep a list of the bids to retrieve.
    bidList.push(getNodeValue(bandId, "bid"));
  }

  // Get the band details, all at once.
  var inst = this;
  var bandListener = {
    onSuccess: function band_onSuccess(aSubject, aTopic) {
      inst._logger.debug("band_onSuccess()");
      var band;
      var bid;
      var item;
      // Enumerate the returned bands, and update the mediaItems.
      var bandList = aSubject.getElementsByTagName("band");
      for (let i = 0; i < bandList.length; i++) {
        band = bandList[i];
        bid = getNodeValue(band, "bid");
        inst._logger.debug("  Processing bid: " + bid);
        item = miHash[bid];
        item.title = getNodeValue(band, "name");
        item.username = getNodeValue(band, "name");
        item.thumbnail = getNodeValue(band, "pic_square");
        item.midSizePhoto = getNodeValue(band, "pic");
        item.largeSizePhoto = getNodeValue(band, "pic_big");
      }
      inst._miList = miList;
      inst._srReturnNextPage(aFlockListener, aCount, aPage, aRequestId);
    },
    onError: function band_onError(aFlockError, aTopic) {
      inst._logger.debug("band_onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  if (bidList.length) {
    var params = {};
    params.bids = bidList.join(",");
    params.fields = "name,pic_square,pic,pic_big";

    gApi.call("socialNetwork.bands.getInfo", params, null,
              CI.flockIWebServiceAPI.GET, bandListener);
  } else {
    aFlockListener.onSuccess(createEnum([]), aRequestId);
  }
};

// Read photos XML and convert to array of flockIMediaItem
BeboService.prototype._srPhotosToArray =
function BeboService__srPhotosToArray(aXML,
                                      aFlockListener,
                                      aQueryHelper,
                                      aCount,
                                      aPage,
                                      aRequestId)
{

  var miList = [];
  var photo;
  var photoList = aXML.getElementsByTagName("photo");

  for (let i = 0; i < photoList.length; i++) {
    photo = photoList[i];
    var mediaItem = CC["@flock.com/photo;1"]
                    .createInstance(CI.flockIMediaItem);
    mediaItem.init(CLASS_SHORT_NAME, null); // flockMediaItemFormatter

    mediaItem.id = getNodeValue(photo, "pid");
    mediaItem.title = getNodeValue(photo, "caption");
    mediaItem.userid = getNodeValue(photo, "owner");
    mediaItem.username = aQueryHelper.username;
    mediaItem.webPageUrl = getNodeValue(photo, "link");
    mediaItem.thumbnail = getNodeValue(photo, "src_small");
    mediaItem.midSizePhoto = getNodeValue(photo, "src");
    mediaItem.largeSizePhoto = getNodeValue(photo, "src_big");
    mediaItem.uploadDate = parseInt(getNodeValue(photo, "created"), 10);
    mediaItem.icon = this.icon;
    miList.push(mediaItem);
  }

  this._miList = miList;
  this._srReturnNextPage(aFlockListener, aCount, aPage, aRequestId);
};

// Call aFlockListener with requested page of media items from cache.
BeboService.prototype._srReturnNextPage =
function BeboService__srReturnNextPage(aFlockListener,
                                       aCount,
                                       aPage,
                                       aRequestId)
{
  this._logger.debug("_srReturnNextPage('" + aCount + "', '" + aPage + "')");
  var miList = [];
  for (let i = aCount * (aPage - 1), j = 0;
       i < this._miList.length && j < aCount;
       i++, j++)
  {
    miList.push(this._miList[i]);
  }
  aFlockListener.onSuccess(createEnum(miList), aRequestId);
};

// void uploadPhotos flockIPhotoUploadAPIListener aUploadListener, ...
BeboService.prototype._uploadPhotos =
function BeboService__uploadPhotos(aUploadListener,
                                   aFilename,
                                   aParams,
                                   aUpload)
{
  this._logger.debug("_uploadPhotos('" + aFilename + "')");

  var inst = this;
  var uploadListener = {
    onResult: function upload_onResult(aResponseText) {
      inst._logger.debug("upload_onResult():\n" + aResponseText);
      var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
                   .createInstance(Components.interfaces.nsIDOMParser);
      var xml = parser.parseFromString(aResponseText, "text/xml");
      inst._logger.debug(CC["@mozilla.org/xmlextras/xmlserializer;1"]
                        .getService(CI.nsIDOMSerializer)
                        .serializeToString(xml));
      var resp = xml.getElementsByTagName("photos_upload_response")[0];
      if (!resp) {
        // Not the expected response; retreieve the error and report it.
        var error = gApi.getXmlError(xml);
        aUploadListener.onError(error);
      } else {
        // Flag the upload as complete.
        aUploadListener.onUploadComplete(aUpload);

        // Bebo does not give us the pid of the uploaded photo, so we
        // cannot go and get the details. Just return an empty media item.
        var mediaItem = CC["@flock.com/photo;1"]
                        .createInstance(CI.flockIMediaItem);
        mediaItem.init(CLASS_SHORT_NAME, null); // flockMediaItemFormatter
        aUploadListener.onUploadFinalized(aUpload, mediaItem);
      }
    },
    onError: function upload_onError(aErrorCode) {
      inst._logger.debug("upload_onError()");
      if (aErrorCode) {
        aUploadListener.onError(gApi.getHTTPError(aErrorCode));
      } else {
        aUploadListener.onError(gApi.getError(null));
      }
    },
    onProgress: function upload_onProgress(aCurrentProgress) {
      aUploadListener.onProgress(aCurrentProgress);
    }
  };

  var uploader = new FlockPhotoUploader();
  uploader.setEndpoint(gStrings["endpoint"]);
  uploader.upload(uploadListener, aFilename, aParams, aUpload);
};

// Updates the account param only if something has changed and returns a
// true or false "dirty" flag
BeboService.prototype._updateAccountParam =
function BeboService__updateAccountParam(aAccount, aName, aNewValue) {
  this._logger.debug("_updateAccountParam('" + aAccount.urn + "', '"
                     + aName + "', '" + aNewValue + "')");

  var param = aAccount.getParam(aName);
  if ((!param && aNewValue) || (param != aNewValue)) {
    this._logger.debug(aName + " value has changed to: " + aNewValue);
    aAccount.setParam(aName, aNewValue ? aNewValue : "");
    return true;
  }

  return false;
};

// Updates the user or friend account depending on the data passed in
BeboService.prototype._updateAccount =
function BeboService__updateAccount(aAccount, aUserNode) {
  this._logger.debug("_updateAccount('" + aAccount.urn + "', ...)");

  var account = aAccount;
  var accountUid = gApi.getSessionUid();
  var uid = getNodeValue(aUserNode, "uid");

  // if we're not updating a user account, we're updating a friend account
  var isFriend = (uid != accountUid);
  if (isFriend) {
    var friendUrn = "urn:flock:identity:bebo:" + accountUid + ":" + uid;
    if (!this._coop.Account.exists(friendUrn)) {
      // Add the new friend (skeleton) to coop, then update
      var newFriend = { accountId: uid };
      aAccount.coopObj.addPerson(newFriend, null);
    }
    account = this.getAccount(friendUrn);
  }

  // TODO JVL: error check account?

  var isDirty = false;
  isDirty = this._updateAccountParam(account,
                                      "name",
                                      getNodeValue(aUserNode, "name"));
  isDirty |= this._updateAccountParam(account,
                                      "URL",
                                      this._makeProfileUrl("profileURL",
                                          getNodeValue(aUserNode, "uid")));
  isDirty |= this._updateAccountParam(account,
                                      "avatar",
                                      getNodeValue(aUserNode, "pic_square"));
  if (isDirty) {
    if (isFriend) {
      // The profile changed, so update the type and time to trigger an RDF
      // observer refresh
      this._updateAccountParam(account, "lastUpdateType", "profile");
      // XXX: Bebo 'profile_update_time' field returns now() instead of
      //      the last time the profile was upated. Need to speak with
      //      Bebo to fix it on their end.
      var time = getLastUpdateInSeconds(aUserNode, "profile_update_time");
      this._updateAccountParam(account, "lastUpdate", time);
    } else {
      // Refresh the MeCard
      this._obs.notifyObservers(account, "flock-acct-refresh", "user-info");
    }
  }

  // If a status update takes place, that annotation takes priority
  // because it has an accurate date.
  isDirty = this._updateAccountParam(account,
                                     "statusMessage",
                                     getNodeValue(aUserNode, "message"));
  if (isDirty) {
    var time = getLastUpdateInSeconds(aUserNode, "time");
    if (isFriend) {
      // The status changed, so update the type and time to trigger an RDF
      // observer refresh
      this._updateAccountParam(account, "lastUpdateType", "status");
      this._updateAccountParam(account, "lastUpdate", time);
    } else {
      this._updateAccountParam(account, "lastStatusMessageUpdateDate", time);
    }
  }

  // Return the account uid processed
  return uid;
};


/*************************************************************************
 * Component: Flock Bebo Service Account
 *************************************************************************/

function BeboAccount() {
  FlockSvcUtils.getLogger(this).init(CLASS_SHORT_NAME + "Account");
  this._logger.info(".init()");

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

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

  // NOTE: Some default methods REQUIRE these properties to be present.
  FlockSvcUtils.getCoop(this);
  this._acUtils = CC["@flock.com/account-utils;1"]
                  .getService(CI.flockIAccountUtils);
  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);
}

/**************************************************************************
 * BeboAccount: XPCOM Component Creation
 **************************************************************************/

BeboAccount.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " Account",
  "",
  "",
  BeboAccount,
  0,
  [
    CI.flockIWebServiceAccount,
    CI.flockIMediaAccount,
    // BUG #15944: Disable photo uploader pending resolution from Bebo.
    //CI.flockIMediaUploadAccount,
    CI.flockISocialAccount
  ]
);

/**************************************************************************
 * BeboAccount: flockIWebServiceAccount Implementation
 **************************************************************************/

// readonly attribute AString urn;
// TODO: XXX getAccount in FlockSvcUtils.jsm SETS this readonly property.
//BeboAccount.prototype.__defineGetter__("urn",
//function BeboAccount_geturn() {
//  return this.coopObj.id();
//});
BeboAccount.prototype.urn = "";

// DEFAULT: flockILoginWebService getService();
// DEFAULT: void login(in flockIListener aFlockListener);
// DEFAULT: void logout(in flockIListener aFlockListener);
// DEFAULT: void keep();
// DEFAULT: boolean isAuthenticated();
// DEFAULT: nsIVariant getParam(in AString aParamName);
// DEFAULT: void setParam(in AString aParamName, in nsIVariant aValue);
// DEFAULT: nsIVariant getCustomParam(in AString aParamName);
// DEFAULT: nsIPropertyBag getAllCustomParams();
// DEFAULT: void setCustomParam(in AString aParamName, in nsIVariant aValue);

/**************************************************************************
 * BeboAccount: flockIMediaAccount Implementation
 **************************************************************************/

// NO METHODS OR PROPERTIES.

/**************************************************************************
 * BeboAccount: flockIMediaUploadAccount Implementation
 **************************************************************************/

// NO METHODS OR PROPERTIES.

/**************************************************************************
 * BeboAccount: flockISocialAccount Implementation
 **************************************************************************/

// readonly attribute boolean hasFriendActions;
BeboAccount.prototype.__defineGetter__("hasFriendActions",
function BeboAccount_gethasFriendActions() {
  return true;
});

// readonly attribute boolean isMeStatusSupported;
BeboAccount.prototype.__defineGetter__("isMeStatusSupported",
function BeboAccount_getisMeStatusSupported() {
  return true;
});

// readonly attribute boolean isFriendStatusSupported;
BeboAccount.prototype.__defineGetter__("isFriendStatusSupported",
function BeboAccount_getisFriendStatusSupported() {
  return true;
});

// readonly attribute boolean isStatusEditable;
BeboAccount.prototype.__defineGetter__("isStatusEditable",
function BeboAccount_getisStatusEditable() {
  return true;
});

// readonly attribute boolean isPostLinkSupported;
BeboAccount.prototype.__defineGetter__("isPostLinkSupported",
function BeboAccount_getisPostLinkSupported() {
  return false;
});

// readonly attribute boolean isMyMediaFavoritesSupported;
BeboAccount.prototype.__defineGetter__("isMyMediaFavoritesSupported",
function BeboAccount_getisMyMediaFavoritesSupported() {
  return false;
});

// void setStatus(in AString aStatusMessage, in flockIListener aFlockListener);
BeboAccount.prototype.setStatus =
function BeboAccount_setStatus(aStatusMessage, aFlockListener) {
  this._logger.error("setStatus('" + aStatusMessage + "')");

  // If status message is blank, clear the current status.
  if (aStatusMessage.replace("/^\s+|\s+$/g", "") == "") {
    this._clearStatus(aFlockListener);
    return;
  }

  // There is no API call to set status. As a workaround we simulate the
  // form post used to set status on the Bebo website.
  var inst = this;
  var req = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
            .createInstance(CI.nsIXMLHttpRequest);

  req.onreadystatechange = function setStatus_onreadystatechange(aEvent) {
    if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      if (Math.floor(req.status / 100) == 2) {
        var now = new Date().getTime();
        now = Math.round(now / 1000);
        inst.setParam("lastStatusMessageUpdateDate", now);
        inst.setParam("statusMessage", aStatusMessage);
        if (aFlockListener) {
          aFlockListener.onSuccess(inst, "setStatus");
        }
      } else {
        if (aFlockListener) {
          var error = gApi.getError(CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR);
          aFlockListener.onError(error, "setStatus");
        }
      }
    }
  };

  req.open("POST", gStrings["setStatusURL"]);
  req.setRequestHeader("Content-Type",
                       "application/x-www-form-urlencoded; charset=UTF-8");

  var postVars = {
    "Member": inst.getParam("accountId"),
    "From": "Profile",
    "FormToken": inst.getCustomParam("beboStatusToken"),
    "SayingTx": aStatusMessage,
    "Save": "Update"
  };

  var postBody = "";
  for (var key in postVars) {
    if (postBody.length) {
      postBody += "&";
    }
    postBody += key + "=" + encodeURIComponent(postVars[key]);
  }

  req.send(postBody);
};

// AString getEditableStatus();
BeboAccount.prototype.getEditableStatus =
function BeboAccount_getEditableStatus() {
  this._logger.debug("getEditableStatus()");

  var status = this.coopObj.statusMessage;
  return this.formatStatusForDisplay(status);
};

// AString formatStatusForDisplay(in AString aStatusMessage);
BeboAccount.prototype.formatStatusForDisplay =
function BeboAccount_formatStatusForDisplay(aStatusMessage) {
  this._logger.debug("formatStatusForDisplay('" + aStatusMessage + "')");
  return aStatusMessage;
};

// DEFAULT: AString formatFriendActivityForDisplay(in AString ...

// AString getMeNotifications();
BeboAccount.prototype.getMeNotifications =
function BeboAccount_getMeNotifications() {
  this._logger.debug("getMeNotifications()");

  var bundle = CC["@mozilla.org/intl/stringbundle;1"]
              .getService(CI.nsIStringBundleService)
              .createBundle(SERVICES_PROPERTIES_FILE);
  var noties = [];
  var inst = this;

  function _addNotie(aType, aCount) {
    var stringName = "flock.noties."
                   + aType + "."
                   + ((parseInt(aCount, 10) <= 0) ? "none" : "some");
    noties.push({
      class: aType,
      tooltip: bundle.GetStringFromName(stringName),
      metricsName: aType,
      count: aCount ? aCount : 0,
      URL: inst._WebDetective.getString(CLASS_SHORT_NAME, aType + "_URL", "")
    });
  }

  _addNotie("meMessages", this.coopObj.accountMessages);

  // mePokes requires bebo.xml string: "mePokes_URL"
  //_addNotie("mePokes", this.getCustomParam("beboPokes"));

  _addNotie("meFriendRequests", this.getCustomParam("beboFriendRequests"));

  // meGroupRequests requires bebo.xml string: "meGroupRequests_URL"
  //_addNotie("meGroupRequests", this.getCustomParam("beboGroupRequests"));

  // meEventRequests requires bebo.xml string: "meEventRequests_URL"
  //_addNotie("meEventRequests", this.getCustomParam("beboEventRequests"));

  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(noties);
};

// DEFAULT: AString getFormattedFriendUpdateType(in AString ...

// AString getFriendActions(in AString aFriendURN);
BeboAccount.prototype.getFriendActions =
function BeboAccount_getFriendActions(aFriendURN) {
  this._logger.debug("getFriendActions('" + aFriendURN + "')");

  var actionNames = ["friendMessage",
                     "friendDrawWB",
                     "friendWriteWB",
                     "friendViewBands",
                     "friendViewProfile",
                     "friendShareFlock"];

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

  var actions = [];
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    for each (var i in actionNames) {
      actions.push({
        label: bundle.GetStringFromName("flock.actions." + i),
        class: i,
        spec: this._WebDetective.getString(CLASS_SHORT_NAME, 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);
};

// DEFAULT: long getFriendCount();
// DEFAULT: nsISimpleEnumerator enumerateFriends();
// DEFAULT: AString getInviteFriendsURL();

// void shareWithFriend(in AString aFriendURN, in nsITransferable ...
BeboAccount.prototype.shareWithFriend =
function BeboAccount_shareWithFriend(aFriendURN, aTransferable) {
  this._logger.debug("shareWithFriend('" + aFriendURN + "')");

  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) {
      this._composeMessage(aFriendURN, message.subject, message.body, true);
    }
  }
};

// AString getProfileURLForFriend(in AString aFriendURN);
BeboAccount.prototype.getProfileURLForFriend =
function BeboAccount_getProfileURLForFriend(aFriendURN) {
  this._logger.debug("getProfileURLForFriend('" + aFriendURN + "')");
  return this._coop.get(aFriendURN).URL;
};

// void postLink(in nsITransferable aTransferable);
BeboAccount.prototype.postLink =
function BeboAccount_postLink(aTransferable) {
  this._logger.debug("postLink()");
};

// void markAllMeNotificationsSeen(in AString aType);
BeboAccount.prototype.markAllMeNotificationsSeen =
function BeboAccount_markAllMeNotificationsSeen(aType) {
  this._logger.debug("markAllMeNotificationsSeen('" + aType + "')");
  switch (aType) {
    case "meMessages":
      this.coopObj.accountMessages = 0;
      break;
    case "mePokes":
      this.setCustomParam("beboPokes", 0);
      break;
    case "meFriendRequests":
      this.setCustomParam("beboFriendRequests", 0);
      break;
    case "meGroupRequests":
      this.setCustomParam("beboGroupRequests", 0);
      break;
    case "meEventRequests":
      this.setCustomParam("beboEventRequests", 0);
      break;
    default:
      break;
  }
};

// void shareFlock(in AString aFriendURN);
BeboAccount.prototype.shareFlock =
function BeboAccount_shareFlock(aFriendURN) {
  this._logger.debug("shareFlock('" + aFriendURN + "')");

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(SERVICES_PROPERTIES_FILE);
  var subj = bundle.GetStringFromName("flock.friendShareFlock.subject");
  var body = bundle.GetStringFromName(SERVICES_SHARE_FLOCK_MESSAGE + "0");
  try {
    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);
  this._composeMessage(aFriendURN, subj, body, false);
};

// void friendAction(in AString aAction, in AString aFriendURN, in AString..
BeboAccount.prototype.friendAction =
function BeboAccount_friendAction(aAction, aFriendURN, aActivityURN) {
  this._logger.debug("friendAction('" + aAction + "', '" + aFriendURN + "')");
};


/*************************************************************************
 * BeboAccount: Private Data and Functions
 *************************************************************************/

BeboAccount.prototype._composeMessage =
function BeboAccount__composeMessage(aFriendURN,
                                       aSubject,
                                       aBody,
                                       addBreadCrumb)
{
  var body = aBody;
  var subject = aSubject;
  var c_friend = this._coop.get(aFriendURN);
  var url = this._WebDetective.getString(CLASS_SHORT_NAME, "bebomail_URL", "")
                              .replace("%friendid%", c_friend.accountId);
  var wm = CC["@mozilla.org/appshell/window-mediator;1"]
           .getService(CI.nsIWindowMediator);
  var win = wm.getMostRecentWindow("navigator:browser");

  if (win) {
    var browser = win.getBrowser();
    var newTab = browser.loadOneTab(url, null, null, null, false, false);
    var obs = CC["@mozilla.org/observer-service;1"]
             .getService(CI.nsIObserverService);
    var inst = this;
    var observer = {
      observe: function openSendMessageTabForFill_observer(aContent,
                                                           aTopic,
                                                           aContextUrl)
      {
        var contentWindow = newTab.linkedBrowser.docShell
                                  .QueryInterface(CI.nsIInterfaceRequestor)
                                  .getInterface(CI.nsIDOMWindow);
        function insertContent(aWebDicString, aMessage) {
          var xpathquery = inst._WebDetective.getString(CLASS_SHORT_NAME,
                                                        aWebDicString, "");
          var doc = contentWindow.document;
          var formItems = doc.evaluate(xpathquery, doc, null,
          CI.nsIDOMXPathResult.ANY_TYPE, null);
          if (formItems) {
            var formItem = formItems.iterateNext();
            if (formItem) {
              // formItem is null if user hasn't verified their email address.
              if (formItem.hasAttribute("value")) {
                formItem.setAttribute("value", aMessage);
              } else {
                var textNode = doc.createTextNode(aMessage);
                formItem.appendChild(textNode);
                inst._logger.info("aMessage " + aMessage);
              }
            } else {
              return false;
            }
          } else {
            return false;
          }
          return true;
        }
        if (contentWindow == aContent) {
          obs.removeObserver(this, "EndDocumentLoad");
          if (insertContent("bebomail_subjectXPath", subject)) {
            if(addBreadCrumb) {
              // Add breadcrumb to message body
              var breadcrumb = CC[FLOCK_RDNDS_CONTRACTID]
                               .getService(CI.flockIRichDNDService)
                               .getBreadcrumb("plain");
              if (breadcrumb) {
                body += breadcrumb;
              }
            }
            insertContent("bebomail_bodyXPath", body);
          }
        }
      }
    };
    obs.addObserver(observer, "EndDocumentLoad", false);
  }
};

// To clear the Bebo status, we simulate the form post used to clear
// the status on the Bebo website.
BeboAccount.prototype._clearStatus =
function BeboAccount__clearStatus(aFlockListener) {
  var inst = this;

  // If the status is already empty, do nothing.
  if (this.getParam("statusMessage") == "") {
    aFlockListener.onSuccess(inst, "setStatus");
    return;
  }

  statusIdListener = {
    onSuccess: function statusIdListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("statusIdListener_onSuccess(..., '" + aTopic + "')");
      var req = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
                .createInstance(CI.nsIXMLHttpRequest);

      req.onreadystatechange = function _clearStatus_onreadystatechange(aEvent) {
        if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
          if (Math.floor(req.status / 100) == 2) {
            var now = new Date().getTime();
            now = Math.round(now / 1000);
            inst.setParam("lastStatusMessageUpdateDate", now);
            inst.setParam("statusMessage", "");
            if (aFlockListener) {
              aFlockListener.onSuccess(inst, "setStatus");
            }
          } else {
            if (aFlockListener) {
              var error =
                gApi.getError(CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR);
              aFlockListener.onError(error, "setStatus");
            }
          }
        }
      };

      req.open("POST", gStrings["deleteStatusURL"]);
      req.setRequestHeader("Content-Type",
                           "application/x-www-form-urlencoded; charset=UTF-8");

      var postVars = {
        "From": "Profile",
        "SayingId": aSubject,
        "Yes": ""
      };

      var postBody = "";
      for (var key in postVars) {
        if (postBody.length) {
          postBody += "&";
        }
        postBody += key + "=" + encodeURIComponent(postVars[key]);
      }

      req.send(postBody);
    },
    onError: function statusIdListener_onError(aFlockError, aTopic) {
      inst._logger.debug("statusIdListener_onError(..., '" + aTopic + "')");
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, "setStatus");
      }
    }
  };

  this._getCurrentStatusId(statusIdListener);
};

// Scrape the current status ID. We need this ID to clear the user's status.
BeboAccount.prototype._getCurrentStatusId =
function BeboAccount__getCurrentStatusId(aFlockListener) {
  var inst = this;
  var req = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
            .createInstance(CI.nsIXMLHttpRequest);

  req.onreadystatechange = function _getCurrentStatusId_onreadystatechange(aEvent) {
    if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      if (Math.floor(req.status / 100) == 2) {
        var results = FlockSvcUtils.newResults();
        if (inst._WebDetective.detectNoDOM(CLASS_SHORT_NAME,
                                           "sayingid",
                                           null,
                                           req.responseText,
                                           results))
        {
          var sayingId = results.getPropertyAsAString("sayingid");
          if (aFlockListener) {
            aFlockListener.onSuccess(sayingId, arguments.callee.name);
          }
        }
      } else {
        if (aFlockListener) {
          var error = gApi.getError(CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR);
          aFlockListener.onError(error, arguments.callee.name);
        }
      }
    }
  };

  req.open("GET", gStrings["statusIdScrapeURL"]);
  req.send(null);
};


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

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

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


/*************************************************************************
 * Component: BeboAPI
 *************************************************************************/
function BeboAPI() {
  FlockSvcUtils.getLogger(this).init(CLASS_SHORT_NAME + "API");
  this._logger.debug("constructor");

  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);

  FlockSvcUtils.flockIWebServiceAPI.addDefaultMethod(this, "getRequestMethod");

  this.deauthenticate();
}


/*************************************************************************
 * BeboAPI: Component Creation
 *************************************************************************/
// 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().
BeboAPI.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " API",
  BEBO_API_CID,
  BEBO_API_CONTRACTID,
  BeboAPI,
  0,
  [
    // Not an XPCOM object, and never QI'ed; no interfaces required.
  ]
);


/*************************************************************************
 * BeboAPI: Private Data and Methods
 *************************************************************************/

// Builds the query string for the HTTP request to the API server.
BeboAPI.prototype._createApiCall =
function BeboAPI__createApiCall(aMethod, aParams) {
  this._logger.debug("_createApiCall('" + aMethod + "', ...)");

  // Generate the signature (MUST be the last item added to the params list).
  var params = aParams;
  params.method = aMethod;
  params.sig = this.generateSignature(params, BEBO_API_SECRET);

  var apiCall = gStrings["endpoint"];
  var isFirstParam = true;
  for (var p in params) {
    if (isFirstParam) {
      apiCall += "?";
      isFirstParam = false;
    } else {
      apiCall += "&";
    }
    this._logger.debug("    " + p + ": " + params[p]);
    apiCall += p + "=" + encodeURIComponent(params[p]);
  }

  return apiCall;
};

// Returns the session key
BeboAPI.prototype._getSessionKey =
function BeboAPI__getSessionKey() {
  this._logger.debug("_getSessionKey()");
  var sessionKey = null;
  try {
    sessionKey = this._authedAccount.getCustomParam("ApiSessionKey");
  } catch(ex) {
    // Not set yet; use default as initialized.
  }
  return sessionKey;
};

// Returns TRUE if the session expired
BeboAPI.prototype._sessionExpired =
function BeboAPI__sessionExpired() {
  var now = new Date().getTime();
  var end = null;  // If not set yet, consider it expired.
  try {
    end = this._authedAccount.getCustomParam("ApiSessionExpires");
  } catch(ex) {
    // Not set yet; use default as initialized.
  }
  if (end !== null && (end === 0 || now < end)) {
    this._logger.debug("_sessionExpired(): NO");
    return false;
  }
  this._logger.debug("_sessionExpired(): YES");
  return true;
};

// Performs the sequence of calls that are required for obtaining a session.
BeboAPI.prototype.authenticate =
function BeboAPI_authenticate(aFlockListener) {
  if (!this._authedAccount) {
    var err = this.getError(CI.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN);
    aFlockListener.onError(err, arguments.callee.name);
    return;
  } else {
    var sessionKey = this._getSessionKey();
    if (sessionKey!= null && !this._sessionExpired()) {
      this._logger.debug("Using cached session key");
      aFlockListener.onSuccess(sessionKey, null);
      return;
    }
  }
  var inst = this;
  var sessionListener = {
    onSuccess: function sessionListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("sessionListener_onSuccess(..., '" + aTopic + "')");

      // Store the session values from the response returned
      var node = getNode(aSubject, "auth_getSession_response");
      if (node) {
        for each (node in node.childNodes) {
          switch (getNodeName(node)) {
            case "session_key":
              var sessionKey = getNodeValue(node);
              inst._logger.debug("Session key retrieved: " + sessionKey);
              inst._authedAccount.setCustomParam("ApiSessionKey", sessionKey);
              break;
            case "uid":
              var sessionUid = getNodeValue(node);
              var accountUid = inst._authedAccount.getParam("accountId");
              if (sessionUid !== accountUid) {
                inst._logger.debug("Session uid MISMATCH: '"
                                   + sessionUid
                                   + "' != '"
                                   + accountUid
                                   + "'");
              }
              break;
            case "expires":
              var sessionExpires = parseInt(getNodeValue(node), 10);
              inst._logger.debug("Session expiry retrieved: "
                                 + sessionExpires);
              inst._authedAccount.setCustomParam("ApiSessionExpires",
                                                 sessionExpires);
              break;
            default:
              break;
          }
        }
        aFlockListener.onSuccess(sessionKey, aTopic);
        return;
      }

      inst._logger.debug("Couldn't get the session!");
      var err = inst.getError(CI.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN);
      aFlockListener.onError(err, arguments.callee.name);
    },

    onError: aFlockListener.onError
  };

  var authToken;
  var authTokenListener = {
    onSuccess: function authTokenListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("authTokenListener_onSuccess(..., '" + aTopic + "')");
      if (inst._WebDetective.detectNoDOM(CLASS_SHORT_NAME,
                                         "accountinfo", null, aSubject, null))
      {
        var params = {};
        params.auth_token = authToken;
        inst.addBasicParams(params);
        inst.doCall("socialNetwork.auth.getSession", params, null,
                    CI.flockIWebServiceAPI.GET, sessionListener);
      } else {
        inst._logger.debug("Token was not authorized!");
        var err = inst.getError(CI.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN);
        aFlockListener.onError(err, arguments.callee.name);
      }
    },

    onError: aFlockListener.onError
  };

  var tokenListener = {
    onSuccess: function tokenListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("tokenListener_onSuccess(..., '" + aTopic + "')");

      var nodeValue = getNodeValue(aSubject, "auth_createToken_response");
      if (nodeValue.length > 0) {
        // Store the token for later use
        authToken = nodeValue;
        inst._logger.debug("Token retrieved: " + authToken);

        // Simulate the login.php POST to continue the authentication process
        // See http://www.bebo.com/docs/auth
        var postVars = {
          api_key: BEBO_API_KEY,
          auth_token: authToken,
          skipcookie: "false",
          //infinite: "Y",    // Do NOT use infinite sessions.
          Y: "Yes"
        };

        inst.doCall(gStrings["userApprovalCall"], null, postVars,
                    CI.flockIWebServiceAPI.POST, authTokenListener);
      } else {
        inst._logger.debug("Couldn't get token!");
        var err = inst.getError(CI.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN);
        aFlockListener.onError(err, arguments.callee.name);
      }
    },

    onError: aFlockListener.onError
  };

  var params = {};
  inst.addBasicParams(params);
  this.doCall("socialNetwork.auth.createToken", params, null,
              CI.flockIWebServiceAPI.GET, tokenListener);
};


/*************************************************************************
 * BeboAPI: Flock Bebo API Implementation
 *************************************************************************/

// Returns the session uid
BeboAPI.prototype.getSessionUid =
function BeboAPI_getSessionUid() {
  this._logger.debug("getSessionUid()");
  var sessionUid = "";
  try {
    sessionUid = this._authedAccount.getParam("accountId");
  } catch(ex) {
    // Not set yet; use default as initialized.
  }
  return sessionUid;
};

// Performs an authenticated call to the BEBO server.
BeboAPI.prototype.call =
function BeboAPI_call(aMethod,
                      aParams,
                      aPostVars,
                      aRequestMethod,
                      aFlockListener)
{
  var inst = this;
  var authListener = {
    onSuccess: function authListener_onSuccess(aSubject, aTopic) {
      var params = aParams ? aParams : {};
      params.session_key = aSubject;
      inst.addBasicParams(params);
      inst.doCall(aMethod, params, aPostVars, aRequestMethod, aFlockListener);
    },
    onError: function authListener_onError(aFlockError, aTopic) {
      aFlockListener.onError(aFlockError, aTopic);
    }
  };
  // We don't have an API session key, or the one we have has expired.
  this.authenticate(authListener);
}

// Performs a call to the BEBO server.
BeboAPI.prototype.doCall =
function BeboAPI_doCall(aApiMethod,
                        aParams,
                        aPostVars,
                        aRequestMethod,
                        aFlockListener)
{
  this._logger.debug("######### BeboAPI.doCall('" + aApiMethod + "', ...)");

  var inst = this;
  var req = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
            .createInstance(CI.nsIXMLHttpRequest)
            .QueryInterface(CI.nsIJSXMLHttpRequest);
  req.onreadystatechange =
    function BeboAPI_doCall_onReadyStateChange() {
      if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
        inst._logger.debug("call() Status = " + req.status);
        if (Math.floor(req.status / 100) == 2) {
          inst._logger.debug("\n===== begin xhr response =====\n"
                             + req.responseText
                             + "\n====== end xhr response ======");

          // Use XML by preference, but fall back to text/html/JSON
          var responseData = req.responseXML ? req.responseXML
                                             : req.responseText;
          // If we ever request JSON responses, this test needs to be
          // expanded to look for error responses in the returned JSON.
          // In any case, if the error response is not present, the data is OK.
          if (req.responseXML &&
              getNode(req.responseXML, gStrings["errorResponse"]))
          {
            // The result XML contains an error message.
            inst._logger.debug("call() Error");
            aFlockListener.onError(inst.getXmlError(req.responseXML),
                                   aApiMethod);
          } else {
            // The result contains the requested data in some format).
            inst._logger.debug("call() Success");
            aFlockListener.onSuccess(responseData, aApiMethod);
          }
        } else {
          // HTTP errors (0 for connection lost)
          inst._logger.debug("call() HTTP Error");
          aFlockListener.onError(inst.getHttpError(req.status), aApiMethod);
        }
      }
    };

  var apiCall = null;
  var userApprovalNeeded = (aApiMethod == gStrings["userApprovalCall"]);
  if (userApprovalNeeded) {
    // Special case: Endpoint for authenticate token call
    apiCall = gStrings["authTokenEndpoint"] + aApiMethod;
  } else {
    apiCall = this._createApiCall(aApiMethod, aParams);
  }
  this._logger.debug("API call: " + apiCall);

  req.open(this.getRequestMethod(aRequestMethod), apiCall, true);

  var body = null;
  if (userApprovalNeeded) {
    req.setRequestHeader("Content-Type",
                         "application/x-www-form-urlencoded; charset=UTF-8");
    body = "";
    for (var p in aPostVars) {
      if (body.length > 0) {
        body += "&";
      }
      body += p + "=" + encodeURIComponent(aPostVars[p]);
    }
  }

  req.send(body);
};

// Generates a signature string for API call according to Bebo rules.
BeboAPI.prototype.generateSignature =
function BeboAPI_generateSignature(aParams, aSecret) {
  this._logger.debug("generateSignature(...)");

  /* Generate a signature, format as follows:
   *
   * 1) Remove the URL portion
   * 2) Alphabetailly sort the parameters by parameter name
   * 3) Remove any ampersands above in 1)
   * 4) Append secret
   * 5) MD5 the result, this becomes the sig
   * 6) take original URL+parameter list and add 5) as the sig value
   *
   * eg:  http://api.bebo.com/restserver.php?b=2&a=1&c=3
   *
   * 1) b=2&a=1&c=3
   * 2) a=1&b=2&c=3
   * 3) a=1b=2c=3
   * 4) a=1b=2c=3SDJ8zgMdDshUdJeO2nyovl6dAYFUhmWnJD9B
   * 5) MD5(above) = b371d148e1df5062d89dcab5fa73efc2
   * 6) http://api.bebo.com/restserver.php?b=2&a=1&c=3&sig=b371d148e1df5062d89dcab5fa73efc2
   */
  if (!(aParams && aSecret)) {
    return "";
  }

  // Sort Comparison helper method
  var sortBy = function generateSignature_sortBy(aElem1, aElem2) {
    return aElem1 > aElem2;
  };

  // To generate the signature...
  var sig = "";

  // 1) Alphabetically sort and add the parameters by parameter name
  var params = [];
  for (let p in aParams) {
    params.push(p + "=" + aParams[p]);
  }

  params.sort(sortBy);
  for each (let p in params) {
    sig += p;
  }

  // 2) Append the secret
  sig += aSecret;

  // 3) MD5 the result
  var converter = CC["@mozilla.org/intl/scriptableunicodeconverter"]
                  .createInstance(CI.nsIScriptableUnicodeConverter);
  converter.charset = "UTF-8";
  var inputStream = converter.convertToInputStream(sig);
  md5 = FlockCryptoHash.md5Stream(inputStream);

  // Result: Signature goodness
  this._logger.debug("Signature: " + sig + ", MD5: " + md5);
  return md5;
};

// Adds generic default values.
BeboAPI.prototype.addBasicParams =
function BeboAPI_addBasicParams(aParams) {
  aParams.api_key = BEBO_API_KEY;
  aParams.call_id = new Date().getTime();
  aParams.v = BEBO_API_VERSION;
  aParams.verbose = true;
  // Response format defaults to XML if not defined
  if (aParams.format) {
    aParams.format = aParams.format.toUpperCase();
  }
};

// Build a flockIError and populate it with the supplied value.
BeboAPI.prototype.getError =
function BeboAPI_getError(aErrorCode) {
  this._logger.debug("getError('" + aErrorCode + "')");
  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
  error.errorCode = aErrorCode;
  return error;
};

// Build a flockIError for the indicated HTTP error code.
BeboAPI.prototype.getHttpError =
function BeboAPI_getHttpError(aHttpErrorCode) {
  this._logger.debug("getHttpError('" + aHttpErrorCode + "')");
  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
  error.errorCode = aHttpErrorCode;
  error.serviceErrorCode = aHttpErrorCode;
  return error;
};

// Build a flockIError and extract an error value from the supplied XML.
// SIDE EFFECT: If a "session expired" error is detected, this function
//              marks the API session as "expired".
BeboAPI.prototype.getXmlError =
function BeboAPI_getXmlError(aXML) {
  this._logger.debug("getXmlError(...)");
  aXML.QueryInterface(CI.nsIDOMDocument);

  var errResp = getNode(aXML, gStrings["errorResponse"]);
  if (errResp) {
    var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
    error.serviceErrorCode = getNodeValue(aXML, gStrings["errorCode"]);
    error.serviceErrorString = getNodeValue(aXML, gStrings["errorMessage"]);
    this._logger.debug("errorResponse is: [" + error.serviceErrorCode
                       + "]:" + error.serviceErrorString);

    // Map the service error code to a Flock error code.
    switch (error.serviceErrorCode) {
      case "102":
        // The session expired; update our notion of session status.
        if (this._authedAccount) {
          // ZERO means the session never expires; 1 is in the past.
          this._authedAccount.setCustomParam("ApiSessionExpires", 1);
        }
        error.errorCode = CI.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN;
        break;
      default:
        error.errorCode = CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR;
        break;
    }
    // If the service name is set, there MUST be a bundle somewhere for the
    // error string to be retrieved from. Results in:
    // flock:flockerror:error] The entity flock.service.errorCode_null is \
    // missing, this localization is incomplete!
    // error.serviceName = CLASS_SHORT_NAME;
    return error;
  }

  // Something went drastically wrong, return unknown error
  return this.getError(CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR);
};

// Identify the authenticated flockIWebServiceAccount to the API object
BeboAPI.prototype.setAuthedAccount =
function BeboAPI_setAuthedAccount(aAccount) {
  this._logger.debug("setAuthedAccount(aAccount)");

  try {
    aAccount.QueryInterface(CI.flockIWebServiceAccount);
  } catch(ex) {
    this._authedAccount = null;
    this._logger.debug("authenticate(): Invalid aAccount argument");
    return;
  }
  this._authedAccount = aAccount;
};

// End the association of a flockIWebServiceAccount with the API object.
BeboAPI.prototype.deauthenticate =
function BeboAPI_deauthenticate() {
  this._logger.debug("deauthenticate()");
  this._authedAccount = null;
};

