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

const MODULE_NAME = "Picasa";
const CLASS_NAME = "Flock Picasa Service";
const CLASS_SHORT_NAME = "picasa";
const CLASS_TITLE = "Picasa";
const CLASS_ID = Components.ID("{a01db128-4b4a-42a7-9645-95fbc2c03e9f}");
const CONTRACT_ID = "@flock.com/photo/picasa;1";
const FLOCK_ERROR_CONTRACTID = "@flock.com/error;1";
const FLOCK_PHOTO_ALBUM_CONTRACTID  = "@flock.com/photo-album;1";

const SERVICE_ENABLED_PREF = "flock.service.picasa.enabled";
const FAVICON = "chrome://flock/content/services/picasa/favicon.png";
const PICASA_API_POST_URL = "http://picasaweb.google.com/data/feed/api/user/";
const SERVICE_PROPERTIES = "chrome://flock/locale/services/picasa.properties";
const XMLHTTPREQUEST_CONTRACTID = "@mozilla.org/xmlextras/xmlhttprequest;1";
const GPHOTO_NAMESPACE = "http://schemas.google.com/photos/2007";
const GDATA_ALBUM_SCHEMA = "http://schemas.google.com/photos/2007#album";
const GDATA_PHOTO_SCHEMA = "http://schemas.google.com/photos/2007#photo";
const MEDIA_NAMESPACE = "http://search.yahoo.com/mrss/";
const ATOM_NAMESPACE = "http://www.w3.org/2005/Atom";

const PICASA_API_LOGIN_URL = "https://www.google.com/accounts/ClientLogin";
// this is an arbitrary application id required by the Picasa api
// c.f. http://code.google.com/apis/accounts/AuthForInstalledApps.html
const PICASA_APPLICATION_ID = "flock-flockbrower-1.1";

const SERVICES_STRING_BUNDLE = "services/services";
const SERVICES_PHOTO_FEED_COMMENT_STRING = "flock.photo.feeds.comment.title";
const XMLHTTPREQUEST_READYSTATE_COMPLETED = 4;
const STR_EMAIL = "email";

var gPicasaApi = null;

// String defaults... may be updated later through Web Detective
 var gStrings = {
  "userprofile": "http://picasaweb.google.com/%accountid%",
  "commentsRSS": "http://picasaweb.google.com/data/feed/base/user/%accountid%?kind=comment&alt=rss&hl=en_US"
 };

function _getLoader() {
  return CC["@mozilla.org/moz/jssubscript-loader;1"]
         .getService(CI.mozIJSSubScriptLoader);
}


function createEnum(aEnumArray) {
  return {
    QueryInterface: function (iid) {
      if (iid.equals(CI.nsISimpleEnumerator)) {
        return this;
      }
      throw CR.NS_ERROR_NO_INTERFACE;
    },
    hasMoreElements: function() {
      return (aEnumArray.length>0);
    },
    getNext: function() {
      return aEnumArray.shift();
    }
  }
}

// Override the buildTooptip function in order to use the
// large img for the tooltip image.
var flockMediaItemFormatter = {
  canBuildTooltip: true,
  buildTooltip: function fmif_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;
  }
};

/*************************************************************************
 * Component: PicasaServiceAPI
 *************************************************************************/

function flockPicasaAPI() {
  this._acUtils = CC["@flock.com/account-utils;1"]
                 .getService(CI.flockIAccountUtils);
  this._WebDetective = CC["@flock.com/web-detective;1"]
                      .getService(CI.flockIWebDetective);
  this._logger = CC["@flock.com/logger;1"].createInstance(CI.flockILogger);
  this._logger.init("picasaAPI");

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

  this._authKey = null;
  this._authAccountId = null;
  this._flockGoogleAuth = new flockGoogleAuth("picasa");
}

flockPicasaAPI.prototype.setAuthAccountId =
function picasaAPI_setAuthAccountId(aAcctIds) {
  this._logger.debug("setAuthAccountId(aAcctIds)");
  if (aAcctIds && aAcctIds.email && aAcctIds.username) {
    this._authAccountId = aAcctIds.email;
    this._accountUsername = aAcctIds.username;
  } else {
    this._authAccountId = null;
    this._accountUsername = null;
  }
  this._logger.debug("setAuthAccountId: _authAccountId: " + this._authAccountId
                     + ", _accountUsername: " + this._accountUsername);
};

flockPicasaAPI.prototype.deauthenticate =
function picasaAPI_deauthenticate() {
  this._logger.debug("deauthenticate()");
  this._authKey = null;
  this.setAuthAccountId(null);
}

flockPicasaAPI.prototype.authenticate =
function picasaAPI_authenticate(aFlockListener) {
  var api = this;
  var dcListener = {
    onSuccess: function dcl_onSuccess(aAuthKey) {
      api._logger.debug("dcl_onSuccess() - " + aAuthKey);
      if (aAuthKey) {
        api._logger.debug("setting authKey = " + aAuthKey);
        api._authKey = aAuthKey;
        if (aFlockListener) {
          aFlockListener.onSuccess(null, null);
        }
      } else {
        var svc = CC[CONTRACT_ID].getService(CI.flockIWebService);
        api._acUtils.markAllAccountsAsLoggedOut(svc);
        if (aFlockListener) {
          aFlockListener.onError(null, null);
        }
      }
    },
    onError: function dcl_onError(aFlockError) {
      var svc = CC[CONTRACT_ID].getService(CI.flockIWebService);
      api._acUtils.markAllAccountsAsLoggedOut(svc);
      api._logger.debug("dcl_onError()");
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, null);
      }
    }
  };
  var credentials = {
    authAccountId: this._authAccountId,
    service: "lh2",
    urn: this.svc.urn,
    username: this._accountUsername
  };
  this._flockGoogleAuth.authenticate(dcListener, credentials);
}


flockPicasaAPI.prototype.doCall =
function picasaAPI_doCall(aListener, aUrl, aParams) {
  this._logger.debug("doCall() " + this._authKey + " " + this._authAccountId);
  if (!this._authKey && this._authAccountId) {
    this._authNeeded(aListener, "doCall", aListener, aUrl, aParams)
    return;
  }
  this._flockGoogleAuth.doCall(this._authKey, aListener, aUrl, aParams);
}

flockPicasaAPI.prototype._doXMLCall =
function picasaAPI__doXMLCall(aListener, aUrl, aParams, aXml, aMethod) {
  this._logger.debug("_doXMLCall()");
  if (!this._authKey && this._authAccountId) {
    this._authNeeded(aUploadListener, "_doXMLCall", aUrl,
                     aParams, aXml, aMethod);
    return;  
  }
  this._flockGoogleAuth.doXMLCall(this._authKey, aListener, aUrl,
                                  aParams, aXml, aMethod);
}

flockPicasaAPI.prototype._authNeeded =
function picasaAPI__authNeeded(aListener, aMethod, aP1, aP2, aP3, aP4, aP5)  {
  var api = this;
  var listener = {
    onSuccess: function doCall_onSucess(aSubject, aTopic) {
       api[aMethod](aP1, aP2, aP3, aP4, aP5);
    },
    onError: function doCall_onError(aError) {
      aListener.onError(aError);
    }
  };
  this.authenticate(listener);
}

flockPicasaAPI.prototype._doUpload =
function picasaAPI___doUpload(aUploadListener, aUpload, aFilename, aParams) {
  this._logger.debug("picasaAPI___doUpload(aUploadListener, '" +
                                           aFilename + "','"+
                                           aParams + "','"+
                                           aUpload +"')");
  var api = this;
  if (!api._authKey) {
    this._authNeeded(aUploadListener, "_doUpload", aUpload, aFilename, aParams);
    return;  
  }

  var albumsArray = [];

  var listener = {
    onResult: function listener_onResult(aXml) {
      var xmlString = CC["@mozilla.org/xmlextras/xmlserializer;1"]
                      .getService(CI.nsIDOMSerializer)
                      .serializeToString(aXml);
      api._logger.debug("Uploaded content xml is: " + xmlString);

      // Due to a bug in Picasa api, we have to attach the photo tags with
      // a separate api call.
      // c.f. https://bugzilla.flock.com/show_bug.cgi?id=11342
      if (aUpload.tags) {
        var tagUrl;
        var links = aXml.getElementsByTagName("link");
        for (var j = 0; j < links.length; j++) {
          var link = links[j];
          if (link.getAttribute("rel") == "edit") {
            tagUrl = link.getAttribute("href");
            break;
          }
        }
        api.svc._lastXML = aXml;
        api._doPhotoTag(aUploadListener, tagUrl, aUpload);
      } else {
        aUploadListener.onUploadComplete(aUpload);

        photoNode = aXml.getElementsByTagName("entry")[0];
        var resultItem = api.svc
                            ._mediaItemFromEntryNode(photoNode, albumsArray);
        aUploadListener.onUploadFinalized(aUpload, resultItem);
      }
    },
    onError: function listener_onError(aErrorCode) {
      aUploadListener.onError(api._getError(aErrorCode));
    },
    onProgress: function listener_onProgress(aCurrentProgress) {
      aUploadListener.onProgress(aCurrentProgress);
    }
  };
  var params = {};
  params.dataXml = aParams.dataXml; 
  params.boundaryString = "END_OF_PART";
  params.headers = [];
  params.headers["Content-Type"] = 'multipart/related; boundary="' +
                                    params.boundaryString + '"';
                                    
  api._logger.debug("looking for authKey " + api._authKey);
  

  api._logger.debug("authKey " + api._authKey);
  params.headers["Authorization"] = "GoogleLogin auth=" + api._authKey;

  var endPointUrl = PICASA_API_POST_URL + aParams.accountId;
  var uploader = new FlockPhotoUploader();

  const BRAND = brandGetString("brand", "brandShortName");
  const DEFAULT_ALBUM_NAME =
    flockGetString("services/picasa",
                   "flock.picasa.uploader.defaultAlbumName",
                   [BRAND]);

  var getAlbumListener = {
    onSuccess: function ga_al_onSuccess(aSubject, aTopic) {
      api._logger.debug("ga_al_onSuccess()");

      // If no album was selected and default album "Flock Photos" exists
      // upload to it. Otherwise create an album named "Flock Photos" and
      // upload to it.
      var albumId;
      if (aUpload.album) {
        albumId = aUpload.album;
        aSubject.QueryInterface(CI.nsISimpleEnumerator);
        while (aSubject.hasMoreElements()) {
          var album = aSubject.getNext();
          albumsArray[album.id] = album.isPublic;
        }
      } else {
        // No album specified so try to find default album.
        aSubject.QueryInterface(CI.nsISimpleEnumerator);
        while (aSubject.hasMoreElements()) {
          var album = aSubject.getNext();
          if (album.title == DEFAULT_ALBUM_NAME) {
            albumId = album.id;
          }
        }
      }

      if (albumId) {
        // Upload to specific album.
        endPointUrl += "/albumid/" + albumId;
        uploader.setEndpoint(endPointUrl);
        uploader.uploadWithXml(listener, aFilename, params, aUpload);
      } else {
        // Create default Flock album.
        var createAlbumListener = {
          onSuccess: function ca_al_onSuccess(aSubject, aTopic) {
            api._logger.debug("ca_al_onSuccess()");
            aSubject.QueryInterface(CI.flockIPhotoAlbum);
            endPointUrl += "/albumid/" + aSubject.id;
            uploader.setEndpoint(endPointUrl);
            uploader.uploadWithXml(listener, aFilename, params, aUpload);
          },
          onError: function ca_al_onError(aFlockError, aTopic) {
            api._logger.debug("ca_al_onError()");
            aUploadListener.onError(aFlockError);
          }
        };

        api.svc.createAlbum(createAlbumListener,
                            aParams.accountId,
                            DEFAULT_ALBUM_NAME);
      }
    },
    onError: function ga_al_onError(aFlockError, aTopic) {
      api._logger.debug("ga_al_onError()");
      aUploadListener.onError(aFlockError);
    }
  };

  this.svc.getAlbums(getAlbumListener, aParams.accountId);
}

flockPicasaAPI.prototype._doPhotoTag =
function picasaAPI__doPhotoTag(aUploadListener, aUrl, aUpload) {
  this._logger.debug("_doPhotoTag()");

  var api = this;

  // Strip out double quotes in tags
  var tags = aUpload.tags.replace(/"/g, "");

  default xml namespace = ATOM_NAMESPACE;
  var tagXml = <entry xmlns={ATOM_NAMESPACE} xmlns:media={MEDIA_NAMESPACE}>
                 <media:group>
                   <media:keywords>{tags}</media:keywords>
                 </media:group>
                 <category term={GDATA_PHOTO_SCHEMA}
                           scheme="http://schemas.google.com/g/2005#kind" />
               </entry>;

  var tagListener = {
    onResult: function tl_onResult(aXml) {
      api._logger.debug("tl_onResult()");
      aUploadListener.onUploadComplete(aUpload);
      photoNode = api.svc._lastXML.getElementsByTagName("entry")[0];
      var resultItem = api.svc._mediaItemFromEntryNode(photoNode, []);

      aUploadListener.onUploadFinalized(aUpload, resultItem);
    },
    onError: function listener_onError(aError) {
      api._logger.debug("tl_onError()");
      aUploadListener.onError(aError);
    }
  };

  var params = {};
  params.contentType = "application/atom+xml";
  this._doXMLCall(tagListener, aUrl, params, tagXml, "PUT");
}

flockPicasaAPI.prototype._getParamString =
function picasaAPI__getParamString(aParams) {
  var rval = "";

  var count = 0;
  for(var p in aParams) {
    if(count++ != 0) {
      rval += "&";
    }

    // The gdata api uses some params with "_". since JS does not allow "-"
    // in var names, we use "_" in the params obj and convert back to "-"
    // before passing to google
    // e.g. start-index, max-results
    if (p.indexOf("_") != -1) {
      rval += encodeURIComponent(p.replace("_", "-"))
           + "=" + encodeURIComponent(aParams[p]);
    } else {
      rval += encodeURIComponent(p) + "=" + encodeURIComponent(aParams[p]);
    }
  }

  return rval;
}

flockPicasaAPI.prototype._getError =
function picasaAPI__getError(aErrorCode) {
  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);

  this._logger.debug("._getError() Picasa error code: " + aErrorCode);
  switch (aErrorCode) {
    case "BadAuthentication":
    // The login request used a username or password that is not recognized.
      error.errorCode = CI.flockIError.PHOTOSERVICE_LOGIN_FAILED;
      break;
    case "20":
      error.errorCode = CI.flockIError
                          .PHOTOSERVICE_PHOTOS_IN_ALBUM_LIMIT_REACHED;
      break;
    default:
      error.errorCode = CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR;
  }

  return error;
}

flockPicasaAPI.prototype._getHTTPError =
function picasaAPI__getHTTPError(aHTTPErrorCode) {
  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
  error.errorCode = aHTTPErrorCode;

  return error;
}

/*************************************************************************
 * Component: PicasaService
 *************************************************************************/
function PicasaService() {
  FlockSvcUtils.flockIAuthenticateNewAccount
               .addDefaultMethod(this, "authenticateNewAccount");

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

  FlockSvcUtils.flockILoginWebService.addDefaultMethod(this, "loginURL");
  FlockSvcUtils.flockILoginWebService.addDefaultMethod(this, "tooltip");
  FlockSvcUtils.flockILoginWebService.addDefaultMethod(this, "getAccount");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getAuthenticatedAccount");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getAccountUrns");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "removeAccount");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "logout");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getAccountIDFromDocument");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getCredentialsFromForm");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "ownsDocument");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "ownsLoginForm");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getSessionValue");

  FlockSvcUtils.flockIMediaWebService.addDefaultMethod(this, "getChannel");
  FlockSvcUtils.flockIMediaWebService.addDefaultMethod(this, "enumerateChannels");
  FlockSvcUtils.flockIMediaWebService.addDefaultMethod(this, "getIconForQuery");
  FlockSvcUtils.flockIMediaUploadWebService.addDefaultMethod(this, "getAlbumsForUpload");

  this._init();
}


/*************************************************************************
 * PicasaService: XPCOM Component Creation
 *************************************************************************/

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

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


/*************************************************************************
 * PicasaService: Private Data and Functions
 *************************************************************************/

// Member variables.
PicasaService.prototype._init =
function fps__init() {
  FlockSvcUtils.getLogger(this);
  this._logger.debug(".init()");

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

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

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

  this._accountClassCtor = PicasaAccount;

  this._baseUrn = "urn:picasa";
  this._serviceUrn = this._baseUrn + ":service";

  // getWD() also adds the "_acUtils" property.
  this.domains = FlockSvcUtils.getWD(this).getString(CLASS_SHORT_NAME,
                                                     "domains",
                                                     "google.com");
  var authenticatedUrn = FlockSvcUtils.initService(this, true);

  // Initialize API
  if(!gPicasaApi) {
    gPicasaApi = new flockPicasaAPI();
  }
  this.api = gPicasaApi;
  this.api.svc = this;

  // Load auth manager
  this._authMgr = CC["@flock.com/authmgr;1"]
                  .getService(CI.flockIAuthenticationManager);
  this._authMgr.joinNetwork(CONTRACT_ID, "google");

  // On browser restart if a Picasa coop account is still marked as
  // authenticated we will also need to reauth the api. We can't use
  // getAuthenticatedAccount because this service is not yet registered.
  if (authenticatedUrn) {
    var acct = this._coop.Account.get(authenticatedUrn);
    var accountParamList = acct.accountParamList;
    if (accountParamList && this.getSessionValue()) {
      var email = accountParamList.getCustomParam(STR_EMAIL);
      this._logger.debug("_init(): email stored as custom param: " + email);
      this.api.setAuthAccountId({ username: acct.accountId, email: email });
    } else {
      acct.isAuthenticated = false;
    }
  }

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

  profiler.profileEventEnd(evtID, "");
};

// Migrate coop objects
PicasaService.prototype._migrateFromCoop =
function fps__migrateFromCoop(aContext) {
  this._logger.debug("_migrateFromCoop(...)");
  if (aContext.majorVersion < 2) {
    // 2.0 changes
    var accountUrns = this.getAccountUrns();
    while (accountUrns.hasMore()) {
      var account = this.getAccount(accountUrns.getNext());
      if (account instanceof CI.flockIWebServiceAccount) {
        // Migrate old coop urn (user@gmail.com or user@googlemail.com) to
        // new urn (user)
        var urn = account.urn.match(/(.+:(.+))@g(?:|oogle)mail\.com/);
        if (urn) {
          // Elements 0, 1 and 2 are valid if urn is non-null
          // Format:
          //   urn[0]: urn:flock:<service>:account:<accountId>@gmail.com
          //   urn[1]: urn:flock:<service>:account:<accountId>
          //   urn[2]: <accountId>
          this._logger.debug("  urn[0]: " + urn[0] + "\n  urn[1]: " + urn[1]
                             + "\n  urn[2]: " + urn[2]);
          var coopAccount = this._coop.get(urn[0]);
          coopAccount.URL =
            coopAccount.URL.replace(/@g(?:|oogle)mail\.com$/, "");
          coopAccount.accountId = urn[2];
          coopAccount.changeId(urn[1]);

          // Update the urn entry in the Password Manager
          var oldAcctId = urn[0].replace(/^urn:flock:picasa:account:/, "");
          var pw = this._acUtils.removePassword(this.urn + ":" + oldAcctId);
          if (pw) {
            this._acUtils.setPassword(this.urn + ":" + urn[2],
                                      pw.username,
                                      pw.password);
          }

          // Update favourite media queries
          var mediaFaves = this._coop.get("urn:media:favorites");
          if (mediaFaves) {
            var mediaFavesEnum = mediaFaves.children.enumerate();
            while (mediaFavesEnum.hasMoreElements()) {
              var mediaFave = mediaFavesEnum.getNext();
              var regExp = new RegExp("^urn:media:favorites:picasa:user:"
                                      + oldAcctId);
              if (mediaFave.id().match(regExp)) {
                regExp = new RegExp("@g(?:|oogle)mail\.com", "g");
                mediaFave.query = mediaFave.query.replace(regExp, "");
                mediaFave.changeId(mediaFave.id().replace(regExp, ""));
              }
            }
          }

          // Update recent media queries
          var recentMedia = this._coop.get("urn:media:recent");
          if (recentMedia) {
            var recentMediaEnum = recentMedia.children.enumerate();
            while (recentMediaEnum.hasMoreElements()) {
              var media = recentMediaEnum.getNext();
              if (media.service == "picasa" &&
                  media.query.match(/user:.+@g(?:|oogle)mail\.com/))
              {
                media.query = media.query.replace(/@g(?:|oogle)mail\.com/g, "");
              }
            }
          }
        }

        // Add account custom params for the first time
        account.setCustomParam(STR_EMAIL, "");
      }
    }
  }
  // Makes this a generator-iterator
  yield true;
};


/*************************************************************************
 * PicasaService: flockIAuthenticateNewAccount Implementation
 *************************************************************************/

// DEFAULT: void authenticateNewAccount();

/*************************************************************************
 * PicasaService: flockIWebService Implementation
 *************************************************************************/

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

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

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

// readonly attribute AString contractId;
PicasaService.prototype.contractId = CONTRACT_ID;

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

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

/**
 * @see flockIWebService#addAccount
 */
PicasaService.prototype.addAccount =
function fps_addAccount(aAccountId, aIsTransient, aFlockListener)
{
  this._logger.info(".addAccount('" + aAccountId + "', "
                                    + aIsTransient + ")");

  var pw = this._acUtils.getPassword(this.urn + ":" + aAccountId);
  var name = (pw) ? pw.username : aAccountId;
  var acctId = aAccountId.replace(/@g(?:|oogle)mail\.com$/, "");
  var url = gStrings["userprofile"].replace("%accountid%", acctId);
  var accountUrn =
    this._acUtils.createAccount(this, acctId, name, url, aIsTransient);

  // Instantiate account component
  var account = this.getAccount(accountUrn);
  account.setParam("service", this._coopObj);
  // Add custom parameters to be used
  account.setCustomParam(STR_EMAIL, "");

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

// DEFAULT: flockIWebServiceAccount getAccount(in AString aUrn);
// DEFAULT: flockIWebServiceAccount getAccountUrns();
// DEFAULT: flockIWebServiceAccount removeAccount(in AString aUrn);
// DEFAULT: flockIWebServiceAccount logout();

/*************************************************************************
 * PicasaService: flockILoginWebService Implementation
 *************************************************************************/

// DEFAULT: boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument aDocument);
// DEFAULT: AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: flockILoginInfo getCredentialsFromForm(in nsIDOMHTMLFormElement aForm);

// boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
PicasaService.prototype.ownsDocument =
function fps_ownsDocument(aDocument)
{
  this._logger.debug(".ownsDocument()");
  this._logger.debug("domain: " + aDocument.domain);

  // We need to override the default ownsDocument in this case because
  // multiple services share the same login page (google.com in this case)
  return this._WebDetective.detect(this.shortName,
                                   "ownsDocument",
                                   aDocument,
                                   null);  
}

// DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);

/**
 * @see flockILoginWebService#updateAccountStatusFromDocument
 */
PicasaService.prototype.updateAccountStatusFromDocument =
function fps_updateAccountStatusFromDocument(aDocument,
                                             aAccount,
                                             aAuthListener)
{
  if (aAccount) {
    // Save the email address, if necessary
    // Note: account._saveEmail() needs to be called before account.login();
    this._saveEmail(aAccount, aDocument);

    if (!aAccount.isAuthenticated()) {
      var inst = this;
      var loginListener = {
        onSuccess: function () {
          inst._logger.debug(".updateAccountStatusFromDocument() loginListener: onSuccess()");
          inst._authMgr.reportCompleteLoginToNetwork(aAccount);
          if (aAuthListener) {
            aAuthListener.onSuccess(aAccount, null);
          }
        },
        onError: function (aError) {
          inst._logger.error(".updateAccountStatusFromDocument() loginListener: onError()");
          inst._logger.error(aError ? (aError.errorString) : "No details");
        }
      };
      aAccount.login(loginListener);
    }
  } else if (this._WebDetective
                 .detect("picasa", "loggedout", aDocument, null))
  {
    // We're logged out (of all accounts)
    this._acUtils.markAllAccountsAsLoggedOut(this);
    // Also de-authenticate the API
    this.api.deauthenticate();
    this._authMgr.reportCompleteLogoutFromNetwork(this.contractId);
  }
}

/*************************************************************************
 * PicasaService: flockIMediaWebService Implementation
 *************************************************************************/
// readonly attribute boolean supportsUsers;
PicasaService.prototype.supportsUsers = true;

// void decorateForMedia(in nsIDOMHTMLDocument aDocument,
//                       in flockIWebServiceAccount aAccount);
PicasaService.prototype.decorateForMedia =
function fps_decorateForMedia(aDocument, aAccount) {
  this._logger.debug(".decorateForMedia()");
  var results = CC["@mozilla.org/hash-property-bag;1"]
                .createInstance(CI.nsIWritablePropertyBag2);

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

    media = {
      name: results.getPropertyAsAString("albumid"),
      query: "user:" + results.getPropertyAsAString("userid")
                     + "|username:" + results.getPropertyAsAString("username")
                     + "|album:" + results.getPropertyAsAString("albumid")
                     + "|albumlabel:" + results.getPropertyAsAString("albumname"),
      label: results.getPropertyAsAString("albumname"),
      favicon: this.icon,
      service: this.shortName
    }
    mediaArr.push(media);
  }

  if (mediaArr.length > 0) {
    if (!aDocument._flock_decorations) {
      aDocument._flock_decorations = {};
    }

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

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

// BEGIN flockIMediaEmbedWebService interface
// boolean checkIsStreamUrl(in AString aUrl)
PicasaService.prototype.checkIsStreamUrl =
function fps_checkIsStreamUrl(aUrl) {
  this._logger.debug(".checkIsStreamUrl(" + aUrl + ")");
 if (this._WebDetective.detectNoDOM("picasa", "isStreamUrl", "", aUrl, null)) {
   this._logger.debug("Checking if url is picasa stream: YES: " + aUrl);
   return true;
 }

 this._logger.debug("Checking if url is picasa stream: NO: " + aUrl);
 return false;
}

// void getMediaQueryFromURL(in AString aUrl, in flockIListener aFlockListener)
PicasaService.prototype.getMediaQueryFromURL =
function fps_getMediaQueryFromURL(aUrl, aFlockListener) {
  this._logger.debug(".getMediaQueryFromURL()");

  var userId = null;
  var userName = null;
  var detectResults = CC["@mozilla.org/hash-property-bag;1"]
                      .createInstance(CI.nsIWritablePropertyBag2);
  if (this._WebDetective.detectNoDOM("picasa",
                                     "person",
                                     "",
                                     aUrl,
                                     detectResults))
  {
    userId = detectResults.getPropertyAsAString("userid");
  }

  if (userId) {
    var inst = this;
    var infoListener = {
      onResult: function il_onSuccess(aXml) {
        var feed = aXml.getElementsByTagName("feed")[0];
        userName =
          feed.getElementsByTagNameNS(GPHOTO_NAMESPACE, "nickname")[0]
              .firstChild.nodeValue;

        var results = CC["@mozilla.org/hash-property-bag;1"]
                      .createInstance(CI.nsIWritablePropertyBag2);
        results.setPropertyAsAString("query",
                                     "user:" + userId + "|username:" + userName);
        results.setPropertyAsAString("title", this.title + " user: " + userName);

        aFlockListener.onSuccess(results, "query");
      },
      onError: function il_onError(aFlockError) {
        aFlockListener.onError(aFlockError, null);
      }
    };

    var params = {
      kind: "album"
    };

    this.api.doCall(infoListener,
                     "http://picasaweb.google.com/data/feed/api/user/"
                       + userId,
                     params);
  } else {
    aFlockListener.onError(null, "Unable to get user.");
  }
}

// boolean getSharingContent(in nsIDOMHTMLElement aSrc,
//                           in nsIWritablePropertyBag2 aProp);
PicasaService.prototype.getSharingContent =
function fps_getSharingContent(aSrc, aProp) {
  this._logger.debug("getSharingContent()");
  if (aSrc && aSrc instanceof CI.nsIDOMHTMLImageElement) {
    var results = FlockSvcUtils.newResults();
    if (this._WebDetective
            .detectNoDOM("picasa", "person", "", aSrc.src, results))
    {
      var url = aSrc.src;
      var title = (aSrc.title == "") ? aSrc.alt : aSrc.title;
      var doc = aSrc.ownerDocument;
      if (this.ownsDocument(doc)) {
        url = doc.URL;
        if (!title) {
          title = doc.title;
        }
      }
      aProp.setPropertyAsAString("url", url);
      aProp.setPropertyAsAString("title", title);
      return true;
    }
  }
  return false;
}
// END flockIMediaEmbedWebService interface

// void createAlbum()
PicasaService.prototype.createAlbum =
function fps_createAlbum(aFlockListener, aAccountId, aAlbumName) {
  this._logger.debug("createAlbum('" + aAlbumName + "')");

  // Trim white space from front and end of string
  var newTitle = aAlbumName.replace(/^\s+|\s+$/g, "");
  if (newTitle) {
   default xml namespace = ATOM_NAMESPACE;
   var albumXml = <entry xmlns={ATOM_NAMESPACE}
                         xmlns:media={MEDIA_NAMESPACE}
                         xmlns:gphoto={GPHOTO_NAMESPACE}>
                    <title type="text">{newTitle}</title>
                    <summary type="text" />
                    <gphoto:location />
                    <gphoto:access>public</gphoto:access>
                    <gphoto:commentingEnabled>true</gphoto:commentingEnabled>
                    <category term={GDATA_ALBUM_SCHEMA}
                              scheme="http://schemas.google.com/g/2005#kind" />
                  </entry>;

    var myListener = {
      onResult: function listener_onResult(aXml) {
        var albumEntry = aXml.getElementsByTagName("entry")[0];
        var id = albumEntry.getElementsByTagNameNS(GPHOTO_NAMESPACE, "id")[0]
                           .firstChild.nodeValue;
        var title = albumEntry.getElementsByTagName("title")[0]
                              .firstChild.nodeValue;

        var newAlbum = CC[FLOCK_PHOTO_ALBUM_CONTRACTID]
                       .createInstance(CI.flockIPhotoAlbum);
        newAlbum.title = title;
        newAlbum.id = id;
        aFlockListener.onSuccess(newAlbum, "");
      },
      onError: function listener_onError(aFlockError) {
        aFlockListener.onError(aFlockError, null);
      }
    }
    var params = {};
    params.contentType = "application/atom+xml";
    this.api._doXMLCall(myListener,
                        PICASA_API_POST_URL + aAccountId,
                        params,
                        albumXml,
                        "POST");
  } else {
    var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
    error.errorCode = CI.flockIError.PHOTOSERVICE_EMPTY_ALBUMNAME;
    aFlockListener.onError(error, null);
  }
}

// void getAlbums(in flockIListener aFlockListener, in AString aUsername)
PicasaService.prototype.getAlbums =
function fps_getAlbums(aFlockListener, aUsername) {
  this._logger.debug(".getAlbums(aFlockListener, " + aUsername + ")");
  var inst = this;
  var albumListener = {
    onResult: function album_onSuccess(aXml) {
      inst._logger.debug("album_onSuccess()");
      var resultsArray = inst._handleAlbumResults(aXml);
      aFlockListener.onSuccess(createEnum(resultsArray), "");
    },
    onError: function album_onError(aFlockError) {
      inst._logger.debug("album_onError()");
      aFlockListener.onError(aFlockError, null);
    }
  };

  var params = {
    kind: "album"
  };

  this.api.doCall(albumListener,
                   "http://picasaweb.google.com/data/feed/api/user/"
                     + aUsername,
                   params);
}

// DEFAULT: flockIMediaChannel getChannel(in AString AChannelId)
// DEFAULT: nsISimpleEnumerator enumerateChannels()
// DEFAULT: void getAlbumsForUpload(aListener, aUsername);

// flockIMediaItemFormatter getMediaItemFormatter()
PicasaService.prototype.getMediaItemFormatter =
function PicasaService_getMediaItemFormatter() {
  return flockMediaItemFormatter;
}

// void migrateAccount(in AString aId, in AString aUsername)
PicasaService.prototype.migrateAccount =
function fps_migrateAccount(aUrn, aUsername) {
  this._logger.debug(".migrateAccount()");
  throw CR.NS_ERROR_NOT_IMPLEMENTED;
}

PicasaService.prototype.search =
function fps_search(aFlockListener, aQueryString, aCount, aPage, aRequestId) {
  this._logger.debug(".search() with query - " +aQueryString);
  var query = new QueryHelper(aQueryString);

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

  if (!aPage) {
    aPage = 1;
  }

  if (query.user) {
    this._searchUserPhotos(aFlockListener, aQueryString, aCount, aPage, aRequestId);
    return;
  }

  var inst = this;

  var listener = {
    onResult: function l_onResult(aXml) {
      inst._logger.debug("sl_onResult()");
      var resultsArray = inst._handlePhotoResults(aXml);
      aFlockListener.onSuccess(createEnum(resultsArray), aRequestId);
    },
    onError: function l_onError(aFlockError) {
      inst._logger.debug("sl_onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  var params = {
    kind: "photo",
    q: query.search,
    start_index: (aPage - 1) * aCount + 1,
    max_results: aCount
  };

  this.api.doCall(listener,
                   "http://picasaweb.google.com/data/feed/api/all",
                   params);
}

// boolean supportsFeature(in AString aFeature)
PicasaService.prototype.supportsFeature =
function fps_supportsFeature(aFeature) {
  this._logger.debug(".supportsFeature('" + aFeature + "')");
  var supports = {};
  supports.tags = true;
  supports.title = false;
  supports.fileName = true;
  supports.contacts = true;
  supports.description = true;
  supports.privacy = false;
  supports.albumCreation = true;
  return (supports[aFeature] == true);
}

// boolean supportsSearch(in AString aQuery)
PicasaService.prototype.supportsSearch =
function fps_supportsSearch(aQuery) {
  this._logger.debug(".supportsSearch()");
  return false;
}

// void upload(in flockIPhotoUploadAPIListener aUploadListener,
//             in flockIPhotoUpload aUpload, in AString aFilename)
PicasaService.prototype.upload =
function fps_upload(aUploadListener, aUpload, aFilename) {
  this._logger.debug("upload(" + aUpload + "','"+ aFilename +"')");
  var accountUrn = this.getAuthenticatedAccount();
  if (!accountUrn) {
    aUploadListener.onError(this._createLoginError());
    return;
  }
  var coopAcct = this._coop.get(accountUrn);

  // Strip out double quotes in tags
  var tags = aUpload.tags.replace(/"/g, "");
  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 += this.getStringBundle()
                   .GetStringFromName("flock.picasa.uploader.breadcrumb");
  }

  default xml namespace = ATOM_NAMESPACE;
  var photoXml = <entry xmlns={ATOM_NAMESPACE} xmlns:media={MEDIA_NAMESPACE}>
                   <title>{aFilename}</title>
                   <summary>{description}</summary>
                   <media:group>
                     <media:keywords>{tags}</media:keywords>
                   </media:group>
                   <category term={GDATA_PHOTO_SCHEMA}
                             scheme="http://schemas.google.com/g/2005#kind" />
                 </entry>;

  // Mozilla uses UTF-16 internally, but Picasa wants UTF-8.
  var converter = CC["@mozilla.org/intl/scriptableunicodeconverter"]
                  .createInstance(CI.nsIScriptableUnicodeConverter);
  converter.charset = "UTF-8";
  var UTF8DataXML = converter.ConvertFromUnicode(photoXml.toString())
                  + converter.Finish();
  var params = {
    dataXml: UTF8DataXML,
    accountId: coopAcct.accountId
  };
  this.api._doUpload(aUploadListener, aUpload, aFilename, params);
}

// void findByUsername(in flockIListener aFlockListener, in AString aUsername);
PicasaService.prototype.findByUsername =
function fps_findByUsername(aFlockListener, aUsername) {
  // need to call this to trigger mediaObserver
  aFlockListener.onError(null, null);
}

// void getAccountStatus(in flockIListener aFlockListener)
PicasaService.prototype.getAccountStatus =
function fps_getAccountStatus(aFlockListener) {
  this._logger.debug(".getAccountStatus()");
  var inst = this;
  var accountListener = {
    onResult:function accl_onResult(aXml) {
      inst._logger.info('we got a result for account status...');
      var result = CC["@mozilla.org/hash-property-bag;1"]
                   .createInstance(CI.nsIWritablePropertyBag2);
      var username = aXml.getElementsByTagNameNS(GPHOTO_NAMESPACE, "nickname")[0]
                         .firstChild.nodeValue;
      var usedSpace = aXml.getElementsByTagNameNS(GPHOTO_NAMESPACE, "quotacurrent")[0]
                          .firstChild.nodeValue;
      var maxSpace = aXml.getElementsByTagNameNS(GPHOTO_NAMESPACE, "quotalimit")[0]
                         .firstChild.nodeValue;
      result.setPropertyAsAString("username", username);
      result.setPropertyAsAString("maxSpace", maxSpace);
      result.setPropertyAsAString("usedSpace", usedSpace);
      result.setPropertyAsAString("maxFileSize", 20 * 1024 * 1024); // 20megs
      result.setPropertyAsAString("usageUnits", "bytes");
      result.setPropertyAsBool("isPremium", false);
      aFlockListener.onSuccess(result, "");
    },
    onError: function accl_onError(aFlockError) {
      aFlockListener.onError(aFlockError, null);
    }
  };

  var c_acct = this._coop.get(this.getAuthenticatedAccount());

  var params = {
    kind: "album"
  };

  this.api.doCall(accountListener,
                  "http://picasaweb.google.com/data/feed/api/user/"
                    + c_acct.accountId,
                  params);
}

PicasaService.prototype._searchUserPhotos =
function fps__searchUserPhotos(aFlockListener,
                               aQueryString,
                               aCount,
                               aPage,
                               aRequestId)
{
  this._logger.debug("._searchUserPhotos() with query - " +aQueryString);
  var query = new QueryHelper(aQueryString);
  var inst = this;
  var endpointUrl = "http://picasaweb.google.com/data/feed/api/user/"
                    + query.user;
  if (query.album) {
    endpointUrl += "/albumid/" + query.album;
  }

  var params = {
    kind: "photo",
    start_index: (aPage - 1) * aCount + 1,
    max_results: aCount
  };

  var albumsResult = null;
  var searchUserListener = {
    onResult: function sul_onResult(aXml) {
      inst._logger.debug("sul_onResult()");
      var albumsArray = [];
      if (albumsResult) {
        albumsResult.QueryInterface(CI.nsISimpleEnumerator);
        while(albumsResult.hasMoreElements()) {
          var album = albumsResult.getNext();
          albumsArray[album.id] = album.isPublic;
        }
      }
      var resultsArray = inst._handleUserPhotoResults(aXml, albumsArray);
      aFlockListener.onSuccess(createEnum(resultsArray), aRequestId);
    },
    onError: function sul_onError(aFlockError) {
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  var getAlbumListener = {
    onSuccess: function sup_al_onSuccess(aSubject, aTopic) {
      inst._logger.debug("sup_al_onSuccess()");
      albumsResult = aSubject;
      inst.api.doCall(searchUserListener,
                       endpointUrl,
                       params);
    },
    onError: function sup_al_onError(aFlockError, aTopic) {
      inst._logger.debug("sup_al_onError()");
      if (!aFlockError) {
        aFlockError = inst.api._getError(null);
      }
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  // Private photos are only displayed for the case when the user loads
  // their own user stream. If it is not their stream OR it is a search
  // based stream, private photos are not displayed.
  var accountUrn = this.getAuthenticatedAccount();
  if (accountUrn && this._coop.Account.get(accountUrn).accountId == query.user)
  {
    // Get photo privacy by querying the user's album privacy
    this.getAlbums(getAlbumListener, query.user);
  } else {
    // No need to check for photo privacy
    this.api.doCall(searchUserListener,
                     endpointUrl,
                     params);
  }
}

PicasaService.prototype._handlePhotoResults =
function fps__handlePhotoResults(aXml) {
  this._logger.debug(".handlePhotoResults()");
  var rval = [];
  var photoList = aXml.getElementsByTagName("entry");
  for (var i = 0; i < photoList.length; i++) {
    var photo = photoList[i];

    var id = photo.getElementsByTagNameNS(GPHOTO_NAMESPACE, "id")[0]
                  .firstChild.nodeValue;
    var title = photo.getElementsByTagName("title")[0]
                     .firstChild.nodeValue;
    var desc = "";
    if (photo.getElementsByTagName("summary")[0].firstChild) {
      desc = photo.getElementsByTagName("summary")[0]
                  .firstChild.nodeValue;
    }
    var ownerName = photo.getElementsByTagName("name")[0]
                         .firstChild.nodeValue;
    var owner = photo.getElementsByTagName("email")[0]
                     .firstChild.nodeValue;
    var pageUrl;
    var links = photo.getElementsByTagName("link");
    for(var j = 0; j < links.length; j++) {
      var link = links[j];
      if (link.getAttribute("rel") == "alternate") {
        pageUrl = link.getAttribute("href");
        break;
      }
    }

    var uploadDate = 0;
    var tsNode = photo.getElementsByTagNameNS(GPHOTO_NAMESPACE, "timestamp");
    if (photo.getElementsByTagName("published").length) {
      // Published date return in the format 2008-02-08T22:13:17.000Z
      var published = photo.getElementsByTagName("published")[0]
                           .firstChild.nodeValue;
      uploadDate = ISO8601DateUtils.parse(published).getTime();
    } else if (tsNode.length) {
      uploadDate = tsNode[0].firstChild.nodeValue;
    }

    var thumbs = photo.getElementsByTagNameNS(MEDIA_NAMESPACE, "thumbnail");
    var smallUrl = thumbs[0].getAttribute("url");
    var medUrl = thumbs[1].getAttribute("url");
    var largeUrl = thumbs[2].getAttribute("url");

    var newMediaItem = CC["@flock.com/photo;1"]
                       .createInstance(CI.flockIMediaItem);
    newMediaItem.init(this.shortName, flockMediaItemFormatter);
    newMediaItem.webPageUrl = pageUrl;
    newMediaItem.thumbnail = medUrl;
    newMediaItem.midSizePhoto = medUrl;
    newMediaItem.largeSizePhoto = largeUrl;
    newMediaItem.username = ownerName;
    newMediaItem.userid = owner;
    newMediaItem.title = (desc) ? desc : title;
    newMediaItem.id = id;
    newMediaItem.uploadDate = uploadDate;
    newMediaItem.is_public = true;
    newMediaItem.is_video = false;

    rval.push(newMediaItem);
  }

  return rval;
}

PicasaService.prototype._mediaItemFromEntryNode =
function _mediaItemFromEntryNode(photo, aAlbumsArray) {
  var id = photo.getElementsByTagNameNS(GPHOTO_NAMESPACE, "id")[0]
                .firstChild.nodeValue;
  var title = photo.getElementsByTagName("title")[0]
                   .firstChild.nodeValue;
  var desc = "";
  if (photo.getElementsByTagName("summary")[0].firstChild) {
    desc = photo.getElementsByTagName("summary")[0]
                .firstChild.nodeValue;
  }
  var pageUrl;
  var links = photo.getElementsByTagName("link");
  for (var j = 0; j < links.length; j++) {
    var link = links[j];
    if (link.getAttribute("rel") == "alternate") {
      pageUrl = link.getAttribute("href");
      break;
    }
  }
  var uploadDate =
    photo.getElementsByTagNameNS(GPHOTO_NAMESPACE, "timestamp")[0]
         .firstChild.nodeValue;
  var albumId =
    photo.getElementsByTagNameNS(GPHOTO_NAMESPACE, "albumid")[0]
         .firstChild.nodeValue;

  var thumbs = photo.getElementsByTagNameNS(MEDIA_NAMESPACE, "thumbnail");
  var smallUrl = thumbs[0].getAttribute("url");
  var medUrl = thumbs[1].getAttribute("url");
  var largeUrl = thumbs[2].getAttribute("url");
  var contents = photo.getElementsByTagNameNS(MEDIA_NAMESPACE, "content");

  var is_video = false;
  for (var k = 0; k < contents.length; k++) {
    if (contents[k].getAttribute("medium") == "video") {
      is_video = true;
    }
  }

  var newMediaItem = CC["@flock.com/photo;1"]
                     .createInstance(CI.flockIMediaItem);
  newMediaItem.init(this.shortName, flockMediaItemFormatter);
  newMediaItem.webPageUrl = pageUrl;
  newMediaItem.thumbnail = medUrl;
  newMediaItem.midSizePhoto = medUrl;
  newMediaItem.largeSizePhoto = largeUrl;
  newMediaItem.title = (desc) ? desc : title;
  newMediaItem.id = id;
  newMediaItem.uploadDate = uploadDate;
  newMediaItem.is_public = (aAlbumsArray && aAlbumsArray[albumId] === undefined)
                           ? true
                           : aAlbumsArray[albumId];
  newMediaItem.is_video = is_video;
  return newMediaItem;
}

PicasaService.prototype._handleUserPhotoResults =
function fps__handleUserPhotoResults(aXml, aAlbumsArray) {
  this._logger.debug(".handleUserPhotoResults()");
  var rval = [];

  var ownerName = aXml.getElementsByTagName("name")[0]
                      .firstChild.nodeValue;
  var owner = aXml.getElementsByTagNameNS(GPHOTO_NAMESPACE, "user")[0]
                   .firstChild.nodeValue;
  var category = aXml.getElementsByTagName("category")[0]
                     .getAttribute("term");
  var icon;
  if (category != GDATA_ALBUM_SCHEMA) {
  icon = aXml.getElementsByTagName("icon")[0]
             .firstChild.nodeValue;
  }

  var photoList = aXml.getElementsByTagName("entry");
  for (var i = 0; i < photoList.length; i++) {
    var photo = photoList[i];
    var newMediaItem = this._mediaItemFromEntryNode(photo, aAlbumsArray);
    newMediaItem.username = ownerName;
    newMediaItem.userid = owner;
    newMediaItem.icon = icon;
    rval.push(newMediaItem);
  }
  return rval;
}

PicasaService.prototype._handleAlbumResults =
function fps__handleAlbumResults(aXml) {
  this._logger.debug(".handleAlbumsResults()");
  var rval = [];
  var photosetList = aXml.getElementsByTagName("entry");
  for (var i = 0; i < photosetList.length; i++) {
    var photoset = photosetList[i];
    var id = photoset.getElementsByTagNameNS(GPHOTO_NAMESPACE, "id")[0]
                     .firstChild.nodeValue;
    var title = photoset.getElementsByTagName("title")[0]
                        .firstChild.nodeValue;
    var rights = null;
    try {
      rights = photoset.getElementsByTagName("rights")[0]
                        .firstChild.nodeValue;
    } catch (ex) {
      this._logger.error("API seems to be missing rights nodes.");
    }

    var newAlbum = Components.classes[FLOCK_PHOTO_ALBUM_CONTRACTID]
                             .createInstance(CI.flockIPhotoAlbum);
    newAlbum.id = id;
    newAlbum.title = title;
    newAlbum.isPublic = (rights == "public") ? true : false;

    rval.push(newAlbum);
  }
  return rval;
}

PicasaService.prototype._createLoginError =
function fps__createLoginError() {
  this._logger.debug("._createLoginError()");
  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
  error.errorCode = CI.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN;
  return error;
}

/*************************************************************************
 * PicasaService: flockIPollingService Implementation
 *************************************************************************/

// void refresh(in AString aUrn, in flockIPollingListener aPollingListener)
PicasaService.prototype.refresh =
function fps_refresh(aUrn, aPollingListener) {
  this._logger.debug(".refresh(" + aUrn + ")");
  var refreshItem = this._coop.get(aUrn);

  if (refreshItem instanceof this._coop.Account) {
    this._logger.debug("refreshing an Account");
    if (refreshItem.isAuthenticated) {
      this._refreshAccount(refreshItem, aPollingListener);
    } else {
      // If the user is not logged in, return a success without
      // refreshing anything
      aPollingListener.onResult();
    }
  } else {
    this._logger.error("can't refresh " + aUrn + " (unsupported type)");
    aPollingListener.onError(null);
  }
};

PicasaService.prototype._refreshAccount =
function fps__refreshAccount(aCoopAcct, aPollingListener) {
  this._logger.debug("._refreshAccount(" + aCoopAcct.urn + ")");
  var inst = this;
  var infoListener = {
    onResult: function album_onSuccess(aXml) {

      var feed = aXml.getElementsByTagName("feed")[0];
      var userName =
        feed.getElementsByTagNameNS(GPHOTO_NAMESPACE, "nickname")[0]
            .firstChild.nodeValue;
      var avatar =
        feed.getElementsByTagNameNS(GPHOTO_NAMESPACE, "thumbnail")[0]
            .firstChild.nodeValue;

      aCoopAcct.name = userName;
      aCoopAcct.avatar = avatar;

      aPollingListener.onResult();
    },
    onError: function album_onError(aFlockError) {
      aPollingListener.onError(aFlockError);
    }
  };

  var params = {
    kind: "album"
  };

  this.api.doCall(infoListener,
                   "http://picasaweb.google.com/data/feed/api/user/"
                     + aCoopAcct.accountId,
                   params);
}


/**************************************************************************
 * PicasaService: flockIMigratable interface
 **************************************************************************/
 
PicasaService.prototype.__defineGetter__("migrationName",
function fps_getter_migrationName() {
  this._logger.debug("_getter_migrationName()");
  return flockGetString("common/migrationNames", "migration.name.picasa");
});
 
// boolean needsMigration(in string aOldVersion);
PicasaService.prototype.needsMigration =
function fps_needsMigration(aOldVersion) {
  this._logger.debug("needsMigration(" + aOldVersion + ")");
  // Version: major.minor.revision
  var major = parseInt(aOldVersion[0], 10);
  var minor = parseInt(aOldVersion[2], 10);
  if (major == 1 && (minor == 1 || minor == 2)) {
    // Grackle and Grouse: This service was added for Grackle and changed for
    // Ibis.
    return true;
  }
  return false;
};

// nsISupports startMigration(in string aOldVersion,
//                            in flockIMigrationProgressListener aListener);
PicasaService.prototype.startMigration =
function fps_startMigration(aOldVersion, aFlockMigrationProgressListener) {
  this._logger.debug("startMigration(" + aOldVersion
                     + ", aFlockMigrationProgressListener)");
  var ctxt = {
    majorVersion: parseInt(aOldVersion[0], 10),
    minorVersion: parseInt(aOldVersion[2], 10),
    listener: aFlockMigrationProgressListener
  };
  return { wrappedJSObject: ctxt };
};

// boolean doMigrationWork(in nsISupports aMigrationContext);
PicasaService.prototype.doMigrationWork =
function fps_doMigrationWork(aMigrationContext) {
  this._logger.debug("doMigrationWork(aMigrationContext)");
  var context = aMigrationContext.wrappedJSObject;
  if (!context.picasaMigrator) {
    context.picasaMigrator = this._migrateFromCoop(context);
  }
  if (context.picasaMigrator.next()) {
    context.picasaMigrator = null;
  }
  return (context.picasaMigrator != null);
};

// void finishMigration(in nsISupports aMigrationContext);
PicasaService.prototype.finishMigration =
function fps_finishMigration(aMigrationContext) {
  this._logger.debug("finishMigration(aMigrationContext)");
};

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

PicasaService.prototype._saveEmail =
function fps__saveEmail(aAccount, aDocument) {
  // Email already saved, don't do it again
  if (aAccount.getCustomParam(STR_EMAIL)) {
    this._logger.debug("_saveEmail() called");
    return;
  }
  var results = FlockSvcUtils.newResults();
  if (this._WebDetective
          .detect(CLASS_SHORT_NAME, "accountinfo", aDocument, results))
  {
    aAccount.setCustomParam(STR_EMAIL, results.getPropertyAsAString(STR_EMAIL));
    this._logger.debug("_saveEmail(): " + aAccount.getCustomParam(STR_EMAIL));
  }
};

/*************************************************************************
 * Component: PicasaAccount
 *************************************************************************/

function PicasaAccount() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("picasaAccount");

  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);
  // BC: Do not use FlockSvcUtils.getWD() here, because it would load
  //     the Web detective file a second time
  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);

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

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

  this.api = gPicasaApi;
}

/*************************************************************************
 * PicasaAccount: XPCOM Component Creation
 *************************************************************************/

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

/*************************************************************************
 * PicasaAccount: flockIWebServiceAccount Implementation
 *************************************************************************/

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

// void login(in flockIListener aFlockListener);
PicasaAccount.prototype.login =
function fpa_login(aFlockListener) {
  this._logger.debug(".login()");
  this.api.setAuthAccountId({ username: this.coopObj.accountId,
                              email: this.getCustomParam(STR_EMAIL) });
  this._acUtils.ensureOnlyAuthenticatedAccount(this);
  // Force refresh on login
  var pollerSvc = CC["@flock.com/poller-service;1"]
                  .getService(CI.flockIPollerService);
  pollerSvc.forceRefresh(this.urn);
  if (aFlockListener) {
    aFlockListener.onSuccess(this, "login");
  }
}

// void logout(in flockIListener aFlockListener);
PicasaAccount.prototype.logout =
function fpa_logout(aFlockListener) {
  this._logger.debug(".logout()");
  if (this.coopObj.isAuthenticated) {
    this.getService().logout();
    this.api.deauthenticate();
  }
  if (aFlockListener) {
    aFlockListener.onSuccess(this, "logout");
  }
}

// void keep();
PicasaAccount.prototype.keep =
function fpa_keep() {
  this._logger.debug(".keep()");
  this.coopObj.isTransient = false;
  var urn = this.urn.replace("account:", "service:").replace("flock:", "");
  this._acUtils.makeTempPasswordPermanent(urn);

  // Subscribe to the comments activity RSS feed for this account
  var feedMgr = CC["@flock.com/feed-manager;1"]
                .getService(CI.flockIFeedManager);
  var inst = this;
  var feedListener = {
    onGetFeedComplete: function keep_feed_complete(aFeed) {
      inst._logger.debug("keep_feed_complete()");
      feedMgr.getFeedContext("news").getRoot().subscribeFeed(aFeed);
      CC["@flock.com/metrics-service;1"]
        .getService(CI.flockIMetricsService)
        .report("FeedsSidebar-AddFeed",  "PicasaAccountKeep");

      var coop = FlockSvcUtils.getCoop(this);

      var title = flockGetString(SERVICES_STRING_BUNDLE,
                                 SERVICES_PHOTO_FEED_COMMENT_STRING);
      var username = coop.get(inst.urn).name;

      var feedTitle = title.replace("%s", username);

      coop.get(aFeed.id()).name = feedTitle;
    },
    onError: function keep_feed_error(aError) {
      inst._logger.debug("There was a problem subscribing to the recent "
                         + "activity feed for Picasa account "
                         + inst.coopObj.accountId);
    }
  };
  try {
    var feedURI = CC["@mozilla.org/network/standard-url;1"]
                  .createInstance(CI.nsIURI);
    feedURI.spec = gStrings["commentsRSS"]
                   .replace("%accountid%", this.coopObj.accountId);
    feedMgr.getFeed(feedURI, feedListener);
  } catch (ex) {
    this._logger.debug("There was an error subscribing to the recent "
                       + "activity feed for Picasa account "
                       + this.coopObj.accountId);
  }
}

// DEFAULT: boolean isAuthenticated();

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

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

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