// 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 Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;

const CLASS_ID = Components.ID("{8DAEC4A0-8623-11DB-B606-0800200C9A66}");
const CLASS_NAME = "piczo";
const CLASS_DESC = "Flock Piczo Service";
const CONTRACT_ID = "@flock.com/people/piczo;1";
const FLOCK_PHOTO_ALBUM_CONTRACTID = "@flock.com/photo-album;1";
const FLOCK_PHOTO_ACCOUNT_CONTRACTID = "@flock.com/photo-account;1";
const SERVICE_ENABLED_PREF = "flock.service.piczo.enabled";
const CATEGORY_COMPONENT_NAME       = "Piczo JS Component"
const CATEGORY_ENTRY_NAME           = "piczo"

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 gTimers = [];  // For use with the scheduler

// String defaults... may be updated later through Web Detective
var gStrings = {
  "domains": "piczo.com",
  "sessioncookie": "JSESSIONID",
  "mainpage": "http://www.piczo.com/",
  "favicon": "chrome://flock/content/services/piczo/favicon.png",
  "userlogin": "http://www.piczo.com/",
  "userprofile": "http://%accountid%.piczo.com/",
  "api-agent-string": "Flock",
  "api-setup-URL": "http://api.piczo.com/api-1/setup?api-agent=%agent%",
  "api-login-URL": "http://api.piczo.com/api-1/login",
  "api-friends-URL": "http://api.piczo.com/api-1/friends",
};

var gAPI = null;
var gSVC = null;

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

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

// ==============================================
// ========== BEGIN piczoService class ==========
// ==============================================
var _logger = null;
function piczoService()
{
  _logger = Cc['@flock.com/logger;1'].createInstance(Ci.flockILogger);
  _logger.init("piczo");
  _logger.info("CONSTRUCTOR");

  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 = gStrings["mainpage"];
  this.mIsInitialized = false;
  this._ctk = {
    interfaces: [
      "nsISupports",
      "nsIClassInfo",
      "nsISupportsCString",
      "nsIObserver",
      "flockIWebService",
      "flockIManageableWebService",
      "flockIPollingService",
      "flockIAuthenticateNewAccount",
      "flockIMediaWebService",
      "flockIMediaUploadWebService",
      "flockIPiczoService"
    ],
    shortName: "piczo",
    fullName: "Piczo",
    description: CLASS_DESC,
    domains: gStrings["domains"],
    favicon: gStrings["favicon"],
    CID: CLASS_ID,
    contractID: CONTRACT_ID,
    accountClass: piczoAccount
  };
}

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

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

piczoService.prototype.iconUrl = gStrings["favicon"];

piczoService.prototype.handleDragDrop = 
function piczoService_handleDragDrop (aURL, aXLoc, aYLoc) {
  return gAPI.handleDragDrop(aURL, aXLoc, aYLoc);
}

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

piczoService.prototype.login =
function piczoService_login(aAccountURN, aListener) {
  _logger.info("piczoService.login()");
}

piczoService.prototype.__defineGetter__('isLoggedIn', function () { return gAPI.isLoggedIn(); })

piczoService.prototype.findByUsername = 
function piczoService_findByUsername(aListener, aUsername) {
  // Needed to have observer call .search();
  aListener.onError(null, null, null);
}

piczoService.prototype.serviceName = "Piczo";
piczoService.prototype.shortName = "piczo";

piczoService.prototype.search =
function piczoService_search(aListener,
                             aQueryString,
                             aCount,
                             aPage,
                             aRequestId)
{
  // Piczo doesn't support paging
  if(aPage > 1) return;

  // XXX: TODO
  _logger.debug("piczoService_search: " + aQueryString);

  var aQuery = new queryHelper(aQueryString);
  var myListener = {
    onSuccess: function (enumResults, status) {
      aListener.onSuccess(enumResults, aRequestId);
    },
    onError: function (aFlockError) {
      //_logger.info("piczoService_search ERROR: " + aFlockError.serviceErrorString + "\n");
      aListener.onError(null, aRequestId, aFlockError);
    }
  }

  var params = Components.classes["@mozilla.org/hash-property-bag;1"]
                        .createInstance(Components.interfaces.nsIWritablePropertyBag2);
  params.setPropertyAsAString("subj_id", aQuery.search);
  params.setPropertyAsAString("albumlabel", aQuery.albumlabel);
  if (gAPI.isLoggedIn())
  {
    gAPI.getPhotos(myListener, params);
  } else {
    _logger.info("piczoService wasnt logged in....");
    // We are not logged in, force user to log in now.
    var aError = Cc['@flock.com/error;1'].createInstance(Ci.flockIError);
    aError.errorCode   = Ci.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN;
    aListener.onError(null, null, aError);
  }
}

piczoService.prototype.cancelUpload =
function piczoService_cancelUpload() {
  try {
    _logger.info("Cancelling upload...");
    gAPI.uploader.req.abort();
  } catch (e) {
    _logger.info("Error cancelling upload.");
  }
}

piczoService.prototype.upload =
function piczoService_upload(aListener, aUpload, aFile) {
  _logger.info("piczoService_upload");

  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;
  */
  gAPI.upload(aListener, aFile, params, aUpload);
}

piczoService.prototype.getAlbums = 
function piczoService_getAlbums(aListener, aUserID) {
  _logger.info("[piczoAPI].getAlbum call...");
  var myListener = {
    onResult: function (enumResults, status) {
      _logger.info("[piczoAPI].getAlbums onResult!");
      aListener.onSuccess(enumResults, "");
    },
    onError: function (flockError) {
      _logger.info("[piczoAPI].getAlbums onError!" + flockError.errorString);
      aListener.onError(null, null, null);
    }
  }

  // NOTE: the uid needs to be changed to the uid of aUsername for general user searches
  gAPI.getAlbums(myListener, aUserID);
}

/**
 * getAlbumsForUpload
 * @see flockIMediaWebService#getAlbumsForUpload
 */
piczoService.prototype.getAlbumsForUpload =
function piczoService_getAlbumsForUpload(aListener, aUsername) {
  // Needs specific implementation for uploading albums
  _logger.info("[piczoAPI].getAlbumsForUpload call...");
  var myListener = {
    onResult: function (enumResults, status) {
      _logger.info("[piczoAPI].getAlbumsForUpload onResult!");
      aListener.onSuccess(enumResults, "");
    },
    onError: function (flockError) {
      _logger.info("[piczoAPI].getAlbumsForUpload onError!" + flockError.errorString);
      aListener.onError(null, null, flockError);
    }
  }

  // NOTE: the uid needs to be changed to the uid of aUsername for general user searches
  gAPI.getAlbumsForUpload(myListener, aUsername);
}

piczoService.prototype.getAccountStatus = 
function piczoService_getAccountStatus(aListener) {
  var inst = this;
  
  var listener = {
    onSuccess: function (aXML) {
      _logger.info("piczoService: getAccountStatus Success!");
      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');
      result.setPropertyAsAString("maxSpace", "500000");
      result.setPropertyAsAString("usedSpace", "0");
      result.setPropertyAsAString("maxFileSize", "500000");
      result.setPropertyAsAString("usageUnits", "bytes");
      result.setPropertyAsBool("isPremium", false);
      aListener.onSuccess(result, "");
    },
    onError: function (aError) {
      aListener.onError(aError);
      _logger.info("piczoService: getAccountStatus ERROR");
    }
  }
  
  gAPI.getAccountStatus(listener);
}

piczoService.prototype.call =
function piczoService_call(aListener, aMethod, aDictionary, aAuth)
{
  _logger.info("piczoService_call");
  return this.doCall(aListener, aMethod, aDictionary, aAuth);
}

piczoService.prototype.createAlbum =
function piczoService_createAlbum(aListener, aPath)
{
  // XXX: TODO  
  // aListener.onSuccess(newAlbum, "");
  var error = Cc["@flock.com/error;1"].createInstance(Ci.flockIError);
  error.errorCode = error.PHOTOSERVICE_UNKNOWN_ERROR;
  error.errorString = "NS_ERROR_NOT_IMPLEMENTED";
  aListener.onError(null, null, error);
}

piczoService.prototype.supportsSearch =
function piczoService_supportsSearch(aQueryString ) {
  _logger.debug("piczoService_supportsSearch: " + aQueryString);
  return false;
}


// BEGIN flockIWebService interface
piczoService.prototype.addAccount =
function piczoService_addAccount(aAccountID, aIsTransient, aListener)
{
  _logger.info("{flockIWebService}.addAccount('" + aAccountID + "', " + aIsTransient + ")");
  var accountURN = "urn:flock:piczo:account:"+aAccountID;
  var c_acct = new this._coop.Account(
    accountURN, {
      name: aAccountID,
      accountId: aAccountID,
      serviceId: CONTRACT_ID,
      service: this.c_svc,
      favicon: gStrings["favicon"],
      URL: gStrings["userprofile"].replace("%accountid%", aAccountID),
      isTransient: aIsTransient,
      isPollable: true,
    }
  );
  this._coop.accounts_root.children.add(c_acct);

  // Instanciate account component
  var acct = this.getAccount(c_acct.id());
  if (aListener) aListener.onSuccess(acct, "addAccount");
  return acct;
}
// END flockIWebService interface


piczoService.prototype.authenticateNewAccount =
function flickrService_authenticateNewAccount(aListener)
{
  _logger.info("{flockIAuthenticateNewAccount}.authenticateNewAccount(aListener)");
  var wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
  var win = wm.getMostRecentWindow('navigator:browser');
  win.openUILinkIn(this.url, "tab");
}


// BEGIN flockIMediaWebService interface
piczoService.prototype.decorateForMedia =
function piczoService_decorateForMedia(aDocument)
{
  _logger.info("{flockIMediaWebService}.decorateForMedia()");
  // XXX TODO FIXME: implement
}
piczoService.prototype.migrateAccount =
function piczoService_migrateAccount(aId, aUsername) {
}
// END flockIMediaWebService interface


// BEGIN flockIManageableWebService interface
piczoService.prototype.getAccountIDFromDocument =
function piczoService_getAccountIDFromDocument(aDocument)
{
  _logger.info("{flockIManageableWebService}.getAccountIDFromDocument()");
  aDocument.QueryInterface(Components.interfaces.nsIDOMHTMLDocument);
  var acctID;
  var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                          .createInstance(Components.interfaces.nsIWritablePropertyBag2);
  if (this.webDetective.detect("piczo", "accountinfo", aDocument, results)) {
    // If this page has the user GUID on it, then we can do a Coop lookup
    // for accounts with that GUID
    acctID = this._getAccountIDFromGUID(results.getPropertyAsAString("piczo_guid"));
  }
  if (!acctID) {
    // We haven't determined the account yet.
    // Since some login landing pages don't have account info on them that we
    // can detect, we can try seeing if there's a session cookie stored as a
    // temp password entry.  (This is a hack.)
    var sessionValue = this.acUtils.getCookie("http://piczo.com",
                                              gStrings["sessioncookie"]);
    if (!sessionValue) {
      sessionValue = this.acUtils.getCookie("http://www.piczo.com",
                                            gStrings["sessioncookie"]);
    }
    var pw = this.acUtils.getTempPassword("piczo:session:"+sessionValue);
    if (pw) {
      _logger.debug("Sneaky! Got acctID from a temp password associated with session cookie!");
      acctID = pw.user;
    }
  }
  return acctID;
}

piczoService.prototype.getCredentialsFromForm =
function piczoService_getCredentialsFromForm(aForm)
{
  // Convenience method for Web Detective calls
  var inst = this;
  var detectForm = function piczo_detectForm(aType, aResults) {
    return inst.webDetective.detectForm("piczo", aType, aForm, aResults);
  };

  // Try to detect login, then signup, then changepassword, in that order
  var formType = "login";
  var results = getCompTK().newResults();
  if (!detectForm(formType, results)) {
    // No login detected, so check for signup
    formType = "signup";
    results = getCompTK().newResults();
    if (!detectForm(formType, results)) {
      // No signup detected, so try changepassword
      formType = "changepassword";
      results = getCompTK().newResults();
      if (!detectForm(formType, results)) {
        results = null;
      }
    }
  }

  if (results) {
    // We were able to get results from at least one of the checks above
    var pw = {
      QueryInterface: function(aIID) {
        if (!aIID.equals(Components.interfaces.nsISupports) &&
            !aIID.equals(Components.interfaces.nsIPassword) &&
            !aIID.equals(Components.interfaces.flockIPassword))
        { 
          throw Components.interfaces.NS_ERROR_NO_INTERFACE;
        }
        return this;
      },
      user: results.getPropertyAsAString("username"),
      password: results.getPropertyAsAString("password"),
      host: null,
      formType: formType
    };

    // Doing a bit of a hack here...
    // Since the Piczo login landing page doesn't always reveal which
    // account is logged in, we will need to look at the session cookie and
    // see if its the same as what it was when the user last logged in.  So
    // at this point we're just storing the account username as a temp
    // password entry associated with the session cookie token.
    var sessionValue = this.acUtils.getCookie("http://piczo.com",
                                              gStrings["sessioncookie"]);
    if (!sessionValue) {
      sessionValue = this.acUtils.getCookie("http://www.piczo.com",
                                            gStrings["sessioncookie"]);
    }
    this.acUtils.clearTempPassword("piczo:session:" + sessionValue);
    this.acUtils.setTempPassword("piczo:session:" + sessionValue, pw.user, "",
                                 formType);
    return pw;
  }
  return null;
}

piczoService.prototype._getAccountIDFromGUID =
function piczoService__getAccountIDFromGUID(aGUID)
{
  _logger.info("_getAccountIDFromGUID('" + aGUID + "')");
  var accts = this._coop.Account.find({serviceId: CONTRACT_ID, piczo_guid: aGUID});
  return (accts.length) ? accts[0].accountId : null;
}

piczoService.prototype.updateAccountStatusFromDocument =
function piczoService_updateAccountStatusFromDocument(aDocument, aAcctURN)
{
  _logger.info("updateAccountStatusFromDocument('" + aAcctURN + "')");
  if (aAcctURN) {
    // We know we are logged in to this account, but we still need to grab some
    // information off the page
    var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                            .createInstance(Components.interfaces.nsIWritablePropertyBag2);
    var accountID = null;
    var profileURL = null;
    if (this.webDetective.detect("piczo", "accountinfo", aDocument, results)) {
      var guid = results.getPropertyAsAString("piczo_guid");
      accountID = this._getAccountIDFromGUID(guid);
      // The profileURL is not always returned, so we need to test for it within
      // a try/catch -> getPropertyAsAString will throw if it does not find the
      // given property.
      try {
        profileURL = results.getPropertyAsAString("profileURL");
      } catch (ex) {
        // Didn't find profileURL. Just carry on.
        _logger.debug("updateAccountStatusFromDocument: no profileURL: " + ex);
      }
    } else {
      // Since some login landing pages don't have account info on them that we
      // can detect, we can try seeing if there's a session cookie stored as a
      // temp password entry.  (This is a hack.)
      var sessionValue = this.acUtils.getCookie("http://piczo.com",
                                                gStrings["sessioncookie"]);
      if (!sessionValue) {
        sessionValue = this.acUtils.getCookie("http://www.piczo.com",
                                              gStrings["sessioncookie"]);
      }
      var pw = this.acUtils.getTempPassword("piczo:session:"+sessionValue);
      if (pw) {
        _logger.debug("Sneaky! Got acctID from a temp password associated with session cookie!");
        accountID = pw.user;
      }
    }
    var c_acct = this._coop.get(aAcctURN);
    var results2 = getCompTK().newResults();
    if (this.webDetective.detect("piczo", "avatar", aDocument, results2)) {
      c_acct.avatar = results2.getPropertyAsAString("avatarURL");
    }
    if (!c_acct.isAuthenticated) {
      var acct = this.getAccount(aAcctURN);
      acct.activate(null);
      // Assume the API login will succeed.  The listener should handle any
      // failures...
      c_acct.isAuthenticated = true;
    }
    if (profileURL && profileURL.length) {
      c_acct.URL = profileURL;
    }
  } else if (this.webDetective
                 .detect("piczo", "loggedout", aDocument, null))
  {
    // We're logged out (of all accounts)
    this.acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
  }
}
// END flockIManageableWebService interface

///////////////////////////////////////
// BEGIN flockIPollingService interface
///////////////////////////////////////
piczoService.prototype.refresh =
function piczoService_refresh(aURN, aPollingListener)
{
  _logger.info("{flockIPollingService}.refresh('" + aURN + "')");
  aPollingListener.onResult();
}
// END flockIPollingService interface


piczoService.prototype.init =
function piczoService_init()
{
  var _logger = Cc['@flock.com/logger;1'].createInstance(Ci.flockILogger);
  _logger.init("piczoservice");

  _logger.info(".init()");

  // Prevent re-entry
  if (this.mIsInitialized) return;
  this.prefService = Components.classes["@mozilla.org/preferences-service;1"]
                               .getService(Components.interfaces.nsIPrefBranch);
  if (this.prefService.getPrefType(SERVICE_ENABLED_PREF) &&
      !this.prefService.getBoolPref(SERVICE_ENABLED_PREF))
  {
    _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;
  }
  this.mIsInitialized = true;

  // Initialize API
  this.mAPI = new piczoAPI();
  gAPI = this.mAPI;
  gAPI.defAlbum = {};

  // Init global service handle
  gSVC = this;

  // Initialize Coop, service and actions
  this._coop = Components.classes["@flock.com/singleton;1"]
                         .getService(Components.interfaces.flockISingleton)
                         .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                         .wrappedJSObject;

  this.urn = "urn:piczo:service";
  this.c_svc = new this._coop.Service(
    this.urn, {
      name: CLASS_NAME,
      desc: CLASS_DESC,
      serviceId: CONTRACT_ID
    }
  );

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

  // Load Web Detective and update strings
  this.webDetective = this.acUtils.useWebDetective("piczo.xml");
  for (var s in gStrings) {
    gStrings[s] = this.webDetective.getString("piczo", s, gStrings[s]);
  }
  this.url            = gStrings["mainpage"];
  this.domains        = gStrings["domains"];
  this.c_svc.domains  = gStrings["domains"];
  this.icon           = gStrings["favicon"];
  this.c_svc.loginURL = gStrings["userlogin"];

  // Update account auth states
  if (this.webDetective.detectCookies("piczo", "loggedout", null)) {
    this.acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
  } else {
    var acctURN = this.acUtils.getFirstAuthenticatedAccountForService(CONTRACT_ID);
    try {
      if (acctURN) {
        var acct = this.getAccount(acctURN);
        acct.activate(null);
      }
    } catch(e) {
      _logger.info(".init() ERROR: " + e);
    }
  }
}

// ========== END piczoService class ==========

// ============================================================================================================================
// ========== BEGIN piczoAPI class ============================================================================================
// ============================================================================================================================

function piczoAPI()
{
  _logger.info("[piczoAPI] CONSTRUCTOR");
  this.mAPIToken = null;
  this.mAPITokenID = null;
  this.mSession = {
    token: null,
    userGUID: null,
    username: null,
    password: null
  };
  this.mTS = 0;
  this.cryptoHash = Components.classes["@mozilla.org/security/hash;1"]
                              .createInstance(Components.interfaces.nsICryptoHash);
}

piczoAPI.prototype.getAuthUser =
function() {
  return this.mSession; 
}

piczoAPI.prototype.QueryInterface = function(iid) {
  if (!iid.equals(Ci.nsIClassInfo) &&
      !iid.equals(Ci.nsISupports)) {
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
  return this;
}

piczoAPI.prototype.getTS =
function piczoAPI_getTS()
{
  return (this.mTS++);
}

piczoAPI.prototype.call =
function piczoAPI_call(aMethod, aURL, aCustomHeaders, aPostVars, aListener, aSign)
{
  aMethod = aMethod.toUpperCase();
  _logger.debug("[piczoAPI].call('" + aMethod + "', '" + aURL + "')");
  var hr = Components.classes['@mozilla.org/xmlextras/xmlhttprequest;1']
                     .createInstance(Components.interfaces.nsIXMLHttpRequest);
  hr.QueryInterface(Components.interfaces.nsIJSXMLHttpRequest);
  hr.open(aMethod, aURL, true);
  if (aMethod == "POST") {
    hr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded", false);
  }
  // Per Bug #9373, Piczo API chokes on '+' characters in post vars, so we
  // need this function to explicitly encode them.
  var customEscape = function piczoAPI_customEscape(aText) {
    return escape(aText).replace("+", "%2B");
  };
  if (aSign) {
    // Need to compose message string and generate signature
    var url = aURL;
    var params = [];
    var qmIdx = url.indexOf("?");
    if (qmIdx > -1) {
      var qString = url.substring(qmIdx + 1);
      url = url.substring(0, qmIdx);
      var qArr = qString.split("&");
      for (var i = 0; i < qArr.length; i++) {
        var prm = qArr[i].split("=");
        params.push([prm[0], prm[1]]);
      }
    }
    if (aCustomHeaders) {
      for (var h in aCustomHeaders) {
        params.push([h, aCustomHeaders[h]]);
      }
    }
    if (aPostVars) {
      for (var p in aPostVars) {
        params.push([p, aPostVars[p]]);
      }
    }
    params.sort();

    ////////////////////////////////////////////////////////
    // Piczo Digital Signature format:
    // ds := base-64( md5( {api-token} + {access-token} ) )
    ////////////////////////////////////////////////////////
    var apitoken = "";
    var accesstoken = "";
    for (var i = 0; i < params.length; i++) {
      if(params[i][0] == "at") {
        accesstoken = params[i][1];
      } else if(params[i][0] == "api-token") {
        apitoken = params[i][1];
      }
    }
    var msg = apitoken + accesstoken;
    var msgArr = [];
    for (var i = 0; i < msg.length; i++) {
      msgArr.push(msg.charCodeAt(i));
    }
    this.cryptoHash.init(Components.interfaces.nsICryptoHash.MD5);
    this.cryptoHash.update(msgArr, msgArr.length);
    var sig = this.cryptoHash.finish(true);
    var dsValue = escape(sig);
    dsValue = dsValue.replace(/\+/g,"%2B");
    dsValue = dsValue.replace(/\//g,"%2F");
    dsValue = dsValue.replace(/\@/g,"%40");
    _logger.debug("message: " + msg + "  sig: " + dsValue);
    hr.setRequestHeader("ds", dsValue);
  }
  if (aCustomHeaders) {
    for (var h in aCustomHeaders) {
      var val = escape(aCustomHeaders[h]);
      val = val.replace(/\+/g,"%2B");
      val = val.replace(/\//g,"%2F");
      val = val.replace(/\@/g,"%40");
      hr.setRequestHeader(h, val);
    }
  }
  hr.onreadystatechange = function (aEvent) {
    _logger.debug("[piczoAPI].call(): hr.onreadystatechange(): " + hr.readyState);
    if (aListener) {
      if (hr.readyState == 4) {
        try {
          if (hr.status/100 == 2) {
            try {
              aListener.onSuccess(hr, aURL);
            } catch (ex) {
              _logger.info(ex);
              aListener.onError(hr, aURL, null);
            }
          } else {
            // HTTP error
            var err = {
              errorCode: hr.status,
              errorString: "HTTP error."
            };
            aListener.onError(hr, aURL, err);
          }
        } catch (ex) {
          _logger.info(ex);
          var err = {
            errorCode: Components.interfaces.flockIError.PHOTOSERVICE_UNAVAILABLE,
            errorString: "Request did not complete."
          };
          aListener.onError(hr, aURL, err);
        }
      }
    }
  };
  var postBody = "";
  if (aPostVars) {
    for (var v in aPostVars) {
      if (postBody.length) { postBody += "&"; }
      postBody += v + "=" + customEscape(aPostVars[v]);
    }
  }
  if ((aMethod == "POST") && postBody && postBody.length) {
    hr.send(postBody);
  } else {
    hr.send(null);
  }
}

piczoAPI.prototype.getAPIToken =
function piczoAPI_getAPIToken(aListener)
{
  _logger.debug("[piczoAPI].getAPIToken()");
  var api = this;
  var setupListener = {
    onSuccess: function (aSubject, aTopic) {
      _logger.debug("[piczoAPI].getAPIToken() setupListener.onSuccess()");
      _logger.debug(aSubject.responseText);
      var xmlDoc = aSubject.responseXML;
      var replyEl = xmlDoc.getElementsByTagName("setup.reply").item(0);
      var tokenEl = replyEl.getElementsByTagName("api-token").item(0);
      var tokenIDEl = replyEl.getElementsByTagName("id").item(0);
      api.mAPIToken = tokenEl.childNodes.item(0).nodeValue;
      api.mAPITokenID = tokenIDEl.childNodes.item(0).nodeValue;
      _logger.debug(" got API token ID:" + api.mAPITokenID);
      _logger.debug(" got API token: "+api.mAPIToken);
      if (aListener) { aListener.onSuccess(aSubject, aTopic); }
    },
    onError: function (aSubject, aTopic, aError) {
      _logger.info("[piczoAPI].getAPIToken() setupListener.onError()");
      if (aError) {
        for (var p in aError) {
          _logger.info("error[" + p + "] = " + aError[p]);
        }
      }
      for (var p in aSubject) {
        _logger.debug("response[" + p + "] = " + aSubject[p]);
      }
      if (aListener) { aListener.onError(aSubject, aTopic, aError); }
    }
  };
  var url = gStrings["api-setup-URL"].replace("%agent%", gStrings["api-agent-string"]);
  var postVars = {
    "api-agent": gStrings["api-agent-string"]
  };
  this.call("post", url, null, postVars, setupListener);
}

piczoAPI.prototype.getPhotosCall =
function piczoService_authenticatedGetPhotosCall(aListener, aMethod, aDictionary)
{
  var api = this;
  api.pageID = null;
  api.pageToFind = aDictionary.albumlabel;

  _logger.debug("piczoService_authenticatedCall " + aMethod);
  if (!this.isLoggedIn) {
    _logger.debug("not logged in, so trying unauth call for " + aMethod);
    return this.call(aListener, aMethod, aDictionary);
  }
  
  var pageToFind = aDictionary.albumlabel;

  var callGetPhotos = function () {
    var url = "http://api.piczo.com/api-1/lookup";

    if (api.pageID) {
      url += "/" + api.pageID; 
    }

    url += "/images?reply=rss";

    var headers = {
      "api-token": this.gAPI.mAPIToken,
      "at": this.gAPI.mSession.token
    };
    api.call("get", url, headers, null, aListener, true);
  };

  var pagesListener = {
    onSuccess: function pagesListener_onSuccess(aSubject, aTopic) {
      var xml = aSubject.responseXML;
      _logger.debug("[piczoAPI].getPages returned: "
                    + aSubject.responseText);

      var items = xml.getElementsByTagName("item");

      for (var i = 0; i < items.length; i++) {
        var guid = items[i].getElementsByTagName("guid")[0]
                   .firstChild.nodeValue;
        var pagelabel = items[i].getElementsByTagName("description")[0]
                        .firstChild.nodeValue;
        
        if (api.pageToFind == pagelabel) {
          api.pageID = guid;
        }
      }

      callGetPhotos();
      return;
    },
    onError: function pagesListener_onError(aSubject, aTopic, aFlockError) {
      _logger.error("Error getting pages" + aFlockError.errorString);
      if (api.handleStaleToken(aSubject)) {
        // Resubmit the request with the new token
        api.getPhotosCall(aListener, "piczo.photos.get", aDictionary);
      } else {
        aListener.onError(aFlockError);
      }
    } 
  };

  // Get Page ID first
  var callGetPages = function callGetPages_function() {
    var url = "http://api.piczo.com/api-1/lookup/pages?reply=rss";

    var headers = {
      "api-token": this.gAPI.mAPIToken,
      "at": this.gAPI.mSession.token
    };
    api.call("get", url, headers, null, pagesListener, true);
  }

  if (this.mAPIToken) {
    _logger.info("piczoService Calling getphotos");
    if (pageToFind != null) {
      callGetPages();
    } else {
      callGetPhotos();
    }
  } else {
    _logger.info("piczoService wasnt logged in....");
    // We are not logged in, force user to log in now.
    var aError = Cc['@flock.com/error;1'].createInstance(Ci.flockIError);
    aError.errorCode   = Ci.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN;
    aListener.onError(aError);
  } 
}

piczoAPI.prototype.getAccountStatus = 
function piczoAPI_getAccountStatus(aListener) {
  _logger.info("[piczoAPI].getAccountStatus");
}

/**
 * getAlbums
 * @see flockIMediaWebService#getAlbums
 */
piczoAPI.prototype.getAlbums = 
function piczoAPI_getAlbums(aListener, aParams) {
  _logger.info("[piczoAPI].getAlbums('" + aParams + "')");

  var api = this;
  var pagesListener = {
    onSuccess: function pagesListener_onSuccess(aSubject, aTopic) {
      var xml = aSubject.responseXML;
      _logger.debug("[piczoAPI].getPages returned: "
                    + aSubject.responseText);

      var photoAlbums = [];

      var items = xml.getElementsByTagName("item");

      for (var i = 0; i < items.length; i++) {
        var guid = items[i].getElementsByTagName("guid")[0]
                           .firstChild.nodeValue;
        var pagelabel = items[i].getElementsByTagName("description")[0]
                                .firstChild.nodeValue;
        var newAlbum = Cc[FLOCK_PHOTO_ALBUM_CONTRACTID]
                                 .createInstance(Ci.flockIPhotoAlbum);

        newAlbum.id = guid;
        newAlbum.title = pagelabel;

        if (!gAPI.defAlbum.id) {
          gAPI.defAlbum = newAlbum;
        }

        photoAlbums.push(newAlbum);
      }

      var albumsEnum = {
        hasMoreElements: function albumsEnum_hasMoreElements() {
          return (photoAlbums.length > 0);
        },
        getNext: function albumsEnum_getNext() {
          return photoAlbums.shift();
        },
        QueryInterface: function albumsEnum_QI(aIID) {
          if (aIID.equals(Ci.nsISimpleEnumerator)) {
            return this;
          }
          throw Cr.NS_ERROR_NO_INTERFACE;
        }
      }

      aListener.onResult(albumsEnum, null);
    },
    onError: function pagesListener_onError(aSubject, aTopic, aFlockError) {
      _logger.error("Error getting pages" + aFlockError.errorString);
      if (api.handleStaleToken(aSubject)) {
        // Resubmit the request with the new token
        api.getAlbums(aListener, "piczo.photos.get");
      } else {
        aListener.onError(aFlockError);
      }
    } 
  };

  // Get Page ID first
  var callGetPages = function callGetPages_function() {
    var url = "http://api.piczo.com/api-1/lookup/pages?reply=rss";

    var headers = {
      "api-token": this.gAPI.mAPIToken,
      "at": this.gAPI.mSession.token
    };
    api.call("get", url, headers, null, pagesListener, true);
  }

  if (this.mAPIToken) {
    _logger.debug("piczoService Calling getInbox");
    callGetPages();
  } else {
    _logger.debug("piczoService wasnt logged in....");
    // We are not logged in, force user to log in now.
    var aError = Cc['@flock.com/error;1'].createInstance(Ci.flockIError);
    aError.errorCode   = Ci.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN;
    aListener.onError(aError);
  }
}

/**
 * getAlbumsForUpload
 * @see flockIMediaWebService#getAlbumsForUpload
 * But note: aListener is NOT a flockIListener here!
 */
piczoAPI.prototype.getAlbumsForUpload = 
function piczoAPI_getAlbumsForUpload(aListener, aParams) {
  _logger.debug("[piczoAPI].getAlbumsForUpload('" + aParams + "')");

  var api = this;

  // Make inbox call
  //
  var listener = {
    onSuccess: function(aSubject, aTopic) {
      //try {
        var xml          = aSubject.responseXML;
        _logger.debug("[piczoAPI].getInbox returned: " + aSubject.responseText);
        var destinations = xml.getElementsByTagName("destination");

        var photoAlbums = [];
        _logger.debug("[piczoAPI].getInbox parsing data: " + destinations.length);

        gAPI.defAlbum = {};

        for (var i = 0; i < destinations.length; i++) {
          var name = destinations[i].getElementsByTagName('name')[0].firstChild.nodeValue;
          var uri = destinations[i].getElementsByTagName('action-uri')[0].firstChild.nodeValue;

          _logger.debug("[piczoAPI].getInbox data: " + name + ", " + uri);

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

          if(!gAPI.defAlbum.id) {
            gAPI.defAlbum = newAlbum;
          }

          photoAlbums.push(newAlbum);
        }

        var albumsEnum = {
          hasMoreElements: function albumsEnum_hasMoreElements() {
            return (photoAlbums.length > 0);
          },
          getNext: function albumsEnum_getNext() {
            return photoAlbums.shift();
          },
          QueryInterface: function albumsEnum_QI(aIID) {
            if (aIID.equals(Ci.nsISimpleEnumerator)) {
              return this;
            }
            throw Cr.NS_ERROR_NO_INTERFACE;
          }
        }

        _logger.debug("[piczoAPI].getInbox returning results...");
        aListener.onResult(albumsEnum, null);
    },
    onError: function(aSubject, aTopic, aFlockError) {
      _logger.info('Error getting inbox' + aFlockError.errorString);
      if (api.handleStaleToken(aSubject)) {
        // Recursively resubmit the request with the new token.
        api.getAlbumsForUpload(aListener, aParams);
      } else {
        aListener.onError(aFlockError);
      }
    }
  };

  var callGetInbox = function () {
    var url = "http://api.piczo.com/api-1/inbox" + "/images";

    var headers = {
      "api-token": this.gAPI.mAPIToken,
      "at": this.gAPI.mSession.token
    };
    api.call("get", url, headers, null, listener, true);
  };


  if (this.mAPIToken) {
    _logger.info("piczoService Calling getInbox");
    callGetInbox();
  } else {
    _logger.info("piczoService wasnt logged in....");
    // We are not logged in, force user to log in now.
    var aError = Cc['@flock.com/error;1'].createInstance(Ci.flockIError);
    aError.errorCode   = Ci.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN;
    aListener.onError(aError);
    return;
  }
}

// Handle the case where the session token goes stale
// If stale reset and return true, otherwise return false
piczoAPI.prototype.handleStaleToken =
function(aSubject) {
  
  // When a session token goes stale the piczo api  
  // marks the response with an http error (401 - unauthorized)
  // and includes error information with a new session token 
  
  _logger.debug("handleStaleToken()");
  var api = this;
  if (aSubject.status == 401) {
    var xml = aSubject.responseXML;
    _logger.debug(aSubject.responseText);
    
    var errorEl = xml.getElementsByTagName("errors").item(0);
    if (!errorEl || (errorEl.length == 0)) {
      return false;
    }

    var invalidCrdntlEl = errorEl.getElementsByTagName("error.invalid-credential").item(0);
    if (!invalidCrdntlEl || (invalidCrdntlEl.length == 0)) {
      return false;
    }

    var arglinkEl = invalidCrdntlEl.getElementsByTagName("argument").item(0);
    if (!arglinkEl || (arglinkEl.length == 0)) {
      return false;
    }

    var problem = arglinkEl.getAttribute("problem");
    if (!problem) {
      return false;
    }

    _logger.debug("problem = " + problem);
    if (problem == "expired-access") { 
      // Our access token went stale so grab the new one from the response
      var sessiontokenEl = errorEl.getElementsByTagName("at").item(0);
      var sessiontoken = sessiontokenEl.childNodes.item(0).nodeValue;
      _logger.debug("new access token = " + sessiontoken);
      api.mSession.token = sessiontoken;
      return true;
    }
  }
  return false; 
}

piczoAPI.prototype.getPhotos = 
function piczoAPI_getPhotos(aListener, aParams) {
  _logger.debug("[piczoAPI].getPhotos('" + aParams + "')");
  var api = this;
  var listener = {
    onSuccess: function(aSubject, aTopic) {
      var photos = [];
      _logger.debug("[piczoAPI].getPhotos returned: " + aSubject.responseText);
      var xml = aSubject.responseXML;

      // XXX Make this loop yield to avoid burning 100% cpu.
      var photoList = xml.getElementsByTagName("item");
      for (var i = 0; i < photoList.length; i++) {
        var photo = photoList[i];

        var id = photo.getElementsByTagName('guid')[0].firstChild.nodeValue;
        var link = photo.getElementsByTagName('link')[0].firstChild.nodeValue;
        var description = photo.getElementsByTagName('description')[0].firstChild.nodeValue;
        var thumbnail = photo.getElementsByTagName("thumbnail")
                             .item(0)
                             .getAttribute("url");

        var newMediaItem = Cc["@flock.com/photo;1"]
                           .createInstance(Ci.flockIMediaItem);
        newMediaItem.init("piczo", null);   // XXX Get this from service.
        newMediaItem.webPageUrl = link;
        newMediaItem.thumbnail = link;
        newMediaItem.midSizePhoto = thumbnail;
        newMediaItem.largeSizePhoto = link;
        newMediaItem.title = description;
        newMediaItem.id = id;
        newMediaItem.is_public = true;
        newMediaItem.is_video = false;

        photos.unshift(newMediaItem);
      }

      aListener.onSuccess(createEnum(photos), 'success');
    },
    onError: function(aSubject, aTopic, aFlockError) {
      if(aFlockError) {
        _logger.info('Error getting photos' + aFlockError.errorString);
      } else {
        _logger.info("Error getting photos");
      }

      if (api.handleStaleToken(aSubject)) {
        // Resubmit the request with the new token
        api.getPhotos(aListener,aParams);
      } else {
        aListener.onError(aFlockError);
      }
    }
  };

  var params = {
    subj_id: aParams.getPropertyAsAString("subj_id"),
    albumlabel: aParams.getPropertyAsAString("albumlabel")
  };

  _logger.info("calling getphotoscall"); 
  api.getPhotosCall(listener, 'piczo.photos.get', params);
}

piczoAPI.prototype.login =
function piczoAPI_login(aUsername, aPassword, aListener)
{
  _logger.debug("[piczoAPI].login('" + aUsername + "')");
  var api = this;
  if (aUsername) { this.mSession.username = aUsername; }
  if (aPassword) { this.mSession.password = aPassword; }

  var loginListener = {
    onSuccess: function (aSubject, aTopic) {
      _logger.info("[piczoAPI].login() loginListener.onSuccess()");
      _logger.debug(aSubject.responseText);
      var xmlDoc = aSubject.responseXML;
      var replyEl = xmlDoc.getElementsByTagName("login.reply").item(0);
      var userlinkEl = replyEl.getElementsByTagName("user.id").item(0);
      api.mSession.userGUID = userlinkEl.getAttribute("guid");
      _logger.debug(" got user GUID: "+api.mSession.userGUID);
      var sessiontokenEl = replyEl.getElementsByTagName("at").item(0);
      api.mSession.token = sessiontokenEl.childNodes.item(0).nodeValue;
      _logger.debug(" got session token: "+api.mSession.token);
  
      if (aListener) { aListener.onSuccess(aSubject, aTopic); }
    },
    onError: function (aSubject, aTopic, aError) {
      _logger.info("[piczoAPI].login() loginListener.onError()");
      if (aListener) { aListener.onError(aSubject, aTopic, aError); }
    }
  };

  var callLogin = function () {
    var url = gStrings["api-login-URL"];
    var headers = {
      "ts": api.getTS()
    };
    var postVars = {
      "user-name": api.mSession.username,
      "user-password": api.mSession.password,
      "id": api.mAPITokenID,
      "api-token": api.mAPIToken
    };
    api.call("post", url, headers, postVars, loginListener);
  };

  if (this.mAPIToken) {
    callLogin();
  } else {
    // Need to get an API token before we can log in
    var tokenListener = {
      onSuccess: function (aSubject, aTopic) {
        callLogin();
      },
      onError: function (aSubject, aTopic, aError) {
        if (aListener) { aListener.onError(aSubject, aTopic, aError); }
      }
    };
    this.getAPIToken(tokenListener);
  }
}

piczoAPI.prototype.logout =
function piczoAPI_logout()
{
  _logger.info("[piczoAPI].logout()");
  this.mSession = [];
  
  var api = this;

  var logoutListener = {
    onSuccess: function (aSubject, aTopic) {
      _logger.info("[piczoAPI].logout() logoutListener.onSuccess()");
      _logger.debug(aSubject.responseText);
      var xmlDoc = aSubject.responseXML;
      var replyEl = xmlDoc.getElementsByTagName("logout.reply").item(0);
    },
    onError: function (aSubject, aTopic, aError) {
      _logger.info("[piczoAPI].logout() logoutListener.onError()");
    }
  };

  var callLogout = function () {
    var url = gStrings["api-logout-URL"];
    var headers = {
      "ts": api.getTS()
    };
    var postVars = {
      "user-name": api.mSession.username,
      "user-password": api.mSession.password,
      "id": api.mAPITokenID,
      "api-token": api.mAPIToken
    };
    api.call("post", url, headers, postVars, logoutListener);
  };

  if (this.mAPIToken) {
    callLogout();
  } else {
    // Need to get an API token before we can log out
    var tokenListener = {
      onSuccess: function (aSubject, aTopic) {
        callLogout();
      },
      onError: function (aSubject, aTopic, aError) {
        if (aListener) { aListener.onError(aSubject, aTopic, aError); }
      }
    };
    this.getAPIToken(tokenListener);
  }  
}

piczoAPI.prototype.upload = 
function piczoAPI_upload(aListener, aPhoto, aParams, aUpload) {
  _logger.info("piczoAPI_upload");
  // XXX: TODO
  var inst = this;
  this.uploader = new PhotoUploader();
  var myListener = {
    onResult: function(aXML) {
      _logger.debug("piczoAPI_upload: Got a result back from uploader: " + aXML.responseText);
      aListener.onUploadComplete(aUpload);
    },
    onError: function(aErrorCode) {
      _logger.info("piczoAPI_upload: " + aErrorCode);
      aListener.onError(aErrorCode);
    },
    onProgress: function(aCurrentProgress) {
      _logger.info("piczoAPI_upload: Progress");
      aListener.onProgress(aCurrentProgress);
    }
  }
  gAPI.convertBools(aParams);
  gAPI.convertTags(aParams);

  var dsvalue = this.getDSValue();

  // Check for default album seleciton
  if(!aUpload.album) {
    // auto-select the first album
    aUpload.album = gAPI.defAlbum.id;
  }

  this.uploader.setEndpoint(aUpload.album + "&ds=" + dsvalue);
  this.uploader.upload(myListener, aPhoto, aParams);
}

piczoAPI.prototype.getDSValue = 
function piczAPI_getDSValue() {
  _logger.info("piczoAPI_getDSValue");
  var msg = gAPI.mAPIToken + gAPI.mSession.token;
  var msgArr = [];
  for (var i = 0; i < msg.length; i++) {
    msgArr.push(msg.charCodeAt(i));
  }
  this.cryptoHash.init(Components.interfaces.nsICryptoHash.MD5);
  this.cryptoHash.update(msgArr, msgArr.length);
  var sig = this.cryptoHash.finish(true);
  var dsValue = escape(sig);
  dsValue = dsValue.replace(/\+/g,"%2B");
  dsValue = dsValue.replace(/\//g,"%2F");
  dsValue = dsValue.replace(/\@/g,"%40");

  return dsValue;
}

piczoAPI.prototype.convertBools = 
function piczoAPI_convertBools(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";
  }
}

piczoAPI.prototype.convertTags = 
function piczoAPI_convertTags(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(",");
  }
}

piczoAPI.prototype.isLoggedIn =
function piczoAPI_isLoggedIn()
{
  return (this.mSession.token && this.mSession.token.length);
}

piczoAPI.prototype.handleDragDrop = 
function piczoAPI_handleDragDrop(url, locX, locY) {
  _logger.info("piczoAPI_handleDragDrop: " + url + ", " + locX + ", " + locY);
  // http://server/go/placeimage
  // parameters:
  // imgid  (Image Id)
  // pageid (Destination page id)
  // sl     (x position)
  // st     (y position)

  // Make sure we are logged in
  // Hack it up
  var imageID;
  var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                          .createInstance(Components.interfaces.nsIWritablePropertyBag2);
  if (gSVC.webDetective.detectNoDOM("piczo", "piczoImageID", null, url, results)) {
    imageID = results.getPropertyAsAString("imageID");
  }

  var currTab;
  // Get active tab
  var wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
  var win = wm.getMostRecentWindow('navigator:browser');
  if (win) {
    var gBrowser = win.gBrowser;

    for(var i=0;i<gBrowser.mTabs.length;i++) {
      var browser = gBrowser.mTabs[i].linkedBrowser;

      if(browser.currentURI.spec == gBrowser.currentURI.spec) {
        // We found the activce tab
        currTab = browser;
      }
    }
  }

  // Get active tab's top-level document
  _logger.debug("piczoAPI: document: " + currTab.contentDocument); 
  var theDoc = currTab.contentDocument;

  // Make sure we're in a piczo edit page
  results = Components.classes["@mozilla.org/hash-property-bag;1"]
                      .createInstance(Components.interfaces.nsIWritablePropertyBag2);
  if (!gSVC.webDetective.detect("piczo", "piczoedit", theDoc, results)) {
    // not a piczo edit page, return.
    _logger.info("[piczoAPI] handleDragDrop() - Not a piczo edit page");
    return false;
  }

  const PAGEID_STR = "pageid";
  var serverLoc;
  var loc = currTab.currentURI.spec;
  if(loc.match(/:\/\/([^\/]*)\//)) {
    serverLoc = RegExp.$1;
  }
  
  _logger.debug("piczoAPI: ServerLoc = " + serverLoc);

  // Get array of top-level documents' immediate children documents (sub-frames)
  var frames = theDoc.getElementsByTagName('frame');
  var iframes = theDoc.getElementsByTagName('iframe');

  // apply webdetective rule to each document and grab pageid
  var frameFound = null;
  var pageID = null;
  results = Components.classes["@mozilla.org/hash-property-bag;1"]
                          .createInstance(Components.interfaces.nsIWritablePropertyBag2);
  for (var i = 0; i < frames.length; i++) {
    if (gSVC.webDetective.detect("piczo", "webedit", frames[i].contentDocument, results)) {
      pageID = results.getPropertyAsAString(PAGEID_STR);
      frameFound = frames[i];
      break;
    }
  }
  for (var i = 0; i < iframes.length && !frameFound; i++) {
    if (gSVC.webDetective.detect("piczo", "webedit", iframes[i].contentDocument, results)) {
      pageID = results.getPropertyAsAString(PAGEID_STR);
      frameFound = iframes[i];
      break;
    }
  }
  _logger.debug("piczoAPI: pageid=" + pageID);

  // Setup the url 
  var aURL = "http://" + serverLoc
              + "/go/placeimage?imgid=" + imageID
              + "&pageid=" + pageID
              + "&sl=" + locX 
              + "&st=" + locY;

  // Handler for response.
  var listener = {
    onSuccess: function (aSubject, aTopic) {
      _logger.info("[piczoAPI].handleDragDrop() handleDragDropListener.onSuccess()");
      _logger.debug("page frame before string mangling: " + frameFound.src);

      // need to do a little string mangling to fool the service to reload a
      // child frame instead of the parent page
      // pass in the child frame pageid and reload
      var re = /pageid=\d+/;
      frameFound.src = frameFound.src.replace(re, PAGEID_STR + "=" + pageID);

      _logger.debug("page frame after string mangling: " + frameFound.src);
      return true;
    },
    onError: function (flockError) {
      return false;
    }
  }

  // Make simple xmlhttprequest to "move" the image to the right location
  var hr = Components.classes['@mozilla.org/xmlextras/xmlhttprequest;1']
                     .createInstance(Components.interfaces.nsIXMLHttpRequest);
  hr.QueryInterface(Components.interfaces.nsIJSXMLHttpRequest);
  hr.onreadystatechange = function (aEvent) {
    _logger.debug("[piczoAPI].call(): hr.onreadystatechange for Moving URL: "+hr.readyState);
    if (listener) {
      if (hr.readyState == 4) {
        try {
          if (hr.status/100 == 2) {
            try {
              listener.onSuccess(hr, aURL);
            } catch (ex) {
              _logger.debug(ex);
              listener.onError(null);
            }
          } else {
            // HTTP error
            listener.onError(null);
          }
        } catch (ex) {
          _logger.info(ex);
          var err = {
            errorCode: Components.interfaces.flockIError.PHOTOSERVICE_UNAVAILABLE,
            errorString: "Request did not complete."
          };
          listener.onError(err);
        }
      }
    }
  };
  hr.open("GET", aURL, true);
  hr.send(null);

  // We are returning true at this point because we made it to the send() call,
  // but we won't know if the send actually succeeded because it is being called
  // asynchronously (and cannot be called synchronously, as we need to use the
  // callbacks).
  return true;
}

piczoAPI.prototype.getFriends =
function piczoAPI_getFriends(aListener)
{
  _logger.info("[piczoAPI].getFriends()");
  var api = this;

  var getNodeSubvalue = function (aNode) {
    var longest = "";
    for (var i = 0; i < aNode.childNodes.length; i++) {
      var item = aNode.childNodes.item(i);
      if (item.nodeValue.length > longest.length) {
        longest = item.nodeValue;
      }
    }
    return longest;
  };

  var friendsListener = {
    onSuccess: function (aSubject, aTopic) {
      _logger.info("[piczoAPI].getFriends() friendsListener.onSuccess()");
      _logger.debug(aSubject.responseText);
      var replyEl = aSubject.responseXML.getElementsByTagName("lookup.reply").item(0);
      var friendsEl = replyEl.getElementsByTagName("friends").item(0);
      var friendEls = friendsEl.getElementsByTagName("friend");
      var friends = [];
      for (var i = 0; i < friendEls.length; i++) {
        var friendEl = friendEls.item(i);
        var primaryProfEl = friendEl.getElementsByTagName("primary-profile").item(0);
        var thumbEl = primaryProfEl.getElementsByTagName("thumbnail").item(0);
        var websiteEl = primaryProfEl.getElementsByTagName("website-link").item(0);
        friends.push({
          id: friendEl.getAttribute("guid"),
          name: getNodeSubvalue(friendEl.getElementsByTagName("name").item(0)),
          avatarURL: getNodeSubvalue(thumbEl.getElementsByTagName("uri").item(0)),
          URL: getNodeSubvalue(websiteEl.getElementsByTagName("uri").item(0))
        });
      }
      aListener.onSuccess(friends, aTopic);
    },
    onError: function (aSubject, aTopic, aFlockError) {
      _logger.info("[piczoAPI].getFriends() friendsListener.onError()");
      if (api.handleStaleToken(aSubject)) {
        // Resubmit the request with the new token
        callGetFriends();
      } else {
        aListener.onError(aFlockError);
      }
    }
  };

  var callGetFriends = function () {
    var url = gStrings["api-friends-URL"];
    var headers = {
      "ts": api.getTS(),
      "sn": api.mSession.token,
    };
    api.call("get", url, headers, null, friendsListener, true);
  };

  callGetFriends();
}

// ========== END piczoAPI class ==========



// ==============================================
// ========== BEGIN piczoAccount class ==========
// ==============================================

function piczoAccount()
{
  _logger.info("piczoAccount CONSTRUCTOR");
  this.acUtils = Components.classes["@flock.com/account-utils;1"]
                           .getService(Components.interfaces.flockIAccountUtils);
  this._coop = Components.classes["@flock.com/singleton;1"]
                         .getService(Components.interfaces.flockISingleton)
                         .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                         .wrappedJSObject;
  this.mAPI = gAPI;
  this._ctk = {
    interfaces: [
      "nsISupports",
      "flockIWebServiceAccount",
      "flockIMediaWebServiceAccount",
      "flockIMediaUploadAccount",
    ],
  };
  getCompTK().addAllInterfaces(this);

  _logger.debug("piczoAccount Constructor End.");
}

piczoAccount.prototype.urn = null;

// BEGIN flockIWebServiceAccount interface
piczoAccount.prototype.login =
function piczoAccount_login(aListener)
{
  var account = this;
  var c_acct = account._coop.get(account.urn);
  var loginListener = {
    onSuccess: function (aSubject, aTopic) {
      account.acUtils.ensureOnlyAuthenticatedAccount(account.urn);
      c_acct.piczo_guid = account.mAPI.mSession.userGUID;
      if (aListener) { aListener.onSuccess(aSubject, aTopic); }
    },
    onError: function (aSubject, aTopic, aError) {
      c_acct.isAuthenticated = false;
      if (aListener) { aListener.onError(aSubject, aTopic, aError); }
    }
  };
  var pw = this.acUtils.getPassword("urn:piczo:service"+':'+c_acct.accountId);
  this.mAPI.login(pw.user, pw.password, loginListener);
}

piczoAccount.prototype.logout =
function piczoAccount_logout(aListener)
{
  _logger.info("{flockIWebServiceAccount}.logout()");
  var c_acct = this._coop.get(this.urn);
  if (c_acct.isAuthenticated) {
    try {
      this.mAPI.logout();
    } catch (ex) {
      _logger.warn("API logout call failed with error: "+ex);
    }
    Cc[CONTRACT_ID].getService(Ci.flockIWebService).logout();
    c_acct.isAuthenticated = false;
  }
}

piczoAccount.prototype.activate =
function piczoAccount_activate(aListener)
{
  _logger.info("{flockIWebServiceAccount}.activate()");
  var c_acct = this._coop.get(this.urn);
  //c_acct.isTransient = false;
  var account = this;
  var loginListener = {
    onSuccess: function (aSubject, aTopic) {
      _logger.info(".activate() loginListener.onSuccess()");
      // XXX TODO FIXME: API login
      if (aListener) aListener.onSuccess(account, "accountAuthorized");
      c_acct.isPollable = true;
      account.acUtils.ensureOnlyAuthenticatedAccount(account.urn);
    },
    onError: function (aSubject, aTopic, aError) {
      _logger.info(".activate() loginListener.onError()");
      c_acct.isAuthenticated = true;
    }
  };
  //this.acUtils.makeTempPasswordPermanent(this.service.urn+':'+c_acct.accountId);
  this.login(loginListener);
  // Assume login will succeed.  Listener will handle failures.
  c_acct.isAuthenticated = true;
}
// END flockIWebServiceAccount interface

// ========== END piczoAccount class ==========



// =========================================
// ========== BEGIN XPCOM 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: piczoService,
    CID: CLASS_ID,
    contractID: CONTRACT_ID,
    componentName: CATEGORY_COMPONENT_NAME,
    implementationFunc: function (aComp) { getCompTK().addAllInterfaces(aComp); aComp.init(); },
    categories: [ 
      { category: "wsm-startup", entry: CATEGORY_COMPONENT_NAME, value: CONTRACT_ID },
      { category: "flockWebService", entry: CATEGORY_ENTRY_NAME, value: CONTRACT_ID },
      { category: "flockMediaProvider", entry: CATEGORY_ENTRY_NAME, value: CONTRACT_ID }
    ]
  });
}

// ========== END XPCOM Support ==========
