// 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

// Constants
const CC = Components.classes;
const CI = Components.interfaces;
const CR = Components.results;
const CU = Components.utils;

CU.import("resource:///modules/FlockCryptoHash.jsm");
CU.import("resource:///modules/FlockSvcUtils.jsm");

const FACEBOOK_API_CID = Components.ID("{53b077f0-6155-11db-b0de-0800200c9a66}");
const FACEBOOK_CONTRACTID     = "@flock.com/people/facebook;1";
const FACEBOOK_API_CONTRACTID = "@flock.com/api/facebook;1";

// From developer.facebook.com...
const FACEBOOK_API_VERSION = "1.0";
const FACEBOOK_API_HOSTNAME = "http://api.facebook.com/";
const FACEBOOK_API_ENDPOINT_URL = FACEBOOK_API_HOSTNAME + "restserver.php";

// The extended permissions we need
// see http://wiki.developers.facebook.com/index.php/Extended_permissions
const EXTENDED_PERMISSIONS = ["status_update", "photo_upload"];

// From nsIXMLHttpRequest.idl
// 0: UNINITIALIZED open() has not been called yet.
// 1: LOADING       send() has not been called yet.
// 2: LOADED        send() has been called, headers and status are available.
// 3: INTERACTIVE   Downloading, responseText holds the partial data.
// 4: COMPLETED     Finished with all operations.
const XMLHTTPREQUEST_READYSTATE_UNINITIALIZED = 0;
const XMLHTTPREQUEST_READYSTATE_LOADING = 1;
const XMLHTTPREQUEST_READYSTATE_LOADED = 2;
const XMLHTTPREQUEST_READYSTATE_INTERACTIVE = 3;
const XMLHTTPREQUEST_READYSTATE_COMPLETED = 4;

const HTTP_CODE_OK = 200;
const HTTP_CODE_FOUND = 302;

const XMLHTTPREQUEST_CONTRACTID = "@mozilla.org/xmlextras/xmlhttprequest;1";
const FLOCK_ERROR_CONTRACTID = "@flock.com/error;1";

const FLOCK_PHOTO_ALBUM_CONTRACTID  = "@flock.com/photo-album;1";

const FLOCK_SNOWMAN_URL = "chrome://flock/skin/services/common/no_avatar.png";

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

function loadLibraryFromSpec(aSpec) {
  _getLoader().loadSubScript(aSpec);
}


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


function _validateUid(aUid) {
  if (aUid.match(/[^0-9]/)) {
    throw "Invalid UID '" + aUid + "': the UID must be a numeric value";
  }
}

/**
 * Represents the Facebook API
 */
function facebookAPI() {
  loadLibraryFromSpec("chrome://flock/content/photo/photoAPI.js");
  this.acUtils = CC["@flock.com/account-utils;1"]
                 .getService(CI.flockIAccountUtils);
  var obService = CC["@mozilla.org/observer-service;1"]
                  .getService(CI.nsIObserverService);
  this._WebDetective = this.acUtils.useWebDetective("facebook.xml");

  this.api_key = this._WebDetective.getString("facebook",
                                              "facebookApiKey",
                                              "");
  var apiSecretHash = this._WebDetective.getString("facebook",
                                                   "facebookApiSecretHash",
                                                   "");
  this.api_secret = FlockSvcUtils.scrambleString(apiSecretHash);
  this.endpoint = FACEBOOK_API_ENDPOINT_URL;
  this.session_key = null;
  this.prettyName = "Facebook";
  this.uid = null;
  this._friendsCache = {
    value: null,
    lastUpdate: new Date(0)
  };

  this._logger = CC["@flock.com/logger;1"].createInstance(CI.flockILogger);
  this._logger.init("facebookAPI");
  this.wrappedJSObject = this;
  this._permissionUrl = this._WebDetective
                            .getString("facebook",
                                       "facebookApi_PERM_URL",
                                       "")
                            .replace("%apikey%", this.api_key);
  this._permissionBody = this._WebDetective
                             .getString("facebook",
                                        "facebookApi_PERM_BODY",
                                        "");
}

//****************************************
//* Authentication & Session Calls
//****************************************/

facebookAPI.prototype.sessionPing =
function facebookAPI_sessionPing(aFlockListener) {
  this.authCall(aFlockListener, "facebook.session.ping", {}, true);
}

facebookAPI.prototype.authenticate =
function facebookAPI_authenticate(aFlockListener) {
  if (this.session_key) {
    aFlockListener.onSuccess(null, "authenticated");
    return;
  }
  this._logger.debug("authenticate()");
  var api = this;
  var tokenListener = {
    onSuccess: function auth_onSuccess(aToken, aTopic) {
      var authUrl = api.getAuthUrl(aToken);

      var req = CC[XMLHTTPREQUEST_CONTRACTID].
                createInstance(CI.nsIXMLHttpRequest);

      var onReadyStateFunc = function onReadyStateFunc(eEvt) {
        if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
          api._logger.debug("req: \n" + req.responseText);
          var results = CC["@mozilla.org/hash-property-bag;1"]
                        .createInstance(CI.nsIWritablePropertyBag2);

          // Check if Facebook is redirecting us to the TOS.
          if (api._WebDetective.detectNoDOM("facebook",
                                            "apiAuth0",
                                            "",
                                            req.responseText,
                                            results))
          {
            // Follow the redirect to get to the TOS.
            authUrl = results.getPropertyAsAString("authUrl")
                             .replace("\\/", "/", "g");
            api._logger.debug("apiAuth0 authUrl: \n" + authUrl);

            var req0 = CC[XMLHTTPREQUEST_CONTRACTID]
                       .createInstance(CI.nsIXMLHttpRequest);
            req0.onreadystatechange = function onReadyStateFunc0(eEvt) {
              if (req0.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
                api._logger.debug("req0: \n" + req0.responseText);
                api._handleTosRequest(req0, aToken, aFlockListener);
              }
            };
            req0.mozBackgroundRequest = true;
            req0.open("GET", authUrl, true);
            req0.overrideMimeType("text/txt");
            req0.send(null);
          } else {
            // Already at the TOS.
            api._handleTosRequest(req, aToken, aFlockListener);
          }
        }
      };

      req.onreadystatechange = onReadyStateFunc;
      req.mozBackgroundRequest = true;
      req.open("GET", authUrl, true);
      req.overrideMimeType("text/txt");
      req.send(null);
    },
    onError: aFlockListener.onError
  };
  this.createToken(tokenListener);
}

facebookAPI.prototype.deauthenticate =
function facebookAPI_deauthenticate() {
  this._logger.debug(".deauthenticate()");
  this.session_key = null;
  this.secret = null;
  this.uid = null;
  this._friendsCache.value = null;
}


// INTERNAL
facebookAPI.prototype._handleTosRequest =
function facebookAPI__handleTosRequest(aRequest, aToken, aFlockListener) {
  this._logger.debug(arguments.callee.name);
  // If we get the Facebook Platform User Terms of Service (apiAuth1) we
  // know that the user has not authorized Flock yet and we must send
  // another request to complete the authorization.
  var results = CC["@mozilla.org/hash-property-bag;1"]
                .createInstance(CI.nsIWritablePropertyBag2);
  if (this._WebDetective.detectNoDOM("facebook",
                                     "apiAuth1",
                                     "",
                                     aRequest.responseText,
                                     results))
  {
    var postBody = "grant_perm=1"
                 + "&save_login=on"
                 + "&next="
                 + "&api_key="
                 + this.api_key
                 + "&auth_token="
                 + aToken
                 + "&post_form_id="
                 + results.getPropertyAsAString("formId");

    var api = this;
    var req = CC[XMLHTTPREQUEST_CONTRACTID]
              .createInstance(CI.nsIXMLHttpRequest);

    req.onreadystatechange = function reqOnReadyStateFunc (eEvt) {
      if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
        api._logger.debug("TOS req: \n" + req.responseText);
        if (api._WebDetective.detectNoDOM("facebook",
                                          "apiAuth2",
                                          "",
                                          req.responseText,
                                          null))
        {
          // Authorization received
          api.getSession(aToken, aFlockListener);
        } else {
          // We were not able to complete the authorization step
          api._logger.debug(aRequest.responseText);
          var error = CC[FLOCK_ERROR_CONTRACTID]
                      .createInstance(CI.flockIError);
          error.errorCode = error.PHOTOSERVICE_LOGIN_FAILED;
          api._logger.debug("apiAuth2 Failed.");
          aFlockListener.onError(error, arguments.callee.name);
        }
      }
    };

    req.mozBackgroundRequest = true;
    req.open("POST", results.getPropertyAsAString("postUrl"), true);
    req.setRequestHeader("Content-Type",
                         "application/x-www-form-urlencoded");
    req.overrideMimeType("text/txt");
    req.send(postBody);
  } else {
    if (this._WebDetective.detectNoDOM("facebook",
                                       "apiAuth2",
                                       "",
                                       aRequest.responseText,
                                       null))
    {
      // The user has previously authorized Flock
      this.getSession(aToken, aFlockListener);
    } else {
      // Authorization failed
      var error = CC[FLOCK_ERROR_CONTRACTID]
                  .createInstance(CI.flockIError);
      error.errorCode = error.PHOTOSERVICE_LOGIN_FAILED;
      this._logger.debug("apiAuth2 Failed.");
      aFlockListener.onError(error, arguments.callee.name);
    }
  }
}

// INTERNAL
facebookAPI.prototype.createToken =
function facebookAPI_createToken(aFlockListener) {
  this._logger.debug("createToken()\n");
  var api = this;
  var tokenListener = {
    onSuccess: function onSuccess(aXml, aTopic) {
      api._logger.debug(".createToken().onSuccess " + aXml);
      try {
        var token = aXml.getElementsByTagName("token")[0].firstChild.nodeValue;
        aFlockListener.onSuccess(token, aTopic);
      } catch (err) {
        try {
          var token = aXml.getElementsByTagName("auth_createToken_response")[0]
                          .firstChild
                          .nodeValue;
          aFlockListener.onSuccess(token, aTopic);
        } catch (err) {
          api._logger.error("createToken " + err);
          aFlockListener.onError(null, null);
        }
      }
    },
    onError: aFlockListener.onError
  };
  this.call(tokenListener, "facebook.auth.createToken");
}

// INTERNAL
// Grant Flock with special permissions
// see http://wiki.developers.facebook.com/index.php/Extended_permissions
facebookAPI.prototype._grantPermission =
function facebookAPI__grantPermission(aPermission, aFlockListener) {
  this._logger.debug("grantPermission(" + aPermission + ")");
  var api = this;
  var url = this._permissionUrl.replace("%perm%", aPermission);

  // Actually grant the permission
  function grantPermission (aPostFormId) {
    var xhr2 = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
              .createInstance(CI.nsIXMLHttpRequest);
    xhr2.mozBackgroundRequest = true;
    xhr2.open("POST", url, true);
    xhr2.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

    xhr2.onreadystatechange = function (aEvent) {
      if (xhr2.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
        if (xhr2.status != HTTP_CODE_OK) {
          if (aFlockListener) {
            aFlockListener.onError (xhr2.statusText);
          }
          return;
        }
        api._logger.info("Successfully granted permission: " + aPermission);
        if (aFlockListener) {
          aFlockListener.onSuccess();
        }
      }
    }
    var body = api._permissionBody.replace("%postformid%", aPostFormId);
    xhr2.send(body);
  }

  // Show the page to get the form
  var xhr = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
            .createInstance(CI.nsIXMLHttpRequest);
  xhr.mozBackgroundRequest = true;
  xhr.open("GET", url, true);
  xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

  xhr.onreadystatechange = function (aEvent) {
    if (xhr.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      if (xhr.status != HTTP_CODE_OK) {
        if (aFlockListener) {
          aFlockListener.onError (xhr.statusText);
        }
        return;
      }

      // Get the post_form_id and grant the permission for good
      var results = CC["@mozilla.org/hash-property-bag;1"]
                    .createInstance(CI.nsIWritablePropertyBag2);
      try {
        if (api._WebDetective.detectNoDOM("facebook",
                                          "permission",
                                          "",
                                          xhr.responseText,
                                          results))
        {
          var postFormId = results.getPropertyAsAString("postFormId");
          grantPermission(postFormId);
        }
      } catch (ex) {
        api._logger.debug("._grantPermission() exception:" + ex);
        if (aFlockListener) {
          aFlockListener.onError();
        }
      }
    }
  }

  xhr.send(null);
}


// INTERNAL
// Get all extended permissions we need
facebookAPI.prototype._grantAllPermissions =
function facebookAPI__grantAllPermissions(aFlockListener) {
  var completed = 0;
  var error = false;

  function finalize() {
    if (completed >= EXTENDED_PERMISSIONS.length) {
      if (error) {
        aFlockListener.onError();
      } else {
        aFlockListener.onSuccess(null, "authenticated");
      }
    }
  }

  var listener = {
    onSuccess: function onSuccess(aSubject, aTopic) {
      completed++;
      finalize();
    },
    onError: function onError(aFlockError, aTopic) {
      completed++;
      error = true;
      finalize();
    }
  };

  for each (let perm in EXTENDED_PERMISSIONS) {
    this._grantPermission(perm, aFlockListener);
  }
}

// INTERNAL
facebookAPI.prototype.getSession =
function facebookAPI_getSession(aToken, aFlockListener) {
  var api = this;
  var aParams = {
    auth_token: aToken
  };
  var sessionListener = {
    onSuccess: function onSuccess(aXml, aTopic) {
      api.session_key = aXml.getElementsByTagName("session_key")[0]
                            .firstChild
                            .nodeValue;
      api.secret = aXml.getElementsByTagName("secret")[0]
                       .firstChild
                       .nodeValue;
      api.uid = aXml.getElementsByTagName("uid")[0]
                    .firstChild
                    .nodeValue;
      api._friendsCache.value = null;
      api._grantAllPermissions(aFlockListener);
    },
    onError: aFlockListener.onError
  };
  api.call(sessionListener, "facebook.auth.getSession", aParams);
}

// INTERNAL
facebookAPI.prototype.getAuthUrl =
function facebookAPI_getAuthUrl(aToken) {
  return "http://www.facebook.com/login.php?api_key=" + this.api_key
         + "&v=" + FACEBOOK_API_VERSION + "&auth_token=" + aToken;
}

facebookAPI.prototype.getFacebookURL =
function facebookAPI_getFacebookURL(aUrlType, aFriendId) {

  switch (aUrlType) {
    case "profile":
      return "http://www.facebook.com/profile.php?uid="
             + aFriendId
             + "&api_key="
             + this.api_key;
      break;
    case "myprofile":
      return "http://www.facebook.com/profile.php?id="
             + aFriendId
             + "&api_key="
             + this.api_key;
      break;
    case "poke":
      return "http://www.facebook.com/poke.php?uid="
             + aFriendId
             + "&api_key="
             + this.api_key;
      break;
    case "message":
      return "http://www.facebook.com/message.php?uid="
             + aFriendId
             + "&api_key="
             + this.api_key;
      break;
    case "editprofile":
      return "http://www.facebook.com/editprofile.php";
      break;
    case "photos":
      return "http://www.facebook.com/photo_search.php?uid="
             + aFriendId
             + "&api_key="
             + this.api_key;
      break;
    case "userphotos":
      return "http://www.facebook.com/photo_search.php?uid="
             + aFriendId
             + "&api_key="
             + this.api_key;
      break;
    case "myphotos":
      return "http://www.facebook.com/photos.php?id="
             + aFriendId;
      break;
    case "postwall":
      return "http://www.facebook.com/wallpost.php?uid="
             + aFriendId
             + "&api_key="
             + this.api_key;
      break;
    case "addfriend":
      return "http://www.facebook.com/addfriend.php?uid="
             + aFriendId
             + "&api_key="
             + this.api_key;
      break;
    case "friendrequests":
      return "http://www.facebook.com/reqs.php";
      break;
    case "messages":
      return "http://www.facebook.com/mailbox.php";
      break;
    case "homepage":
      return "http://www.facebook.com/home.php";
      break;
  }
  return "";
}

//****************************************
//* Friends Calls
//****************************************/


facebookAPI.prototype.friendsGet =
function facebookAPI_friendsGet(aFlockListener) {
  var api = this;
  var listener = {
    onSuccess: function listener_onSuccess(aResult) {
      var peeps = {};
      for (var i in aResult) {
        var uid = aResult[i].uid;
        peeps[uid] = aResult[i];

        if (!peeps[uid].pic_square) {
          peeps[uid].pic_square = "";
        }

        if (!peeps[uid].profile_update_time) {
          peeps[uid].profile_update_time = 0;
        }

        if (!peeps[uid].status) {
          peeps[uid].status = {
            time: 0,
            message: ""
          }
        }

        api._logger.debug("Got facebook person named "
                          + peeps[uid].name);
      }

      var result = {
        wrappedJS: peeps
      };
      aFlockListener.onSuccess(result, "success");
    },
    onError: function listener_onError(aFlockError) {
      api._logger.debug(".friendsGet() error: " + aFlockError.errorCode);
    }
  }

  // For security let's make sure this.uid is what
  // it's supposed to be: just a number
  _validateUid(this.uid);

  var friendsQuery = "SELECT uid,name,pic_square,status,profile_update_time "
            + "FROM user "
            + "WHERE uid IN (SELECT uid2 FROM friend WHERE uid1 = "+this.uid+")";

  this.authCall(listener, "facebook.fql.query", { query: friendsQuery }, true);
}


facebookAPI.prototype.getUpdatedMediaFriends =
function facebookAPI_getUpdatedMediaFriends(aFlockListener, aLastSeen) {
  var api = this;
  var listener = {
    onSuccess: function gumfListener_onSuccess(aResult) {
      // We put everything in a hash first to avoid duplicates
      var friends = {};
      for (var i in aResult) {
        var owner = aResult[i]["owner"];
        // 10 for Base 10
        var created = parseInt(aResult[i]["created"], 10);
        if (friends[owner]) {
          friends[owner].count++;
          if (created > friends[owner].latest) {
            friends[owner].latest = created;
          }
        } else {
          friends[owner] = {
            count: 1,
            latest: created
          };
        }
      }
      var result = [];
      for (var i in friends) {
        var person = CC["@mozilla.org/hash-property-bag;1"]
                     .createInstance(CI.nsIWritablePropertyBag2);
        person.setPropertyAsAString("uid", i);
        person.setPropertyAsInt32("count", friends[i].count);
        person.setPropertyAsInt32("latest", friends[i].latest);
        result.push(person);
      }
      aFlockListener.onSuccess(createEnum(result), "success");
    },
    onError: function gumfListener_onError(aFlockError, aTopic) {
      api._logger.debug(".getUpdatedMediaFriends() error: "
                        + aFlockError.errorCode);
    }
  }

  // For security let's make sure this.uid is what
  // it's supposed to be: just a number
  _validateUid(this.uid);

  var friendsQuery = "SELECT uid1 "
                   + "FROM friend "
                   + "WHERE uid2=" + this.uid;
  var albumQuery = "SELECT aid "
                 + "FROM album "
                 + "WHERE modified > " + aLastSeen
                 + " AND owner IN " + "(" + friendsQuery + ")";
  var query = "SELECT owner,created "
            + "FROM photo "
            + "WHERE created > " + aLastSeen
              + " AND " + "aid IN (" + albumQuery + ")";

  this.authCall(listener, "facebook.fql.query", { query: query }, true);
};

//****************************************
//* Users Info Call
//****************************************/

facebookAPI.prototype.usersGetInfo =
function facebookAPI_usersGetInfo(aFlockListener, users, fields) {
  var params = {
    uids: users,
    fields: "name,pic_square,status"
  }

  if (fields) {
    this._logger.debug("WARNING - arbitrary fields are not handled yet");
    params.fields = fields;
  }

  var api = this;
  var listener = {
    onSuccess: function ugiListener_onSuccess(aResult) {
      var peeps = [];
      for (var i in aResult) {
        var user = aResult[i];
        var person = CC["@mozilla.org/hash-property-bag;1"]
                     .createInstance(CI.nsIWritablePropertyBag2);
        person.setPropertyAsAString("uid", user.uid);
        person.setPropertyAsAString("name", user.name);
        person.setPropertyAsAString("avatar", user.pic_square);
        var status = "";
        var lastStatusMessageUpdateDate = 0;
        if (user.status) {
          status = user.status.message ? user.status.message : "";
          lastStatusMessageUpdateDate = user.status.time;
        }

        person.setPropertyAsAString("statusMessage", status);
        person.setPropertyAsAString("lastStatusMessageUpdateDate", lastStatusMessageUpdateDate);

        peeps.push(person);
      }

      aFlockListener.onSuccess(createEnum(peeps), "success");
    },
    onError: function ugiListener_onError(aFlockError) {
      api._logger.debug(".usersGetInfo() error:" + aFlockError.errorString);
    }
  }

  this.authCall(listener, "facebook.users.getInfo", params, true);
}


facebookAPI.prototype.notificationsGet =
function facebookAPI_notificationsGet(aFlockListener) {
  // FIXME: Use JSON or at least E4X (bug 9573)
  var api = this;
  var listener = {
    onSuccess: function ngListener_onSuccess(aXml) {
      var notifications = [];

      var meNotifications = CC["@mozilla.org/hash-property-bag;1"]
                            .createInstance(CI.nsIWritablePropertyBag2);
      meNotifications.setPropertyAsAString("messages",
        aXml.getElementsByTagName("messages")[0]
            .getElementsByTagName("unread")[0]
            .childNodes[0]
            .nodeValue);
      meNotifications.setPropertyAsAString("pokes",
        aXml.getElementsByTagName("pokes")[0]
            .getElementsByTagName("unread")[0]
            .firstChild
            .nodeValue);
      meNotifications.setPropertyAsAString("friendRequests",
        aXml.getElementsByTagName("friend_requests")[0]
            .getElementsByTagName("uid")
            .length);
      meNotifications.setPropertyAsAString("groupInvites",
        aXml.getElementsByTagName("group_invites")[0]
            .getElementsByTagName("gid")
            .length);
      meNotifications.setPropertyAsAString("eventInvites",
        aXml.getElementsByTagName("event_invites")[0]
            .getElementsByTagName("eid")
            .length);
      notifications.push(meNotifications);

      aFlockListener.onSuccess(createEnum(notifications), "success");
    },
    onError: function ngListener_onError(aFlockError) {
      api._logger.debug(".notificationsGet() error:" + aFlockError.errorString);
    }
  };

  this.authCall(listener, "facebook.notifications.get", {});
}


facebookAPI.prototype.setStatus =
function facebookAPI_setStatus(aNewStatus, aFlockListener) {
  var params = {
    status_includes_verb: true // Tell FB not to prepend "is"
  };
  if (aNewStatus && aNewStatus != "") {
    params.status = aNewStatus;
  } else {
    params.clear = true;
  }

  // The function requires the extended permission "status_update".
  // We're setting that at login time (see _grantAllPermissions)
  this.authCall(aFlockListener, "facebook.users.setStatus", params);
}


//****************************************
//* Photo Call
//****************************************/

facebookAPI.prototype.getAlbums =
function facebookAPI_getAlbums(aFlockListener, userID) {
  var api = this;
  var listener = {
    onSuccess: function(xml) {
      var albums = [];
      var eAlbums = xml.getElementsByTagName('album')
      for (var i=0; i<eAlbums.length; i++) {
        var id = eAlbums[i].getElementsByTagName('aid')[0]
                           .firstChild
                           .nodeValue;
        var title = eAlbums[i].getElementsByTagName('name')[0]
                              .firstChild
                              .nodeValue;

        var newAlbum = CC[FLOCK_PHOTO_ALBUM_CONTRACTID]
                       .createInstance(CI.flockIPhotoAlbum);
        newAlbum.id = id;
        newAlbum.title = title;
        api._logger.debug(".getAlbums() Found album: title = " + title +
                          ", id = " + id);
        albums.push(newAlbum);
      }
      aFlockListener.onSuccess(createEnum(albums), "success");
    },
    onError: function(aFlockError) {
      api._logger.debug(".getAlbums() error: " + aFlockError.errorString);
      aFlockListener.onError(aFlockError, null);
    }
  }
  var params = {
    uid: userID
  }
  this.authCall(listener, 'facebook.photos.getAlbums', params);
}


facebookAPI.prototype.createAlbum =
function facebookAPI_createAlbum(aFlockListener, aAlbumTitle) {
  var api = this;
  var listener = {
    onSuccess: function ca_onSuccess(aXml) {
      var id = aXml.getElementsByTagName("aid")[0].firstChild.nodeValue;
      var title = aXml.getElementsByTagName("name")[0].firstChild.nodeValue;

      var newAlbum = CC[FLOCK_PHOTO_ALBUM_CONTRACTID]
                     .createInstance(CI.flockIPhotoAlbum);
      newAlbum.title = title;
      newAlbum.id = id;
      aFlockListener.onSuccess(newAlbum, "success");
    },
    onError: function ca_onError(aFlockError) {
      api._logger.debug(".createAlbum() error: " + aFlockError.errorString);
      aFlockListener.onError(aFlockError, null);
    }
  }
  var params = {
    name: aAlbumTitle
  }
  this.authCall(listener, "facebook.photos.createAlbum", params);
}


facebookAPI.prototype.getPhotos =
function facebookAPI_getPhotos(aFlockListener, aSubjectId, aAlbumId, aPidList) {
  var api = this;
  var listener = {
    onSuccess: function gp_onSuccess(aResult) {
      var photos = [];
      for (var i in aResult) {
        var photo = aResult[i];

        var newMediaItem = api.createPhoto();
        newMediaItem.id = photo.pid;
        newMediaItem.webPageUrl = photo.link;
        newMediaItem.thumbnail = photo.src_small;
        newMediaItem.midSizePhoto = photo.src;
        newMediaItem.largeSizePhoto = photo.src_big;
        newMediaItem.userid = photo.owner;
        newMediaItem.title = photo.caption ? photo.caption : "";
        newMediaItem.uploadDate = parseInt(photo.created) * 1000;
        // FIXME: need to get the full name and avatar of user - DP
        newMediaItem.username = photo.owner;
        newMediaItem.icon = FLOCK_SNOWMAN_URL;

        photos.push(newMediaItem);
      }
      aFlockListener.onSuccess(createEnum(photos), "success");
    },
    onError: aFlockListener.onError
  };
  var params = {};
  if (aSubjectId) {
    params.subj_id = aSubjectId;
  }
  if (aAlbumId) {
    params.aid = aAlbumId;
  }
  if (aPidList) {
    params.pids = aPidList;
  }
  this.authCall(listener, "facebook.photos.get", params, true);
}


facebookAPI.prototype._finalizePhoto =
function facebookAPI__finalizePhoto(aUploadListener,
                                    aUpload,
                                    aId)
{
  var api = this;
  var getPhotoListener = {
    onSuccess: function fp_onSuccess(aPhotos) {
      if (aPhotos.hasMoreElements()) {
        var photo = aPhotos.getNext();
        photo.QueryInterface(CI.flockIMediaItem);
        photo.init("facebook", api.flockMediaItemFormatter);
        aUploadListener.onUploadFinalized(aUpload, photo);
      } else {
        // Empty result
        aUploadListener.onError(null);
      }
    },
    onError: function fp_onError(aError) {
      aUploadListener.onError(null);
    }
  };

  this.getPhotos(getPhotoListener, null, null, aId);
}

facebookAPI.prototype.uploadPhotosViaUploader =
function facebookAPI_uploadPhotosViaUploader(aUploadListener,
                                             aPhoto,
                                             aParams,
                                             aUpload) {
  var api = this;

  var pid;
  var listener = {
    onResult: function listener_onResult(aResponseText) {
      var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
                   .createInstance(Components.interfaces.nsIDOMParser);
      var xml = parser.parseFromString(aResponseText, "text/xml");
      api._logger.debug(CC["@mozilla.org/xmlextras/xmlserializer;1"]
                        .getService(CI.nsIDOMSerializer)
                        .serializeToString(xml));
      var resp = xml.getElementsByTagName("photos_upload_response")[0];
      if (!resp) {
        var error = api.getXMLError(xml);
        aUploadListener.onError(error);
      } else {
        pid = xml.getElementsByTagName("pid")[0].childNodes[0].nodeValue;

        // tag photo if user requested
        if (aUpload.notes) {
          var addTagListener = {
            onSuccess: function addTagListener_success(xml, aStatus) {
              api._logger.debug("tagged successfully\n");
            },
            onError: function addTagListener_error(aFlockError, aTopic) {
              api._logger.error("error tagging photo\n");
            }
          };
          api.addTag(addTagListener, pid, "", "", "", "", aUpload.notes);
        }

        aUploadListener.onUploadComplete(aUpload);
        api._finalizePhoto(aUploadListener, aUpload, pid);
      }
    },
    onError: function listener_onError(aErrorCode) {
      if (aErrorCode) {
        aUploadListener.onError(api.getHTTPError(aErrorCode));
      } else {
        aUploadListener.onError(api.getError(null, null));
      }
    },
    onProgress: function listener_onProgress(aCurrentProgress) {
      aUploadListener.onProgress(aCurrentProgress);
    }
  };

  var params = {};
  params.method = "facebook.photos.upload";
  params.api_key = this.api_key;
  params.session_key = this.session_key;
  params.call_id = new Date().getTime();
  params.v = FACEBOOK_API_VERSION;
  params.aid = aUpload.album;
  params.caption = aParams.getProperty("description");

  var uploader = new PhotoUploader();

  params = api.appendSignature(params, api.secret);
  uploader.setEndpoint(FACEBOOK_API_ENDPOINT_URL);
  uploader.upload(listener, aPhoto, params);

  return;
}

facebookAPI.prototype.addTag =
function facebookAPI_addTag(aFlockListener, aPid, aTagUid, aTagText, aX, aY, aTags) {
  this._logger.debug(".addTag()");
  var json;
  if (aTags) {
    json = aTags;
  } else {
    json = '[{"x":"' + aX + '","y":"' + aY + '"';
    if (aTagUid) {
      json += ',"tag_uid":' + aTagUid;
    } else if (aTagText) {
      json += ',"tag_text":"' + aTagText + '"';
    }
    json += '}]';
  }
  this._logger.debug("tag json: " + json);

  var params = {
    pid: aPid,
    tags: json
  };
  this.authCall(aFlockListener, "facebook.photos.addTag", params);
}

//****************************************
//* FQL Query calls
//****************************************/
facebookAPI.prototype.doFQLQuery=
function facebookAPI_doFQLQuery(aFlockListener, aFQLQuery) {
  var params = {
    format: "xml",
    query: aFQLQuery
  };
  this.authCall(aFlockListener, 'facebook.fql.query', params);
}


//******************************************
//* internal functions for making requests
//******************************************

/* --- authCall is used for all authenticated calls --- */

facebookAPI.prototype.authCall =
function facebookAPI_authCall(aFlockListener, aMethod,
                              aParams, aJSON, aIsDryRun) {
  var inst = this;
  var flockListener = {
    onSuccess: function authListener_onSuccess(aSubject, aTopic) {
      aParams.api_key = inst.api_key;
      aParams.session_key = inst.session_key;
      aParams.call_id = new Date().getTime();
      aParams.method = aMethod;
      if (aJSON) {
        aParams.format = "JSON";
      }
      aParams.v = FACEBOOK_API_VERSION;
      var paramString = inst.getParamString(aParams, inst.secret);
      var url = inst.endpoint + "?" + paramString;
      inst._logger.debug("===[" + aMethod + "]===> " + url);
      if (!aIsDryRun) {
        if (aJSON) {
          inst._doCallJSON(aFlockListener, url, null);
        } else {
          inst._doCall(aFlockListener, url, null);
        }
      }
    },
    onError: function authListener_onError(aFlockError, aTopic) {
      inst.acUtils.markAllAccountsAsLoggedOut(FACEBOOK_CONTRACTID);
      aFlockListener.onError(aFlockError, aTopic);
    }
  }
  this.authenticate(flockListener);
}

/* --- call is only used to start authentication by getting a token --- */

facebookAPI.prototype.call =
function facebookAPI_call(aFlockListener, aMethod, aParams, aIsDryRun) {
  if (!aParams) aParams = {};
  aParams.api_key = this.api_key;
  aParams.method = aMethod;
  aParams.v = FACEBOOK_API_VERSION;
  var paramString = this.getParamString(aParams, this.api_secret);
  var url = this.endpoint + "?" + paramString;
  this._logger.debug("===[" + aMethod + "]===> " + url);
  if (!aIsDryRun) {
    this._doCall(aFlockListener, url, null);
  }
}

/* --- convert dictionary to a request string, adding the signature --- */

facebookAPI.prototype.getParamString =
function facebookAPI_getParamString(aParams, secret) {
  var params = this.appendSignature(aParams, secret);
  var rval = "";

  var count = 0;
  for(var p in params) {
    if(count++!=0) rval += "&";
    rval += encodeURIComponent(p) + "=" + encodeURIComponent(params[p]);
  }

  return rval;
};

/* --- calculate and add the signature to the request parameters --- */

facebookAPI.prototype.appendSignature =
function facebookAPI_appendSignature(aParams, secret) {
  var keys = new Array();

  for (var p in aParams) keys.push(p);
  keys.sort();

  var preHash = '';
  for (var i=0; i<keys.length; ++i) preHash += keys[i] + '=' + aParams[keys[i]];
  preHash += secret;

  var converter = CC["@mozilla.org/intl/scriptableunicodeconverter"]
                  .createInstance(CI.nsIScriptableUnicodeConverter);

  converter.charset = "UTF-8";
  var inputStream = converter.convertToInputStream(preHash);
  var newParams = aParams;
  newParams.sig = FlockCryptoHash.md5Stream(inputStream);
  return newParams;
};


facebookAPI.prototype.getHTTPError =
function facebookAPI_getHTTPError(aHTTPErrorCode) {
  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
  error.errorCode = aHTTPErrorCode;

  return error;
}

facebookAPI.prototype.getXMLError =
function facebookAPI_getXMLError(aXML) {
  // FIXME: Use E4X (bug 9573)
  var fbErrorCode;
  var fbErrorMessage;

  // <error_response xmlns="http://api.facebook.com/1.0/">
  //   <error_code>324</error_code>
  //   <error_msg>Missing or invalid image file</error_msg>
  //   <request_args/>
  // </error_response>
  try {
    fbErrorCode = aXML.getElementsByTagName("error_code")[0]
                      .childNodes[0].nodeValue;
    fbErrorMessage = aXML.getElementsByTagName("error_msg")[0]
                         .childNodes[0].nodeValue;
  } catch (ex) {
    // <result method="" type="struct">
    //   <fb_error type="struct">
    //     <code>101</code>
    //     <msg>Invalid API key</msg>
    //     <your_request/>
    //   </fb_error>
    // </result>
    try {
      fbErrorCode = aXML.getElementsByTagName("code")[0]
                        .childNodes[0].nodeValue;
      fbErrorMessage = aXML.getElementsByTagName("msg")[0]
                           .childNodes[0].nodeValue;
    } catch (ex2) {
      // in case the error xml is invalid
      fbErrorCode = "999";
    }
  }

  return this.getError(fbErrorCode, fbErrorMessage);
}


facebookAPI.prototype.getError =
function facebookAPI_getError(aFBErrorCode, aFBErrorMessage) {
  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);

  this._logger.debug(".getError() Facebook error code: " + aFBErrorCode + "\n");
  switch (aFBErrorCode) {
    case "2":
      // 2: The service is not available at this time.
      error.errorCode = CI.flockIError.PHOTOSERVICE_UNAVAILABLE;
      break;
    case "4":
      // Facebook is not returning the expected error for logged in users.
      // c.f. https://bugzilla.flock.com/show_bug.cgi?id=12841
      if (this.session_key) {
        // 4: Application request limit reached.
        error.errorCode = CI.flockIError.PHOTOSERVICE_REQUEST_LIMIT_REACHED;
      } else {
        error.errorCode = CI.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN;
      }
      error.serviceName = this.prettyName;
      break;
    case "100":
      // 100: One of the parameters specified was missing or invalid.
    case "103":
      // 103: The submitted call_id was not greater than the previous call_id
      //      for this session.
    case "104":
      // 104: Incorrect signature.
      error.errorCode = CI.flockIError.PHOTOSERVICE_INVALID_QUERY;
      break;
    case "101":
      // 101: The api key submitted is not associated with any known
      //      application.
      error.errorCode = CI.flockIError.PHOTOSERVICE_INVALID_API_KEY;
      break;
    case "102":
      // 102: The session key was improperly submitted or has reached its
      //      timeout. Direct the user to log in again to obtain another key.
      error.errorCode = CI.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN;
      error.serviceName = this.prettyName;
      break;
    case "321":
      // 321: Album is full
      error.errorCode = CI.flockIError
                          .PHOTOSERVICE_PHOTOS_IN_ALBUM_LIMIT_REACHED;
      break;
    case "1":
      // 1: An unknown error occurred. Please resubmit the request.
    case "999":
      error.errorCode = CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR;
      break;
    default:
      error.errorCode = CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR;
      error.serviceErrorString = aFBErrorMessage;
  }
  return error;
}

/* --- actually make the http request --- */

facebookAPI.prototype._doCall =
function facebookAPI_doCall(aFlockListener, aUrl, aContent) {
  var api=this;

  // copied from flickr service --- potential issues:
  // 1) shouldn't be using xmlhttprequest
  // 2) keeping a reference to the request via this.req
  //    keeps it referenced after we are done with it
  //    (until the next call)

  this.req = CC[XMLHTTPREQUEST_CONTRACTID].
             createInstance(CI.nsIXMLHttpRequest);
  this.req.QueryInterface(CI.nsIJSXMLHttpRequest);
  this.req.mozBackgroundRequest = true;
  this.req.open("GET", aUrl, true);
  var req = this.req;
  this.req.onreadystatechange = function (aEvt) {
    api._logger.debug("._doCall() ReadyState = " + req.readyState);
    if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      api._logger.debug("._doCall() Status = " + req.status);
      if (Math.floor(req.status/100) == 2) {
        try {
          api._logger.debug("._doCall() response = \n" + req.responseText);
          var fb_error = req.responseXML
                            .getElementsByTagName("error_response");
          if (fb_error.length > 0) {
            api._logger.debug("._doCall() Error");
            aFlockListener.onError(api.getXMLError(req.responseXML), null);
          } else {
            api._logger.debug("._doCall() Success");
            aFlockListener.onSuccess(req.responseXML, null);
          }
        } catch (ex) {
          // error parsing xml
          api._logger.error("._doCall() Parse error = " + ex);
          aFlockListener.onError(api.getError(null, null), null);
        }
      } else {
        // HTTP errors (0 for connection lost)
        api._logger.debug("._doCall() HTTP Error");
        aFlockListener.onError(api.getHTTPError(req.status), null);
      }
    }
  }
  try {
    // just for debug output
    aUrl.match(/call_id=(.+)&/);
    var call_id = RegExp.$1;
    this._logger.debug("sending call_id: " + call_id);
  } catch (ex) {
  }
  this.req.send(null);
};

facebookAPI.prototype._doCallJSON =
function facebookAPI_doCallJSON(aFlockListener, aUrl, aContent) {
  var api = this;

  this.req = CC[XMLHTTPREQUEST_CONTRACTID]
             .createInstance(CI.nsIXMLHttpRequest);
  this.req.QueryInterface(CI.nsIJSXMLHttpRequest);
  this.req.mozBackgroundRequest = true;
  this.req.open("GET", aUrl, true);
  var req = this.req;
  this.req.onreadystatechange =
  function doCallJSON_onreadystatechange(aEvt) {
    api._logger.debug("._doCall() ReadyState = " + req.readyState);
    if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      api._logger.debug("._doCall() Status = " + req.status);
      if (Math.floor(req.status/100) == 2) {
        api._logger.debug("._doCall() response = \n" + req.responseText);
        var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
        var result = nsJSON.decode(req.responseText);
        if (result && result.error_code && result.error_msg) {
          api._logger.error("._doCall() Error");
          aFlockListener.onError(api.getError(result.error_code,
                                              result.error_msg), null);
        } else {
          api._logger.debug("._doCall() Success");
          aFlockListener.onSuccess(result, null);
        }
      } else {
        // HTTP errors (0 for connection lost)
        api._logger.debug("._doCall() HTTP Error");
        aFlockListener.onError(api.getHTTPError(req.status), null);
      }
    }
  }
  try {
    // just for debug output
    aUrl.match(/call_id=(.+)&/);
    var call_id = RegExp.$1;
    this._logger.debug("sending call_id: " + call_id);
  } catch (ex) {
  }
  this.req.send(null);
};


//*****************************************************
//* xpcom constructor/info
//*****************************************************


facebookAPI.prototype.flags = CI.nsIClassInfo.SINGLETON;
facebookAPI.prototype.classDescription = "Facebook JS API";
facebookAPI.prototype.getInterfaces = function (count) {
  var interfaceList = [
    CI.flockIFacebookAPI,
    CI.nsIClassInfo
  ];
  count.value = interfaceList.length;
  return interfaceList;
}

facebookAPI.prototype.getHelperForLanguage = function (count) { return null; }

facebookAPI.prototype.QueryInterface = function (iid) {
  if (!iid.equals(CI.flockIFacebookAPI) &&
      !iid.equals(CI.nsIClassInfo) &&
      !iid.equals(CI.nsISupports)) {
    throw CR.NS_ERROR_NO_INTERFACE;
  }
  return this;
}

facebookAPI.prototype.createPhoto = function () {
  var newMediaItem = CC["@flock.com/photo;1"]
                     .createInstance(CI.flockIMediaItem);
  newMediaItem.init("facebook", this.flockMediaItemFormatter);
  return newMediaItem;
}

facebookAPI.prototype.flockMediaItemFormatter = {
  canBuildHTML: true,
  canBuildLargeHTML: true,

  buildHTML: function fmif_buildHTML(aMediaItem) {
    return "<a title=\"" + aMediaItem.title + "\" href=\""
                         + aMediaItem.webPageUrl + "\">\n  <img src=\""
                         + aMediaItem.midSizePhoto + "\" border=\"0\" />\n</a>";
  },

  buildLargeHTML: function fmif_buildLargeHTML(aMediaItem) {
    return "<a title=\"" + aMediaItem.title + "\" href=\""
                         + aMediaItem.webPageUrl + "\">\n  <img src=\""
                         + aMediaItem.largeSizePhoto + "\" border=\"0\" />\n</a>";
  }
};

var FB_API_Module = new Object();

FB_API_Module.registerSelf = function (compMgr, fileSpec, location, type) {
  compMgr = compMgr.QueryInterface(CI.nsIComponentRegistrar);

  compMgr.registerFactoryLocation(FACEBOOK_API_CID,
                                  "Flock Facebook API JS Component",
                                  FACEBOOK_API_CONTRACTID,
                                  fileSpec,
                                  location,
                                  type);
}

FB_API_Module.getClassObject = function (compMgr, cid, iid) {
  if (!cid.equals(FACEBOOK_API_CID)) {
    throw CR.NS_ERROR_NO_INTERFACE;
  }
  if (!iid.equals(CI.nsIFactory)) {
    throw CR.NS_ERROR_NOT_IMPLEMENTED;
  }
  return FB_API_ServiceFactory;
}

FB_API_Module.canUnload = function (compMgr) {
  return true;
}

/* factory object */
var FB_API_ServiceFactory = new Object();

FB_API_ServiceFactory.createInstance = function (outer, iid) {
  if (outer != null) {
    throw CR.NS_ERROR_NO_AGGREGATION;
  }
  return (new facebookAPI()).QueryInterface(iid);
}

/* entrypoint */
function NSGetModule(compMgr, fileSpec) {
  return FB_API_Module;
}
