// vim: ts=2 sw=2 expandtab cindent
//
// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2008
// http://flock.com
//
// This file may be used under the terms of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// END FLOCK GPL

const ENABLE_DEBUG = true; // switch to turn off slow debug code for production
function DEBUG(x) { if (ENABLE_DEBUG) debug("flockFlickrService: "+x+"\n"); }

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;

Components.utils.import("resource:///modules/FlockCryptoHash.jsm");
Components.utils.import("resource:///modules/FlockScheduler.jsm");
Components.utils.import("resource://gre/modules/JSON.jsm");
Components.utils.import("resource:///modules/FlockStringBundleHelpers.jsm");
Components.utils.import("resource:///modules/FlockSvcUtils.jsm");

const FLICKR_TITLE                  = "Flickr Web Service";
const FLICKR_FAVICON                = "chrome://flock/content/services/flickr/favicon.png";
const FLICKR_CID                    = Components.ID("{db720a5c-6315-49bf-a39f-b4d4aa5ed142}");
const FLICKR_CONTRACTID             = "@flock.com/?photo-api-flickr;1";
const SERVICE_ENABLED_PREF          = "flock.service.flickr.enabled";
const CATEGORY_COMPONENT_NAME       = "Flickr JS Component"
const CATEGORY_ENTRY_NAME           = "flickr"

const flockIError                   = Components.interfaces.flockIError;
const flockIPhotoAlbum              = Components.interfaces.flockIPhotoAlbum;

const FLOCK_PHOTO_CONTRACTID        = "@flock.com/photo;1";
const FLOCK_PHOTOPERSON_CONTRACTID  = "@flock.com/photo-person;1";
const FLOCK_PHOTO_ALBUM_CONTRACTID  = "@flock.com/photo-album;1";
const FLOCK_ERROR_CONTRACTID        = "@flock.com/error;1";
const FLOCK_RDDS_CONTRACTID         = "@flock.com/rich-dnd-service;1";

const PEOPLE_PROPERTIES = "chrome://flock/locale/people/people.properties";

const FLICKR_IDENTITY_URN_PREFIX = "urn:flock:identity:flickr:";
const RDFS = Components.classes["@mozilla.org/rdf/rdf-service;1"]
                       .getService(Components.interfaces.nsIRDFService);

// The delay between two refreshes when the sidebar is closed (in seconds)
const FLICKR_REFRESH_INTERVAL = 3600; // 60 minutes
// The delay between two refreshes when the sidebar is open (in seconds)
const FLICKR_SHORT_INTERVAL = 3600; // 60 minutes
// The first time, only get photos not older than one week
const MEDIA_INITIAL_FETCH = 7 * 24 * 3600;
const MILLISEC = 1000;

var gCompTK;
function getCompTK() {
  if (!gCompTK) {
    gCompTK = Components.classes["@flock.com/singleton;1"]
                        .getService(Components.interfaces.flockISingleton)
                        .getSingleton("chrome://browser/content/flock/services/common/load-compTK.js")
                        .wrappedJSObject;
  }
  return gCompTK;
}

var gFlickrAPI;

function Namespace(ns) { return function (arg) { return RDFS.GetResource (ns+arg); } }
const FLRDF = Namespace("http://flock.com/rdf#");

const FLICKR_STRING_BUNDLE = "chrome://flock/locale/services/flickr.properties";
const SERVICES_STRING_BUNDLE = "services/services";
const SERVICES_PHOTO_FEED_COMMENT_STRING = "flock.photo.feeds.comment.title";

// String defaults... may be updated later through Web Detective
var gStrings = {
  "domains": "flickr.com,yahoo.com",
  "userlogin": "http://flickr.com/signin/",
  "userprofile": "http://www.flickr.com/people/%accountid%/",
  "photopage": "http://www.flickr.com/photos/%accountid%/%photoid%/",
  "commentsreceivedRSS": "http://api.flickr.com/services/feeds/activity.gne?id=%accountid%&format=rss_200",
  "staticimage": "http://static.flickr.com/%server%/%photoid%_%secret%%size%.jpg",
  "buddyicon": "http://www.flickr.com/images/buddyicon.jpg",
  "staticbuddyicon": "http://static.flickr.com/%iconserver%/buddyicons/%owner%.jpg",
  "nofriendavataricon": "http://static.flickr.com/0/buddyicons/%owner%.jpg",
  "nomeavataricon": "http://www.flickr.com/images/buddyicon.jpg?%owner%",
  "apikey": "92c2a562f0e9c2ed8dfe78f42a7734c7",
  "endpoint": "http://www.flickr.com/services/rest/",
  "uploadendpoint": "http://www.flickr.com/services/upload/",
  "authendpoint": "http://www.flickr.com/services/auth/"
};


// ====================================================
// ========== BEGIN General helper functions ==========
// ====================================================

function loadSubScript(spec)
{
  var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
              .getService(Components.interfaces.mozIJSSubScriptLoader);
  var context = {};
  loader.loadSubScript(spec, context);
  return context;
}

function loadLibraryFromSpec(aSpec)
{
  var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
                         .getService(Components.interfaces.mozIJSSubScriptLoader);
  loader.loadSubScript(aSpec);
}

loadSubScript("chrome://browser/content/utilityOverlay.js");

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

function flock_flickrBuildPhotoUrl(server, id, secret, size)
{
  var convertedSize = "";
  switch (size) {
    case "square": convertedSize = "_s"; break;
    case "thumbnail": convertedSize = "_t"; break;
    case "small": convertedSize = "_m"; break;
    case "medium": convertedSize = "_d"; break;
    case "large": convertedSize = "_b"; break;
    case "original": convertedSize = "_o"; break;
    default: convertedSize = "";
  }
  return gStrings["staticimage"].replace("%server%", server)
                                .replace("%photoid%", id)
                                .replace("%secret%", secret)
                                .replace("%size%", convertedSize);
}

function _getIdentityUrn(aAccountId, aUid) {
  var result = FLICKR_IDENTITY_URN_PREFIX
             + aAccountId + ":"
             + aUid;
  return result;
}

function flock_flickrBuildPageUrl(aUserID, aPhotoID) {
  return gStrings["photopage"].replace("%accountid%", aUserID).replace("%photoid%", aPhotoID);
}

// refresh the mediabar if it is open and contains private media
function flock_refreshMediabarIfHasPrivate() {
  var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                     .getService(Components.interfaces.nsIWindowMediator);
  var win = wm.getMostRecentWindow("navigator:browser");
  // if the mediabar is open
  if (win &&
      win.document.getElementById("mediabar") &&
      win.document.getElementById("mediabar").getAttribute("hidden") != "true" &&
      win.TopbarJSCtx().gPhotoDrawer &&
      win.TopbarJSCtx().gPhotoDrawer.mHasPrivateMedia)
  {
      win.TopbarJSCtx().gPhotoDrawer.refreshView();
  }
}

// ===============================================
// ========== BEGIN flickrService class ==========
// ===============================================

function flickrService()
{
  this.obs = Components.classes["@mozilla.org/observer-service;1"]
                       .getService(Components.interfaces.nsIObserverService);

  this.acUtils = Components.classes["@flock.com/account-utils;1"]
                           .getService(Components.interfaces.flockIAccountUtils);

  this.url = "http://www.flickr.com";
  this.mIsInitialized = false;
  this._ctk = {
    interfaces: [
      "nsISupports",
      "nsIClassInfo",
      "nsISupportsCString",
      "nsIObserver",
      "flockIPollingService",
      "flockIWebService",
      "flockIAuthenticateNewAccount",
      "flockIManageableWebService",
      "flockIMediaWebService",
      "flockIMediaUploadWebService",
      "flockIMediaEmbedWebService",
      "flockISocialWebService",
      "flockIRichContentDropHandler"
    ],
    shortName: "flickr",
    fullName: "Flickr",
    description: FLICKR_TITLE,
    favicon: FLICKR_FAVICON,
    CID: FLICKR_CID,
    contractID: FLICKR_CONTRACTID,
    accountClass: flickrAccount,
    needPassword: false
  };
  FlockSvcUtils.flockIMediaEmbedWebService
               .addDefaultMethod(this, "getSharingContent");
  this._logger = Cc['@flock.com/logger;1'].createInstance(Ci.flockILogger);
  this._logger.init('flickr');
  this._profiler = Cc["@flock.com/profiler;1"].getService(Ci.flockIProfiler);

  var sbs = Cc["@mozilla.org/intl/stringbundle;1"]
          .getService(Ci.nsIStringBundleService);
  var bundle = sbs.createBundle(FLICKR_STRING_BUNDLE);

  this._channels = {
    "special:recent": {
      title: bundle.GetStringFromName("flock.flickr.title.recent"),
      supportsSearch: false
    },
    "special:interestingness": {
      title: bundle.GetStringFromName("flock.flickr.title.interestingness"),
      supportsSearch: true
    }
  };
  this.init();
}

flickrService.prototype.serviceName = "Flickr";
flickrService.prototype.shortName = "flickr";

flickrService.prototype.handlePhotosResult =
function (aXML, aUserid)
{
  var rval = [];
  var photoList = aXML.getElementsByTagName("photo");
  for (var i = 0; i < photoList.length; i++) {
    var photo = photoList[i];

    var id = photo.getAttribute("id");
    var server = photo.getAttribute("server");
    var secret = photo.getAttribute("secret");
    var title = photo.getAttribute("title");
    var owner = photo.getAttribute("owner");
    if (!owner) owner = aUserid;
    var owner_name = photo.getAttribute("ownername");
    var date_upload = photo.getAttribute("dateupload");
    var date_added = photo.getAttribute("dateadded");
    
    var icon_server = photo.getAttribute("iconserver");
    var square_url = flock_flickrBuildPhotoUrl(server,id,secret,'square');
    var small_url = flock_flickrBuildPhotoUrl(server,id,secret,'small');
    var med_url = flock_flickrBuildPhotoUrl(server,id,secret,'medium');
    var page_url = flock_flickrBuildPageUrl(owner,id);
    var newMediaItem = Cc["@flock.com/photo;1"]
                       .createInstance(Ci.flockIMediaItem);
    newMediaItem.init(this.shortName, null);
    newMediaItem.webPageUrl = page_url;
    newMediaItem.thumbnail = square_url;
    newMediaItem.midSizePhoto = small_url;
    newMediaItem.largeSizePhoto = med_url;
    newMediaItem.username = owner_name;
    newMediaItem.userid = owner;
    newMediaItem.title = title;
    newMediaItem.id = id;
    if (icon_server == '1') {
      newMediaItem.icon = gStrings["buddyicon"];
    }
    else {
      newMediaItem.icon = gStrings["staticbuddyicon"]
                      .replace("%iconserver%", icon_server)
                      .replace("%owner%", owner);
    }
    newMediaItem.uploadDate = parseInt(date_added ? date_added : date_upload)
                            * 1000;

    // if the photo result set doesn't return back the ispublic
    // attribute, we assume that the server is returning only
    // public photos back in the response
    if (photo.hasAttribute("ispublic")) {
      var ispublic = photo.getAttribute("ispublic");
      if (ispublic =="1") ispublic = "true";
      else ispublic = "false";
    } else {
      ispublic = "true";
    }

    newMediaItem.is_public = ispublic;
    newMediaItem.is_video = false;

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


flickrService.prototype.handleContactsResult =
function (aXML)
{
  var rval = [];
  var contactList = aXML.getElementsByTagName("contact");
  for (var i = 0; i < contactList.length; i++) {
    var contact = contactList[i];
    var nsid = contact.getAttribute("nsid");
    var username = contact.getAttribute("username");
    var fullname = contact.getAttribute("fullname");
    var family = contact.getAttribute("family");
    var friend = contact.getAttribute("friend");
    var icon_server = contact.getAttribute("iconserver");

    if (true || family == "1" || friend == "1") {
      this._logger.debug(".handleContactsResult(): id=" + nsid
                        + " username=" + username
                        + " family=" + family
                        + " friend=" + friend);

      var newContact = {};
      newContact.id = nsid;
      newContact.username = username;
      newContact.fullname = username;
      
      // If friend has no avatar, set it to null here and people sidebar
      // code will fill it with the standard Flock no avatar image.
      var noAvatarUrl = gStrings["nofriendavataricon"].replace("%owner%", nsid);
      var avatarUrl = gStrings["staticbuddyicon"]
                             .replace("%iconserver%", icon_server)
                             .replace("%owner%", nsid);
      if (noAvatarUrl == avatarUrl) {
        newContact.avatarUrl = null;
      } else {
        newContact.avatarUrl = avatarUrl;
      }

      rval[nsid] = newContact;
    }
  }
  return rval;
}

flickrService.prototype.handleAlbumsResult =
function (aXML)
{
  var rval = [];
  var photosetList = aXML.getElementsByTagName("photoset");
  var titleList = aXML.getElementsByTagName("title");
  for (var i = 0; i < photosetList.length; i++) {
    var id = photosetList[i].getAttribute("id");
    var title = titleList[i].firstChild.nodeValue;

    var newAlbum = Components.classes[FLOCK_PHOTO_ALBUM_CONTRACTID]
                             .createInstance(flockIPhotoAlbum);
    newAlbum.id = id;
    newAlbum.title = title;

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

flickrService.prototype._getContacts =
function flickrService__getContacts(aListener)
{
  this._logger.info("._getContacts(...)");
  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      var peeps = inst.handleContactsResult(aXML);
      var result = {
        wrappedJS: peeps
      }
      aListener.onGetContactsResult(result);
    },
    onError: function (aError) {
      aListener.onError(aError);
    }
  }
  var params = {};
  var dict = params2Dictionary(params);
  this.authenticatedCall(myListener, "flickr.contacts.getList", dict);
}


flickrService.prototype.createAlbum =
function (aListener, aAlbumName)
{
  // trim white space from front and end of string
  aAlbumName = aAlbumName.replace(/^\s+|\s+$/g, "");
  if (aAlbumName) {
    var newAlbum = Components.classes[FLOCK_PHOTO_ALBUM_CONTRACTID]
                             .createInstance(flockIPhotoAlbum);
    newAlbum.title = aAlbumName;
    var date = new Date();  //hopefully this won't collide with an actual id!
    newAlbum.id = date.getTime();
    this.api.fakeAlbums[newAlbum.id] = newAlbum;
    aListener.onSuccess(newAlbum, "");
  } else {
    var error = Cc[FLOCK_ERROR_CONTRACTID].createInstance(Ci.flockIError);
    error.errorCode = error.PHOTOSERVICE_EMPTY_ALBUMNAME;
    aListener.onError(null, null, error);
  }
}

flickrService.prototype.getAlbums =
function flickrService_getAlbums(aListener, aUsername)
{
  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      try {
        var rval = inst.handleAlbumsResult(aXML);
        var albumsEnum = {
          hasMoreElements: function albumsEnum_hasMoreElements() {
            return (rval.length > 0);
          },
          getNext: function albumsEnum_getNext() {
            return rval.shift();
          },
          QueryInterface: function albumsEnum_QI(aIID) {
            if (aIID.equals(Ci.nsISimpleEnumerator)) {
              return this;
            }
            throw Cr.NS_ERROR_NO_INTERFACE;
          }
        };
        aListener.onSuccess(albumsEnum, "");
      } catch(e) {
        aListener.onError(null, null, null);
      }
    },
    onError: function (aXML) {
      aListener.onError(null, null, null);
    }
  }
  var params = {};
  if (aUsername) {
    params.user_id  = aUsername;
  }
  var dict = params2Dictionary(params);
  if (this.api.getAuthUser()) {
    this.authenticatedCall(myListener, "flickr.photosets.getList", dict);
  } else {
    this.call(myListener, "flickr.photosets.getList", dict);
  }
}

flickrService.prototype.getAccountStatus =
function flickrService_getAccountStatus(aListener)
{
  var inst = this;
  var myListener = {
    onResult:function (aXML) {
      try {
        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.getElementsByTagName('username')[0].firstChild.nodeValue;
        var bandwidth = aXML.getElementsByTagName('bandwidth')[0];
        var maxFileSize = aXML.getElementsByTagName('filesize')[0];
        var isPro = aXML.getElementsByTagName('user')[0].getAttribute('ispro');
        var maxSpace = bandwidth.getAttribute("max");
        var usedSpace = bandwidth.getAttribute("used");
        result.setPropertyAsAString("username", username);
        result.setPropertyAsAString("maxSpace", maxSpace);
        result.setPropertyAsAString("usedSpace", usedSpace);
        result.setPropertyAsAString("maxFileSize", maxFileSize);
        result.setPropertyAsAString("usageUnits", "bytes");
        result.setPropertyAsBool("isPremium", (isPro == "1"));
        aListener.onSuccess(result, "");
      } catch (ex) {
        inst._logger.info('getaccountstatus error >>>>>>>>>>>>>>>>>>>>' + ex);
      }
    },
    onError: function (aError) {
      aListener.onError(aError);
    }
  }
  var params = {};
  var dict = params2Dictionary(params);
  this.authenticatedCall(myListener, "flickr.people.getUploadStatus", dict);
}

flickrService.prototype.getContactsPhotos =
function flickrService_getContactsPhotos(aListener)
{
  // return; // XXX TODO FIXME (JMC): Save me from contacts' photos hanging my browser!
  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      var rval = inst.handlePhotosResult(aXML);
      var resultsEnum = {
        hasMoreElements: function resultsEnum_hasMoreElements() {
          return (rval.length>0);
        },
        getNext: function resultsEnum_getNext() {
          return rval.shift();
        },
        QueryInterface: function resultsEnum_QueryInterface(aIID) {
          if (aIID.equals(Ci.nsISimpleEnumerator)) {
            return this;
          }
          throw Cr.NS_ERROR_NO_INTERFACE;
        }
      }
      aListener.onSuccess(resultsEnum, "");
    },
    onError: function (aXML) {
      aListener.onError(null, null, null);
    }
  }
  var params = {};
  params.single_photo = "1";
  params.include_self = "1";
  params.extras = "owner_name,license,date_upload,icon_server";
  var dict = params2Dictionary(params);
  this.authenticatedCall(myListener, "flickr.photos.getContactsPhotos", dict);
}

flickrService.prototype.findByUsername =
function flickrService_findByUsername(aListener, aUsername)
{
  var inst = this;
  var userListener =
  function fls_findByUsername_ul(aListener) {
    this.mListener = aListener;
  }
  userListener.prototype.onGetInfo =
  function fls_findByUsername_ul_onGetInfo(aPerson) {
    this.mListener.onSuccess(aPerson, "");
  }
  userListener.prototype.onError =
  function fls_findByUsername_ul_onError(aError) {
    inst._logger.info(aError);
    this.mListener.onError(null, null, null);
  }
  var myListener = {
    onResult: function fls_findByUsername_ml_onResult(aXML) {
      var user = aXML.getElementsByTagName("user")[0];
      var id = user.getAttribute("id");
      inst.people_getInfo(new userListener(aListener), id);
    },
    onError: function fls_findByUsername_ml_onError(aXML) {
      aListener.onError(null, null, null);
    }
  }
  var params = {};
  params.username = aUsername;
  var dict = params2Dictionary(params);
  this.call(myListener, "flickr.people.findByUsername", dict);
}


flickrService.prototype.lookupUser =
function (aListener, aURL)
{
  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      var newUserObj = Components.classes[FLOCK_PHOTOPERSON_CONTRACTID]
                                 .createInstance(flockIPhotoPerson);
      var user = aXML.getElementsByTagName("user")[0];
      newUserObj.service = inst;
      newUserObj.id = user.getAttribute("id");
      newUserObj.username = user.getElementsByTagName("username")[0].childNodes[0].nodeValue;
      newUserObj.fullname = newUserObj.username;
      aListener.onLookupUser(newUserObj);
    },
    onError: function (aXML) {
      aListener.onError(aXML);
    }
  }
  var params = {};
  params.url = aURL;
  var dict = params2Dictionary(params);
  this.call(myListener, "flickr.urls.lookupUser", dict);
}

flickrService.prototype.people_getInfo =
function (aListener, aUserId)
{
  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      var newUserObj = Cc["@flock.com/person;1"]
                       .createInstance(Ci.flockIPerson);
      var person = aXML.getElementsByTagName("person")[0];
      var icon_server = person.getAttribute("iconserver");
      var count = person.getElementsByTagName("photos")[0]
                        .getElementsByTagName("count")[0]
                        .firstChild
                        .nodeValue;
      newUserObj.serviceId = inst.contractId;
      newUserObj.accountId = person.getAttribute("id");
      newUserObj.name = person.getElementsByTagName('username')[0]
                              .firstChild
                              .nodeValue;
      newUserObj.avatar = gStrings["staticbuddyicon"]
                          .replace("%iconserver%", icon_server)
                          .replace("%owner%", newUserObj.accountId);
      newUserObj.totalMedia = parseInt(count);
      aListener.onGetInfo(newUserObj);
    },
    onError: function (aXML) {
      aListener.onError(aXML);
    }
  }
  var params = {};
  params.user_id = aUserId;
  var dict = params2Dictionary(params);
  this.call(myListener, "flickr.people.getInfo", dict);
}

flickrService.prototype.queryChannel =
function flickrService_queryChannel(aListener,
                                    aQueryString,
                                    aCount,
                                    aPage,
                                    aRequestId)
{
  var aQuery = new queryHelper(aQueryString);
  // return; // XXX TODO FIXME: This is killing performance!
  var inst=this;
  var myListener = {
    onResult: function (aXML) {
      var rval = inst.handlePhotosResult(aXML);
      var resultsEnum = {
        hasMoreElements: function resultsEnum_hasMoreElements() {
          return (rval.length>0);
        },
        getNext: function resultsEnum_getNext() {
          return rval.shift();
        },
        QueryInterface: function resultsEnum_QueryInterface(aIID) {
          if (aIID.equals(Ci.nsISimpleEnumerator)) {
            return this;
          }
          throw Cr.NS_ERROR_NO_INTERFACE;
        }
      }
      aListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function (aError) {
      aListener.onError(null, aRequestId, aError);
    }
  }

  var params = {
    per_page: aCount,
    page: aPage,
    extras: "owner_name,icon_server,date_upload,license"
  };

  if (aQuery.getKey('special') == 'recent') {
    if (aQuery.hasKey('search')) {
      params.text = aQuery.getKey('search');
      var dict = params2Dictionary(params);
      this.call(myListener, "flickr.photos.search", dict);
    } else {
      var dict = params2Dictionary(params);
      this.call(myListener, "flickr.photos.getRecent", dict)
    }
  }

  if (aQuery.getKey("special") == 'interestingness') {
    if (aQuery.hasKey('search')) {
      params.text = aQuery.getKey('search');
      params.sort = 'interestingness-desc';
      var dict = params2Dictionary(params);
      this.call(myListener, "flickr.photos.search", dict);
    } else {
      var dict = params2Dictionary(params);
      this.call(myListener, "flickr.interestingness.getList", dict)
    }
  }

  if (aQuery.hasKey('search')) {
    aText = aQuery.getKey("search");
    // If search string only contains whitespace, consider it blank
    if (aText.replace(/\s/g, "") == "") {
      aText = "";
    }
    if (aText && aText.length) {
      if (aQuery.sort) {
        params.sort = aQuery.sort;
      }
      params.text = aText;
      var dict = params2Dictionary(params);
      this.call(myListener, "flickr.photos.search", dict);
    } else {
      // No search string so just return empty result
      var rval = [];
      var emptyEnum = {
        hasMoreElements: function emptyEnum_hasMoreElements() {
          return (rval.length>0);
        },
        getNext: function emptyEnum_getNext() {
          return rval.shift();
        },
        QueryInterface: function emptyEnum_QueryInterface(aIID) {
          if (aIID.equals(Ci.nsISimpleEnumerator)) {
            return this;
          }
          throw Cr.NS_ERROR_NO_INTERFACE;
        }
      };
      aListener.onSuccess(emptyEnum, aRequestId);
    }
  }
}

flickrService.prototype.poolSearch =
function flickrService_poolSearch(aListener,
                                  aQueryString,
                                  aCount,
                                  aPage,
                                  aRequestId)
{
  var aQuery = new queryHelper(aQueryString);
  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      var rval = inst.handlePhotosResult(aXML);
      var resultsEnum = {
        hasMoreElements: function resultsEnum_hasMoreElements() {
          return (rval.length>0);
        },
        getNext: function resultsEnum_getNext() {
          return rval.shift();
        },
        QueryInterface: function resultsEnum_QueryInterface(aIID) {
          if (aIID.equals(Ci.nsISimpleEnumerator)) {
            return this;
          }
          throw Cr.NS_ERROR_NO_INTERFACE;
        }
      }
      aListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function (aError) {
      aListener.onError(null, aRequestId, aError);
    }
  }

/*
  var aText;

  if (aQuery.split('?').length > 1) {
    aText = aQuery.split('?')[1];
    aQuery = aQuery.split('?')[0];
  }

  aQuery = aQuery.split(':')[1]
  */

  var params = {
    group_id: aQuery.pool,
    per_page: aCount,
    page: aPage,
    extras: "owner_name,icon_server,date_upload,license"
  };

  if (aQuery.search) {
    params.tags = aQuery.search;
    var dict = params2Dictionary(params);
    this.call(myListener, "flickr.groups.pools.getPhotos", dict);
  } else {
    var dict = params2Dictionary(params);
    this.call(myListener, "flickr.groups.pools.getPhotos", dict)
  }
}

flickrService.prototype.search =
function flickrService_search(aListener,
                              aQueryString,
                              aCount,
                              aPage,
                              aRequestId)
{
  this._logger.info(".search(" + aQueryString + ")");

  var aQuery = new queryHelper(aQueryString);
  if (aQuery.pool) {
    this.poolSearch(aListener, aQueryString, aCount, aPage, aRequestId);
    return;
  }
  if (!aQuery.user) {
    this.queryChannel(aListener, aQueryString, aCount, aPage, aRequestId);
    return;
  }
  var aUserid = aQuery.user;
  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      var rval = inst.handlePhotosResult(aXML, aUserid);
      var resultsEnum = {
        hasMoreElements: function resultsEnum_hasMoreElements() {
          return (rval.length>0);
        },
        getNext: function resultsEnum_getNext() {
          return rval.shift();
        },
        QueryInterface: function resultsEnum_QueryInterface(aIID) {
          if (aIID.equals(Ci.nsISimpleEnumerator)) {
            return this;
          }
          throw Cr.NS_ERROR_NO_INTERFACE;
        }
      }

      aListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function (aError) {
      aListener.onError(null, aRequestId, aError);
    }
  }
  // Flickr return the photo in the reverse side when calling flickr.photosets.getPhotos!
  // So we need to enumerate backward :-/
  var myPhotosetListener = {
    onResult: function (aXML) {
      var rval = inst.handlePhotosResult(aXML, aUserid);
      var resultsEnum = {
        hasMoreElements: function resultsEnum_hasMoreElements() {
          return (rval.length>0);
        },
        getNext: function resultsEnum_getNext() {
          return rval.pop();
        },
        QueryInterface: function resultsEnum_QueryInterface(aIID) {
          if (aIID.equals(Ci.nsISimpleEnumerator)) {
            return this;
          }
          throw Cr.NS_ERROR_NO_INTERFACE;
        }
      }
      aListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function (aError) {
      aListener.onError(null, aRequestId, aError);
    }
  }

  var params = {};
  if (aUserid && aUserid.length) params.user_id = aUserid;
  params.per_page = aCount;
  params.page = aPage;

  if (aQuery.search) params.text = aQuery.search;

  params.tag_mode = "all";
  params.extras = "owner_name,license,date_upload,icon_server";
  if (aQuery.album) {
    params.photoset_id = aQuery.album;
  }

  var dict = params2Dictionary(params);
  if (params.photoset_id) {
    if (aPage > 1) return; // this is because the flickr call doesn't support pagination [bug 4770]
    dict = params2Dictionary(params);
    if (this.getAuthUser())
      this.authenticatedCall(myPhotosetListener, "flickr.photosets.getPhotos", dict);
    else
      this.call(myPhotosetListener, "flickr.photosets.getPhotos", dict);
  } else if (params.user_id || params.text) {
    if (this.api.getAuthUser()) {
      this.authenticatedCall(myListener, "flickr.photos.search", dict);
    }
    else {
      this.call(myListener, "flickr.photos.search", dict);
    }
  }
}

flickrService.prototype.supportsSearch =
function flickrService_supportsSearch( aQueryString ) {
  var aQuery = new queryHelper(aQueryString);

  if (aQuery.special) {
    var channel = this._channels["special:" + aQuery.special];
    if (channel) {
      return channel.supportsSearch;
    }
  }

  if (aQuery.album) {
    return false;
  }
  if (aQuery.user) {
    return true;
  }
  if (aQuery.search) {
    return false;
  }
  return false;
}

flickrService.prototype.mSupportsTitle = true;
flickrService.prototype.mSupportsTags = true;
flickrService.prototype.mSupportsContacts = true;
flickrService.prototype.mSupportsPrivacy = true;

flickrService.prototype.supportsFeature =
function flickrService_supportsFeature(aFeature)
{
  var supports = {};
  supports.tags = true;
  supports.title = true;
  supports.fileName = false;
  supports.contacts = true;
  supports.privacy = true;
  supports.albumCreation = true;
  return (supports[aFeature] == true);
}

flickrService.prototype.init =
function flickrService_init()
{
  // Prevent re-entry
  if (this.mIsInitialized) return;
  this.mIsInitialized = true;

  this._logger.info(".init()");
  var evtID = this._profiler.profileEventStart("flickr-init");

  // Determine whether this service has been disabled, and unregister if so.
  this.prefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
  if ( this.prefService.getPrefType(SERVICE_ENABLED_PREF) &&
       !this.prefService.getBoolPref(SERVICE_ENABLED_PREF) )
  {
    this._logger.info("Pref "+SERVICE_ENABLED_PREF+" set to FALSE... not initializing.");
    var catMgr = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
    catMgr.deleteCategoryEntry("wsm-startup", CATEGORY_COMPONENT_NAME, true);
    catMgr.deleteCategoryEntry("flockWebService", CATEGORY_ENTRY_NAME, true);
    catMgr.deleteCategoryEntry("flockMediaProvider", CATEGORY_ENTRY_NAME, true);
    return;
  }

  if (!gFlickrAPI) {
    gFlickrAPI = new FlickrAPI();
  }
  this.api = gFlickrAPI;
  var inst = this;

  this.faves_coop = Components.classes["@flock.com/singleton;1"]
                              .getService(Components.interfaces.flockISingleton)
                              .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                              .wrappedJSObject;

  this.urn = "urn:flickr:service";
  this.svcCoopObj = new this.faves_coop.Service(
    this.urn,
    {
      name: "flickr",
      desc: "The Flickr Service"
    }
  );
  this.svcCoopObj.serviceId = FLICKR_CONTRACTID;

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

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

  // Load auth manager
  this._authMgr = Cc["@flock.com/authmgr;1"]
                  .getService(Ci.flockIAuthenticationManager);
  this._authMgr.joinNetwork(FLICKR_CONTRACTID, "yahoo");

  // Update auth states
  try {
    if (this.webDetective.detectCookies("flickr", "loggedout", null)) {
      this.acUtils.markAllAccountsAsLoggedOut(FLICKR_CONTRACTID);
      this.api.logout();
    }
  } catch (ex) {
    this._logger.error("ERROR updating auth states for Flickr: "+ex);
  }

  // On browser restart if a Flickr coop account is still marked as
  // authenticated we also need to reauth the api. We do this by calling
  // login() on the xpcom account obj. However, we have to wait while
  // the Flickr service is finished constructing itself.
  var reloginCb = {
    notify: function reloginCallback_notify(aTimer) {
      inst._logger.info("reloginCallback.notify()")
      var acctURN = inst.acUtils
                        .getFirstAuthenticatedAccountForService(inst.svcCoopObj
                                                                    .serviceId);

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

  var timerFunc = {
    inProgress: false,
    count: 0,
    observe: function(subject, topic, state) {
      //if (topic == 'nsPref:changed' && state == FLICKR_TOKEN_PREF_ID) {
      //  var oldToken = flock_getCharPref(FLICKR_TOKEN_PREF_ID);
      //  if (oldToken && oldToken.length && !inst.api.isLoggedIn()) {
      //    this.inProgress = true;
      //    inst.api.checkToken(this, oldToken);
      //  }
      //}
    },
    notify: function(aTimer) {
      if (this.inProgress) return;
      if (inst.api.isLoggedIn()) {
        inst.api.processPendingUploadTickets();
        inst.api.processPendingAlbumAdditions();
      }
    },
    onResult: function(aXML) {
      this.inProgress = false;
    },
    onSuccess: function(aXML) {
      this.inProgress = false;
    },
    onError: function(aError) {
      inst.logout();
      this.inProgress = false;
    }
  }
  //timerFunc.observe(null, 'nsPref:changed', FLICKR_TOKEN_PREF_ID);
  timerFunc.notify();
  //var prefs = Components.classes['@mozilla.org/preferences-service;1']
  //                      .getService(Components.interfaces.nsIPrefBranch2);
  //prefs.addObserver(FLICKR_TOKEN_PREF_ID, timerFunc, false);
  timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  timer.initWithCallback(timerFunc, 5 * 1 * 1000, 1);  //re-check token

  this._profiler.profileEventEnd(evtID, "");
}

flickrService.prototype.__defineGetter__('supportsTitle', function () { return this.mSupportsTitle; })
flickrService.prototype.__defineGetter__('supportsTags', function () { return this.mSupportsTags; })
flickrService.prototype.__defineGetter__('supportsContacts', function () { return this.mSupportsContacts; })
flickrService.prototype.__defineGetter__('supportsPrivacy', function () { return this.mSupportsPrivacy; })

flickrService.prototype.getAuthState =
function flickrService_getAuthState()
{
  return this.state;
}

flickrService.prototype.__defineGetter__('authState', function () { return this.state; })

flickrService.prototype.logout =
function flickrService_logout()
{
  this.api.logout();
  this.acUtils.removeCookies(this.webDetective.getSessionCookies("flickr"));
}

flickrService.prototype.getAuthUser = function () {
  return this.api.getAuthUser();
}

flickrService.prototype.__defineGetter__('isUploading', function () { return this.running; })

flickrService.prototype.dictionary2Params =
function flickrService_dictionary2Params(aDictionary)
{
  var params = {};
  var obj = {};
  var count = {};
  var keys = aDictionary.getKeys(count, obj);
  for (var i = 0; i < keys.length;++i) {
    var supports = aDictionary.getValue(keys[i]);
    var supportsString = supports.QueryInterface(Components.interfaces.nsISupportsString);
    var val = supportsString.toString();
    params[keys[i]] = val;
  }
  return params;
}

flickrService.prototype.call =
function flickrService_call(aListener, aMethod, aDictionary)
{
  var params = this.dictionary2Params(aDictionary);
  this.api.call(aListener, aMethod, params);
}

flickrService.prototype.authenticatedCall =
function flickrService_authenticatedCall(aListener, aMethod, aDictionary)
{
  if (!this.api.getAuthUser()) {
    throw "not logged in";
  }
  var params = this.dictionary2Params(aDictionary);
  this.api.authenticatedCall(aListener, aMethod, params);
}

flickrService.prototype.upload =
function flickrService_upload(aListener, aUpload, aFile)
{
  var params = {};
  params.title = aUpload.title;
  params.description = aUpload.description;
  params.is_family = aUpload.is_family;
  params.is_friend = aUpload.is_friend;
  params.is_public = aUpload.is_public;
  params.async = "1";
  params.tags = aUpload.tags;
  this.api.upload(aListener, aFile, params, aUpload);
}

flickrService.prototype.cancelUpload =
function flickrService_cancelUpload()
{
  try { this.api.uploader.req.abort(); }
  catch(e){};
}

flickrService.prototype.addCoopPerson =
function flickrService_addCoopPerson(aPhotoPerson, aCoopAccount) {
  var person = aPhotoPerson;
  var profileURL = gStrings["userprofile"].replace("%accountid%", person.id);
  // We include the current accountId in the identity urn to prevent friend
  // collisions if multiple Facebook accounts have the same friend.
  var identityUrn = _getIdentityUrn(aCoopAccount.accountId,
                                    person.id);
  var updating = this.faves_coop.Identity.exists(identityUrn);
  var identity;
  if (updating) {
    identity = this.faves_coop.get(identityUrn);
    if (this._personUpdateRequired (aCoopAccount,person)) {
      // update data of the identity coop obj here
      identity.name = person.username;
      identity.avatar = person.avatarUrl;
      identity.URL = profileURL;
    }
  } else {
    identity = new this.faves_coop.Identity(
      identityUrn,
      {
        name: person.username,
        serviceId: FLICKR_CONTRACTID,
        accountId: person.id,
        avatar: person.avatarUrl,
        statusMessage: '',
        lastUpdate: 0,
        lastUpdateType: "media",
        URL: profileURL
      }
    );
    aCoopAccount.friendsList.children.add(identity);
  }

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

flickrService.prototype._incrementMedia =
function flickrService__incrementMedia(aUid, aCount, aLatest) {
  this._logger.info("._incrementMedia('" + aUid + "')");
  var currAcct = this.getAuthenticatedAccount();
  var currAcctId = currAcct.getParam("accountId");

  // Update the count on the identity...
  var identityUrn = _getIdentityUrn(currAcctId, aUid);
  identity = this.faves_coop.get(identityUrn);

  if (!identity.lastUpdate) {
    identity.lastUpdate = aLatest;
    var lastweek = parseInt(Date.now() / 1000) - MEDIA_INITIAL_FETCH;
    // Friend uploaded pictures in the last week!
    if (aLatest > lastweek) {
      identity.unseenMedia += aCount;
    }
  } else if (aLatest > identity.lastUpdate) {
    identity.unseenMedia += aCount;
    identity.lastUpdate = aLatest;
  }
}

flickrService.prototype._personUpdateRequired =
function flickrService__personUpdateRequired(aCoopPerson, aPerson) {
  return (aCoopPerson.name != aPerson.fullname)
      || (aCoopPerson.avatar != aPerson.pic_square);
}

flickrService.prototype._updateUserCommentCount =
function flickrService__updateUserCommentCount(aCoopAccount)
{
  this._logger.info("._updateUserCommentCount('" + aCoopAccount.id() + "')");
  var timespanMS = 0;
  var lastClearedDateSec = 0;
  if (!aCoopAccount.lastUpdateDate) {
    // First time refreshing this account, so notify on all photo comments for
    // the past week.
    aCoopAccount.lastUpdateDate = new Date();
    var updateTimeMS =  aCoopAccount.lastUpdateDate.getTime();
    var lastweek = updateTimeMS - (MEDIA_INITIAL_FETCH * MILLISEC);
    timespanMS = updateTimeMS - lastweek;
    this.acUtils.setAccountCustomParamValue(aCoopAccount.id(),
                                            "flickr_comments", 0);
    this.acUtils.setAccountCustomParamValue(aCoopAccount.id(),
                                            "flickr_comments_timespan", 0);
    lastClearedDateSec = parseInt(lastweek / MILLISEC);
  } else {
    var today = new Date();
    var updateTimeMS =  aCoopAccount.lastUpdateDate.getTime();
    timespanMS = today.getTime() - updateTimeMS;
    lastClearedDateSec = parseInt(updateTimeMS / MILLISEC);
  }
  // This controls the time span (in hours) that photo comments will be shown
  // for when the user chooses to view comment noties.
  var timespanHrs = timespanMS / (60 * 60 * MILLISEC);
  var acc_timespanHrs = this.acUtils.getAccountCustomParam(aCoopAccount.id(),
                                                           "flickr_comments");
  var timespan = acc_timespanHrs + timespanHrs;
  this.acUtils.setAccountCustomParamValue(aCoopAccount.id(),
                                          "flickr_comments_timespan", timespan);
  
  var api_key = this.api_key;
  var params = {
    api_key: api_key,
    timeframe: timespanHrs + "h",
  };
  var inst = this;
  var commentListener = {
    onResult: function commentListener_onResult(aXML) {
      var items = aXML.getElementsByTagName("item");
      var lightUp = false;
      for (var i = 0; i< items.length; i++) {
        var events = items[i].getElementsByTagName("event");
        for (var j = 0; j< events.length; j++) {
          var dateadded = events[j].getAttribute("dateadded");
          // Dateadded greater than last checked time,
          // we have new comment to see! Light up!
          dateadded = parseInt(dateadded);
          if (lastClearedDateSec < dateadded) {
            var newdate = new Date();
            newdate.setTime(dateadded * MILLISEC);
            aCoopAccount.lastUpdateDate = newdate;
            var comments = inst.acUtils.getAccountCustomParam(aCoopAccount.id(),
                                                              "flickr_comments");
            inst.acUtils.setAccountCustomParamValue(aCoopAccount.id(),
                                                    "flickr_comments", ++comments);
            lightUp = true;
          }
        }
      }
      if (lightUp) {
        inst._lightPeopleIcon();
      }
    },
    onError: function commentListener_onError(aXML) {
    }
  };
  var dict = params2Dictionary(params);
  this.authenticatedCall(commentListener, "flickr.activity.userPhotos", dict);
}

flickrService.prototype._updateUserAccount =
function flickrService__updateUserAccount(aCoopAccount)
{
  this._logger.info("._updateUserAccount('" + aCoopAccount.id() + "')");
  var inst = this;
  var hr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
           .createInstance(Ci.nsIXMLHttpRequest);
  hr.onreadystatechange = function updateAccount_onreadystatechange(aEvent) {
    if (hr.readyState == 4) {
      var results = Cc["@mozilla.org/hash-property-bag;1"]
                    .createInstance(Ci.nsIWritablePropertyBag2);
      if (inst.webDetective.detectNoDOM("flickr", "accountinfo", "",
                                        hr.responseText, results))
      {
        inst._updateUserStatusFromResults(aCoopAccount, results);
        inst._updateUserCommentCount(aCoopAccount);
      }
    }
  }
  hr.open("GET", this.webDetective.getString("flickr", "scrapeAccountInfo",
                                             "http://www.flickr.com/"));
  hr.send(null);
}

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

flickrService.prototype._updateUserStatusFromResults =
function flickrService__updateUserStatusFromResults(aCoopAccount, aResults)
{
  this._logger.info("._updateUserStatusFromResults('" + aCoopAccount.id() + "')");
  var username = aResults.getPropertyAsAString("username");
  if (username && username.length) {
    aCoopAccount.name = username;
  }
  var avatarURL = aResults.getPropertyAsAString("avatarURL");
  if (avatarURL && avatarURL.length) {
    // If user account has no avatar, set it to null here and people sidebar
    // code will fill it with the standard Flock no avatar image.
    var accountid = aResults.getPropertyAsAString("accountid");
    var noAvatarUrl = gStrings["nomeavataricon"].replace("%owner%", accountid);
    if (noAvatarUrl == avatarURL) {
      aCoopAccount.avatar = null;
    } else {
      aCoopAccount.avatar = avatarURL;
    }
  }
  var messages = "0";
  try {
    messages = aResults.getPropertyAsAString("messages");
    if (messages && messages.length) {
      if (aCoopAccount.accountMessages < messages) {
        this._lightPeopleIcon();
      }
    }
    else {
      messages = "0";
    }
  } catch(ex) {}
  aCoopAccount.accountMessages = messages;
}

// BEGIN flockIPollingService
flickrService.prototype.refresh =
function flickrService_refresh(aURN, aListener)
{
  this._logger.info("{flockIPollingService}.refresh('"+aURN+"', ...)");
  var refreshItem = this.faves_coop.get(aURN);
  var inst = this;
  if (refreshItem instanceof this.faves_coop.Account) {
    var peopleHash;

    if (!this.api.isLoggedIn()) {
      this._logger.debug("api is not logged in - skipping refresh");
      aListener.onResult();
      return;
    }
    this._updateUserAccount(refreshItem);
    var recentPhotosListener = {
      onResult: function recentPhotosListener_onResult(aXML) {
        var photosList = aXML.getElementsByTagName("photo");
        for (var i = 0; i < photosList.length; i++) {
          var photo = photosList[i];
          var personId = photo.getAttribute("owner");
          var latest = photo.getAttribute("dateupload");

          // Add media info to people hash
          peopleHash[personId].media = {
            count: 1,
            latest: latest
          };
        }

        // Now that we have all we need, update the RDF
        function myWorker(aShouldYield) {
          // ADD or update existing people
          for (var uid in peopleHash) {
            inst.addCoopPerson(peopleHash[uid], refreshItem);
            if (aShouldYield()) {
              yield;
            }
          }

          // REMOVE locally people removed on the server
          var localEnum = refreshItem.friendsList.children.enumerate();
          while (localEnum.hasMoreElements()) {
            var identity = localEnum.getNext();
            if (!peopleHash[identity.accountId]) {
              inst._logger.info("Friend " + identity.accountId
                                          + " has been deleted on the server");
              refreshItem.friendsList.children.remove(identity);
              identity.destroy();
            }
          }

          if (inst.acUtils.isPeopleSidebarOpen()) {
            refreshItem.nextRefresh = new Date(Date.now()
                                               + FLICKR_SHORT_INTERVAL * 1000);
          }
          aListener.onResult();
        } // end myWorker()
        FlockScheduler.schedule(null, 0.05, 10, myWorker);
      },
      onError: function recentPhotosListener_onError(aXML) {
        inst._logger.error("recentPhotosListener error");
      }
    };

    this._getContacts({
      onGetContactsResult: function getContacts_onGetContactsResult(aResult) {
        peopleHash = aResult.wrappedJS;

        var api_key = inst.api_key;
        var params = {
          api_key: api_key,
          single_photo: 1,
          count: 50,
          extras: "date_upload"
        };

        var dict = params2Dictionary(params);
        inst.authenticatedCall(recentPhotosListener,
                               "flickr.photos.getContactsPhotos",
                               dict);
      },
      onError: function getContacts_onError(aError) {
        aListener.onError(aError);
      }
    });

  } else {
    throw CR.NS_ERROR_ABORT;
    this._logger.error("can't refresh " + aURN + " (unsupported type)");
    aListener.onError(null);
  }
}
// END flockIPollingService


flickrService.prototype.migrateAccount =
function flickrService_migrateAccount( aId, aUsername ) {
  this.init();

  var token = '';
  /*
  try {
    token = flock_getCharPref('flock.photo.flickr.token');
  } catch (e) { }
  */

  this.addAccount( aId, false, null, aUsername, token);
}

// BEGIN flockIWebService interface
flickrService.prototype.addAccount =
function flickrService_addAccount(aAccountID, aIsTransient, aListener, aUsername, aToken)
{
  this._logger.info("{flockIWebService}.addAccount('"+aAccountID+"', "+aIsTransient+")");

  if (!aUsername) {
    // Get the password associated with this account
    var pw = this.acUtils.getPassword(this.urn+':'+aAccountID);
    // FIX ME - name shouldn't be email address (pw.user) - ja
    var name = (pw) ? pw.user : aAccountID;
    var token = '';
    var pollable = false;
    var auth = false;
  } else {
    var name = aUsername;
    var token = aToken;
    var pollable = true;
    var auth = true;
  }
  // Account
  var url = gStrings["userprofile"].replace("%accountid%", aAccountID);
  var accountUrn = this.acUtils.createAccount(this,
                                              aAccountID,
                                              name,
                                              url,
                                              aIsTransient);
  var coopAccount = this.faves_coop.get(accountUrn);
  coopAccount.refreshInterval = FLICKR_REFRESH_INTERVAL;
  var list = Cc["@mozilla.org/hash-property-bag;1"]
             .createInstance(Ci.nsIWritablePropertyBag2);
  list.setPropertyAsAString("flickr_username", "");
  list.setPropertyAsAString("flickr_fullname", "");
  list.setPropertyAsAString("flickr_nsid", "");
  list.setPropertyAsInt32("flickr_comments", 0);
  list.setPropertyAsInt32("flickr_comments_timespan", 0);
  this.acUtils.addCustomParamsToAccount(list, coopAccount.id());
  // Instanciate account component
  var acct = this.getAccount(coopAccount.id());
  if (aListener) {
    aListener.onSuccess(acct, "addAccount");
  }
  return acct;
}
// END flockIWebService interface


// BEGIN flockIAuthenticateNewAccount interface
flickrService.prototype.authenticateNewAccount =
function flickrService_authenticateNewAccount(aListener)
{
  this._logger.info("{flockIAuthenticateNewAccount}.authenticateNewAccount(aListener)");
  aListener.onStart(null, "newaccountstarted");
  var inst = this;
  var frobListener = {
    onResult: function (aXML) {
      inst._logger.info(".authenticateNewAccount(): frobListener: onResult()");
      var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                         .getService(Components.interfaces.nsIWindowMediator);
      var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                         .getService(Components.interfaces.nsIWindowWatcher);
      var topWin = wm.getMostRecentWindow(null);
      var frob = aXML.getElementsByTagName("frob")[0].firstChild.nodeValue;
      var authURL = inst.api.getAuthUrl(frob, "write");
      var url = "chrome://browser/content/flock/photo/photoLoginWindow.xul?"
              + "url="+escape(authURL)+"&finalString=logout.gne";
      var chrome = "chrome,close,titlebar,resizable=yes,toolbar,dialog=no,"
                 + "scrollbars=yes,modal,centerscreen";
      topWin.open(url, "_blank", chrome);

      var acctURN = inst.acUtils.getFirstAuthenticatedAccountForService(FLICKR_CONTRACTID);
      var account = null;
      try {
        account = inst.getAccount(acctURN);
        if (!account) throw "ACCOUNT WAS NOT CREATED";
      } catch (ex) {
        inst._logger.info(".authenticateNewAccount(): frobListener: onResult(): ERROR: account was not created");
        if (aListener) aListener.onError(ex);
      }
      if (aListener) aListener.onSuccess(account, "", null);
    },
    onError: function (aError) {
      inst._logger.info(".authenticateNewAccount(): frobListener: onError()");
      if (aListener) aListener.onError(aError);
    }
  };
  this.api.call(frobListener, "flickr.auth.getFrob", null);
}
// END flockIAuthenticateNewAccount


// BEGIN flockIManageableWebService interface
flickrService.prototype.updateAccountStatusFromDocument =
function flickrService_updateAccountStatusFromDocument(aDocument, aAcctURN)
{
  this._logger.info("updateAccountStatusFromDocument('" + aAcctURN + "')");
  if (aAcctURN) {
    // We're logged in to this account
    var c_acct = this.faves_coop.get(aAcctURN);
    if (!c_acct.isAuthenticated) {
      var inst = this;
      var loginListener = {
        onSuccess: function (aSubject, aTopic) {
          inst._logger.info(".updateAccountStatusFromDocument(): loginListener: onSuccess()");
          inst._authMgr.reportCompleteLoginToNetwork(aAcctURN);
        },
        onError: function (aError) {
          inst._logger.info(".updateAccountStatusFromDocument(): loginListener: onError()");
          inst._logger.info(aError ? (aError.errorString) : "No details");
          c_acct.isAuthenticated = false;
        }
      };
      this.getAccount(aAcctURN).login(loginListener);
      // I'm going to somewhat prematurely assume that the authentication will
      // succeed.  If it fails, the listener will catch it.
      c_acct.isAuthenticated = true;
    }
  } else {
    // No login was detected.  So let's see if we can detect a logged out
    // condition.
    var flickrLogout =
      this.webDetective.detect("flickr", "loggedout", aDocument, null);
    var yahooNetworkLogout =
      this.webDetective.detect("flickr", "completeLogout", aDocument, null);
    if (flickrLogout || yahooNetworkLogout) {
      // Yes, we're logged out.  We do slightly different things depending on
      // whether we've just logged out of Flickr, or the whole Yahoo network.
      var loggedoutURN = null;
      if (flickrLogout) {
        loggedoutURN =
          this.acUtils.getFirstAuthenticatedAccountForService(FLICKR_CONTRACTID);
      }
      this.acUtils.markAllAccountsAsLoggedOut(FLICKR_CONTRACTID);
      this.api.logout();
      flock_refreshMediabarIfHasPrivate();
      if (flickrLogout && loggedoutURN) {
        this._authMgr.reportAccountAuthState(loggedoutURN, false);
      }
      if (yahooNetworkLogout) {
        this._authMgr.reportCompleteLogoutFromNetwork(FLICKR_CONTRACTID);
      }
    }
  }
}

flickrService.prototype.ownsDocument =
function flickrService_ownsDocument(aDocument)
{
  this._logger.debug("domain: " + aDocument.domain);

  // We need to override the default ownsDocument in this case because
  // multiple services share the same login page (yahoo.com in this case)
  return this.webDetective.detect("flickr", "ownsDocument", aDocument, null);
}
// END flockIManageableWebService interface


// BEGIN flockISocialWebService interface
flickrService.prototype.maxStatusLength = 0;
// END flockISocialWebService interface


// BEGIN flockIMediaWebService interface
// readonly attribute boolean supportsUsers;
flickrService.prototype.supportsUsers = true;

flickrService.prototype.decorateForMedia =
function flickrService_decorateForMedia(aDocument)
{
  this._logger.info("{flockIMediaWebService}.decorateForMedia(aDocument)");
  var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                          .createInstance(Components.interfaces.nsIWritablePropertyBag2);
  var mediaArr = [];
  if (this.webDetective.detect("flickr", "person", aDocument, results)) {
    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);
  }
  
  if (this.webDetective.detect("flickr", "pool", aDocument, results)) {
    var media = {
      name: results.getPropertyAsAString("groupname"),
      query: "pool:" + results.getPropertyAsAString("groupid") + "|albumlabel:" + results.getPropertyAsAString("groupname"),
      label: results.getPropertyAsAString("groupname"),
      favicon: this.icon,
      service: this.shortName
    }
    mediaArr.push(media);
  }
  
  if (mediaArr.length > 0) {
    if (!aDocument._flock_decorations) {
      aDocument._flock_decorations = {};
    }
    aDocument._flock_decorations.mediaArr = mediaArr;
    this.obs.notifyObservers(aDocument, "media", "media:update");
  }
}

flickrService.prototype.checkIsStreamUrl =
function flickrService_checkIsStreamUrl(aUrl)
{
  if (this.webDetective.detectNoDOM("flickr", "isStreamUrl", "", aUrl, null)) {
    this._logger.debug("Checking if url is flickr stream: YES: " + aUrl);
    return true;
  }
  this._logger.debug("Checking if url is flickr stream: NO: " + aUrl);
  return false;
}

flickrService.prototype.getMediaQueryFromURL =
function flickrService_getMediaQueryFromURL(aUrl, aListener)
{
  var myListener = {
    onResult:function (aXML) {
      try {
        var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                                .createInstance(Components.interfaces.nsIWritablePropertyBag2);
        var userID = aXML.getElementsByTagName('user')[0].getAttribute('id');
        var userName = aXML.getElementsByTagName('username')[0].firstChild.nodeValue;

        results.setPropertyAsAString("query", "user:" + userID + "|username:" + userName);
        results.setPropertyAsAString("title", userName);
        aListener.onSuccess(results, "query");
      } catch (ex) {
        aListener.onError(null, "Unable to get user.", null);
      }
    },
    onError: function (aError) {
        aListener.onError(null, aError.errorString, null);
    }
  }

  this._logger.debug("Finding User ID from Url: " + aUrl);
  var params = {};
  params.url = aUrl;
  var dict = params2Dictionary(params);
  this.call(myListener, "flickr.urls.lookupUser", dict);
}
// END flockIMediaWebService

// BEGIN flockIMediaEmbedWebService interface
// DEFAULT: boolean getSharingContent(in nsIDOMHTMLElement aSrc,
//                                    in nsIWritablePropertyBag2 aProp);
// END flockIMediaEmbedWebService interface

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


// BEGIN flockIRichContentDropHandler
flickrService.prototype.handleDrop =
function flickrService_handleDrop(aFlavours, aTextarea)
{
  this._logger.info(".handleDrop()");

  var dropCallback = function flickr_dropCallback(aFlav) {
    var data = {}, len = {};
    aFlavours.getTransferData(aFlav, data, len);
    var caretPos = aTextarea.selectionEnd;
    var currentValue = aTextarea.value;
    // Add a trailing space so that we don't mangle the url
    var nextChar = currentValue.charAt(caretPos);
    var trailingSpace = ((nextChar == "") ||
                         (nextChar == " ") || 
                         (nextChar == "\n"))
                      ? ""
                      : " ";
    // Only add a breadcrumb if the insertion point is at the end of
    // the text so that we don't duplicate breadcrumbs
    var breadcrumb = (aTextarea.value.length == aTextarea.selectionEnd)
                   ? Cc[FLOCK_RDDS_CONTRACTID]
                       .getService(Ci.flockIRichDNDService)
                       .getBreadcrumb("plain")
                   : "";

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

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


// ========== END flickrService class ==========



// ================================================
// ========== BEGIN XPCOM Module support ==========
// ================================================

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

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

// ========== END XPCOM Module support ==========


// ===========================================
// ========== BEGIN FlickrAPI class ==========
// ===========================================

function FlickrAPI() {
  this._logger = Components.classes['@flock.com/logger;1'].createInstance(Components.interfaces.flockILogger);
  this._logger.init('flickrAPI');
  this._logger.info('Created Flickr API Object');
  this.acUtils = Components.classes["@flock.com/account-utils;1"]
                           .getService(Components.interfaces.flockIAccountUtils);
  this.webDetective = this.acUtils.useWebDetective("flickr.xml");
  this.faves_coop = Components.classes["@flock.com/singleton;1"]
                              .getService(Components.interfaces.flockISingleton)
                              .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                              .wrappedJSObject;

  var inst = this;
  this.need2CreateAlbum = false;
  this.api_key = gStrings["apikey"];
  this.api_secret = "17b26c20558cf979";
  this.endpoint = gStrings["endpoint"];
  this.upload_endpoint = gStrings["uploadendpoint"];
  this.auth_endpoint = gStrings["authendpoint"];
  this.fakeAlbums = {};
  this.realishAlbums = {};

  this.req = null;
  this.frob = null;
  this.lastToken = new Date();
  this.authUser = null;
  this.filesToDelete = [];
  this.setEndpoint = function(aEndpoint) {
    this.endpoint = aEndpoint;
  };

  this.reset = function() {
    this.hasCreateAlbum = null;
  };

  this.getAuthUser = function() {
    return this.authUser;
  };
  this.getStoredToken = function(aListener) {
  };
  this.checkToken = function(aListener, aToken) {
    var tokenListener = {
      onResult: function (aXML) {
        var flickrUser = new FlickrUser();
        flickrUser.token = aXML.getElementsByTagName("token")[0].firstChild.nodeValue;
        var user = aXML.getElementsByTagName("user")[0];
        flickrUser.nsid = user.getAttribute("nsid");
        flickrUser.id = user.getAttribute("nsid");
        flickrUser.username = user.getAttribute("username");
        flickrUser.fullname = user.getAttribute("fullname");
        inst.authUser = flickrUser;

        inst._logger.info("token checks out");
        inst.lastToken = new Date();
        aListener.onSuccess(null, "");
      },
      onError: function (aError) {
        inst._logger.info("Token does not pass muster");
        inst.frob = null;
        aListener.onError(null);
      }
    }
    var token = aToken;
    var params = {
      auth_token: token
    };
    this.call(tokenListener, "flickr.auth.checkToken", params);
  };
  this.getToken = function(aAccountURN, aListener) {
    inst._logger.info("entering getToken");
    var acctCoopObj = this.faves_coop.get(aAccountURN);
    var flickrUser = new FlickrUser();

    // we have an existing token, let's see if it's valid
    if (acctCoopObj.authToken && acctCoopObj.authToken.length) {
      var checkTokenListener = {
        onSuccess : function(aSubject, aTopic) {
          // the token is aiighttt lets reuse it
          inst._reuseToken(aAccountURN, aListener);
        },

        onError: function(aError) {
          // something
          inst._getNewToken(aAccountURN, aListener);
        }
      };
      this.checkToken(checkTokenListener, acctCoopObj.authToken);
    } else {
      this._getNewToken(aAccountURN, aListener);
    }
  };
  this._reuseToken = function(aAccountURN, aListener)  {
    inst._logger.info("entering _reuseToken");
    var acctCoopObj = inst.faves_coop.get(aAccountURN);
    var flickrUser = new FlickrUser();
    var customAccountParams = inst.acUtils.getAllCustomAccountParam(aAccountURN);

    flickrUser.token = acctCoopObj.authToken;
    flickrUser.nsid = customAccountParams.getPropertyAsAString("flickr_nsid");
    flickrUser.username = customAccountParams.getPropertyAsAString("flickr_username");
    flickrUser.fullname = customAccountParams.getPropertyAsAString("flickr_fullname");
    this.authUser = flickrUser;
    inst.acUtils.ensureOnlyAuthenticatedAccount(aAccountURN);

  };
  this._getNewToken = function(aAccountURN, aListener, aFrob) {
    inst._logger.info("entering _getNewToken");
    var acctCoopObj = inst.faves_coop.get(aAccountURN);
    var flickrUser = new FlickrUser();
    var tokenListener = {
        onResult: function (aXML) {
          flickrUser.token = aXML.getElementsByTagName("token")[0].firstChild.nodeValue;
          var user = aXML.getElementsByTagName("user")[0];
          flickrUser.nsid = user.getAttribute("nsid");
          flickrUser.id = user.getAttribute("nsid");
          flickrUser.username = user.getAttribute("username");
          flickrUser.fullname = user.getAttribute("fullname");
          inst.authUser = flickrUser;

          acctCoopObj.authToken = flickrUser.token;
          inst.acUtils.setAccountCustomParamValue(acctCoopObj.id(),
                                                  "flickr_username",
                                                  flickrUser.username);
          inst.acUtils.setAccountCustomParamValue(acctCoopObj.id(),
                                                  "flickr_fullname",
                                                  flickrUser.fullname);
          inst.acUtils.setAccountCustomParamValue(acctCoopObj.id(),
                                                  "flickr_nsid",
                                                  flickrUser.fullname);

          inst.acUtils.ensureOnlyAuthenticatedAccount(aAccountURN);

          inst._logger.info("got token");
          this.lastToken = new Date();
          aListener.onSuccess();
        },
        onError: function (aError) {
          inst._logger.info("error getting token");
          this.frob = null;
          if (!aError.errorCode) aError.errorCode = 1002;
          aListener.onError(aError);
        }
      }

      var frob = aFrob;
      if (!frob) {
        frob = this.frob;
      }
      var params = {
        frob: frob
      };
      this.call(tokenListener, "flickr.auth.getToken", params);
  },
  this.isLoggedIn = function() {
    if (this.authUser) return true;
    return false;
  };
  this.logout = function() {
    this.authUser = null;
    this.frob = null;
    this.token = null;
    flock_refreshMediabarIfHasPrivate()
  };
  this.login = function(aAccountURN, aListener) {
    inst._logger.info("api.login('"+aAccountURN+"')");
    this.authUser = null;
    this.frob = null;
    var api = this;

    // for http://bugzilla.flock.com/show_bug.cgi?id=6255 --
    // ensure that the frob and auth token synch up
    if (0 /*acctCoopObj.authToken && acctCoopObj.authToken.length*/) {
      inst._logger.info("api.login('"+aAccountURN+"'): trying to reuse token");
      var reuseTokenListener = {
        onResult: function (aXML) {
          inst._logger.info("api.login('"+aAccountURN+"') reuseTokenListener: onResult()");
          aListener.onSuccess();
        },
        onError: function (aError) {
          inst._logger.info("api.login('"+aAccountURN+"') reuseTokenListener: onError()");
          aListener.onError(aError);
        }
      };

     this.getToken(aAccountURN, reuseTokenListener);
    } else {
     // get new frob
     inst._logger.info("api.login('"+aAccountURN+"'): need new token");
      var frobListener = {
        onResult: function (aXML) {
          try {
            var frobNode = aXML.getElementsByTagName("frob")[0];
            var frob = frobNode.firstChild.nodeValue;
            var authUrl = api.getAuthUrl(frob, "write");
            inst.frob = frob;
            api.frob = frob;

            var hr = Components.classes['@mozilla.org/xmlextras/xmlhttprequest;1']
                               .createInstance(Components.interfaces.nsIXMLHttpRequest);

            var onReadyStateFunc = function (eEvt) {
              if (hr.readyState == 4) {
                if (api.webDetective.detectNoDOM("flickr", "apiAuth2", "", hr.responseText, null)) {
                  inst._logger.info("Successfully authorized2!");
                  api._getNewToken(aAccountURN, aListener, frob);
                } else {
                  var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                              .createInstance(Components.interfaces.nsIWritablePropertyBag2);
                  //dump("CDC: apiAuth1: "+hr.responseText+"\n");
                  if (api.webDetective.detectNoDOM("flickr", "apiAuth1", "", hr.responseText, results)) {

                    var hr2 = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
                                        .createInstance(Components.interfaces.nsIXMLHttpRequest);
                    var postBody = "magic_cookie=" + results.getPropertyAsAString("magic_cookie")
                                + "&api_key=" + api.api_key
                                + "&api_sig=" + results.getPropertyAsAString("api_sig")
                                + "&perms=write"
                                + "&frob=" + frob
                                + "&done_auth=1";
                    hr2.onreadystatechange = function (eEvt2) {
                      if (hr2.readyState == 4) {
                        if (api.webDetective.detectNoDOM("flickr", "apiAuth2", "", hr2.responseText, null)) {
                          inst._logger.info("Successfully authorized!");
                          api._getNewToken(aAccountURN, aListener, frob);
                        } else {
                          inst._logger.info("NOT successfully authorized!");
                          inst._logger.info(hr2.responseText);
                          aListener.onError(null);
                        }
                      }
                    }
                    hr2.backgroundRequest = true;
                    hr2.overrideMimeType("text/txt");
                    hr2.open("POST", gStrings["authendpoint"]);
                    hr2.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
                    hr2.send(postBody);

                  } else if (api.webDetective.detectNoDOM("flickr", "apiAuth3", "", hr.responseText, null)) {
                    inst._logger.info("Successfully authorized with apiAuth3!");
                    api._getNewToken(aAccountURN, aListener, frob);
                  } else {
                    inst._logger.debug("Error - did not detect apiAuth1 or apiAuth3");
                    aListener.onError(null);
                  }
                }
              }
            };

            hr.onreadystatechange = onReadyStateFunc;
            hr.backgroundRequest = true;
            hr.overrideMimeType("text/txt");
            hr.open("GET", authUrl,true);
            hr.send(null);
            //var topWindow = windowManagerInterface.getMostRecentWindow(null);
            //var url = "chrome://browser/content/flock/photo/photoLoginWindow.xul?"+"url="+escape(authUrl)+"&finalString=logout.gne";
            //topWindow.open(url,"_blank", "chrome,close,titlebar,resizable=yes,toolbar,dialog=no,scrollbars=yes,modal,centerscreen");
          }
          catch(e) {
            inst._logger.info("caught this one in login: " + e);
          }
        },
        onError: function (aError) {
          inst._logger.debug("There was an error getting the frob.");
          aListener.onError(aError);
        }
      };
      this.call(frobListener, "flickr.auth.getFrob", null);
    }
  };
  this.add2Album = function(aUploadListener, aUpload, aID) {
    var inst = this;
    var listener = {
      onResult: function(aXML) {
        inst.finalizePhoto(aUploadListener, aUpload, aID);
      },
      onError: function(aError) {
        inst.finalizePhoto(aUploadListener, aUpload, aID);
      }
    }
    var params = {};
    params.photoset_id = aUpload.album;
    params.photo_id = aID;
    this.authenticatedCall(listener, "flickr.photosets.addPhoto", params);
  };
  this.reallyCreateAlbum =
  function flickrAPI_reallyCreateAlbum(aListener, aTitle, aID) {
    var myListener = {
      onResult: function fAPIrCA_onResult(aXML) {
        var newAlbum = Cc[FLOCK_PHOTO_ALBUM_CONTRACTID]
                       .createInstance(Ci.flockIPhotoAlbum);
        var photoset = aXML.getElementsByTagName("photoset")[0];
        newAlbum.title = aTitle;
        newAlbum.id = photoset.getAttribute("id");;
        aListener.onCreateAlbumResult(newAlbum);
      },
      onError: function fAPIrCA_onError(aXML) {
        aListener.onError(aXML);
      }
    }
    var params = {
      title: aTitle,
      primary_photo_id: aID
    };
    this.authenticatedCall(myListener, "flickr.photosets.create", params);
  };

  this._handleInfoResult = function(aXML) {
    var photo = aXML.getElementsByTagName("photo")[0];
    var owner = aXML.getElementsByTagName("owner")[0];
    var visibility = aXML.getElementsByTagName("visibility")[0];
    var title = aXML.getElementsByTagName("title")[0].firstChild.nodeValue;
    var dates =  aXML.getElementsByTagName("dates")[0];
    var id = photo.getAttribute("id");
    var server = photo.getAttribute("server");
    var secret = photo.getAttribute("secret");
    var lastUpdate = dates.lastupdate;
    var uploadDate = dates.posted;
    var square_url = flock_flickrBuildPhotoUrl(server,id,secret,'square');
    var small_url = flock_flickrBuildPhotoUrl(server,id,secret,'small');
    var med_url = flock_flickrBuildPhotoUrl(server,id,secret,'medium');
    var page_url = flock_flickrBuildPageUrl(owner.getAttribute('nsid'),id);
    var icon_server = photo.getAttribute("iconserver");
    var newMediaItem = Cc["@flock.com/photo;1"]
                       .createInstance(Ci.flockIMediaItem);
    newMediaItem.init("flickr", null);
    newMediaItem.webPageUrl = page_url;
    newMediaItem.thumbnail = square_url;
    newMediaItem.midSizePhoto = small_url;
    newMediaItem.largeSizePhoto = med_url;
    newMediaItem.username = owner.getAttribute("username");
    newMediaItem.title = title;
    newMediaItem.id = id;
    newMediaItem.lastUpdate = lastUpdate;
    newMediaItem.uploadDate = parseInt(uploadDate) * 1000;
    if (icon_server == "1") {
      newMediaItem.icon = gStrings["buddyicon"];
    } else {
      newMediaItem.icon = gStrings["staticbuddyicon"]
                      .replace("%iconserver%", icon_server)
                      .replace("%owner%", owner);
    }
    var visibility_ispublic = visibility.getAttribute("ispublic");
    var ispublic = (visibility_ispublic == "1") ? "true" : "false";
    newMediaItem.is_public = ispublic;
    newMediaItem.is_video = false;
    return newMediaItem;
  }

  this._getPhoto =
  function (aListener, aID) {
    var inst = this;
    var myListener = {
      onResult: function (aXML) {
        var mediaItem = inst._handleInfoResult(aXML);
        aListener.onGetPhoto(mediaItem);
      },
      onError: function (aError) {
        aListener.onError(aError);
      }
    }
    var params = {};
    params.photo_id = aID;
    if (this.getAuthUser()) {
      this.authenticatedCall(myListener, "flickr.photos.getInfo", params);
    } else {
      this.call(myListener, "flickr.photos.getInfo", params);
    }
  }

  this.finalizePhoto =
  function flickrAPI_finalizePhoto(aUploadListener, aUpload, aID) {
    try {
      var getPhotoListener = {
        onGetPhoto: function(aMediaItem) {
          aUploadListener.onUploadFinalized(aUpload, aMediaItem);
        },
        onError: function(aError) {
          aUploadListener.onError(null);
        }
      }
      this._getPhoto(getPhotoListener, aID);
    } catch(e) {
      this._logger.error(e);
    }
  };
  this.mAddingAlbums = false;
  this.mPhotos2Album = [];
  this.processPendingAlbumAdditions = function() {
    if (this.mAddingAlbums) return;
    if (this.mPhotos2Album.length == 0) return;
    this.mAddingAlbums = true;

    var obj = this.mPhotos2Album[0];
    var photoid = obj.photoid;
    var albumid = obj.albumid;
    var inst = this;

    if (this.fakeAlbums[albumid] && !this.realishAlbums[albumid]) {
      var listener = {
        onCreateAlbumResult: function(aAlbum) {
          inst.realishAlbums[albumid] = aAlbum.id;
          inst.mAddingAlbums = false;
        },
        onError: function(aError) {
          inst.mAddingAlbums = false;
        }
      }
//      var params = {};
//      params.photoset_id = photoid;
//      params.photo_id = albumid;
      var fakeAlbum = this.fakeAlbums[albumid];
      this.reallyCreateAlbum(listener,  fakeAlbum.title, photoid);
    }
    else {
      if (inst.realishAlbums[albumid]) {
        albumid = inst.realishAlbums[albumid];
      }
      var listener = {
        onResult: function(aXML) {
          inst.finalizePhoto(obj.listener, obj.upload, photoid);
          inst.mPhotos2Album.shift();
          inst.mAddingAlbums = false;
          inst.processPendingAlbumAdditions();
          //re-enter to optimize
        },
        onError: function(aError) {
          inst.finalizePhoto(obj.listener, obj.upload, photoid);
          inst.mPhotos2Album.shift();
          inst.mAddingAlbums = false;
          //re-enter to optimize?? this is an error condition, por
          //supuesto
        }
      }
      var params = {};
      params.photoset_id = albumid;
      params.photo_id = photoid;
      this.authenticatedCall(listener, "flickr.photosets.addPhoto", params);
    }
  };
  this.mCheckTicketsInProcess = false;
  this.processPendingUploadTickets = function() {
    if (this.mCheckTicketsInProcess) return;

    var ticketList = "";

    for (var p in this.mPendingTickets) {
      var obj = this.mPendingTickets[p];
      if (!obj) continue;//really should be removeing these - make it an array?
      ticketList += obj.ticketid + ",";
    }
    if (ticketList.length == 0) return;

    this.mCheckTicketsInProcess = true;

    var params = {
      tickets: ticketList
    };

    var inst = this;

    var ticketListener = {
      onResult: function (aXML) {
        inst.mCheckTicketsInProcess = false;
        var ticketList = aXML.getElementsByTagName("ticket");
        for (var i = 0; i < ticketList.length; i++) {
          var ticket = ticketList[i];
          var id = ticket.getAttribute("id");
          var photoid = ticket.getAttribute("photoid");
          var complete = ticket.getAttribute("complete");
          var invalid = ticket.getAttribute("invalid");
          var obj = inst.mPendingTickets[id];

          if (complete =="0") {
            continue;
          }
          else if (invalid) {
            obj.listener.onError(null);
          }
          else if (complete =="1") {
            if (obj.upload.album && obj.upload.album.length>0) {
              obj.photoid = photoid;
              obj.albumid = obj.upload.album;
              obj.listener = obj.listener;
              inst.mPhotos2Album.push(obj);
            }
            else {
              inst.finalizePhoto(obj.listener, obj.upload, photoid);
            }
          }
          else if (complete =="2") {
            obj.listener.onError(null);
          }
          inst.mPendingTickets[id] = null;//that doesn't really remove it tho
        }
      },
      onError: function (aXML) {
        inst.mCheckTicketsInProcess = false;
      }
    }

    this.call(ticketListener, "flickr.photos.upload.checkTickets", params);
  };
  this.mPendingTickets = {};
  this.upload = function(aListener, aPhoto, aParams, aUpload) {
    var inst = this;
    this.uploader = new PhotoUploader();
    var myListener = {
      onResult: function(aXML) {
        var rsp = aXML.getElementsByTagName("rsp")[0];
        var stat = rsp.getAttribute("stat");
        if (stat !="ok") {
          var err = aXML.getElementsByTagName("err")[0];
          var error= inst.getError('SERVICE_ERROR', aXML, null);
          aListener.onError(error);
        }
        else {
          aListener.onUploadComplete(aUpload);
          var ticketid = aXML.getElementsByTagName("ticketid")[0].firstChild.nodeValue;
          inst._logger.info(ticketid + "pre ticketid");
          inst.mPendingTickets[ticketid] = {
            ticketid: ticketid,
            upload: aUpload,
            listener: aListener
          }
          inst._logger.info(ticketid + "post ticketid");
          /*
          if (aUpload.album && aUpload.album.length>0) {
            var ticketid = aXML.getElementsByTagName("ticketid")[0].firstChild.nodeValue;
            inst.mPendingTickets[ticketid] = {
              ticketid: ticketid,
              upload: aUpload,
              listener: aListener,
            }
          }
          else {
            aListener.onUploadFinalized(aUpload, null);
          }
          */
        }
      },
      onError: function(aErrorCode) {
        if (aErrorCode) {
          aListener.onError(inst.getError('HTTP_ERROR', null, aErrorCode));
        } else {
          aListener.onError(inst.getError('SERVICE_ERROR', null));
        }
      },
      onProgress: function(aCurrentProgress) {
        aListener.onProgress(aCurrentProgress);
      }
    }
    this.convertBools(aParams);
    this.convertTags(aParams);

    aParams.auth_token = this.authUser.token;
    aParams.api_key = this.api_key;
    aParams = this.appendSignature(aParams);

    this.uploader.setEndpoint(this.upload_endpoint);
    this.uploader.upload(myListener, aPhoto, aParams);
    return;
  };
  this.convertBools = function(aParams) {
    for (var p in aParams) {
      if (!p.match(/^is/)) continue;
      // I hope that this doesn't break anything
      if (aParams[p]=="true") aParams[p] = "1";
      if (aParams[p]=="false") aParams[p] = "0";
    }
  };
  this.convertTags = function(aParams) {
    for (var p in aParams) {
      if (p != "tags") continue;
      var tags = aParams[p].split(",");
      for (var i = 0; i < tags.length;++i) {
        tags[i] = '"' + tags[i] + '"';
        tags[i] = tags[i].replace(/\"+/g,'"');
      }
      aParams[p] = tags.join(",");
    }
  };
  this.authenticatedCall = function(aListener, aMethod, aParams) {
    if (!aParams) aParams = {};
    aParams.auth_token = this.authUser.token;
    this.call(aListener, aMethod, aParams);
    var paramString = this.getParamString(aParams, true);
  };
  this.call = function(aListener, aMethod, aParams) {
    this.convertBools(aParams);
    this.convertTags(aParams);
    if (aParams == null) aParams = [];
    if (aMethod) aParams.method = aMethod;
    aParams.api_key = this.api_key;
    var paramString = this.getParamString(aParams, (aMethod == null));
    var url = this.endpoint + "?" + paramString;
    this._doCall(aListener, url, null);
  };
  this._doCall = function flApi__doCall(aListener, aUrl, aContent) {
    inst._logger.info("Sending " + aUrl);
    this.req = Components.classes['@mozilla.org/xmlextras/xmlhttprequest;1']
                         .createInstance(Components.interfaces.nsIXMLHttpRequest);
    this.req.QueryInterface(Components.interfaces.nsIJSXMLHttpRequest);
    this.req.open('GET', aUrl, true);
    var req = this.req;
    this.req.onreadystatechange =
    function flApi__doCall_onreadystatechange(aEvt) {
      if (req.readyState == 4) {
        try {
          if (req.status/100 == 2) {
             try {
              //inst._logger.info(req.responseText);
              var rsp = req.responseXML.getElementsByTagName("rsp")[0];
              var stat = rsp.getAttribute("stat");
              if (stat !="ok") {
                var err = req.responseXML.getElementsByTagName("err")[0];
                var error= inst.getError('SERVICE_ERROR', req.responseXML, null);

                aListener.onError(error);
              }
              else {
                aListener.onResult(req.responseXML);
              }
            } catch (ex) {
              // error parsing xml
              inst._logger.error(ex);
              aListener.onError(inst.getError('SERVICE_ERROR', null, null));
            }
          }
          else {
            //  http errors
            aListener.onError(inst.getError("HTTP_ERROR", null, req.status));
          }
        } catch(e) {
          // XMHTTPERROR (connection lost)
          inst._logger.info(e);
          aListener.onError(inst.getError('HTTP_ERROR',null, "9001"));
        }
      }
    }
    this.req.send(null);
  };
  this.getError = function(aErrorType, aXML, aHTTPErrorCode) {
    var error = Components.classes[FLOCK_ERROR_CONTRACTID].createInstance(flockIError);
    if  (aErrorType == "HTTP_ERROR") {
      error.errorCode = aHTTPErrorCode;
    } else if (aErrorType == "SERVICE_ERROR") {
      var errorCode;
      var errorMessage;
      var serviceErrorMessage;
      try {
        errorCode = aXML.getElementsByTagName("err")[0].getAttribute('code');
        serviceErrorMessage = aXML.getElementsByTagName("err")[0].getAttribute('msg');
      } catch (ex) {
        errorCode = "999" // in case the error xml is invalid
      }

      switch (errorCode) {
        case "1":
          error.errorCode = error.PHOTOSERVICE_INVALID_USER;
        break;

        case "3":
          error.errorCode = error.PHOTOSERVICE_UPLOAD_ERROR;
        break;

        case "4":
          error.errorCode = error.PHOTOSERVICE_FILE_IS_OVER_SIZE_LIMIT;
        break;

        case "5":
          error.errorCode = error.PHOTOSERVICE_INVALID_UPLOAD_FILE;
        break;

        case "6":
          error.errorCode = error.PHOTOSERVICE_BANDWIDTH_REACHED;
        break;

        case "10":

        break;

        case "98":
          error.errorCode = error.PHOTOSERVICE_LOGIN_FAILED;
        break;

        case "99":
          error.errorCode = error.PHOTOSERVICE_USER_NOT_LOGGED_IN;
        break;

        case "100":
          error.errorCode = error.PHOTOSERVICE_INVALID_API_KEY;
          break;

        case "105":
          error.errorCode = error.PHOTOSERVICE_UNAVAILABLE;
        break;

        case "999":
          error.errorCode = error.PHOTOSERVICE_UNKNOWN_ERROR;
        break;

        default:
          error.errorCode = error.PHOTOSERVICE_UNKNOWN_ERROR;
          error.serviceErrorString = serviceErrorMessage;
        break;
      }
    }
    //inst._logger.info('<<<<<<<<<<<<<<'  + error.errorString + '\n');
    return error;
  };
  this.getAuthUrl = function(aFrob, aPerms) {
    var api_key = this.api_key;
    var params = {
      api_key: api_key,
      perms: aPerms,
      frob: aFrob
    }

    var paramString = this.getParamString(params, true);
    return this.auth_endpoint + "?" + paramString;
  };
  this.getParamString = function(aParams, aNoMethod) {
    aParams = this.appendSignature(aParams);
    var rval = "";
    if (!aNoMethod) rval += "method=" + aParams.method + "&";

    var count = 0;
    for (var p in aParams) {
      if (p.match(/method/)) continue;

      if (count++ != 0) rval += "&";
      rval += encodeURIComponent(p) + "=" + encodeURIComponent(aParams[p]);
    }
    return rval;
  };
  this.appendSignature = function(aParams) {
    var keys = [];
    for (var p in aParams) {
      keys.push(p);
    }
    keys.sort();
    var preHash = this.api_secret;
    for (var i = 0; i < keys.length; ++i) {
      preHash += keys[i] + aParams[keys[i]];
    }
    inst._logger.info("preHash " + preHash);

    var converter =
      Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
                .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-8";

    var inputStream = converter.convertToInputStream(preHash);
    aParams.api_sig = FlockCryptoHash.md5Stream(inputStream);
    return aParams;
  };
}

FlickrAPI.prototype = {};

// ========== END FlickrAPI class ==========



// ============================================
// ========== BEGIN FlickrUser class ==========
// ============================================

function FlickrUser() {
  this.token = "";
  this.perms = "";
  this.nsid = "";
  this.id = "";
  this.username = "";
  this.fullname = "";
}

FlickrUser.prototype = {};

// ========== END FlickrUser class =========



// ===============================================
// ========== BEGIN flickrAccount class ==========
// ===============================================

function flickrAccount()
{
  this._logger = Components.classes['@flock.com/logger;1'].createInstance(Components.interfaces.flockILogger);
  this._logger.init('flickrAccount');
  this._logger.info('Created Flickr Account Object');

  this.acUtils = Components.classes["@flock.com/account-utils;1"]
                           .getService(Components.interfaces.flockIAccountUtils);
  this.svc = Components.classes[FLICKR_CONTRACTID]
                       .getService(Components.interfaces.flockIMediaWebService)
                       .QueryInterface(Components.interfaces.flockIWebService);
  if (!gFlickrAPI) {
    gFlickrAPI = new FlickrAPI();
  }
  this.api = gFlickrAPI;

  this._coop = Components.classes["@flock.com/singleton;1"]
                         .getService(Components.interfaces.flockISingleton)
                         .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                         .wrappedJSObject;
  this.webDetective = Cc["@flock.com/web-detective;1"]
                      .getService(Ci.flockIWebDetective);
  this._ctk = {
    interfaces: [
      "nsISupports",
      "flockIWebServiceAccount",
      "flockISocialWebServiceAccount",
      "flockIMediaWebServiceAccount",
      "flockIMediaUploadAccount",
      "flockIFlickrAccount"
    ]
  };
  getCompTK().addAllInterfaces(this);
}


// BEGIN flockIWebServiceAccount interface
flickrAccount.prototype.urn = null;

flickrAccount.prototype.login =
function flickrAccount_login(aListener)
{
  this._logger.info("{flockIWebServiceAccount}.login()");

  var inst = this;
  var myListener = {
    onSuccess: function (aSubject, aTopic) {
      inst.acUtils.ensureOnlyAuthenticatedAccount(inst.urn);
      // force refresh on login
      inst.setParam("isPollable", true);
      var pollerSvc = Cc["@flock.com/poller-service;1"]
                      .getService(Ci.flockIPollerService);
      pollerSvc.forceRefresh(inst.urn);
      if (aListener) {
        aListener.onSuccess(inst, "login");
      }
    },
    onError: function (aError) {
      if (aListener) {
        aListener.onError(inst, "login", aError);
      }
    }
  }
  this.api.logout();
  this.acUtils.markAllAccountsAsLoggedOut(FLICKR_CONTRACTID);

  this.api.login(this.urn, myListener);
}

flickrAccount.prototype.logout =
function flickrAccount_logout(aListener)
{
  this._logger.info("{flockIWebServiceAccount}.logout()");
  var acctCoopObj = this._coop.get(this.urn);
  if (acctCoopObj.isAuthenticated) {
    acctCoopObj.isAuthenticated = false;
    this.svc.logout();
  }
}

flickrAccount.prototype.activate =
function flickrAccount_activate(aListener)
{
  this._logger.info("{flockIWebServiceAccount}.activate()");
  var acctCoopObj = this._coop.get(this.urn);
  var inst = this;
  var listener = {
    onSuccess: function (aSubject, aTopic) {
      if (aListener) aListener.onSuccess(inst, "accountAuthorized");
      acctCoopObj.nextRefresh = new Date(0);
      acctCoopObj.isPollable = true;
      inst.acUtils.ensureOnlyAuthenticatedAccount(inst.urn);
    },
    onError: function (aError) {
      inst._logger.info( "Flickr account activation ERROR: " +
                         (aError ? aError.errorString : "No details. :("));
      acctCoopObj.isAuthenticated = false;
    }
  };
  this.login(listener);
  // I'm going to jump the gun a bit here and assume that the authentication is
  // going to succeed.  If it fails, it will be caught by the listener.  But at
  // least this will prevent any further attempts to authenticate until this
  // attempt completes.
  acctCoopObj.isAuthenticated = true;
}

flickrAccount.prototype.keep =
function flickrAccount_keep()
{
  this._logger.info(".keep(): "+this.urn);
  var c_acct = this._coop.get(this.urn);
  c_acct.isTransient = false;
  this.acUtils.makeTempPasswordPermanent(this.svc.urn + ":"
                                         + c_acct.accountId);

  // Subscribe to the recent 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) {
      feedMgr.getFeedContext("news").getRoot().subscribeFeed(aFeed);
      Cc["@flock.com/metrics-service;1"]
        .getService(Ci.flockIMetricsService)
        .report("FeedsSidebar-AddFeed",  "FlickrAccountKeep");

      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 Flickr account "
                         + c_acct.accountId);
    }
  };
  try {
    var feedURI = Cc["@mozilla.org/network/standard-url;1"]
                  .createInstance(Ci.nsIURI);
    feedURI.spec = gStrings["commentsreceivedRSS"]
                   .replace("%accountid%", c_acct.accountId);
    feedMgr.getFeed(feedURI, feedListener);
  } catch (ex) {
    this._logger.debug("There was an error subscribing to the recent "
                       + "activity feed for Flickr account "
                       + c_acct.accountId);
  }
}
// END flockIWebServiceAccount interface


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

flickrAccount.prototype.setStatus =
function flickrAccount_setStatus(aStatusMessage, aListener)
{
  this._logger.info("{flockISocialWebServiceAccount}.setStatus('"+aStatusMessage+"',"+
                                                              "'"+aListener+"')");
}

flickrAccount.prototype.getEditableStatus =
function flickrAccount_getEditableStatus()
{
  this._logger.info("{flockISocialWebServiceAccount}.getEditableStatus()");
  return "";
}

flickrAccount.prototype.formatStatusForDisplay =
function flickrAccount_formatStatusForDisplay(aStatusMessage)
{
  return "";
}

flickrAccount.prototype.markAllMeNotificationsSeen =
function flickrAccount_markAllMeNotificationsSeen(aType) {
  this._logger.debug(".markAllMeNotificationsSeen('" + aType + "')");
  var c_acct = this._coop.get(this.urn);
  switch (aType) {
    case "meMessages":
      c_acct.accountMessages = 0;
      break;
    case "meCommentActivity":
      var comments = this.acUtils.getAccountCustomParam(c_acct.id(),
                                                        "flickr_comments");
      if (comments > 0) {
        c_acct.lastUpdateDate = new Date();
        this.acUtils.setAccountCustomParamValue(c_acct.id(),
                                                "flickr_comments",
                                                0);
        this.acUtils.setAccountCustomParamValue(c_acct.id(),
                                                "flickr_comments_timespan",
                                                0);
      }
      break;
      
    default:
      break;
  }
}

flickrAccount.prototype.getMeNotifications =
function flickrAccount_getMeNotifications()
{
  this._logger.info(".getMeNotifications()");

  var sbs = Cc["@mozilla.org/intl/stringbundle;1"]
            .getService(Ci.nsIStringBundleService);
  var bundle = sbs.createBundle(FLICKR_STRING_BUNDLE);

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

  var timespan = this.acUtils.getAccountCustomParam(c_acct.id(),
                                                    "flickr_comments_timespan");
  timespan = (timespan) ? timespan : "";
  url = this.webDetective.getString("flickr", "meCommentActivity_URL", "")
            .replace("%timespan%", timespan);

  var comments = this.acUtils.getAccountCustomParam(c_acct.id(),
                                                    "flickr_comments");
  comments = (comments) ? comments : 0;
  _addNotie("meCommentActivity", comments, url);
  return JSON.toString(noties);
}

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

  var actionNames = ["friendMessage",
                     "friendViewProfile",
                     "friendShareFlock"];

  var sbs = Cc["@mozilla.org/intl/stringbundle;1"]
            .getService(Ci.nsIStringBundleService);
  var bundle = sbs.createBundle(FLICKR_STRING_BUNDLE);

  var actions = [];
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    var c_acct = this._coop.get(this.urn);
    for each (var i in actionNames) {
      actions.push({
        label: bundle.GetStringFromName("flock.flickr.actions." + i),
        class: i,
        spec: this.webDetective.getString("flickr", i, "")
                  .replace("%accountid%", c_acct.accountId)
                  .replace("%friendid%", c_friend.accountId)
      });
    }
  }
  return JSON.toString(actions);
}

flickrAccount.prototype.getSharingAction =
function flickrAccount_getSharingAction(aFriendURN, aTransferable)
{
  this._logger.info(".getSharingAction('" + aFriendURN + "')");

  var sharingAction = "";
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    // Flavors we want to support, in order of preference
    var flavors = ["text/x-flock-media",
                   "text/x-moz-url",
                   "text/unicode",
                   "text/html"];

    var message = Cc[FLOCK_RDDS_CONTRACTID]
                  .getService(Ci.flockIRichDNDService)
                  .getMessageFromTransferable(aTransferable,
                                              flavors.length,
                                              flavors);
    if (message.body) {
      sharingAction = this.webDetective.getString("flickr", "shareAction", "")
                          .replace("%friendid%", c_friend.accountId);
    }
  }
  this._logger.info("sharingAction = "+sharingAction);
  return sharingAction;
}

flickrAccount.prototype.getProfileURLForFriend = 
function flickrAccount_getProfileURLForFriend(aFriendURN)
{
  this._logger.info(".getProfileURLForFriend('" + aFriendURN + "')");

  var url = "";
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    url = this.webDetective.getString("flickr", "friendprofile", "")
                           .replace("%accountid%", c_friend.accountId);
  }

  return url;
}

flickrAccount.prototype.getPostLinkAction =
function flickrAccount_getPostLinkAction(aTransferable)
{
  return "";
}
// END flockISocialWebServiceAccount interface


// BEGIN flockIFlickrAccount interface
flickrAccount.prototype.shareFlock =
function flickrAccount_shareFlock(aFriendURN) 
{
  this._logger.info(".shareFlock('" + aFriendURN + "')");
  var sbs = Cc["@mozilla.org/intl/stringbundle;1"]
              .getService(Ci.nsIStringBundleService);
  var bundle = sbs.createBundle(FLICKR_STRING_BUNDLE);
  var body = bundle.GetStringFromName("flock.flickr.friendShareFlock.message");
  var subj = bundle.GetStringFromName("flock.flickr.friendShareFlock.subject");
  this._composeMessage(aFriendURN, subj, body, false);
}

flickrAccount.prototype.flickrMail =
function flickrAccount_flickrMail(aFriendURN, aTransferable)
{
  this._logger.info(".flickrMail('" + aFriendURN + "')");

  var flavors = ["text/x-flock-media",
                 "text/x-moz-url",
                 "text/unicode",
                 "text/html"];

  var message = Cc[FLOCK_RDDS_CONTRACTID]
                  .getService(Ci.flockIRichDNDService)
                  .getMessageFromTransferable(aTransferable,
                                              flavors.length,
                                              flavors);
  if (message.body) {
    this._composeMessage(aFriendURN, message.subject, message.body, true);
  }
}

flickrAccount.prototype._composeMessage =
function flickrAccount__composeMessage(aFriendURN, aSubject, aBody, addBreadCrumb)
{
  var body = aBody;
  var subject = aSubject;
  var c_friend = this._coop.get(aFriendURN);
  var url = this.webDetective.getString("flickr", "flickrmail_URL", "")
                             .replace("%friendid%", c_friend.accountId);
  var wm = Cc["@mozilla.org/appshell/window-mediator;1"]
             .getService(Ci.nsIWindowMediator);
  var win = wm.getMostRecentWindow("navigator:browser");
  if (win) {
   var browser = win.getBrowser();
   var newTab = browser.loadOneTab(url, null, null, null, false, false);
   var obs = Cc["@mozilla.org/observer-service;1"]
             .getService(Ci.nsIObserverService);
   var inst = this;
   var observer = {
     observe: function openSendMessageTabForFill_observer(aContent,
                                                          aTopic,
                                                          aContextUrl)
     {
       var contentWindow = newTab.linkedBrowser.docShell
                                 .QueryInterface(Ci.nsIInterfaceRequestor)
                                 .getInterface(Ci.nsIDOMWindow);
       function insertContent(aWebDicString, aMessage) {
         var xpathquery = inst.webDetective.getString("flickr", aWebDicString, "");
         var doc = contentWindow.document;
         var formItems = doc.evaluate(xpathquery, doc, null,
                                      Ci.nsIDOMXPathResult.ANY_TYPE, null);
         if (formItems) {
           var formItem = formItems.iterateNext();
           if (formItem.hasAttribute("value")) {
             formItem.setAttribute("value", aMessage);
           } else {
             var textNode = doc.createTextNode(aMessage);
             formItem.appendChild(textNode);
             inst._logger.info("aMessage " + aMessage);
           }
         }
       }
       if (contentWindow == aContent) {
         obs.removeObserver(this, "EndDocumentLoad");
         insertContent("flickrmail_subjectXPath", subject);
         if(addBreadCrumb)
         {
            // Add breadcrumb to message body
            var breadcrumb = Cc[FLOCK_RDDS_CONTRACTID]
                               .getService(Ci.flockIRichDNDService)
                               .getBreadcrumb("plain");
           if (breadcrumb) {
             body += breadcrumb;
           }
        }
         insertContent("flickrmail_bodyXPath", body);
       }
     }
   };
   obs.addObserver(observer, "EndDocumentLoad", false);
  }
}
// END flockIFlickrAccount interface

// ========== END flickrAccount class ==========
