// 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/FlockOAuthLib.jsm");
CU.import("resource:///modules/FlockPrefsUtils.jsm");
CU.import("resource:///modules/FlockScheduler.jsm");
CU.import("resource:///modules/FlockStringBundleHelpers.jsm");
CU.import("resource:///modules/FlockSvcUtils.jsm");
CU.import("resource:///modules/FlockXMLUtils.jsm");
CU.import("resource:///modules/FlockPhotoUploader.jsm");
CU.import("resource:///modules/FlockMediaUtils.jsm");

const MODULE_NAME = "MySpace";
const API_CLASS_NAME = "Flock MySpace API";
const API_CLASS_ID = Components.ID("{bc03a95e-0ed3-4532-b54b-473c1dfddca9}");
const API_CONTRACT_ID = "@flock.com/webservice/api/myspace;1";
const CLASS_NAME = "Flock MySpace Service";
const CLASS_SHORT_NAME = "myspace";
const CLASS_TITLE = "MySpace";
const CLASS_ID = Components.ID("{18440fbe-2f8c-4354-b4fb-dee7a009124b}");
const CONTRACT_ID = "@flock.com/webservice/myspace;1";
const FLOCK_ERROR_CONTRACTID = "@flock.com/error;1";
const XMLHTTPREQUEST_CONTRACTID = "@mozilla.org/xmlextras/xmlhttprequest;1";
const HASH_PROPERTY_BAG_CONTRACTID = "@mozilla.org/hash-property-bag;1";
const FLOCK_RDNDS_CONTRACTID = "@flock.com/rich-dnd-service;1";
// From nsIXMLHttpRequest.idl
const XMLHTTPREQUEST_READYSTATE_COMPLETED = 4;

const SERVICE_ENABLED_PREF = "flock.service.myspace.enabled";

const FAVICON = "chrome://flock/content/services/myspace/myspaceFavicon.png";

const MYSPACE_API_URL = "http://api.myspace.com/";
const MYSPACE_API_VERSION = "v1";

const SERVICES_PROPERTIES_FILE = "chrome://flock/locale/services/services.properties";
const MYSPACE_PROPERTIES = "chrome://flock/locale/services/myspace.properties";

const FLOCK_PHOTO_ALBUM_CONTRACTID  = "@flock.com/photo-album;1";
const SERVICES_SHARE_FLOCK_SUBJECT = "flock.friendShareFlock.subject";
const SERVICES_SHARE_FLOCK_MESSAGE = "flock.friendShareFlock.message";
const MYSPACE_NO_STATUS_MSG = "flock.people.sidebar.mecard.statusMsg.none";

// The delay between two refreshes when the sidebar is closed (in seconds)
const REFRESH_INTERVAL = 1800; // 30 minutes
// The delay between two refreshes when the sidebar is open (in seconds)
const SHORT_INTERVAL = 1800; // 30 minutes

// CONSTANTS FOR PHOTO UPLOADING
// XXX This number is not based in reality.  I just figured 500MB of space is
//     enough for now.
//
// Maximum space available for photos in the user's account
const MYSPACE_MAX_PHOTO_SPACE = 100 * 5 * 1024 * 1024;
// Space currently used by photos
const MYSPACE_USED_PHOTO_SPACE = 0;
// MySpace has a maximum file size of 5MB
const MYSPACE_MAX_FILE_SIZE = 5 * 1024 * 1024;
// Name of Myspace default photo album
const DEFAULT_ALBUM_NAME = "My Photos";

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

var gApi = null;

// Override the buildTooptip function in order to use the
// large img for the tooltip image.
var flockMediaItemFormatter = {
  canBuildTooltip: true,
  buildTooltip: function MySpace_buildTooltip(aMediaItem) {
    default xml namespace =
      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

    var 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">{aMediaItem.title}</label>
            <label anonid="ttUserTxt" class="user">{aMediaItem.username}</label>
          </vbox>
        </hbox>
      </vbox>;

    return xml;
  }
};

// Convert a JSON photo to a flockIMediaItem.
function _handlePhotoResult(aPhoto) {
  var wd = CC["@flock.com/web-detective;1"].getService(CI.flockIWebDetective);
  var newMediaItem = CC["@flock.com/photo;1"]
                     .createInstance(CI.flockIMediaItem);

  newMediaItem.init(CLASS_SHORT_NAME, flockMediaItemFormatter);
  newMediaItem.webPageUrl = wd.getString(CLASS_SHORT_NAME, "viewImageUrl", "")
                              .replace("%friendid%", aPhoto.user.userId)
                              .replace("%imageid%", aPhoto.id);
  newMediaItem.thumbnail = aPhoto.imageUri;
  newMediaItem.midSizePhoto = aPhoto.smallImageUri;
  newMediaItem.largeSizePhoto = aPhoto.imageUri;
  newMediaItem.username =
    flockXMLDecode(aPhoto.user.name);
  newMediaItem.userid = aPhoto.user.userId;
  newMediaItem.uploadDate = _fixUploadDate(aPhoto.uploadDate);
  newMediaItem.is_public = true;
  newMediaItem.is_video = false;
  newMediaItem.title = aPhoto.caption;
  newMediaItem.id = aPhoto.id;

  return newMediaItem;
}

// Return seconds since the epoch from the nasty date string format we're
// getting from the MySpace API.
function _fixDate(aUglyDateString) {
  // Get the date in milliseconds
  var date = _fixUploadDate(aUglyDateString);

  // Return the date in seconds
  return Math.floor(date / 1000);
}

// Return milliseconds since the epoch from the nasty date string format we're
// getting from the MySpace API.
function _fixUploadDate(aUglyDateString) {
  // e.g: "2008-11-14T15:21:32"
  // "4/14/2008 1:13:49 PM" would be returned if no gmt param was supplied

  // Myspace should return their GMT dates without adjusting
  // for Daylight Savings.
  var saneDate = new Date(aUglyDateString.replace("-", "/", "g")
                                         .replace("T", " ") + " GMT");

  // Return seconds since the epoch using UTC(GMT). If date older than epoch,
  // return 0.
  return (Math.max(saneDate.getTime(), 0));
}

// Returns true if aText contains any blocked domains,
// otherwise return false.
function _hasBlockedText(aText) {
  if (!aText) {
    return false;
  }

  var wd = CC["@flock.com/web-detective;1"].getService(CI.flockIWebDetective);
  var blocked = wd.getString(CLASS_SHORT_NAME, "blockedDomainStrings", "");
  var blockedArr = blocked.split(",");

  for (var i = 0; i < blockedArr.length; i++) {
    if (aText.indexOf(blockedArr[i]) != -1) {
      return true;
    }
  }

  return false;
}

/*************************************************************************
 * Component: MyspaceAPI
 *************************************************************************/
function MyspaceAPI() {
  FlockSvcUtils.getLogger(this);
  FlockSvcUtils.getACUtils(this);
  this._logger.init("MyspaceAPI");

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

  this._logger.debug("constructor");
  var wsa = FlockSvcUtils.flockIWebServiceAPI;
  wsa.addDefaultMethod(this, "getRequestMethod");

  this._oauthKey = this._wd.getString(CLASS_SHORT_NAME,
                                      "myspaceOAuthKey",
                                      "");
  var secretHash = this._wd.getString(CLASS_SHORT_NAME,
                                      "myspaceOAuthSecretHash",
                                      "");
  this._oauthSecret = FlockSvcUtils.scrambleString(secretHash);
}

MyspaceAPI.prototype = new FlockXPCOMUtils.genericComponent(
  API_CLASS_NAME,
  API_CLASS_ID,
  API_CONTRACT_ID,
  MyspaceAPI,
  0,
  [
    CI.flockIAuthenticatedAPI,
    CI.flockITokenAPI,
    CI.flockIMyspaceAPI,
    CI.flockIWebServiceAPI
  ]
);

/*************************************************************************
 * MyspaceAPI: flockIWebServiceAPI Implementation
 *************************************************************************/

/**
 * void call(in AString aApiMethod,
 *           in nsISupports aParams,
 *           in nsISupports aPostVars,
 *           in nsISupports aRequestMethod,
 *           in flockIListener aFlockListener);
 * @see flockIWebServiceAPI#call
 */
// authenticated call
MyspaceAPI.prototype.call =
function MyspaceAPI_call(aApiMethod,
                         aParams,
                         aPostVars,
                         aRequestMethod,
                         aFlockListener)
{
  this._logger.debug(arguments.callee.name + "('" + aApiMethod + "', ...)");

  // Try to auth the api then make the call.
  var inst = this;
  var authListener = {
    onSuccess: function authListener_onSuccess(aSubject, aTopic) {
      var endpoint = MYSPACE_API_URL + MYSPACE_API_VERSION
                     + "/users/"
                     + aParams.wrappedJSObject.userId + "/"
                     + aApiMethod;
      inst._doCall(endpoint,
                   aParams,
                   aPostVars,
                   aRequestMethod,
                   aFlockListener);
    },
    onError: function authListener_onError(aFlockError, aTopic) {
      var svc = CC[CONTRACT_ID].getService(CI.flockIWebService);
      inst._acUtils.markAllAccountsAsLoggedOut(svc);
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  var credentials = {
    username: aParams.wrappedJSObject.userId
  };

  this.authenticate(credentials, authListener);
};

MyspaceAPI.prototype._doCall =
function MyspaceAPI__doCall(aEndpoint,
                            aParams,
                            aPostVars,
                            aRequestMethod,
                            aFlockListener)
{
  this._logger.debug(arguments.callee.name + "('" + aEndpoint + "', ...)");

  var params = aParams.wrappedJSObject;

  // When using a request method of PUT, we don't supply a response format
  // as MySpace errors out and cannot successfully complete the request

  if (aRequestMethod != CI.flockIWebServiceAPI.PUT) {
    aEndpoint += ".json";
  }

  // Create a valid OAuth request
  var consumer = new OAuthConsumer(this._oauthKey,
                                   this._oauthSecret,
                                   null);
  var accessToken = new OAuthToken(this.token, this.tokenSecret);
  var requestMethodStr = this.getRequestMethod(aRequestMethod);
  var request = OAuthRequest.fromConsumerAndToken(consumer,
                                                  accessToken,
                                                  requestMethodStr,
                                                  aEndpoint,
                                                  params);

  // Sign the request
  var signMethod = new OAuthSignatureMethod_HMAC_SHA1();
  request.sign(signMethod, consumer, accessToken);

  this._logger.debug("_doCall() request.toUrl(): " + request.toUrl());

  this.req = CC[XMLHTTPREQUEST_CONTRACTID]
             .createInstance(CI.nsIXMLHttpRequest);
  this.req.QueryInterface(CI.nsIJSXMLHttpRequest);

  // Don't pop nsIAuthPrompts if auth fails
  this.req.mozBackgroundRequest = true;
  this.req.open(requestMethodStr, request.toUrl(), true);

  var req = this.req;
  var inst = this;
  req.onreadystatechange = function api_call_orsc(aEvent) {
    inst._logger.debug("_doCall() ReadyState: " + req.readyState);

    if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      var status = req.status;
      var responseText = req.responseText;
      inst._logger.debug("_doCall() status: " + status);

      if (Math.floor(status / 100) == 2) {
        inst._logger.debug("_doCall() url: " + aEndpoint
                           + " response:\n" + responseText + "\n");
        // When using the request method PUT (ie setting status), MySpace
        // doesn't return any responseText, the responseXML cannot be parsed
        // although is present as an element.
        if (responseText ||
            (aRequestMethod == CI.flockIWebServiceAPI.PUT && req.responseXML))
        {
          aFlockListener.onSuccess(req, arguments.callee.name);
        } else {
          inst._logger.debug("_doCall() error: "
                             + "empty or poorly formatted result");
          var error = inst.getError(null);
          aFlockListener.onError(error, arguments.callee.name);
        }
      } else {
        // HTTP errors
        inst._logger.debug("_doCall() url: " + aEndpoint
                           + " HTTP Error: " + status + "\n");
        var error = inst.getHttpError(status);
        error.serviceErrorString = responseText;
        error.serviceErrorCode = status;
        aFlockListener.onError(error, arguments.callee.name);
      }
    }
  };

  var postBody = null;
  if (aPostVars) {
    // Set the respective header for URL-encoded form data
    this.req.setRequestHeader("Content-Type",
                              "application/x-www-form-urlencoded; charset=UTF-8");
    // Variable is currently null, a String is needed
    postBody = "";
    for (var key in aPostVars) {
      if (postBody.length) {
        postBody += "&";
      }
      postBody += key + "=" + encodeURIComponent(aPostVars[key]);
    }
  }

  this.req.send(postBody);
};

/**
 * flockIError getError(in AString aErrorCode);
 * @see flockIWebServiceAPI#getError
 */
MyspaceAPI.prototype.getError =
function MyspaceAPI_getError(aErrorCode) {
  this._logger.debug(".getError('" + aErrorCode + "')");

  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
  error.serviceErrorCode = aErrorCode;
  error.serviceName = CLASS_TITLE;

  switch (aErrorCode) {
    default:
      error.errorCode = CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR;
      break;
  }

  return error;
};

/**
 * flockIError getHttpError(in AString aHttpErrorCode);
 * @see flockIWebServiceAPI#getHttpError
 */
MyspaceAPI.prototype.getHttpError =
function MyspaceAPI_getHttpError(aHttpErrorCode) {
  this._logger.debug(".getHttpError('" + aHttpErrorCode + "')");

  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
  if (aHttpErrorCode == "401" || aHttpErrorCode == "403") {
    if (this.tokenSecret) {
      error.errorCode = CI.flockIError.PHOTOSERVICE_FRIENDS_ONLY;
    } else {
      error.serviceName = CLASS_TITLE;
      error.serviceErrorCode = aHttpErrorCode;
      error.errorCode = CI.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN;
    }
  } else {
    error.errorCode = aHttpErrorCode;
  }

  return error;
};


/*************************************************************************
 * MyspaceAPI: flockIAuthenticatedAPI Implementation
 *************************************************************************/

/**
 * void authenticate(in nsILoginInfo aCredentials,
 *                   in flockIListener aFlockListener);
 * @see flockIAuthenticatedAPI#authenticate
 */
MyspaceAPI.prototype.authenticate =
function MyspaceAPI_authenticate(aCredentials, aFlockListener) {
  if (this.tokenSecret) {
    // The API is already auth'ed
    aFlockListener.onSuccess(null, "authenticated");
    return;
  }

  this._logger.debug(".authenticate("
                     + ((aCredentials) ? aCredentials.username : "")
                     + ", ...)");

  var inst = this;
  var getAccessListener = {
    onSuccess: function getAccessListener_onSuccess(aSubject, aTopic) {
      // Currently, even though we are requesting a result of type JSON
      // a plaintext string (per the OAuth spec) is return in the form:
      // oauth_token={1}&oauth_token_secret={2}
      var tokenArray = (aSubject.responseText).split("&");
      inst.token = tokenArray[0].split("=")[1];
      inst.tokenSecret = tokenArray[1].split("=")[1];
      aFlockListener.onSuccess(inst, "authenticated");
    },
    onError: function getAccessListener_onError(aFlockError, aTopic) {
      aFlockListener.onError(aFlockError, arguments.callee.name);
    }
  };

  var params = {
    wrappedJSObject: { }
  };

  this._doCall(MYSPACE_API_URL + "access_token",
               params,
               null,
               CI.flockIWebServiceAPI.GET,
               getAccessListener);
};

/**
 * void deauthenticate();
 * @see flockIAuthenticatedAPI#deauthenticate
 */
MyspaceAPI.prototype.deauthenticate =
function MyspaceAPI_deauthenticate() {
  this._logger.debug(".deauthenticate()");
  this.token = "";
  this.tokenSecret = "";
};


/*************************************************************************
 * MyspaceAPI: flockITokenAPI Implementation
 *************************************************************************/

// readonly attribute AString token;
MyspaceAPI.prototype.token = "";

// readonly attribute AString token secret;
MyspaceAPI.prototype.tokenSecret = "";

/*************************************************************************
 * MyspaceAPI: flockIMyspaceAPI Implementation
 *************************************************************************/

/**
 * void getMyFriends(in AString aUid, in flockIListener aFlockListener);
 * @see flockIMyspaceAPI#getMyFriends
 */
MyspaceAPI.prototype.getMyFriends =
function MyspaceAPI_getMyFriends(aUid, aFlockListener) {
  this._logger.debug(arguments.callee.name + "('" + aUid + "', ...)");

  // Number of friends to pull from ther API at a time (max 100)
  const API_NUM_FRIENDS = 100;

  var inst = this;
  var friendIdArr = [];
  var friendCount = 0;
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);

  var getMoreFriendsListener = {
    onSuccess: function getMoreFriends_onSuccess(aSubject, aTopic) {
      inst._logger.debug(arguments.callee.name);
      inst._logger.debug("getMoreFriends responseText: \n" + aSubject.responseText);
      var result = nsJSON.decode(aSubject.responseText);

      var totalFriends = result.count;

      for (var friend in result.Friends) {
        friendIdArr[result.Friends[friend].userId] = result.Friends[friend];
        friendCount++;
      }

      if (friendCount == totalFriends) {
        var moreWrappedFriends = {
          wrappedJSObject: {
           friends: friendIdArr
          }
        };

        aFlockListener.onSuccess(moreWrappedFriends, "getMoreFriends");
      } else if (friendCount > totalFriends) {
        inst._logger.error("Found too many friends");
        aFlockListener.onError(null, "getMoreFriends");
      }
    },
    onError: function getMoreFriends_onError(aFlockError, aTopic) {
      inst._logger.error("getMoreFriends_onError(..., '" + aTopic + "', ...");
      aFlockListener.onError(aFlockError, "getMoreFriends");
    }
  };

  var getFriendsListener = {
    onSuccess: function getFriendsListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug(arguments.callee.name);
      inst._logger.debug("getFriends responseText: \n" + aSubject.responseText);
      var result = nsJSON.decode(aSubject.responseText);

      var totalFriends = result.count;

      for (var friend in result.Friends) {
        friendIdArr[result.Friends[friend].userId] = result.Friends[friend];
        friendCount++;
      }

      var friendsRemaining = totalFriends - API_NUM_FRIENDS;
      if (friendsRemaining > 0) {
        // Loop thru remaining friend pages
        for (var page = 2; (page - 1) * API_NUM_FRIENDS < totalFriends; page++) {
          var moreParams = {
            wrappedJSObject: {
              userId: aUid,
              show: "status",
              page: page,
              page_size: API_NUM_FRIENDS
            }
          };
          inst.call("friends",
                    moreParams,
                    null,
                    CI.flockIWebServiceAPI.GET,
                    getMoreFriendsListener);
        }
      } else {
        var wrappedFriends = {
          wrappedJSObject: {
           friends: friendIdArr
          }
        };

        aFlockListener.onSuccess(wrappedFriends, "getFriends");
      }
    },
    onError: function getFriendsListener_onError(aFlockError, aTopic) {
      inst._logger.error(arguments.callee.name);
      aFlockListener.onError(aFlockError, "getFriends");
    }
  };

  var params = {
    wrappedJSObject: { userId: aUid,
                       gmt: 0,
                       show: "status",
                       page_size: API_NUM_FRIENDS }
  };

  this.call("friends",
            params,
            null,
            CI.flockIWebServiceAPI.GET,
            getFriendsListener);
};

/**
 * void getMyProfile(in AString aUid, in flockIListener aFlockListener);
 * @see flockIMyspaceAPI#getMyProfile
 */
MyspaceAPI.prototype.getMyProfile =
function MyspaceAPI_getMyProfile(aUid, aFlockListener) {
  var params = {
    wrappedJSObject: { gmt: 0, userId: aUid }
  };
  this.call("status",
            params,
            null,
            CI.flockIWebServiceAPI.GET,
            aFlockListener);
};

/**
 * void getMyIndicators(in AString aUid, in flockIListener aFlockListener);
 * @see flockIMyspaceAPI#getMyIndicators
 */
MyspaceAPI.prototype.getMyIndicators =
function MyspaceAPI_getMyIndicators(aUid, aFlockListener) {
  var params = {
    wrappedJSObject: { userId: aUid }
  };
  this.call("indicators",
            params,
            null,
            CI.flockIWebServiceAPI.GET,
            aFlockListener);
};

/***
 * void setMyStatus(in AString aUid,
 *                  in AString aStatusMessage,
 *                  in flockIListener aFlockListener);
 * @see flockIMyspaceAPI#setStatus
 */
MyspaceAPI.prototype.setMyStatus =
function MyspaceAPI_setMyStatus(aUid, aStatusMessage, aFlockListener) {
  var params = {
    wrappedJSObject: {
      userId: aUid
    }
  };
  this.call("status",
            params,
            {status: aStatusMessage},
            CI.flockIWebServiceAPI.PUT,
            aFlockListener);
};

/**
 * void getMyUserAlbums(in AString aUid, in flockIListener aFlockListener);
 * @call flockIMyspaceAPI#getUserAlbums
 */
MyspaceAPI.prototype.getUserAlbums =
function MyspaceAPI_getUserAlbums(aUid, aFlockListener) {
  var params = {
    wrappedJSObject: {
      userId: aUid,
      page: 1,
      page_size: "all"
    }
  };
  this.call("albums",
            params,
            null,
            CI.flockIWebServiceAPI.GET,
            aFlockListener);
};

/**
 * void uploadPhotosViaUploader(in flockIPhotoUploadAPIListener aUploadListener,
 *                              in AString aFilename,
 *                              in nsIPropertyBag2 aParams,
 *                              in flockIPhotoUpload aUpload);
 * @call flockIMyspaceAPI#uploadPhotosViaUploader
 */
MyspaceAPI.prototype.uploadPhotosViaUploader =
function MyspaceAPI_uploadPhotosViaUploader(aUploadListener,
                                            aFilename,
                                            aParams,
                                            aUpload)
{
  this._logger.debug(arguments.callee.name + "('" + aFilename + "', ...)");

  // Are we attempting to upload to a specific album, if not
  // MySpace uses 0 as the default
  var albumId = aUpload.album ? aUpload.album : 0;

  var endpoint = MYSPACE_API_URL
               + MYSPACE_API_VERSION + "/"
               + "users/" + aParams.getPropertyAsAString("userId")
               + "/albums/" + albumId
               + "/photos";

  // Create a valid OAuth request
  var consumer = new OAuthConsumer(this._oauthKey,
                                   this._oauthSecret,
                                   null);
  var accessToken = new OAuthToken(this.token, this.tokenSecret);
  var postMethodStr = this.getRequestMethod(CI.flockIWebServiceAPI.POST);
  var request = OAuthRequest.fromConsumerAndToken(consumer,
                                                  accessToken,
                                                  postMethodStr,
                                                  endpoint,
                                                  {});
  // Sign the request
  var signMethod = new OAuthSignatureMethod_HMAC_SHA1();
  request.sign(signMethod, consumer, accessToken);

  this._logger.debug(".uploadPhotosViaUploader() req Url: " + request.toUrl());

  var api = this;
  var uploadListener = {
    onResult: function uploadListener_onResult(aXml) {
      api._logger.debug(CC["@mozilla.org/xmlextras/xmlserializer;1"]
                        .getService(CI.nsIDOMSerializer)
                        .serializeToString(aXml));
      aUploadListener.onUploadComplete(aUpload);

      var uri = aXml.getElementsByTagName("uri")[0]
                    .firstChild.nodeValue;
      var uriSplit = uri.split("/");
      photoId = uriSplit[9];

      // We need to make an API call to get the data for the newly
      // uploaded photo.
      var getPhotoListener = {
        onSuccess: function upl_getPhotoListener_onSuccess(aSubject, aTopic) {
          api._logger.debug(arguments.callee.name);
          api._logger.debug("responseText: " + aSubject.responseText);

          var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
          var photo = nsJSON.decode(aSubject.responseText);
          var mediaItem = _handlePhotoResult(photo);

          aUploadListener.onUploadFinalized(aUpload, mediaItem);
        },
        onError: function upl_getPhotoListener_onSuccess(aFlockError, aTopic) {
          api._logger.debug(arguments.callee.name);
          aFlockListener.onError(aFlockError, null);
        }
      };
      var params = {
        wrappedJSObject: { userId: uriSplit[5] }
      };
      gApi.call("photos/" + photoId,
                params,
                null,
                CI.flockIWebServiceAPI.GET,
                getPhotoListener);
    },
    onError: function uploadListener_onError(aErrorCode) {
      api._logger.debug("uploadListener_onError");
      try {
        if (aErrorCode) {
          aUploadListener.onError(api.getHttpError(aErrorCode));
        } else {
          aUploadListener.onError(api.getError(null, null));
        }
      } catch (ex) {
        // Unable to return error code.
      }
    },
    onProgress: function listener_onProgress(aCurrentProgress) {
      aUploadListener.onProgress(aCurrentProgress);
    }
  };

  var fileExtension;
  if (aFilename) {
    fileExtension = aFilename.substring(aFilename.lastIndexOf(".") + 1);
  }

  // Set up the header request parameters, MySpace needs these to be cleared
  // for a successful photo upload
  var headerParams = {
    "Accept": "",
    "Accept-Charset": "",
    "Accept-Encoding": "",
    "Accept-Language": "",
    "Cache-Control": "",
    "Cookie": "",
    "User-Agent": "",
    "Keep-Alive": "",
    "Pragma": "",
    "Content-Type": "multipart/form-data",
    "Content-Disposition": fileExtension,
    "Content-Encoding": "base64"
  };

  var uploader = new FlockPhotoUploader();
  uploader.setEndpoint(request.toUrl());
  uploader.uploadBase64(uploadListener,
                        aFilename,
                        aUpload,
                        postMethodStr,
                        headerParams);
};

/**
 * void createAlbum(in AString aUid,
 *                  in AString aAlbumTitle,
 *                  in flockIListener aFlockListener);
 * @call flockIMyspaceAPI#createAlbum
 */
MyspaceAPI.prototype.createAlbum =
function MyspaceAPI_createAlbum(aUid, aAlbumTitle, aFlockListener) {
  this._logger.debug(arguments.callee.name + "('" + aUid + "','"
                                                  + aAlbumTitle + "' ...)");
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  var api = this;
  var createAlbumListener = {
    onSuccess: function msAPI_createAlbum_onSuccess(aSubject, aTopic) {
      api._logger.debug(arguments.callee.name);
      api._logger.debug("responseText: " + aSubject.responseText);
      var album = nsJSON.decode(aSubject.responseText);

      var albumCountListener = {
        onSuccess: function msAPI_createAlbum_onSuccess(aSubject, aTopic) {
          api._logger.debug(arguments.callee.name);
          var albumsJSON = nsJSON.decode(aSubject.responseText);

          if (aAlbumTitle != "My Photos" && albumsJSON.albums.length == 1) {
            // This is the first album created so we need to call api again
            // to actually create the album.
            api.call("albums",
                      params,
                      postData,
                      CI.flockIWebServiceAPI.POST,
                      createAlbumListener);
          } else {
            // Create the album as normal
            var newAlbum = CC[FLOCK_PHOTO_ALBUM_CONTRACTID]
                          .createInstance(CI.flockIPhotoAlbum);
            newAlbum.title = album.title;
            newAlbum.id = album.id;
            aFlockListener.onSuccess(newAlbum, "success");
          }
        },
        onError: function msAPI_createAlbum_onError(aFlockError, aTopic) {
          // Error in getting the album count - just return the new album
          var newAlbum = CC[FLOCK_PHOTO_ALBUM_CONTRACTID]
                        .createInstance(CI.flockIPhotoAlbum);
          newAlbum.title = album.title;
          newAlbum.id = album.id;
          aFlockListener.onSuccess(newAlbum, "success");
        }
      };

      // Due to a Myspace bug, for an account with no albums, when you
      // create your first album the api actually creates "My Photos".
      // Work around is to check the number of albums after we create one.
      // If number of albums equals 1, then we know this is the first
      // album created so the bug is in effect, so we need to fire off
      // another call to createAlbum to *really* create the album.
      // c.f. https://bugzilla.flock.com/show_bug.cgi?id=15126.
      api.getUserAlbums(aUid, albumCountListener);
    },
    onError: function msAPI_createAlbum_onError(aFlockError, aTopic) {
      api._logger.debug(".createAlbum() error: " + aFlockError.errorString);
      aFlockListener.onError(aFlockError, null);
    }
  };
  var params = {
    wrappedJSObject: { userId: aUid }
  };
  // Create the required POST data
  var postData = {
    title: aAlbumTitle,
    privacy: "Everyone",
    location: ""
  };
  this.call("albums",
            params,
            postData,
            CI.flockIWebServiceAPI.POST,
            createAlbumListener);
};


/*************************************************************************
 * Component: MyspaceService
 *************************************************************************/
function MyspaceService() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("MyspaceService");

  // Determine whether this service has been disabled, and unregister if so.
  var prefService = CC["@mozilla.org/preferences-service;1"]
                    .getService(CI.nsIPrefBranch);
  if (FlockPrefsUtils.getBoolPref(SERVICE_ENABLED_PREF) === false) {
    this._logger.info(SERVICE_ENABLED_PREF + " is FALSE! Not initializing.");
    this.deleteCategories();
    return;
  }

  var profiler = CC["@flock.com/profiler;1"].getService(CI.flockIProfiler);
  var evtID = profiler.profileEventStart(CLASS_SHORT_NAME + "-init");

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

  this._accountClassCtor = MyspaceAccount;

  // Used for pseudo-pagination.
  this._cachedPhotos = null;

  // Convenience variable.
  this._wd = FlockSvcUtils.getWD(this);
  var authenticatedUrn = FlockSvcUtils.initService(this, true);

  // Initialize API
  gApi = CC[API_CONTRACT_ID].createInstance(CI.flockIMyspaceAPI);

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

  // Update auth states
  if (authenticatedUrn) {
    // MySpace's "Remember Me" function appears to only save your username,
    // not any authentication token or state, so always assume we're logged
    // out.
    this._coop.Account.get(authenticatedUrn).isAuthenticated = false;
  }

  profiler.profileEventEnd(evtID, "");

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

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

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

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

  var rcdh = FlockSvcUtils.flockIRichContentDropHandler;
  rcdh.addDefaultMethod(this, "_handleTextareaDrop");

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

MyspaceService.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME,
  CLASS_ID,
  CONTRACT_ID,
  MyspaceService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.flockIWebService,
    CI.flockIAuthenticateNewAccount,
    CI.flockILoginWebService,
    CI.flockIPollingService,
    CI.flockIMediaWebService,
    CI.flockIMediaUploadWebService,
    CI.flockIMediaEmbedWebService,
    CI.flockISocialWebService,
    CI.flockIRichContentDropHandler
  ]
);

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


/*************************************************************************
 * Private Data and Functions
 *************************************************************************/

/**
 * Helper function to insert a user id in an URL from web detective.
 */
MyspaceService.prototype._makeMyspaceUrl =
function MyspaceService__makeMyspaceUrl(aUrlType, aUserId) {
  this._logger.debug("._makeMyspaceUrl(" + aUrlType + ", " + aUserId + ")");

  return this._WebDetective.getString(CLASS_SHORT_NAME, aUrlType, null)
             .replace("%friendid%", aUserId);
};


/**
 * Helper function to determine if the user has customized their avatar based
 * on the passed in URL.
 * @param  aUrl  A string containing the URL of the user's avatar.
 * @return  true if the user is still using the default avatar, else false
 */
MyspaceService.prototype._hasDefaultAvatar =
function MyspaceService__hasDefaultAvatar(aUrl) {
  this._logger.debug("._hasDefaultAvatar(" + aUrl + ")");
  var defaultUrlPattern = this._wd.getString(CLASS_SHORT_NAME, "noAvatar", "");
  var regexp = new RegExp(defaultUrlPattern);

  return (aUrl.match(regexp));
};

/**
 * Create a lightweight quasi-nsISimpleEnumerator from a passed in array.
 *
 * Doing it here in JS prevents having to cross XPCOM boundaries, resulting
 * in a performance bump.
 *
 * @param aArray  The array to create the enumerator from.
 * @returns  the enumerator claiming to be a nsISimpleEnumerator.
 */
MyspaceService.prototype._createEnum =
function MyspaceService__createEnum(aArray) {
  return {
    QueryInterface: function QueryInterface(aIid) {
      if (aIid.equals(CI.nsISimpleEnumerator)) {
        return this;
      }
      throw CR.NS_ERROR_NO_INTERFACE;
    },
    hasMoreElements: function hasMoreElements() {
      return (aArray.length > 0);
    },
    getNext: function getNext() {
      return aArray.shift();
    }
  };
};

/*************************************************************************
 * MyspaceService: flockIWebService Implementation
 *************************************************************************/

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

// readonly attribute AString title;
MyspaceService.prototype.title = CLASS_TITLE;

// readonly attribute AString icon;
MyspaceService.prototype.icon = FAVICON;

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

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


/*************************************************************************
 * MyspaceService: flockILoginWebService Implementation
 *************************************************************************/

// readonly attribute AString domains;
MyspaceService.prototype.__defineGetter__("domains",
function MyspaceService_getdomains() {
  this._logger.debug("Getting attribute: domains");

  return this._wd.getString(CLASS_SHORT_NAME, "domains", "myspace.com");
});

 // readonly attribute boolean needPassword;
MyspaceService.prototype.needPassword = false;

/**
 * @see flockILoginWebService#addAccount
 */
MyspaceService.prototype.addAccount =
function MyspaceService_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;
  }

  var pw = this._acUtils.getPassword(this.urn + ":" + aAccountId);
  var name = (pw) ? pw.username : aAccountId;
  var url = this._makeMyspaceUrl("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);
  // Add custom parameters to be used
  account.setCustomParam("myspaceFriendRequests", "0");
  account.setCustomParam("myspaceComments", "0");
  account.setCustomParam("myspaceEventInvitations", "0");

  account.setParam("refreshInterval", REFRESH_INTERVAL);
  if (aFlockListener) {
    aFlockListener.onSuccess(account, "addAccount");
  }
  return account;
};

// DEFAULT: boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument aDocument);
// DEFAULT: flockIWebServiceAccount getAccount(in AString aAccountUrn);
// DEFAULT: AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: nsIStringEnumerator getAccountUrns();
// DEFAULT: flockIWebServiceAccount getAuthenticatedAccount();
// DEFAULT: nsIPassword getCredentialsFromForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: AString getSessionValue();

/**
 * @see flockILoginWebService#logout
 */
MyspaceService.prototype.logout =
function MyspaceService_logout() {
  this._logger.debug(".logout()");
  this._acUtils.removeCookies(FlockSvcUtils.getWD(this)
               .getSessionCookies(CLASS_SHORT_NAME));
  this._acUtils.markAllAccountsAsLoggedOut(this);
  gApi.deauthenticate();
  if (gTimers.length > 0) {
    FlockScheduler.cancel(gTimers, 0);
  }
};

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

/**
 * void updateAccountStatusFromDocument(in nsIDOMHTMLDocument aDocument,
 *                                      flockIWebServiceAccount aAccount,
 *                                      in flockIListener aAuthListener);
 * @see flockILoginWebService#updateAccountStatusFromDocument
 */
MyspaceService.prototype.updateAccountStatusFromDocument =
function MyspaceService_updateAccountStatusFromDocument(aDocument,
                                                        aAccount,
                                                        aAuthListener)
{
  if (aAccount) {
    // We're logged in to this account
    if (!aAccount.isAuthenticated()) {
      aAccount.login(aAuthListener);
    }
  } else if (this._wd.detect(this.shortName, "loggedout", aDocument, null)) {
    // We're logged out (of all accounts)
    this._acUtils.markAllAccountsAsLoggedOut(this);
    gApi.deauthenticate();
  }
};


/*************************************************************************
 * MyspaceService: flockIPollingService Implementation
 *************************************************************************/

/**
 * @see flockIPollingService#refresh
 */
MyspaceService.prototype.refresh =
function MyspaceService_refresh(aUrn, aPollingListener) {
  this._logger.debug(".refresh('" + aUrn + "', ...)");

  var account = this.getAccount(aUrn);

  if (account.isAuthenticated()) {
    var credentials = {
      username: aUrn.split(":")[4]
    };

    var inst = this;
    var authListener = {
      onSuccess: function authListener_onSuccess(aSubject, aTopic) {
        inst._refreshAccount(account, aPollingListener);
      },
      onError: function authListener_onError(aFlockError, aTopic) {
        inst._acUtils.markAllAccountsAsLoggedOut(inst);
        aPollingListener.onError(aFlockError, aTopic);
      }
    };

    gApi.authenticate(credentials, authListener);
  } else {
    this._logger.debug("account is not logged in - skipping refresh");
    aPollingListener.onResult();
  }
};

MyspaceService.prototype._refreshAccount =
function MyspaceService__refreshAccount(aAccount, aPollingListener) {
  this._logger.debug("._refreshAccount('" + aAccount.coopObj.id() + "', ...)");

  var inst = this;

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

  // This listener handles getting the account owner's information.
  var myProfileListener = {
    onSuccess: function myProfileListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug(arguments.callee.name);
      inst._logger.debug("responseText: \n" + aSubject.responseText);
      var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
      var statusJSON = nsJSON.decode(aSubject.responseText);
      aAccount.setParam("name", flockXMLDecode(statusJSON.user.name));
      aAccount.setParam("lastStatusMessageUpdateDate",
                       (statusJSON.moodLastUpdated) ?
                       _fixDate(statusJSON.moodLastUpdated) : 0);
      var avatar = inst._hasDefaultAvatar(statusJSON.user.image)
                 ? null
                 : statusJSON.user.image;
      aAccount.setParam("avatar", avatar);
      aAccount.setParam("URL", statusJSON.user.webUri);

      var blankStatusMsg = inst.getStringBundle()
                               .GetStringFromName(MYSPACE_NO_STATUS_MSG);
      if (statusJSON.status && (statusJSON.status == blankStatusMsg)) {
        // Bug#14454 - Reinitialize the status message as a blank string
        statusJSON.status = "";
      }
      aAccount.setParam("statusMessage",
                        flockXMLDecode(statusJSON.status));
      inst._obs.notifyObservers(aAccount, "flock-acct-refresh", "user-info");
      pplRefreshListener.onSuccess(null, aTopic);
    },
    onError: function myProfileListener_onError(aFlockError, aTopic) {
      inst._logger.error(arguments.callee.name);
      pplRefreshListener.onError(aFlockError, aTopic);
      aPollingListener.onError(aFlockError, aTopic);
    }
  };

  // This handles getting the user's current status.
  var myIndicatorsListener = {
    onSuccess: function myIndicators_onSuccess(aSubject, aTopic) {
      inst._logger.debug(arguments.callee.name);
      inst._logger.debug("responseText: \n" + aSubject.responseText);
      var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
      var indicatorsJSON = nsJSON.decode(aSubject.responseText);

      if (indicatorsJSON.mailurl) {
        if (aAccount.getParam("accountMessages") == "0") {
          aAccount.setParam("accountMessages", "1");
          inst._ppSvc.togglePeopleIcon(true);
        }
      } else {
        aAccount.setParam("accountMessages", "0");
      }

      if (indicatorsJSON.friendsrequesturl) {
        if (aAccount.getCustomParam("myspaceFriendRequests") == "0") {
          aAccount.setCustomParam("myspaceFriendRequests", "1");
          inst._ppSvc.togglePeopleIcon(true);
        }
      } else {
        aAccount.setCustomParam("myspaceFriendRequests", "0");
      }

      if (indicatorsJSON.commenturl) {
        if (aAccount.getCustomParam("myspaceComments") == "0") {
          aAccount.setCustomParam("myspaceComments", "1");
          inst._ppSvc.togglePeopleIcon(true);
        }
      } else {
        aAccount.setCustomParam("myspaceComments", "0");
      }

      if (indicatorsJSON.eventinvitationurl) {
        if (aAccount.getCustomParam("myspaceEventInvitations") == "0") {
          aAccount.setCustomParam("myspaceEventInvitations", "1");
          inst._ppSvc.togglePeopleIcon(true);
        }
      } else {
        aAccount.setCustomParam("myspaceEventInvitations", "0");
      }

      inst._obs.notifyObservers(aAccount, "flock-acct-refresh", "user-info");
      // XXX myspace doesn't provide a timestamp for the status message.
      //account.setParam("lastStatusMessageUpdateDate", 0);
      pplRefreshListener.onSuccess(null, arguments.callee.name);
    },
    onError: function myIndicators_onError(aFlockError, aTopic) {
      inst._logger.error(arguments.callee.name);
      pplRefreshListener.onError(aFlockError, aTopic);
    }
  };

  // This listener handles getting the user's friends' information.
  var myFriendsListener = {
    onSuccess: function myFriendsListener_onSuccess(aSubject, aTopic) {
      function myWorker(aShouldYield) {
        inst._logger.debug(arguments.callee.name);
        var friends = aSubject.wrappedJSObject.friends;

        // ADD or update existing people
        for each (var friend in friends) {
          if (!aAccount.isAuthenticated()) {
            // Account has just been deleted or logged out
            break;
          }
          inst._addPerson(aAccount, friend);
          if (aShouldYield()) {
            yield;
          }
        }

        // REMOVE locally people removed on the server
        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();
          }
        }

        pplRefreshListener.onSuccess(null, aTopic);
      }
      FlockScheduler.schedule(null, 0.05, 10, myWorker);
    },
    onError: function myFriendsListener_onError(aFlockError, aTopic) {
      inst._logger.error(arguments.callee.name);
      pplRefreshListener.onError(aFlockError, aTopic);
    }
  };

  var uid = aAccount.getParam("accountId");
  gApi.getMyProfile(uid, myProfileListener);
  gApi.getMyIndicators(uid, myIndicatorsListener);
  gApi.getMyFriends(uid, myFriendsListener);
};

MyspaceService.prototype._addPerson =
function MyspaceService__addPerson(aAccount, aPerson) {
  this._logger.debug("._addPerson('" + aAccount.urn + "', ...)");

  var result = aPerson;

  var avatarUrl = null;
  if (result.image) {
    // If avatar returned is the default image, set avatar to null and let
    // the people sidebar code set the Flock common image.
    if (!this._hasDefaultAvatar(result.image)) {
      aPerson.avatar = result.image;
    }
  }

  aPerson.accountId = result.userId;
  aPerson.screenName = result.name;
  aPerson.name = flockXMLDecode(result.name);
  aPerson.URL = result.webUri;
  aPerson.statusMessage = flockXMLDecode(result.status);
  aPerson.lastUpdateType = "status";

  this._logger.debug("Adding person: '" + aAccount.urn + "' '"
                     + aPerson.accountId + "' '"
                     + aPerson.statusMessage + "'");
  this._comparePerson(aAccount, aPerson);
};


/**
 * Did the person's data change from what we had stored?  If so, set the
 * appropriate lastUpdateType.
 */
MyspaceService.prototype._comparePerson =
function MyspaceService__comparePerson(aAccount, aPerson) {
  this._logger.debug("._comparePerson('" + aAccount.urn + "', '"
                                         + aPerson.name + "')");
  var person = aPerson;
  var identityUrn = "urn:flock:identity:myspace:"
                 + aAccount.getParam("accountId") + ":"
                 + aPerson.accountId;
  var cachedPerson = this._coop.get(identityUrn);
  var lastUpdate = _fixDate(person.moodLastUpdated);

  if (cachedPerson) {
    var now = new Date().getTime();
    now = Math.round(now / 1000);
    // We consider the profile changed if either the name or avatar of the
    // user has changed. Status update is handled in Coop.addPerson().
    // If both the status and profile have changed then report the profile
    // change.
    if (person.name != cachedPerson.name) {
      person.lastUpdateType = "profile";
      lastUpdate = now;
    } else if (person.avatar != cachedPerson.avatar) {
      person.lastUpdateType = "profile";
      lastUpdate = now;
    }

    if (cachedPerson.lastUpdate >= lastUpdate) {
      return;
    }
  } else {
    person.lastUpdateType = "status";
  }

  aAccount.coopObj.addPerson(person, lastUpdate);
};


/*************************************************************************
 * MyspaceService: flockIMediaWebService Implementation
 *************************************************************************/

 // readonly attribute boolean flockIMediaWebService::supportsUsers
MyspaceService.prototype.supportsUsers = true;

/**
 * flockIMediaItemFormatter getMediaItemFormatter();
 * @see flockIMediaWebService#getMediaItemFormatter
 */
MyspaceService.prototype.getMediaItemFormatter =
function MyspaceService_getMediaItemFormatter() {
  return flockMediaItemFormatter;
};

/**
 * void decorateForMedia(in nsIDOMHTMLDocument aDocument,
 *                       in flockIWebServiceAccount aAccount);
 * @see flockIMediaWebService#decorateForMedia
 */
MyspaceService.prototype.decorateForMedia =
function MyspaceService_decorateForMedia(aDocument, aAccount) {
  this._logger.debug(".decorateForMedia('" + aDocument.URL + "')");

  // Media detection only offered when we have a logged in user
  if (!aAccount) {
    return;
  }

  var userId;
  var userName = null;
  var results = FlockSvcUtils.newResults();
  var albumId = null;
  var albumName = null;

  if (!this._wd.detect(this.shortName, "image", aDocument, results)) {
    if (!this._wd.detect(this.shortName, "person", aDocument, results) ||
         this._wd.detect(this.shortName, "privatealbum", aDocument, null)) {
      // Private photos, do not offer media detection
      return;
    }
    try {
      userName = flockXMLDecode(results.getPropertyAsAString("username"));
    } catch (ex) {
      this._logger.error("Could not find userName");
    }

    try {
      albumId = results.getPropertyAsAString("albumid");
      albumName = results.getPropertyAsAString("albumname");
    } catch (ex) {
      this._logger.error("Could not find album");
    }
  }
  var mediaArr = [];

  userId = results.getPropertyAsAString("userid");
  var accountId = aAccount.getParam("accountId");

  if (userId == accountId) {
    if (!userName) {
      userName = aAccount.getParam("name");
    }
  } else {
    var friendUrn = "urn:flock:identity:myspace:" + accountId + ":" + userId;
    var cachedPerson = this._coop.get(friendUrn);
    // Only offer media detection for friends
    if (!cachedPerson) {
      return;
    }
    if (!userName) {
      userName = flockXMLDecode(cachedPerson.name);
    }
  }

  var media = {
    name: userName,
    query: "user:" + userId + "|username:" + userName,
    label: userName,
    favicon: this.icon,
    service: this.shortName
  };
  mediaArr.push(media);

  // Second media item for the album-specific stream
  if (albumId && parseInt(albumId)) {
    var secondmedia = {
      name: albumId,
      query: "user:" + userId
                     + "|username:" + userName
                     + "|album:" + albumId
                     + "|albumlabel:" + albumName,
      label: albumName,
      favicon: this.icon,
      service: this.shortName
    };
    mediaArr.push(secondmedia);
  }

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

// DEFAULT: nsISimpleEnumerator enumerateChannels();

/**
 * void findByUsername(in flockIListener aFlockListener, in AString aUsername);
 * @see flockIMediaWebService#findByUsername
 */
MyspaceService.prototype.findByUsername =
function MyspaceService_findByUsername(aFlockListener, aUsername) {
  this._logger.debug(".findByUsername(..., '" + aUsername + "')");
  aFlockListener.onError(null, aUsername);
};

/**
 * void getAlbums(in flockIListener aFlockListener, in AString aUsername);
 * @see flockIMediaWebService#getAlbums
 */
MyspaceService.prototype.getAlbums =
function MyspaceService_getAlbums(aFlockListener, aUsername) {
  this._logger.debug(".getAlbums(..., '" + aUsername + "')");

  var inst = this;

  var albumJSON = {};
  var albumsEnum = {
    hasMoreElements: function albumsEnum_hasMoreElements() {
      return (albumJSON.albums && albumJSON.albums.length > 0);
    },
    getNext: function albumsEnum_getNext() {
      var nextAlbum = albumJSON.albums.shift();
      var newAlbum = CC[FLOCK_PHOTO_ALBUM_CONTRACTID]
                     .createInstance(CI.flockIPhotoAlbum);
      newAlbum.id = nextAlbum.id;
      newAlbum.title = flockXMLDecode(nextAlbum.title);
      return newAlbum;
    },
    QueryInterface: function albumsEnum_QI(aIID) {
      if (aIID.equals(CI.nsISimpleEnumerator)) {
        return this;
      }
      throw CR.NS_ERROR_NO_INTERFACE;
    }
  };

  var myAlbumListener = {
    onSuccess: function myProfile_onSuccess(aSubject, aTopic) {
      inst._logger.debug("myAlbumListener.onSuccess(..., '" + aTopic + "')");
      var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
      albumJSON = nsJSON.decode(aSubject.responseText);

      aFlockListener.onSuccess(albumsEnum, "getAlbums");
    },
    onError: function myProfile_onError(aFlockError, aTopic) {
      inst._logger.error("myAlbumListener.onError(..., '" + aTopic + "', ...)");
      // Bug in Myspace api causes a 500 HTTP error when trying to get albums
      // from a user with no albums.
      // c.f. https://bugzilla.flock.com/show_bug.cgi?id=15032
      if (aFlockError.serviceErrorCode == 500) {
        aFlockListener.onSuccess(albumsEnum, "getAlbums");
      } else {
        aFlockListener.onError(aFlockError, "getAlbums");
      }
    }
  };
  gApi.getUserAlbums(aUsername, myAlbumListener);
};


// DEFAULT: flockIMediaChannel getChannel(in AString aChannelId);

/**
 * void migrateAccount(in AString aId, in AString aUsername);
 * @see flockIMediaWebService#migrateAccount
 */
MyspaceService.prototype.migrateAccount =
function MyspaceService_migrateAccount(aId, aUsername) {
  this._logger.debug(".migrateAccount('" + aId + "', '" + aUsername + "')");
};

/**
 * void search(in flockIListener aFlockListener, in AString aQuery,
 *             in long aCount, in AString aRequestId);
 * @see flockIMediaWebService#search
 */
MyspaceService.prototype.search =
function MyspaceService_search(aFlockListener, aQuery, aCount, aPage, aRequestId) {
  this._logger.debug(".search(..., '" + aQuery + "', " + aCount + ", "
                                      + aPage + ")");

  // Because Myspace photo data is returned from oldest to newest, our media
  // notification code doesn't work. The obvious thing to do would be to simply
  // reverse the stream. However, we also still want to be able to have data
  // pagination. So what we do is:
  //  (1) Get the entire stream data from the api and store into cache.
  //  (2) Shift all photos of "My Photos" album to the end of the cached stream.
  //  (3) Reverse the cached stream and perform pseudo-pagination on it.
  // The reason we need to do (2) is so that after the stream is reversed,
  // the photos belonging to "My Photos" album will appear at the head of the
  // stream. This makes media notification work when uploading to the default
  // album.
  // Note: the one case where media notification won't work is if you have a
  // friend's *user* stream starred, and the friend uploads a photo to a
  // non-default album.
  //
  // TODO: we can make this process much simpler once Myspace API supports data
  // sorting.

  var page = aPage ? aPage : 1;
  var inst = this;
  function _loadFromCache(aCount, aPage) {
    if (inst._cachedPhotos) {
      var photos = [];
      var start = inst._cachedPhotos.length - (aCount * (aPage - 1)) - 1;
      if (start >= 0) {
        // Iterate backwards through the photos since Myspace returns photo
        // data from oldest to newest.
        var diff = start - aCount + 1;
        var end = (diff > 0) ? diff : 0;
        for (var i = start; i >= end; i--) {
          var mediaItem = _handlePhotoResult(inst._cachedPhotos[i]);
          photos.push(mediaItem);
        }
      }
      aFlockListener.onSuccess(inst._createEnum(photos), aRequestId);
    }
  }

  // After the first page, load mediabar photos from cache
  if (page > 1) {
    _loadFromCache(aCount, page);
    return;
  }

  var query = new QueryHelper(aQuery);
  // If the search string only contains whitespace, then return empty result
  if (decodeURIComponent(query.search).replace("/\s/g", "") != "") {
    if (query.user) {
      var photoListener = {
        onSuccess: function photoSearch_onSuccess(aSubject, aTopic) {
          var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
          // Save all the photo data into cache
          inst._cachedPhotos = nsJSON.decode(aSubject.responseText).photos;

          var albumListener = {
            onSuccess: function albumListener_onSuccess(aSubject, aTopic) {
              var albumJSON = nsJSON.decode(aSubject.responseText);
              if (albumJSON.albums.length > 1) {
                // Find the "My Photos" album
                var nextAlbum = albumJSON.albums.shift();
                while (nextAlbum && nextAlbum.title != DEFAULT_ALBUM_NAME) {
                  nextAlbum = albumJSON.albums.shift();
                }

                // Shift "My Photos" photos to the end, because after the stream
                // is reversed, we want them to appear first.
                if (nextAlbum &&
                    nextAlbum.title == DEFAULT_ALBUM_NAME &&
                    nextAlbum.photoCount > 0)
                {
                  // These are the photos in "My Photos"
                  var leftChunk = inst._cachedPhotos
                                      .slice(0, nextAlbum.photoCount);
                  // These are the rest of the photos
                  var rightChunk = inst._cachedPhotos
                                       .slice(nextAlbum.photoCount,
                                              inst._cachedPhotos.length);
                  inst._cachedPhotos = rightChunk.concat(leftChunk);
                }
              }
              _loadFromCache(aCount, 1);
            },
            onError: function albumListener_onError(aFlockError, aTopic) {
              aFlockListener.onError(aFlockError, aRequestId);
            }
          };

          if (query.album || !inst._cachedPhotos.length) {
            _loadFromCache(aCount, 1);
          } else {
            // We need to find the size of the "My Photos" album
            gApi.getUserAlbums(query.user, albumListener);
          }
        },
        onError: function photoSearch_onError(aFlockError, aTopic) {
          // Due to a Myspace API bug, if you try to get photos with param
          // page_size="all" and the user has no photos, you will get an
          // HTTP 500 error. In this case, we handle gracefully by returning
          // an empty set of photos to the mediabar.
          if (aFlockError.errorCode == CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR) {
            aFlockListener.onSuccess(inst._createEnum([]), aRequestId);
          } else {
            aFlockListener.onError(aFlockError, aRequestId);
          }
        }
      };

      var params = {
        wrappedJSObject: {
          userId: query.user,
          page_size: "all"
        }
      };
      var album = "";
      if (query.album) {
        album = "albums/" + query.album + "/";
      }
      gApi.call(album + "photos",
                params,
                null,
                CI.flockIWebServiceAPI.GET,
                photoListener);
    }
  } else {
    aFlockListener.onSuccess(this._createEnum([]), aRequestId);
  }
};

/**
 * boolean supportsSearch(in AString aQuery);
 * @see flockIMediaWebService#supportsSearch
 */
MyspaceService.prototype.supportsSearch =
function MyspaceService_supportsSearch(aQuery) {
  return false;
};


/*************************************************************************
 * MyspaceService: flockIMediaUploadWebService Implementation
 *************************************************************************/

/**
 * void getAccountStatus(in flockIListener aFlockListener);
 *
 * This currently returns hardcoded values so the uploader works.  This
 * should be replaced by actual calls to the MySpace API, or with values
 * screen-scraped by Web Detective.
 *
 * @see flockIMediaWebService#getAccountStatus
 * @todo Use the MySpace API or Web Detective to correctly obtain this
 *       information.
 * @todo Localize the hardcoded strings.
 */
MyspaceService.prototype.getAccountStatus =
function MyspaceService_getAccountStatus(aFlockListener) {
  this._logger.debug(".getAccountStatus(aFlockListener)");

  var result = CC["@mozilla.org/hash-property-bag;1"]
               .createInstance(CI.nsIWritablePropertyBag2);

  result.setPropertyAsAString("maxSpace", MYSPACE_MAX_PHOTO_SPACE);
  result.setPropertyAsAString("usedSpace", MYSPACE_USED_PHOTO_SPACE);
  result.setPropertyAsAString("maxFileSize", MYSPACE_MAX_FILE_SIZE);
  // This is a key, not something displayed to the user. Don't l10n this!
  result.setPropertyAsAString("usageUnits", "bytes");
  result.setPropertyAsBool("isPremium", true); // MySpace has no "pro" level.

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

// DEFAULT: void getAlbumsForUpload(in flockIListener aFlockListener,
//                                  in AString aUsername);

/**
 * void createAlbum(in flockIListener aFlockListener, in AString aAccountId ..);
 * @see flockIMediaUploadWebService#createAlbum
 */
MyspaceService.prototype.createAlbum =
function MyspaceService_createAlbum(aFlockListener, aAccountId, aAlbumName) {
  this._logger.debug(".createAlbum(aFlockListener, '" + aAlbumName + "')");

  // Trim whitespace from front and end of string
  var trimmedTitle = aAlbumName.replace("/^\s+|\s+$/g", "");
  if (trimmedTitle) {
    gApi.createAlbum(aAccountId, trimmedTitle, aFlockListener);
  } else {
    var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
    error.errorCode = CI.flockIError.PHOTOSERVICE_EMPTY_ALBUMNAME;
    aFlockListener.onError(error, null);
  }
};

/**
 * boolean supportsFeature(in AString aFeature);
 * XXX: Verify these are correct for MySpace
 * @see flockIMediaUploadWebService#supportsFeature
 */
MyspaceService.prototype.supportsFeature =
function MyspaceService_supportsFeature(aFeature) {
  this._logger.debug(".supportsFeature('" + aFeature + "')");

  var supports = {
    albumCreation: true,
    contacts: false,
    description: false,
    fileName: false,
    privacy: false,
    tags: false,
    notes: false,
    title: false
  };
  return supports[aFeature] ? supports[aFeature] : false;
};

/**
 * void upload(in flockIPhotoUploadAPIListener aUploadListener,
 *             in flockIPhotoUpload aUpload,
 *             in AString aFilename);
 * @see flockIMediaUploadWebService#upload
 */
MyspaceService.prototype.upload =
function MyspaceService_upload(aUploadListener, aUpload, aFilename) {
  this._logger.debug(".upload(aUploadListener, aUpload, '" + aFilename + "')");

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

  var coopAcct = this._coop.Account.get(this.getAuthenticatedAccount());
  params.setPropertyAsAString("userId", coopAcct.accountId);
  params.setPropertyAsAString("description", description);
  gApi.uploadPhotosViaUploader(aUploadListener, aFilename, params, aUpload);
};

/*************************************************************************
 * MyspaceService: flockIAuthenticateNewAccount Implementation
 *************************************************************************/

// DEFAULT: void authenticateNewAccount();

/*************************************************************************
 * MyspaceService: flockISocialWebService Implementation
 *************************************************************************/

// readonly attribute long maxStatusLength;
MyspaceService.prototype.maxStatusLength = 160;

// void markAllMediaSeen(in AString aPersonUrn);
MyspaceService.prototype.markAllMediaSeen =
function MyspaceService_markAllMediaSeen(aPersonUrn) {
  this._logger.debug(".markAllMediaSeen('" + aPersonUrn + "')");
};


/*************************************************************************
 * MyspaceService: flockIMediaEmbedWebService Implementation
 *************************************************************************/

MyspaceService.prototype.checkIsStreamUrl =
function MyspaceService_checkIsStreamUrl(aUrl) {
  if (this._wd.detectNoDOM(CLASS_SHORT_NAME, "isStreamUrl", "", aUrl, null)) {
    this._logger.debug("Checking if url is myspace stream: YES: " + aUrl);
    return true;
  }
  this._logger.debug("Checking if url is myspace stream: NO: " + aUrl);
  return false;
};

MyspaceService.prototype.getMediaQueryFromURL =
function MyspaceService_getMediaQueryFromURL(aUrl, aFlockListener) {
  this._logger.debug(".getMediaQueryFromURL('" + aUrl + "')");
  var detectResults = CC["@mozilla.org/hash-property-bag;1"]
                      .createInstance(CI.nsIWritablePropertyBag2);
  if (this._wd.detectNoDOM("myspace",
                           "image",
                           "",
                           aUrl,
                           detectResults))
  {
    var userId = detectResults.getPropertyAsAString("userid");
    var imageId = detectResults.getPropertyAsAString("imageid");

    var inst = this;
    var getUserListener = {
      onSuccess: function getUserListener_onSuccess(aSubject, aTopic) {
        inst._logger.debug(arguments.callee.name);
        inst._logger.debug("responseText: \n" + aSubject.responseText);
        var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
        var photoJSON = nsJSON.decode(aSubject.responseText);
        var userName = photoJSON.user.name;

        var results = CC["@mozilla.org/hash-property-bag;1"]
                      .createInstance(CI.nsIWritablePropertyBag2);
        var qry = "user:" + userId + "|username:" + userName;
        results.setPropertyAsAString("query", qry);
        results.setPropertyAsAString("title", userName);
        aFlockListener.onSuccess(results, "query");
      },
      onError: function getUserListener_onError(aFlockError, aTopic) {
        inst._logger.error("getUserListener.onError(..., '" + aTopic + "', ...)");
      }
    };

    // We need to make an API call to get the username
    var params = {
      wrappedJSObject: { userId: userId }
    };
    gApi.call("photos/" + imageId,
              params,
              null,
              CI.flockIWebServiceAPI.GET,
              getUserListener);
  }
};

MyspaceService.prototype.getSharingContent =
function MyspaceService_checkIsStreamUrl(aSrc, aProp) {
  this._logger.debug(".getSharingContent()");
  if (aSrc && aSrc instanceof CI.nsIDOMHTMLImageElement) {
    aProp.setPropertyAsAString("url", aSrc.src);
    aProp.setPropertyAsAString("title", "");
    return true;
  }
};

/**************************************************************************
 * MyspaceService: flockIRichContentDropHandler Implementation
 **************************************************************************/

MyspaceService.prototype.handleDrop =
function MyspaceService_handleDrop(aFlavours, aTargetElement) {
  this._logger.debug(".handleDrop()");

  var inst = this;
  // Handle textarea drops
  if (aTargetElement instanceof CI.nsIDOMHTMLTextAreaElement) {
    var dropCallback = function handleDrop_dropCallback(aFlav) {
      var dataObj = {};
      var len = {};
      aFlavours.getTransferData(aFlav, dataObj, len);

      // If the drop text contains blocked domains then don't drop anything
      // and end the DnD flow.
      // (c.f. https://bugzilla.flock.com/show_bug.cgi?id=14601)
      var dropText = dataObj.value.QueryInterface(CI.nsISupportsString).data;
      if (_hasBlockedText(dropText)) {
        return true;
      }

      var caretPos = aTargetElement.selectionEnd;
      var currentValue = aTargetElement.value;

      aTargetElement.focus();

      // 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)
                           + dropText.replace(/: /, ":\n")
                           + currentValue.substring(caretPos)
                           + breadcrumb;
    };

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

  return false;
};


/*************************************************************************
 * Component: MyspaceAccount
 *************************************************************************/
function MyspaceAccount() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("MyspaceAccount");

  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);

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

  // Convenience variable.
  this._wd = this._WebDetective;

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

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

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


/*************************************************************************
 * MyspaceAccount: flockIWebServiceAccount Implementation
 *************************************************************************/

// readonly attribute AString urn;
MyspaceAccount.prototype.urn = "";

// DEFAULT: nsIPropertyBag getAllCustomParams();
// DEFAULT: nsIVariant getCustomParam(in AString aParamName);
// DEFAULT: nsIVariant getParam(in AString aParamName);
// DEFAULT: flockILoginWebService getService();
// DEFAULT: boolean isAuthenticated();

/**
 * void keep();
 * @see flockILoginWebService#keep
 */
MyspaceAccount.prototype.keep =
function MyspaceAccount_keep() {
  this._logger.debug(".keep()");
  this.coopObj.isTransient = false;
  this._acUtils.makeTempPasswordPermanent(this.urn.replace("account:", ""));
};

/**
 * void login(in flockIListener aFlockListener);
 * @see flockIWebServiceAccount#login
 */
MyspaceAccount.prototype.login =
function MyspaceAccount_login(aFlockListener) {
  this._logger.debug(".login()");
  this._acUtils.ensureOnlyAuthenticatedAccount(this);
  // Force refresh on login is done by the people sidebar
  if (aFlockListener) {
    aFlockListener.onSuccess(this, "login");
  }
};

// DEFAULT: void logout(in flockIListener aFlockListener);
// DEFAULT: void setCustomParam(in AString aParamName, in nsIVariant aValue);
// DEFAULT: void setParam(in AString aParamName, in nsIVariant aValue);


/*************************************************************************
 * MyspaceAccount: flockISocialAccount Implementation
 *************************************************************************/

// readonly attribute boolean hasFriendActions;
MyspaceAccount.prototype.hasFriendActions = true;

// readonly attribute boolean isMeStatusSupported;
MyspaceAccount.prototype.isMeStatusSupported = true;

// readonly attribute boolean isFriendStatusSupported;
MyspaceAccount.prototype.isFriendStatusSupported = true;

// readonly attribute boolean isStatusEditable;
MyspaceAccount.prototype.isStatusEditable = true;

// readonly attribute boolean isPostLinkSupported;
MyspaceAccount.prototype.isPostLinkSupported = true;

// readonly attribute boolean isMyMediaFavoritesSupported;
MyspaceAccount.prototype.isMyMediaFavoritesSupported = false;

// DEFAULT: nsISimpleEnumerator enumerateFriends();

/**
 * AString formatStatusForDisplay(in AString aStatusMessage);
 * @see flockISocialAccount#formatStatusForDisplay
 */
MyspaceAccount.prototype.formatStatusForDisplay =
function MyspaceAccount_formatStatusForDisplay(aStatusMessage) {
  return aStatusMessage;
};

/**
 * void setStatus(in AString aStatusMessage, in flockIListener aFlockListener);
 * @see flockISocialAccount#setStatus
 */

MyspaceAccount.prototype.setStatus =
function MyspaceAccount_setStatus(aStatusMessage, aFlockListener) {
  this._logger.debug(".setStatus('" + aStatusMessage + "', ...)");
  // If attetmpting to clear the status message (ie blank string) MySpace's
  // API won't allow this post and returns with errors.  If clearing the status
  // from their site, a status of "(none)" is set instead of it being blank
  var blankStatusMsg = this.getService()
                           .getStringBundle()
                           .GetStringFromName(MYSPACE_NO_STATUS_MSG);
  if (aStatusMessage.length == 0) {
    // Set the status message to "(none)" for the api request
    aStatusMessage = blankStatusMsg
  }
  var inst = this;
  var setStatusListener = {
    onSuccess: function MSA_setStatusListener_onSuccess(aSubject, aTopic) {
      inst._logger.debug("setStatus_onSuccess()");
      // No response from the API is returned after a successful PUT
      if (aStatusMessage == blankStatusMsg) {
        // Bug#14454 - We must set a blank status message in the coop for the
        // mecard binding to behave as desired (When no status message set it
        // should say "Update your status...", and not "(none)")
        aStatusMessage = "";
      }
      inst.setParam("statusMessage", aStatusMessage);
      if (aFlockListener) {
        aFlockListener.onSuccess(inst, "setStatus");
      }
    },
    onError: function MSA_setStatusListener_onError(aFlockError, aTopic) {
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, "setStatus");
      }
    }
  };
  gApi.setMyStatus(this.getParam("accountId"),
                   aStatusMessage,
                   setStatusListener);
};


/**
 * AString getEditableStatus();
 * @see flockISocialAccount#getEditableStatus
 */
MyspaceAccount.prototype.getEditableStatus =
function MyspaceAccount_getEditableStatus() {
  this._logger.debug(".getEditableStatus()");

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

/**
 * AString getFriendActions(in AString aFriendUrn);
 * @see flockISocialAccount#getFriendActions
 */
MyspaceAccount.prototype.getFriendActions =
function MyspaceAccount_getFriendActions(aFriendUrn) {
  var actionNames = ["friendMessage",
                     "friendAddToFaves",
                     "friendForward",
                     "friendComment",
                     "friendViewProfile",
                     "friendShareFlock"];

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

  var actions = [];
  var friend = this._coop.get(aFriendUrn);
  if (friend) {
    for each (var actionName in actionNames) {
      actions.push({
        label: bundle.GetStringFromName("flock." + CLASS_SHORT_NAME
                                        + ".actions." + actionName),
        class: actionName,
        spec: this._wd.getString(CLASS_SHORT_NAME, actionName, "")
                  .replace("%friendid%", friend.accountId)
      });
    }
  }
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(actions);
};

MyspaceAccount.prototype._forwardFriend =
function MyspaceAccount__forwardFriend(aFriendUrn) {
  // Open it in a new tab
  var win = CC["@mozilla.org/appshell/window-mediator;1"]
            .getService(CI.nsIWindowMediator)
            .getMostRecentWindow("navigator:browser");
  if (win) {
    var friend = this._coop.get(aFriendUrn);
    var url = this._wd.getString(CLASS_SHORT_NAME, "friendForwardUrl", "")
                      .replace("%friendid%", friend.accountId);
    var browser = win.getBrowser();
    var newTab = browser.loadOneTab(url, null, null, null, false, false);
    var contentWindow = newTab.linkedBrowser.docShell
                              .QueryInterface(CI.nsIInterfaceRequestor)
                              .getInterface(CI.nsIDOMWindow);

    // Wait for the tab to finish loading before executing our callback to
    // append the breadcrumb to the message body.
    var inst = this;
    var obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);
    var observer = {
      observe: function ma__ff_observer(aContent, aTopic, aOwnsWeak) {
        if (aContent == contentWindow) {
          obs.removeObserver(this, aTopic);

          // Pass in an empty message to append the breadcrumb.
          var message = {
            subject: "",
            body: "" 
          };
          inst._fillMessageFields(contentWindow, message, true);
        }
      }
    };

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

MyspaceAccount.prototype._buildSendMessageURL =
function MyspaceAccount__buildSendMessageURL(aFriendId, aSubject, aBody) {
  return this._wd.getString("myspace", "sendMessage", "")
             .replace("%friendid%", aFriendId)
             .replace("%subject%", encodeURIComponent(aSubject))
             .replace("%message%", encodeURIComponent(aBody));
};

// DEFAULT: long getFriendCount();

/**
 * AString getMeNotifications();
 * @see flockISocialAccount#getMeNotifications
 */
MyspaceAccount.prototype.getMeNotifications =
function MyspaceAccount_getMeNotifications() {
  this._logger.debug(".getMeNotifications()");

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

  var noties = [];
  var inst = this;
  function _addNotie(aType, aCount, aUrl) {
    var stringName = "flock." + CLASS_SHORT_NAME + ".noties."
                   + aType + "."
                   + ((parseInt(aCount) <= 0) ? "none" : "some");
    noties.push({
      class: aType,
      tooltip: bundle.GetStringFromName(stringName),
      metricsName: aType,
      count: aCount,
      URL: aUrl
    });
  }
  var url = this._wd.getString(CLASS_SHORT_NAME, "meMessages_URL", "");
  _addNotie("meMessages", this.getParam("accountMessages"), url);

  url = this._wd.getString(CLASS_SHORT_NAME, "meComments_URL", "")
                .replace("%id%", this.getParam("accountId"));
  _addNotie("meComments",
            this.getCustomParam("myspaceComments"),
            url);

  url = this._wd.getString(CLASS_SHORT_NAME, "meFriendRequests_URL", "");
  _addNotie("meFriendRequests",
            this.getCustomParam("myspaceFriendRequests"),
            url);

  url = this._wd.getString(CLASS_SHORT_NAME, "meEventInvitations_URL", "");
  _addNotie("meEventInvitations",
            this.getCustomParam("myspaceEventInvitations"),
            url);

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

/**
 * void postLink(in nsITransferable aTransferable);
 * @see flockISocialAccount#postLink
 */
MyspaceAccount.prototype.postLink =
function MyspaceAccount_postLink(aTransferable) {
  this._logger.debug(arguments.callee.name);

  var link;
  if (aTransferable) {
    var flavors = ["text/html",
                   "text/x-moz-url",
                   "text/unicode"];
    // Build the message from the transferable
    var message = CC["@flock.com/rich-dnd-service;1"]
                  .getService(CI.flockIRichDNDService)
                  .getMessageFromTransferable(aTransferable,
                                              flavors.length,
                                              flavors);
    link = message.body;
  } else {
    // Share the current page
    var win = CC["@mozilla.org/appshell/window-mediator;1"]
              .getService(CI.nsIWindowMediator)
              .getMostRecentWindow("navigator:browser");
    if (win) {
      var url = win.gBrowser.currentURI.spec;
      var title = win.gBrowser.contentTitle
                ? win.gBrowser.contentTitle
                : url;
      link = '<a href="' + url + '">' + title + '</a>';
    }
  }

  // Did we end up with a valid link to share?
  this._logger.debug(".postLink() link is: '" + link + "'");
  if (link && !_hasBlockedText(link)) {
    this._doPostLink(link);
  }
};

/**
 * AString getProfileURLForFriend(in AString aFriendUrn);
 * @see flockISocialAccount#getProfileURLForFriend
 */
MyspaceAccount.prototype.getProfileURLForFriend =
function MyspaceAccount_getProfileURLForFriend(aFriendUrn) {
  this._logger.debug(".getProfileURLForFriend('" + aFriendUrn + "')");

  var friend = this._coop.get(aFriendUrn);
  if (friend) {
     return this._wd.getString(CLASS_SHORT_NAME, "profileURL", "")
                    .replace("%friendid%", friend.accountId);
  }
};

/**
 * void shareWithFriend(in AString aFriendUrn,
 *                      in nsITransferable aTransferable);
 * @see flockISocialAccount#shareWithFriend
 */
MyspaceAccount.prototype.shareWithFriend =
function MyspaceAccount_shareWithFriend(aFriendUrn, aTransferable) {
  // Get friend ID
  var friendId = null;
  var friend = this._coop.get(aFriendUrn);
  if (friend) {
    friendId = friend.accountId;
  }

  // Get the message to share
  var message = {};
  if (aTransferable) {
    // Flavours we want to support, in order of preference
    var flavours = ["text/x-flock-media",
                    "text/html",
                    "text/x-moz-url",
                    "text/unicode"];
    // Build the message from the transferable
    message = CC["@flock.com/rich-dnd-service;1"]
              .getService(CI.flockIRichDNDService)
              .getMessageFromTransferable(aTransferable,
                                          flavours.length,
                                          flavours);
  } else {
    // Share the current page
    var win = CC["@mozilla.org/appshell/window-mediator;1"]
              .getService(CI.nsIWindowMediator)
              .getMostRecentWindow("navigator:browser");
    if (win) {
      message.body = win.gBrowser.currentURI.spec;
      message.subject = win.gBrowser.contentTitle;
    }
  }

  // If the drop text contains blocked domains then don't drop anything
  // and end the DnD flow.
  // (c.f. https://bugzilla.flock.com/show_bug.cgi?id=14601)
  if (_hasBlockedText(message.body)) {
    return;
  }

  // Did we end up with a valid friend and message to share?
  if (friendId && message.body) {
    this._shareMessageWithFriend(friendId, message, true);
  }
};

/**
 * void markAllMeNotificationsSeen(in AString aType);
 * @see flockISocialAccount#markAllMediaNotificationsSeen
 */
MyspaceAccount.prototype.markAllMeNotificationsSeen =
function MyspaceAccount_markAllMeNotificationsSeen(aType) {
  this._logger.debug(".markAllMeNotificationsSeen('" + aType + "')");

  switch (aType) {
    case "meMessages":
      this.setParam("accountMessages", "0");
      break;
    case "meFriendRequests":
      this.setCustomParam("myspaceFriendRequests", "0");
      break;
    case "meComments":
      this.setCustomParam("myspaceComments", "0");
      break;
    case "meEventInvitations":
      this.setCustomParam("myspaceEventInvitations", "0");
      break;
    default:
      break;
  }
};

/**
 * void shareFlock(in AString aFriendURN);
 * @see flockISocialAccount#shareFlock
 */
MyspaceAccount.prototype.shareFlock =
function MyspaceAccount_shareFlock(aFriendURN) {
  // Get friend
  var friend = this._coop.get(aFriendURN);
  if (friend) {
    // Get "Share Flock" message text
    var sbs = CC["@mozilla.org/intl/stringbundle;1"]
              .getService(CI.nsIStringBundleService);
    var bundle = sbs.createBundle(SERVICES_PROPERTIES_FILE);
    var subject = bundle.GetStringFromName(SERVICES_SHARE_FLOCK_SUBJECT);
    var body = "";
    try {
      body = bundle.GetStringFromName(SERVICES_SHARE_FLOCK_MESSAGE + "0");
      for (var i = 1; ; i++) {
        body += "\n";
        body += bundle.GetStringFromName(SERVICES_SHARE_FLOCK_MESSAGE + i);
      }
    } catch (ex) {
      // Ignore -- we've hit the end of our message lines
    }
    // Build the URL and send it to the friend
    var message = {
      subject: subject,
      body: body.replace("%SERVICE", CLASS_TITLE)
    };
    this._shareMessageWithFriend(friend.accountId, message, false);
  }
};

// void friendAction(in AString aAction, in AString aFriendURN, in AString..
MyspaceAccount.prototype.friendAction =
function MyspaceAccount_friendAction(aAction, aFriendURN, aActivityURN) {
  switch (aAction) {
    case "forwardFriend":
      this._forwardFriend(aFriendURN);
      break;
    default:
      this._logger.debug("Invalid friendAction: '" + aAction + "'");
      break;
  }
};

// Open an add comment to self window and then stick in the link
MyspaceAccount.prototype._doPostLink =
function MyspaceAccount__doPostLink(aLink) {
  this._logger.debug(arguments.callee.name);

  // Get message URL
  var url = this._wd.getString(CLASS_SHORT_NAME, "postLink","")
                    .replace("%friendid%", this.getParam("accountId"));

  // Open it in a new tab
  var win = CC["@mozilla.org/appshell/window-mediator;1"]
            .getService(CI.nsIWindowMediator)
            .getMostRecentWindow("navigator:browser");
  if (win) {
    var browser = win.getBrowser();
    var newTab = browser.loadOneTab(url, null, null, null, false, false);
    var contentWindow = newTab.linkedBrowser.docShell
                              .QueryInterface(CI.nsIInterfaceRequestor)
                              .getInterface(CI.nsIDOMWindow);

    // Wait for the tab to finish loading before executing our callback to fill
    // in the message information
    var inst = this;
    var obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);
    var observer = {
      observe: function ma__smwf_observer(aContent, aTopic, aOwnsWeak) {
        if (aContent == contentWindow) {
          obs.removeObserver(this, aTopic);
          inst._fillPostLinkBody(contentWindow, aLink);
        }
      }
    };

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

MyspaceAccount.prototype._fillPostLinkBody =
function MyspaceAccount__fillPostLinkBody(aWindow, aLink) {
  if (aLink) {
    var bodyId = this._wd.getString(CLASS_SHORT_NAME, "postLinkBodyId", "");
    if (bodyId) {
      var body = aWindow.document.getElementById(bodyId);
      if (body) {
        body.value = aLink;

        var breadcrumb = CC["@flock.com/rich-dnd-service;1"]
                         .getService(CI.flockIRichDNDService)
                         .getBreadcrumb("plain");
        if (breadcrumb) {
          body.value += breadcrumb;
        }
      }
    }
  }
};

// Open a message window to a friend and then populate it with the info in
// aMessage.
MyspaceAccount.prototype._shareMessageWithFriend =
function MyspaceAccount__shareMessageWithFriend(aFriendId,
                                                aMessage,
                                                aAppendBreadcrumb)
{
  // Get message URL
  var url = this._wd.getString(CLASS_SHORT_NAME, "shareMessageWithFriend", "");
  url = url.replace("%friendid%", aFriendId);

  // Open it in a new tab
  var win = CC["@mozilla.org/appshell/window-mediator;1"]
            .getService(CI.nsIWindowMediator)
            .getMostRecentWindow("navigator:browser");
  if (win) {
    var browser = win.getBrowser();
    var newTab = browser.loadOneTab(url, null, null, null, false, false);
    var contentWindow = newTab.linkedBrowser.docShell
                              .QueryInterface(CI.nsIInterfaceRequestor)
                              .getInterface(CI.nsIDOMWindow);

    // Wait for the tab to finish loading before executing our callback to fill
    // in the message information
    var inst = this;
    var obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);
    var observer = {
      observe: function ma__smwf_observer(aContent, aTopic, aOwnsWeak) {
        if (aContent == contentWindow) {
          obs.removeObserver(this, aTopic);
          inst._fillMessageFields(contentWindow, aMessage, aAppendBreadcrumb);
        }
      }
    };

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

MyspaceAccount.prototype._fillMessageFields =
function MyspaceAccount__fillMessageFields(aWindow,
                                           aMessage,
                                           aAppendBreadcrumb)
{
  if (!aMessage.subject) {
    aMessage.subject = this._parseContentForTitle(aMessage.body);
  }

  var results = FlockSvcUtils.newResults();
  if (aMessage.subject) {
    if (this._wd.detect(CLASS_SHORT_NAME,
                        "messageSubject",
                        aWindow.document,
                        results))
    {
      var subject = results.getPropertyAsInterface("node", CI.nsIDOMNode);
      if (subject) {
        subject.value = aMessage.subject;
      }
    }
  }

  if (this._wd.detect(CLASS_SHORT_NAME,
                      "messageBody",
                      aWindow.document,
                      results))
  {
    var body = results.getPropertyAsInterface("node", CI.nsIDOMNode);
    if (body) {
      body.value += aMessage.body; // aMessage.body can be empty string

      if (aAppendBreadcrumb) {
        var breadcrumb = CC["@flock.com/rich-dnd-service;1"]
                         .getService(CI.flockIRichDNDService)
                         .getBreadcrumb("plain");
        if (breadcrumb) {
          body.value += breadcrumb;
        }
      }
    }
  }
};

MyspaceAccount.prototype._parseContentForTitle =
function MyspaceAccount__parseContentForTitle(aContent) {
  if (aContent) {
    // Parse the content for the title
    var re = /^(.+?): <object|<a.+?title="(.+?)"|<img.+?alt="(.+?)"|<a.+?>(.+?)<\/a/;
    var matches = aContent.match(re) || [];

    // Iterate through the results to find the first value. If we don't find
    // anything, then the title wlll be blank
    for (var idx = 1; idx < matches.length; idx++) {
      if (matches[idx]) {
        return matches[idx];
      }
    }
  }
  return "";
};


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

// Create array of components.
var componentsArray = [MyspaceAPI, MyspaceService];

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