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

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

// From developer.facebook.com...
const FACEBOOK_API_KEY = "7fa235c847b11f157e12586c86660f47";
const FACEBOOK_API_SECRET = "645e5d3f9fde64b65a1a12744486b093";
const FACEBOOK_API_VERSION = "1.0";
const FACEBOOK_API_HOSTNAME = "http://api.facebook.com/";
const FACEBOOK_API_ENDPOINT_URL = FACEBOOK_API_HOSTNAME + "restserver.php";
const FACEBOOK_API_TOS_URL = "http://www.facebook.com/tos.php?api_key="
                           + FACEBOOK_API_KEY
                           + "&v=1.0";
const FACEBOOK_API_SETSTATUS_URL = "http://www.facebook.com/updatestatus.php";

// 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://browser/skin/flock/services/common/no_avatar.png";

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

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

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

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 MyBag() {
}

MyBag.prototype.setProperty =
function mybag_setProperty(aKey, aValue) {
  this[aKey] = aValue;
};

MyBag.prototype.setPropertyAsInt32 =
function mybag_setPropertyAsInt32(aKey, aValue) {
  this.setProperty(aKey, aValue);
};

MyBag.prototype.setPropertyAsAString =
function mybag_setPropertyAsString(aKey, aValue) {
  this.setProperty(aKey, aValue);
};

MyBag.prototype.getProperty =
function mybag_getProperty(aKey) {
  return this[aKey];
};

MyBag.prototype.getPropertyAsInt32 =
function mybag_getPropertyAsString(aKey) {
  return this.getProperty(aKey);
};

MyBag.prototype.getPropertyAsAString =
function mybag_getPropertyAsString(aKey) {
  return this.getProperty(aKey);
};

MyBag.prototype.QueryInterface =
function mybag_QueryInterface(aIid) {
  return this;
};

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

// ====================================================
// ========== BEGIN flockIPhoto object       ==========
// ====================================================

function facebookPhoto() {
}

facebookPhoto.prototype = {
  id: "",
  thumbnail: "",
  webPageUrl: "",
  midSizePhoto: "",
  largeSizePhoto: "",
  title: "",
  username: "",
  userid: "",
  is_public: true,
  is_video: false,
  svcShortName: "facebook",
  buildTooltip: function facebookPhoto_buildTooltip() {
    default xml namespace =
        "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    var tip =
      <vbox>
        <hbox>
          <image src={this.midSizePhoto} style="margin-bottom: 2px;" />
          <spacer flex="1" />
        </hbox>
        <hbox>
          <image src={this.icon} />
          <vbox style="max-width: 250px;">
            <label crop="end">{this.title}</label>
            <label class="user">{this.username}</label>
          </vbox>
        </hbox>
      </vbox>;

    var parser = CC["@mozilla.org/xmlextras/domparser;1"]
                 .createInstance(CI.nsIDOMParser);
    var doc = parser.parseFromString(tip, "text/xml");
    return doc.documentElement;
  },

  buildHTML: function facebookPhoto_buildHTML() {
    var xml =
      <a title={this.title} href={this.webPageUrl}>
        <img src={this.midSizePhoto} border="0" />
      </a>
    return xml;
  },

  buildLargeHTML: function facebookPhoto_buildLargeHTML() {
    var xml =
      <a title={this.title} href={this.webPageUrl}>
        <img src={this.largeSizePhoto} border="0" />
      </a>
    return xml;
  },

  buildBBCode: function facebookPhoto_buildBBCode() {
    return '[url=' + this.webPageUrl + '][img]'+ this.largeSizePhoto +'[/img][/url]';
  },

  QueryInterface: function facebookPhoto_QueryInterface(aIid) {
    if (!aIid.equals(CI.nsISupports) &&
        !aIid.equals(CI.flockIPhoto)) {
      throw CR.NS_ERROR_NO_INTERFACE;
    }
    return this;
  }
};

/**
 * Represents the Facebook API
 */
function facebookAPI() {
  this.api_key = FACEBOOK_API_KEY;
  this.api_secret = FACEBOOK_API_SECRET;
  this.endpoint = FACEBOOK_API_ENDPOINT_URL;
  this.prettyName = "Facebook";
  this.uid = null;
  this._friendsCache = {
    value: null,
    lastUpdate: new Date(0)
  };

  this.acUtils = CC["@flock.com/account-utils;1"]
                 .getService(CI.flockIAccountUtils);
  var obService = CC["@mozilla.org/observer-service;1"]
                  .getService(CI.nsIObserverService);
  var inst = this;
  this.webDetective = CC["@flock.com/web-detective;1"]
                      .getService(CI.flockIWebDetective);

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

//****************************************
//* Authentication & Sesssion Calls
//****************************************/

facebookAPI.prototype.sessionPing =
function facebookAPI_sessionPing(aListener) {
  var pingListener = {
    onSuccess: function onSuccess(aResult) {
      aListener.onSuccess(null, aResult.result);
    },
    onError: function onError(aError) {
      aListener.onError(null, "", null);
    }
  }
  this.authCallJSON(pingListener, "facebook.session.ping");
}

facebookAPI.prototype.authenticate =
function facebookAPI_authenticate(aListener) {
  this._logger.debug("authenticate()");
  var api = this;
  var tokenListener = {
    onSuccess: function(aToken) {
      var authUrl = api.getAuthUrl(aToken);

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

      // We try to authenticate in the normal fashion (apiAuth2) first.
      // If that fails, it may be because we have yet to agree to the
      // Facebook Platform User Terms of Service.  apiAuth1 handles that case.
      var onReadyStateFunc = function onReadyStateFunc(eEvt) {
        if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
          if (api.webDetective.detectNoDOM("facebook", "apiAuth2", "",
                                           req.responseText, null))
          {
            api.getSession(aToken, aListener);
          } else {
            api._logger.debug("req: \n" + req.responseText);
            var results = new MyBag();
            if (api.webDetective.detectNoDOM("facebook",
                                             "apiAuth1",
                                             "",
                                             req.responseText,
                                             results))
            {
              var postBody = "grant_perm=1"
                           + "&next="
                           + "&api_key="
                           + FACEBOOK_API_KEY
                           + "&auth_token="
                           + aToken
                           + "&post_form_id="
                           + results.getPropertyAsAString("formId");

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

              req2.onreadystatechange = function (eEvt) {
                if (req2.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
                  if (api.webDetective.detectNoDOM("facebook",
                                                   "apiAuth2",
                                                   "",
                                                   req2.responseText,
                                                   null))
                  {
                    api.getSession(aToken, aListener);
                  } else {
                    api._logger.debug(req.responseText);
                    var error = CC[FLOCK_ERROR_CONTRACTID]
                                .createInstance(CI.flockIError);
                    error.errorString = "apiAuth2 Failed.";
                    api._logger.debug("apiAuth2 Failed.");
                    aListener.onError(null, null, error);
                  }
                }
              }

              req2.detachLoadGroup = true;
              req2.open("POST", results.getPropertyAsAString("postUrl"), true);
              req2.setRequestHeader("Content-Type",
                                    "application/x-www-form-urlencoded");
              req2.overrideMimeType("text/txt");
              req2.send(postBody);
            } else {
              var error = CC[FLOCK_ERROR_CONTRACTID]
                          .createInstance(CI.flockIError);
              error.errorString = "apiAuth1 Failed.";
              api._logger.debug("apiAuth1 Failed.");
              aListener.onError(null, null, error);
            }
          }
        }
      }

      req.onreadystatechange = onReadyStateFunc;
      req.detachLoadGroup = true;
      req.open("GET", authUrl, true);
      req.overrideMimeType("text/txt");
      req.send(null);
    },
    onError: function onError(aErrorCode) {
      aListener.onError(null, "", aErrorCode);
    }
  };
  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;
  this.isLoggedIn = false;
}


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

// INTERNAL
facebookAPI.prototype.getSession =
function facebookAPI_getSession(aToken, aListener) {
  var api = this;
  var aParams = {
    auth_token: aToken
  };
  var sessionListener = {
    onSuccess: function onSuccess(aXml) {
      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.isLoggedIn = true;
      aListener.onSuccess(null, "authenticated")
    },
    onError: function onError(aErrorCode) {
      api.isLoggedIn = false;
      aListener.onError(null, "", aErrorCode);
    }
  };
  api.isLoggedIn = false;
  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(aListener) {
  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].status) {
          peeps[uid].status = {
            time: 0,
            message: ""
          }
        }

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

      var result = {
        wrappedJS: peeps
      };
      aListener.onSuccess(result, "success");
    },
    onError: function(errorCode) {
      api._logger.debug(".friendsGet() error: " + 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.authCallJSON(listener, "facebook.fql.query", { query: friendsQuery });
}


facebookAPI.prototype.getUpdatedMediaFriends =
function facebookAPI_getUpdatedMediaFriends(aListener, 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 (i in friends) {
        var person = new MyBag();
        person.setPropertyAsAString("uid", i);
        person.setPropertyAsInt32("count", friends[i].count);
        person.setPropertyAsInt32("latest", friends[i].latest);
        result.push(person);
      }
      aListener.onSuccess(createEnum(result), "success");
    },
    onError: function gumfListener_onError(errorCode) {
      api._logger.debug(".getUpdatedMediaFriends() error: " + 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 owner IN " + "(" + friendsQuery + ")";
  var query = "SELECT owner,created "
            + "FROM photo "
            + "WHERE created > " + aLastSeen
              + " AND " + "aid IN (" + albumQuery + ")";

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

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

facebookAPI.prototype.usersGetInfo =
function facebookAPI_usersGetInfo(aListener, 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 = new MyBag();
        person.setPropertyAsAString("uid", user.uid);
        person.setPropertyAsAString("name", user.name);
        person.setPropertyAsAString("avatar", user.pic_square);
        if (user.status) {
          person.setPropertyAsAString("statusMessage",
                                      user.status.message);
        }

        peeps.push(person);
      }

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

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


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

      var meNotifications = new MyBag();
      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);

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

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


facebookAPI.prototype.setStatus =
function facebookAPI_setStatus(aNewStatus, aListener) {
  var api = this;

  function reallySetStatus(aPostFormId) {
    var params = {
      post_form_id: aPostFormId
    };

    if (aNewStatus && aNewStatus != "") {
      params.status = aNewStatus;
    } else {
      params.clear = "1";
    }

    var body = api.getParamString(params);

    var xhr2 = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
               .createInstance(CI.nsIXMLHttpRequest);
    xhr2.open ("POST", FACEBOOK_API_SETSTATUS_URL, true);
    xhr2.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    xhr2.setRequestHeader("Content-Length", body.length);

    xhr2.onreadystatechange = function (aEvent) {
      if (xhr2.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
        if ((xhr2.status != HTTP_CODE_OK) &&
            (xhr2.status != HTTP_CODE_FOUND))
        {
          if (aListener) {
            aListener.onError(xhr2.statusText);
          }
          return;
        }
        if (aListener) {
          aListener.onSuccess(null, "");
        }
      }
    }

    xhr2.send(body);
  }

  function viewTOS() {
    // Show the TOS and to get the form
    var xhr = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
              .createInstance(CI.nsIXMLHttpRequest);
    xhr.open("GET", FACEBOOK_API_TOS_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 (aListener) {
            aListener.onError (xhr.statusText);
          }
          return;
        }

        // Get the post_form_id and setStatus for real
        var postFormId;
        var results = CC["@mozilla.org/hash-property-bag;1"]
                      .createInstance(CI.nsIWritablePropertyBag2);
        try {
          if (api.webDetective.detectNoDOM("facebook",
                                           "setStatus",
                                           "",
                                           xhr.responseText,
                                           results)) 
          {
            postFormId = results.getPropertyAsAString("status");
            reallySetStatus(postFormId);
          }
        } catch (ex) {
          aListener.onError();
        }
      }
    }
    xhr.send(null);
  }

  viewTOS();
}


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

facebookAPI.prototype.getAlbums =
function facebookAPI_getAlbums(aListener, 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);
      }
      aListener.onSuccess(createEnum(albums), 'success');
    },
    onError: function(aFlockError) {
      api._logger.debug(".getAlbums() error: " + aFlockError.errorString);
      aListener.onError(null, null, aFlockError);
    }
  }
  var params = {
    uid: userID
  }
  this.authCall(listener, 'facebook.photos.getAlbums', params);
}


facebookAPI.prototype.createAlbum =
function facebookAPI_createAlbum(aListener, 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;
      aListener.onSuccess(newAlbum, "success");
    },
    onError: function ca_onError(aFlockError) {
      api._logger.debug(".createAlbum() error: " + aFlockError.errorString);
      aListener.onError(aFlockError);
    }
  }
  var params = {
    name: aAlbumTitle
  }
  this.authCall(listener, "facebook.photos.createAlbum", params);
}


facebookAPI.prototype.getPhotos =
function facebookAPI_getPhotos(aListener, 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 newPhoto = new facebookPhoto();
        newPhoto.id = photo.pid;
        newPhoto.webPageUrl = photo.link;
        newPhoto.thumbnail = photo.src_small;
        newPhoto.midSizePhoto = photo.src;
        newPhoto.largeSizePhoto = photo.src_big;
        newPhoto.userid = photo.owner;
        newPhoto.title = photo.caption ? photo.caption : "";
        newPhoto.uploadDate = parseInt(photo.created) * 1000;
        // FIXME: need to get the full name and avatar of user - DP
        newPhoto.username = photo.owner;
        newPhoto.icon = FLOCK_SNOWMAN_URL;

        photos.push(newPhoto);
      }
      aListener.onSuccess(createEnum(photos), 'success');
    },
    onError: function gp_onError(flockError) {
      api._logger.debug(".getPhotos() error: " + flockError.errorString);
      aListener.onError(null, null, flockError);
    }
  };
  var params = {};
  if (aSubjectId) {
    params.subj_id = aSubjectId;
  }
  if (aAlbumId) {
    params.aid = aAlbumId;
  }
  if (aPidList) {
    params.pids = aPidList;
  }
  this.authCallJSON(listener, "facebook.photos.get", params);
}


facebookAPI.prototype._finalizePhoto =
function facebookAPI__finalizePhoto(aUploadListener,
                                    aUpload,
                                    aId)
{
  var getPhotoListener = {
    onSuccess: function fp_onSuccess(aPhotos) {
      if (aPhotos.hasMoreElements()) {
        var photo = aPhotos.getNext();
        photo.QueryInterface(CI.flockIPhoto);
        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(aListener,
                                             aPhoto,
                                             aParams,
                                             aUpload) {
  var api = this;

  var pid;
  var listener = {
    onResult: function listener_onResult(aXml) {
      api._logger.debug(CC["@mozilla.org/xmlextras/xmlserializer;1"]
                        .getService(CI.nsIDOMSerializer)
                        .serializeToString(aXml));
      var resp = aXml.getElementsByTagName("photos_upload_response")[0];
      if (!resp) {
        var error = api.getXMLError(aXml);
        aListener.onError(error);
      } else {
        pid = aXml.getElementsByTagName("pid")[0].childNodes[0].nodeValue;

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

        aListener.onUploadComplete(aUpload);
        api._finalizePhoto(aListener, aUpload, pid);
      }
    },
    onError: function listener_onError(aErrorCode) {
      if (aErrorCode) {
        aListener.onError(api.getHTTPError(aErrorCode));
      } else {
        aListener.onError(api.getError(null, null));
      }
    },
    onProgress: function listener_onProgress(aCurrentProgress) {
      aListener.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(aListener, 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 myListener = {
    onSuccess : function(xml) {
      aListener.onSuccess(xml, "success");
    },
    onError: function(aFlockError) {
      aListener.onError(null, null, aFlockError);
    }
  };

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

//****************************************
//* FQL Query calls
//****************************************/
facebookAPI.prototype.doFQLQuery=
function facebookAPI_doFQLQuery(aListener, aFQLQuery) {
  
  var myListener = {
    onSuccess : function(xml) {
      aListener.onSuccess(xml, "success");
    },
    onError: function(aFlockError) {
      aListener.onError(null, null, aFlockError);
    }
  };

  var params = {
    format: "xml",
    query: aFQLQuery
  };
  this.authCall(myListener, 'facebook.fql.query', params);
}


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

/* --- authCall is used for authenticated (most) calls --- */

facebookAPI.prototype.authCall =
function facebookAPI_authCall(aListener, aMethod, aParams, aIsDryRun) {
  if (!aParams) aParams = {};
  aParams.api_key = this.api_key;
  aParams.session_key = this.session_key;
  aParams.call_id = new Date().getTime();
  aParams.method = aMethod;
  aParams.v = FACEBOOK_API_VERSION;
  var paramString = this.getParamString(aParams, this.secret);
  var url = this.endpoint + "?" + paramString;
  this._logger.debug("===[" + aMethod + "]===> " + url);
  if (!aIsDryRun) {
    this._doCall(aListener, url, null);
  }
}

facebookAPI.prototype.authCallJSON =
function facebookAPI_authCallJSON(aListener, aMethod, aParams, aIsDryRun) {
  if (!aParams) aParams = {};
  aParams.api_key = this.api_key;
  aParams.session_key = this.session_key;
  aParams.call_id = new Date().getTime();
  aParams.method = aMethod;
  aParams.format = "JSON";
  aParams.v = FACEBOOK_API_VERSION;
  var paramString = this.getParamString(aParams, this.secret);
  var url = this.endpoint + "?" + paramString;
  this._logger.debug("===[" + aMethod + "]===> " + url);
  if (!aIsDryRun) {
    this._doCallJSON(aListener, url, null);
  }
}

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

facebookAPI.prototype.call =
function facebookAPI_call(aListener, 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(aListener, 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 "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 = serviceErrorMessage;
  }
  return error;
}

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

facebookAPI.prototype._doCall =
function facebookAPI_doCall(aListener, 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.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) {
      try {
        api._logger.debug("._doCall() Status = " + req.status);
        if(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");
              aListener.onError(api.getXMLError(req.responseXML));
            } else {
              api._logger.debug("._doCall() Success");
              aListener.onSuccess(req.responseXML);
            }
          } catch (ex) {
            // error parsing xml
            api._logger.error("._doCall() Parse error = " + ex);
            aListener.onError(api.getError(null, null));
          }
        } else {
          //  http errors
          api._logger.debug("._doCall() HTTP Error");
          aListener.onError(api.getHTTPError(req.status));
        }
      } catch (ex) {
        // XMLHTTPERROR (connection lost)
        api._logger.debug(".doCall() " + ex);
        aListener.onError(api.getHTTPError("9001"));
      }
    }
  }
  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(aListener, aUrl, aContent) {
  var api = this;

  this.req = CC[XMLHTTPREQUEST_CONTRACTID]
             .createInstance(CI.nsIXMLHttpRequest);
  this.req.QueryInterface(CI.nsIJSXMLHttpRequest);
  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) {
      try {
        api._logger.debug("._doCall() Status = " + req.status);
        if (req.status/100 == 2) {
          api._logger.debug("._doCall() response = \n" + req.responseText);
          var s = new Components.utils.Sandbox("about:blank");
          var result = Components.utils.evalInSandbox(req.responseText, s);
          if (result && result.error_code && result.error_msg) {
            api._logger.debug("._doCall() Error");
            aListener.onError(api.getError(result.error_code,
                                           result.error_msg));
          } else {
            api._logger.debug("._doCall() Success");
            aListener.onSuccess(result);
          }
        } else {
          //  http errors
          api._logger.debug("._doCall() HTTP Error");
          aListener.onError(api.getHTTPError(req.status));
        }
      } catch (ex) {
        // XMLHTTPERROR (connection lost)
        api._logger.debug(".doCall() " + ex);
        aListener.onError(api.getHTTPError("9001"));
      }
    }
  }
  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 () {
  return (new facebookPhoto().QueryInterface(CI.flockIPhoto));
}

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