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

// 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/FlockPrefsUtils.jsm");
CU.import("resource:///modules/FlockStringBundleHelpers.jsm");
CU.import("resource:///modules/FlockSvcUtils.jsm");
CU.import("resource:///modules/FlockTemplates.jsm");
CU.import("resource:///modules/FlockPhotoUploader.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 = "https://api.facebook.com/";
const FACEBOOK_API_ENDPOINT_URL = FACEBOOK_API_HOSTNAME + "method/";

const FACEBOOK_OAUTH_REDIRECT_URL = "http://www.facebook.com/connect/login_success.html";
const FACEBOOK_OAUTH_PARAMS = "client_id=%appid%"
                            + "&redirect_uri=" + FACEBOOK_OAUTH_REDIRECT_URL
                            + "&type=user_agent"
                            + "&scope=%extendedPermissions%";
const FACEBOOK_OAUTH_OLD_PARAMS = "client_id=%appid%"
                                + "&redirect_uri=" + FACEBOOK_OAUTH_REDIRECT_URL
                                + "&type=user_agent";
const FACEBOOK_OAUTH_URL = "https://graph.facebook.com/oauth/authorize";

const PREF_FLOCKCAST = "flock.flockcast.";
const PREF_ACTIVE = PREF_FLOCKCAST + "active";
const NEWSFEED_URN = "flock:newsfeed:history"
const NEWSFEED_CAP = 5;
const NEWSFEED_ICONS = "http://developer.flock.com/facebook/icons/";
const NEWSFEED_PEOPLE_ICON = NEWSFEED_ICONS + "people.png";
const NEWSFEED_MEDIA_ICON = NEWSFEED_ICONS + "media.png";
const NEWSFEED_BLOG_ICON = NEWSFEED_ICONS + "blog.png";
const NEWSFEED_LANDING = "http://www.flock.com/flockcast";
const NEWSFEED_STATUS_BUNDLE = "77091556387";
const NEWSFEED_BLOG_BUNDLE = "77049646387";
const NEWSFEED_MEDIA_BUNDLE = "77053946387";

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

// From http://wiki.developers.facebook.com/index.php/Feed.publishUserAction
const STORY_SIZE_ONELINER = 1;
const STORY_SIZE_SHORT = 2;
const STORY_SIZE_FULL = 4; // deprecated by Facebook

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

const PROFILEBOX_TEMPLATE = "chrome://flock/content/services/facebook/profilebox.tmpl";

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

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.length > 0 ? array.shift() : null;
    }
  }
}


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

/**
 * Represents the Facebook API
 */
function facebookAPI() {
  this.acUtils = CC["@flock.com/account-utils;1"]
                 .getService(CI.flockIAccountUtils);
  var obService = CC["@mozilla.org/observer-service;1"]
                  .getService(CI.nsIObserverService);
  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);
  this._metrics = CC["@flock.com/metrics-service;1"]
                  .getService(CI.flockIMetricsService);
  this.prettyName = "Facebook";
  this.uid = null;
  this._friendsCache = {
    value: null,
    lastUpdate: new Date(0)
  };

  this._profileBoxTemplate = null;

  FlockSvcUtils.getCoop(this);
  this._logger = CC["@flock.com/logger;1"].createInstance(CI.flockILogger);
  this._logger.init("facebookAPI");
  this.wrappedJSObject = this;

  this.access_token = null;
  this.app_id = this._WebDetective
                    .getString("facebook", "apiAppID", "");
  this.app_key = this._WebDetective
                     .getString("facebook", "apiAppKey", "");
  this.app_secret = this._WebDetective
                        .getString("facebook", "apiAppSecret", "");
  this._extendedPermissions =
    this._WebDetective.getString("facebook", "apiExtendedPermissions", "");
  this._oauthParams = FACEBOOK_OAUTH_PARAMS.replace("%appid%", this.app_id)
                                           .replace("%extendedPermissions%",
                                                    this._extendedPermissions);
  this._oauthURL = FACEBOOK_OAUTH_URL + "?" + this._oauthParams;
  this._oauthPermissionsBody =
    this._WebDetective.getString("facebook", "apiPermissionsBody", "");
  this._oldPermissionsBody =
    this._WebDetective.getString("facebook", "apiOldPermissionsBody", "");
  this._forcePermissionsPage =
    this._WebDetective.getString("facebook", "apiForcePermissionsPage", "");

  this._permissionUrl = this._WebDetective
                            .getString("facebook",
                                       "facebookApi_PERM_URL",
                                       "")
                            .replace("%apikey%", this.app_key);
  this._permissionBody = this._WebDetective
                             .getString("facebook",
                                        "facebookApi_PERM_BODY",
                                        "");
  this._editAppUrl = this._WebDetective
                         .getString("facebook",
                                    "facebookApi_EDITAPP_URL",
                                    "");
  this._editAppAjax = this._WebDetective
                          .getString("facebook",
                                     "facebookApi_EDITAPP_AJAX",
                                     "");
  this._addAppBody = this._WebDetective
                         .getString("facebook",
                                    "facebookApi_ADDAPP",
                                    "")
                         .replace("%appid%", this.app_id);
  this._fullNewsBody = this._WebDetective
                           .getString("facebook",
                                      "facebookApi_FULLNEWS",
                                      "")
                           .replace("%appid%", this.app_id);
}

//****************************************
//* 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.access_token) {
    aFlockListener.onSuccess(null, "authenticated");
    return;
  }
  this._logger.debug("authenticate()");

  // Steps to authenticating
  // 1. Request an access_token through OAuth
  // 2. Grant the Flock application extended permissions
  // 3. Update account info (avatar, screenname)
  var api = this;
  var oldPerms = false;
  var post_url = null;

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

  function handleOldGrant(aXHR, aResults, aAuthListener) {
    // Use the "old" way of granting permission.
    api._logger.debug("handleOldGrant()");
    if (!oldPerms) {
      handleOldPermissions(aXHR, aResults, aAuthListener);
      return;
    }

    // Get form values off the page.
    post_url = aResults.getPropertyAsAString("postUrl");
    if (post_url && post_url[0] == "/") {
      // Handle relative URLs
      post_url = "http://www.facebook.com" + post_url;
    }
    var post_form_id = aResults.getPropertyAsAString("formId");
    var fb_dtsg = aResults.getPropertyAsAString("fb_dtsg");
    var next = aResults.getPropertyAsAString("next");
    var cancel = aResults.getPropertyAsAString("cancel");
    // Prepare the post body.
    var postBody = api._oldPermissionsBody
                      .replace("%fb_dtsg%", fb_dtsg)
                      .replace("%next%", encodeURIComponent(next))
                      .replace("%apikey%", api.app_key)
                      .replace("%perms%", "")
                      .replace("%post_form_id%", post_form_id)
                      .replace("%cancel%", encodeURIComponent(cancel));
    // Post the form.
    api._doXHRCall(aAuthListener, "POST", post_url, null, postBody);
  }

  function handleOldPermissions(aXHR, aResults, aAuthListener) {
    // Reissue the authorization request without the new permissions. This
    // will result in the "old grant" page.
    api._logger.debug("handleOldPermissions()");
    oldPerms = true;
    var url = FACEBOOK_OAUTH_URL + "?"
            + FACEBOOK_OAUTH_OLD_PARAMS.replace("%appid%", api.app_id);
    api._doXHRCall(aAuthListener, "GET", url);
  }

  function handleNewPermissions(aXHR, aResults, aAuthListener) {
    api._logger.debug("handleNewPermissions()");
    // Get form values off the page.
    post_url = aResults.getPropertyAsAString("postUrl");
    var post_form_id = aResults.getPropertyAsAString("formId");
    var fb_dtsg = aResults.getPropertyAsAString("fb_dtsg");
    var next = aResults.getPropertyAsAString("next");
    var cancel = aResults.getPropertyAsAString("cancel");
    // Prepare the post body.
    var postBody = api._oauthPermissionsBody
                      .replace("%post_form_id%", post_form_id)
                      .replace("%fb_dtsg%", fb_dtsg)
                      .replace("%app_id%", api.app_id, "g")
                      .replace("%next%", encodeURIComponent(next))
                      .replace("%redirect%", FACEBOOK_OAUTH_REDIRECT_URL)
                      .replace("%perms%", api._extendedPermissions, "g")
                      .replace("%cancel%", encodeURIComponent(cancel));
    // Post the form.
    api._doXHRCall(aAuthListener, "POST", post_url, null, postBody);
  }

  function handleSuccessPage(aXHR) {
    // Successfully authenticated using OAuth.
    api._logger.debug("handleSuccessPage()");
    var getLoggedInUserListener = {
      onSuccess: function getLoggedInUserListener_onSuccess(aXML, aTopic) {
        api.uid = aXML.firstChild.textContent;
        api._logger.debug("service account id = " + api.uid);
        aFlockListener.onSuccess(null, "authenticated");
      },
      onError: aFlockListener.onError
    };

    // Get the page URL, which will contain the access token.
    var url = decodeURIComponent(aXHR.channel.URI.spec);
    api._logger.debug("url = " + url);
    if (url) {
      // Look for the access token in the URL fragment.
      var reStr = api._WebDetective
                     .getString("facebook", "apiAccessTokenRE", "");
      var re = new RegExp(reStr);
      var matches = re.exec(url);
      if (matches && matches.length > 0) {
        // Found it.
        api.access_token = matches[1];
        api._logger.debug("access_token = " + api.access_token);
      }
    }
    if (api.access_token) {
      if (oldPerms) {
        // Have still have to grant permissions the old way.
        var grantPermsListener = {
          onSuccess: function grantPermsListener_onSuccess(aSubject, aTopic) {
            // Get the account ID.
            api.authCall(getLoggedInUserListener,
                         "facebook.users.getLoggedInUser", {});
          },
          onError: aFlockListener.onError
        };
        api._grantAllPermissions(grantPermsListener);
      } else {
        // Get the account ID.
        api.authCall(getLoggedInUserListener,
                     "facebook.users.getLoggedInUser", {});
      }
    } else {
      api._logger.debug("No token.");
      // Open application authentication page for user.
      api.showPermissionsPage();
    }
  }

  function handleRedirectPage(aXHR, aResults, aAuthListener) {
    // Get redirect URL and remove escape characters.
    var redirectUrl = aResults.getPropertyAsAString("redirectUrl")
                              .replace("\\/", "/", "g")
                              .replace("\\u0025", "%", "g");
    api._logger.debug("handleRedirectPage(): " + redirectUrl);
    // Redirect to the next dialog.
    api._doXHRCall(aAuthListener, "GET", redirectUrl);
  }

  var retry = true;
  var authPageListener = {
    onSuccess: function authPageListener_onSuccess(aXHR, aTopic) {
      api._logger.debug("authPageListener onSuccess: " + aXHR.responseText);
      if (api._WebDetective
             .detectNoDOM("facebook", "apiOAuthOldGrant", "",
                          aXHR.responseText, results))
      {
        // Grant Flock access to Facebook using old dialogs.
        handleOldGrant(aXHR, results, this);
      } else if (api._WebDetective
                    .detectNoDOM("facebook", "apiOAuthOldPermissions", "",
                                 aXHR.responseText, results))
      {
        // Already granted access but using old dialogs for permissions.
        handleOldPermissions(aXHR, results, this);
      } else if (api._WebDetective
                    .detectNoDOM("facebook", "apiOAuthNewPermissions", "",
                                 aXHR.responseText, results))
      {
        // Allow Flock access to extended permissions using new dialog.
        handleNewPermissions(aXHR, results, this);
      } else if (api._WebDetective
                    .detectNoDOM("facebook", "apiOAuthSuccess", "",
                                 aXHR.responseText, null))
      {
        // Permission granted! Extract the access_token.
        handleSuccessPage(aXHR);
      } else if (api._WebDetective
                    .detectNoDOM("facebook", "apiOAuthLoggedOut", "",
                                 aXHR.responseText, null))
      {
        // User needs to log in again.
        api._logger.debug("User is logged out");
        var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
        error.errorCode = CI.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN;
        aFlockListener.onError(error, aTopic);
      } else if (api._WebDetective
                    .detectNoDOM("facebook", "apiOAuthRedirect", "",
                                 aXHR.responseText, results))
      {
        // Catch interstitial redirects and move on to next page. This may
        // prevent some error situations when Facebook changes things on us.
        handleRedirectPage(aXHR, results, this);
      } else {
        // Something has gone wrong.
        api._logger.debug("Can't authenticate: " + aXHR.responseText);
        // Open application authentication page for user.
        api.showPermissionsPage();
      }
    },
    onError: function authPageListener_onError(aFlockError, aTopic) {
      if (post_url && retry && aFlockError.errorCode == 500) {
        // There is a Facebook bug where long permission requests such as ours
        // may be truncated before completion, followed by a 500 error. As a
        // sanity check, repeat the process and see if it completes.
        api._logger.debug("Retrying permission request");
        retry = false; 
        api._doXHRCall(authPageListener, "GET", api._oauthURL);
        return;
      }
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  if (this._forcePermissionsPage) {
    // Used for testing the showPermissionsPage code.
    this.showPermissionsPage();
    this._forcePermissionsPage = false;
    return;
  }

  // Go to the application authentication page
  this._doXHRCall(authPageListener, "GET", this._oauthURL);
};

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

facebookAPI.prototype.showPermissionsPage =
function facebookAPI_showPermissionsPage() {
  // Open application authentication page for user. If they allow Flock
  // permissions, they will be taken to the Success page, which will be dealt
  // with in flockFacebookService.updateAccountStatusFromDocument().
  this._logger.debug("showPermissionsPage()");
  var win = CC["@mozilla.org/appshell/window-mediator;1"]
            .getService(CI.nsIWindowMediator)
            .getMostRecentWindow("navigator:browser");
  if (win) {
    win.getBrowser().loadOneTab(this._oauthURL, null, null, null, false, false);
  }
};

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

  var grantListener = {
    onSuccess: function grantListener_onSuccess(aXHR, aTopic) {
      api._logger.debug("._grantPermission() success");
      // 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",
                                          "",
                                          aXHR.responseText,
                                          results))
        {
          // Actually grant the permission
          var postFormId = results.getPropertyAsAString("postFormId");
          var fb_dtsg;
          try {
            fb_dtsg = results.getPropertyAsAString("fb_dtsg");
          } catch (ex) {
            fb_dtsg = "";
          }
          var body = api._permissionBody.replace("%postformid%", postFormId)
                                        .replace("%fb_dtsg%", fb_dtsg);
          api._doXHRCall(aFlockListener, "POST", url, null, body);
        }
      } catch (ex) {
        api._logger.debug("._grantPermission() exception:" + ex);
        var error = CC[FLOCK_ERROR_CONTRACTID]
                    .createInstance(CI.flockIError);
        error.errorCode = error.PHOTOSERVICE_LOGIN_FAILED;
        aFlockListener.onError(error, arguments.callee.name);
      }
    },
    onError: function grantListener_onError(aFlockError, aTopic) {
      api._logger.debug("._grantPermission() error: " + aFlockError.errorCode);
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  // Show the page to get the form
  this._doXHRCall(grantListener, "GET", url);
};

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

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

  var permsListener = {
    onSuccess: function permsListener_onSuccess(aSubject, aTopic) {
      completed++;
      finalize();
    },
    onError: function permsListener_onError(aFlockError, aTopic) {
      error = aFlockError;
      completed++;
      finalize();
    }
  };

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

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

  switch (aUrlType) {
    case "profile":
      return "http://www.facebook.com/profile.php?uid="
             + aFriendId
             + "&api_key="
             + this.app_key;
      break;
    case "myprofile":
      return "http://www.facebook.com/profile.php?id="
             + aFriendId
             + "&api_key="
             + this.app_key;
      break;
    case "poke":
      return "http://www.facebook.com/poke.php?uid="
             + aFriendId
             + "&api_key="
             + this.app_key;
      break;
    case "message":
      return "http://www.facebook.com/message.php?uid="
             + aFriendId
             + "&api_key="
             + this.app_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.app_key;
      break;
    case "userphotos":
      return "http://www.facebook.com/photo_search.php?uid="
             + aFriendId
             + "&api_key="
             + this.app_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.app_key;
      break;
    case "addfriend":
      return "http://www.facebook.com/addfriend.php?uid="
             + aFriendId
             + "&api_key="
             + this.app_key;
      break;
    case "friendrequests":
      return "http://www.facebook.com/reqs.php#friend";
      break;
    case "messages":
      return "http://www.facebook.com/inbox";
      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, aTopic) {
      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,
            status_id: 0,
            message: ""
          }
        }

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

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

  // 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, aTopic) {
      // 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), aTopic);
    },
    onError: function gumfListener_onError(aFlockError, aTopic) {
      api._logger.debug(".getUpdatedMediaFriends() error: "
                        + aFlockError.errorCode);
      aFlockListener.onError(aFlockError, aTopic);
    }
  }

  // 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, aTopic) {
      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), aTopic);
    },
    onError: function ugiListener_onError(aFlockError, aTopic) {
      api._logger.debug(".usersGetInfo() error:" + aFlockError.errorString);
      aFlockListener.onError(aFlockError, aTopic);
    }
  }

  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, aTopic) {
      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), aTopic);
    },
    onError: function ngListener_onError(aFlockError, aTopic) {
      api._logger.debug(".notificationsGet() error:" + aFlockError.errorString);
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  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 onSuccess(xml, aTopic) {
      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), aTopic);
    },
    onError: function onError(aFlockError, aTopic) {
      api._logger.debug(".getAlbums() error: " + aFlockError.errorString);
      aFlockListener.onError(aFlockError, aTopic);
    }
  }
  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, aTopic) {
      api._logger.debug(".createAlbum() error: " + aFlockError.errorString);
      aFlockListener.onError(aFlockError, aTopic);
    }
  }
  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, aTopic) {
      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), aTopic);
    },
    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.access_token = this.access_token;
  params.aid = aUpload.album;
  params.caption = aParams.getProperty("description");

  var uploader = new FlockPhotoUploader();
  uploader.setEndpoint(FACEBOOK_API_ENDPOINT_URL + "facebook.photos.upload");
  uploader.upload(listener, aPhoto, params);

}

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

//****************************************
//* News feed & profile box calls
//****************************************/

facebookAPI.prototype.addProfileBox =
function facebookAPI_addProfileBox(aFlockListener) {
  var api = this;

  var successCount = 0;

  // Add the box to the profile
  function addApp(aPostFormId) {
    var xhr2 = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
              .createInstance(CI.nsIXMLHttpRequest);
    xhr2.mozBackgroundRequest = true;
    xhr2.open("POST", api._editAppAjax, 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 added Flock to the profile");
        if (++successCount >= 2 && aFlockListener) {
          aFlockListener.onSuccess();
        }
      }
    }
    var body = api._addAppBody.replace("%postformid%", aPostFormId);
    xhr2.send(body);
  }

  // Let Flock publish full news stories
  function fullNews(aPostFormId) {
    var xhr2 = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
              .createInstance(CI.nsIXMLHttpRequest);
    xhr2.mozBackgroundRequest = true;
    xhr2.open("POST", api._editAppAjax, 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 authorized Flock to publish full stories");
        if (++successCount >= 2 && aFlockListener) {
          aFlockListener.onSuccess();
        }
      }
    }
    var body = api._fullNewsBody.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", this._editAppUrl, true);

  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");
          addApp(postFormId);
          fullNews(postFormId);
        }
      } catch (ex) {
        api._logger.debug("._grantPermission() exception:" + ex);
        if (aFlockListener) {
          aFlockListener.onError();
        }
      }
    }
  }

  xhr.send(null);
}

// INTERNAL
facebookAPI.prototype._getNewsFeed =
function facebookAPI__getNewsFeed() {
  var newsFeed = this._coop.get(NEWSFEED_URN);
  if (!newsFeed) {
    newsFeed = new this._coop.Folder(NEWSFEED_URN);
  }
  return newsFeed;
}

// INTERNAL
facebookAPI.prototype._insertNewsFeedEvent =
function facebookAPI__insertNewsFeedEvent(aDescription,
                                          aIcon,
                                          aImages) {
  var newsFeed = this._getNewsFeed();
  var newEvent = new this._coop.NewsFeedEvent({
    description: aDescription,
    icon: aIcon
  });
  for each (image in aImages) {
    newEvent.images.addOnce(image);
  }
  // In RDF, the first element is 1 (not 0)
  newsFeed.children.insertAt(newEvent, 1);

  var count = 0;
  var toRemove = [];
  var newsEnum = newsFeed.children.enumerate();
  while (newsEnum.hasMoreElements()) {
    var event = newsEnum.getNext();
    if (!event) continue;
    if (count++ >= NEWSFEED_CAP) {
      toRemove.push(event);
    }
  }
  while (toRemove.length > 0) {
    newsFeed.children.remove(toRemove.pop());
  }
}

// INTERNAL
facebookAPI.prototype._setFBML =
function facebookAPI__setFBML(aFlockListener, aFBML) {
  var api = this;
  var listener = {
    onSuccess: function onSuccess(xml, aTopic) {
      if (aFlockListener) {
        aFlockListener.onSuccess(null, aTopic);
      }
    },
    onError: function onError(aFlockError, aTopic) {
      api._logger.debug("._setFBML() error: " + aFlockError.errorString);
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, aTopic);
      }
    }
  }
  var params = {
    profile_main: aFBML,
    profile: aFBML
  }
  this.authCall(listener, "facebook.Profile.setFBML", params);
}

// INTERNAL
facebookAPI.prototype._publishUserAction =
function facebookAPI__publishUserAction(aFlockListener,
                                        aMessage,
                                        aAttachment)
{
  var api = this;
  var listener = {
    onSuccess: function onSuccess(xml, aTopic) {
      if (aFlockListener) {
        aFlockListener.onSuccess(null, aTopic);
      }
    },
    onError: function onError(aFlockError, aTopic) {
      api._logger.error("publish error: " + aFlockError.errorString);
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, aTopic);
      }
    }
  }
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  var get_flock = flockGetString("services/facebook",
                                 "flock.facebook.flockcast.getFlock",
                                 [ brandGetString("brand", "brandShortName") ]);
  var params = {
    message: aMessage,
    attachment: nsJSON.encode(aAttachment),
    action_links: nsJSON.encode([
      { text: get_flock,
        href: NEWSFEED_LANDING }
    ])
  };
  this.authCall(listener, "facebook.stream.publish", params);
}

// Update
facebookAPI.prototype._updateProfileBox =
function facebookAPI__updateProfileBox(aFlockListener) {
  var newsEnum = this._getNewsFeed().children.enumerate();
  var events = [];
  while (newsEnum.hasMoreElements()) {
    var event = newsEnum.getNext();
    if (!event) continue;
    var images = [];
    var imagesEnum = event.images.enumerate();
    while (imagesEnum.hasMoreElements()) {
      var image = imagesEnum.getNext();
      if (image) {
        images.push(image);
      }
    }
    events.push({
      icon: event.icon,
      description: event.description,
      images: images
    });
  }

  var brand = brandGetString("brand", "brandShortName");
  var welcome = flockGetString("services/facebook",
                               "flock.facebook.profilebox.welcome")
                .replace("%url%", NEWSFEED_LANDING)
                .replace("%brand%", brand);

  if (!this._profileBoxTemplate) {
    this._profileBoxTemplate = FlockTemplates.parseFromChrome(PROFILEBOX_TEMPLATE);
  }

  var fbml = this._profileBoxTemplate.process({
    welcome: welcome,
    events: events
  });

  this._logger.debug("setting the FBML for the profile box:\n" + fbml);
  this._setFBML(aFlockListener, fbml);
}

// INTERNAL
facebookAPI.prototype._shouldReport =
function facebookAPI__shouldReport(aType) {
  return (FlockPrefsUtils.getBoolPref(PREF_ACTIVE) &&
          FlockPrefsUtils.getBoolPref(PREF_NEWSFEED + aType));
}

facebookAPI.prototype._reportMetric =
function facebookAPI_reportMetric(aType, aSourceService) {
  var data = {
    "type": aType,
    "sourceService": aSourceService,
    "targetService": "facebook"
  }
  this._metrics.report("FlockCast-Publish", data);
}

facebookAPI.prototype.reportStatus =
function facebookAPI_reportStatus(aFlockListener,
                                  aAccount,
                                  aStatus)
{
  var trimmedStatus = aStatus.replace(/^\s+|\s+$/g, "");
  var service = aAccount.getService();
  // We don't want to publish Facebook status, because they already go to the
  // news feed directly. Also don't publish blank statuses.
  if (service.shortName == "facebook" || trimmedStatus == "") {
    if (aFlockListener) aFlockListener.onSuccess(null, "");
    return;
  }

  // Did the user opt-in for that?
  if (!aAccount.getCustomParam("publishStoriesstatus")) {
    this._logger.debug("Not reporting the status to Facebook "
                       + "because the user didn't opt in");
    if (aFlockListener) aFlockListener.onSuccess(null, "");
    return;
  }

  this._reportMetric("status", service.shortName);

  // Profile box
  var desc = "<a href='" + service.url + "'>"
           + service.title + "</a>: "
           + trimmedStatus;
  this._insertNewsFeedEvent(desc, NEWSFEED_PEOPLE_ICON, []);
  this._updateProfileBox(null);

  // Feed story
  var acct = aAccount.getProfileURLForFriend(aAccount.urn);
  var localeString = (service.shortName == "twitter")
                     ? "flock.facebook.flockcast.tweeted"
                     : "flock.facebook.flockcast.status.message";
  var message = flockGetString("services/facebook",
                               localeString,
                               [ service.title ]);
  this._publishUserAction(aFlockListener,
                          message,
                          { name: aStatus,
                            href: acct });
}

facebookAPI.prototype.reportBlog =
function facebookAPI_reportBlog(aFlockListener,
                                aAccount,
                                aTitle,
                                aBody,
                                aPermalink)
{
  // Unlike other actions, the user preference (publish or not) is checked
  // before calling this function; so we don't have to check here.
  var service = aAccount.getService();

  // Custom blog have shortnames that correspond to the API: "metaweblog",
  // "atom", "movableType"... That's not very useful for instrumentation,
  // all we need to know is that they're all self-hosted blogs
  // (or hosted on a platform not officially supported)
  var sourceService = (service instanceof CI.flockICustomBlogWebService)
                    ? "customblog"
                    : service.shortName;
  this._reportMetric("blog", sourceService);

  // Profile box
  var desc = "<a href='" + aPermalink + "'>"
           + aTitle
           + "</a>";
  var icon = NEWSFEED_BLOG_ICON;
  this._insertNewsFeedEvent(desc, icon, []);
  this._updateProfileBox(null);

  // Feed story
  var message = flockGetString("services/facebook",
                               "flock.facebook.flockcast.blog.message",
                               []);
  var summary = aBody.replace(/<[^>]+>/g, "");
  this._publishUserAction(aFlockListener,
                          message,
                          { name: aTitle,
                            href: aPermalink,
                            description: summary });
}


facebookAPI.prototype.reportUpload =
function facebookAPI_reportUpload(aFlockListener,
                                  aAccount,
                                  aPhotos) {
  var service = aAccount.getService();
  // We don't want to publish Facebook uploads,
  // because they already go to the news feed.
  if (service.shortName == "facebook") {
    if (aFlockListener) aFlockListener.onSuccess(null, "");
    return;
  }

  // Did the user opted-in for that?
  if (!aAccount.getCustomParam("publishStoriesupload")) {
    this._logger.debug("Not reporting the upload to Facebook "
                       + "because the user didn't opt in");
    if (aFlockListener) aFlockListener.onSuccess(null, "");
    return;
  }

  this._reportMetric("uploads", service.shortName);

  var count = 0;
  var images = [];
  while (aPhotos.hasMoreElements()) {
    var mediaItem = aPhotos.getNext();
    if (count++ < 4) {
      images.push({
        type: "image",
        src: mediaItem.thumbnail,
        href: mediaItem.webPageUrl
      });
    }
  }

  // Profile box
  var message = flockGetString("services/facebook",
                               "flock.facebook.flockcast.uploads.message",
                               [count, service.title]);
  var icon = NEWSFEED_MEDIA_ICON;
  this._insertNewsFeedEvent(message,
                            icon,
                            images.map(function(x) { return x.src }));
  this._updateProfileBox(null);

  // Feed story
  this._publishUserAction(aFlockListener,
                          message,
                          { media: images });
}

//****************************************
//* 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.access_token = inst.access_token;
      if (aJSON) {
        aParams.format = "JSON";
      }
      var url = FACEBOOK_API_ENDPOINT_URL + aMethod;
      inst._logger.debug("===[" + aMethod + "]===> " + url);
      if (!aIsDryRun) {
        if (aJSON) {
          inst._doCallJSON(aFlockListener, url, aParams);
        } else {
          inst._doCall(aFlockListener, url, aParams);
        }
      }
    },
    onError: function authListener_onError(aFlockError, aTopic) {
      var svc = CC[FACEBOOK_CONTRACTID].getService(CI.flockIWebService);
      inst.acUtils.markAllAccountsAsLoggedOut(svc);
      var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
      error.errorCode = CI.flockIError.PHOTOSERVICE_USER_NOT_LOGGED_IN;
      aFlockListener.onError(error, aTopic);
    }
  }
  this.authenticate(flockListener);
}

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.access_token) {
        // 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;
}

facebookAPI.prototype._getParamString =
function facebookAPI__getParamString(aParams) {
  var pString = "";
  for (var p in aParams) {
    if (pString) {
      pString += "&";
    }
    pString += encodeURIComponent(p) + "=" + encodeURIComponent(aParams[p]);
  }
  return pString;
};

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

facebookAPI.prototype._doXHRCall =
function facebookAPI__doXHRCall(aFlockListener, aMethod, aUrl,
                                aParams, aPostBody)
{
  var url = aUrl;
  var postBody = aPostBody ? aPostBody : null;

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

  var pString = this._getParamString(aParams);
  if (pString) {
    url += "?" + pString;
  }
  this._logger.debug("_doXHRCall(" + aMethod + ": " + url +
                     (postBody ? (", BODY=" + postBody) : "") + ")");
  this.req.open(aMethod, url, true);

  if (aMethod == "POST") {
    this.req.setRequestHeader("Content-Type",
                              "application/x-www-form-urlencoded");
    this.req.overrideMimeType("text/txt");
  }

  var api = this;
  var req = this.req;

  this.req.onreadystatechange = function (aEvt) {
    api._logger.debug("._doXHRCall() ReadyState = " + req.readyState);
    if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      api._logger.debug("._doXHRCall() Status = " + req.status);
      if (Math.floor(req.status/100) == 2) {
        aFlockListener.onSuccess(req, null);
      } else {
        // HTTP errors (0 for connection lost)
        aFlockListener.onError(api.getHTTPError(req.status), null);
      }
    }
  };

  this.req.send(postBody);
};

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

  var doXHRListener = {
    onSuccess: function doXHRListener_onSuccess(aXHR, aTopic) {
      try {
        api._logger.debug("._doCall() response = \n" + aXHR.responseText);
        var fb_error = aXHR.responseXML
                           .getElementsByTagName("error_response");
        if (fb_error.length > 0) {
          api._logger.debug("._doCall() Error");
          aFlockListener.onError(api.getXMLError(aXHR.responseXML), null);
        } else {
          api._logger.debug("._doCall() Success");
          aFlockListener.onSuccess(aXHR.responseXML, null);
        }
      } catch (ex) {
        // error parsing xml
        api._logger.error("._doCall() Parse error = " + ex);
        aFlockListener.onError(api.getError(null, null), null);
      }
    },
    onError: aFlockListener.onError
  };

  this._doXHRCall(doXHRListener, "GET", aUrl, aParams);
};

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

  var doXHRListener = {
    onSuccess: function doXHRListener_onSuccess(aXHR, aTopic) {
      api._logger.debug("._doCallJSON() response = \n" + aXHR.responseText);
      var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
      var result = nsJSON.decode(aXHR.responseText);
      if (result && result.error_code && result.error_msg) {
        api._logger.error("._doCallJSON() Error");
        aFlockListener.onError(api.getError(result.error_code,
                                            result.error_msg), null);
      } else {
        api._logger.debug("._doCallJSON() Success");
        aFlockListener.onSuccess(result, null);
      }
    },
    onError: aFlockListener.onError
  };

  this._doXHRCall(doXHRListener, "GET", aUrl, aParams);
};


//*****************************************************
//* 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;
}
