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

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

CU.import("resource:///modules/FlockXPCOMUtils.jsm");
FlockXPCOMUtils.debug = false;

CU.import("resource:///modules/FlockScheduler.jsm");
CU.import("resource:///modules/FlockSvcUtils.jsm");
CU.import("resource://gre/modules/JSON.jsm");

const MODULE_NAME = "Twitter";
const CLASS_NAME = "Flock Twitter Service";
const CLASS_SHORT_NAME = "twitter";
const CLASS_TITLE = "Twitter";
const CLASS_ID = Components.ID("{535BFF20-9154-11DB-B606-0800200C9A66}");
const CONTRACT_ID = "@flock.com/people/twitter;1";

const FLOCK_RICH_DND_CONTRACTID = "@flock.com/rich-dnd-service;1";

const SERVICE_ENABLED_PREF = "flock.service.twitter.enabled";

const FAVICON = "chrome://flock/content/services/twitter/favicon.png";

// From nsIXMLHttpRequest.idl
const XMLHTTPREQUEST_READYSTATE_COMPLETED = 4;

const HTTP_CODE_OK = 200;
const HTTP_CODE_FOUND = 302;

// The delay between two refreshes when the sidebar is closed (in seconds)
const REFRESH_INTERVAL = 1800; // seconds == 30 minutes
// The delay between two refreshes when the sidebar is open (in seconds)
const SHORT_INTERVAL = 300; // seconds == 5 minutes

const TWITTER_URL = "http://www.twitter.com/"

// This is a workaround for the 401 errors we're getting when attempting to
// authenticate to Twitter's API.  If we get a 401, we'll silently retry up to
// this number of times before giving up and showing the user an error.
const TWITTER_MAX_AUTH_ATTEMPTS = 5;

// Twitter returns friends in pages of 100.
const TWITTER_FRIENDS_PAGE_SIZE = 100;

// Twitter returns messages in pages of 20.
const TWITTER_MESSAGES_PAGE_SIZE = 20;

// Twitter API documentation specifies the maximum length of a status message.
const TWITTER_MAX_STATUS_LENGTH = 140;

const TWITTER_PROPERTIES = "chrome://flock/locale/services/twitter.properties";

var gApi = null;

/*************************************************************************
 * Component: flockTwitterService
 *************************************************************************/
function flockTwitterService() {
  this._init();

  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getAccount");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getAccounts");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "logout");

  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "getAccountIDFromDocument");
  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "getCredentialsFromForm");
  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "ownsDocument");
  FlockSvcUtils.flockIManageableWebService
               .addDefaultMethod(this, "ownsLoginForm");

  FlockSvcUtils.flockIRichContentDropHandler
               .addDefaultMethod(this, "_handleTextareaDrop");
}


/*************************************************************************
 * flockTwitterService: XPCOM Component Creation
 *************************************************************************/

flockTwitterService.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME,
  CLASS_ID,
  CONTRACT_ID,
  flockTwitterService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.nsIObserver,
    CI.flockIWebService,
    CI.flockIManageableWebService,
    CI.flockIPollingService,
    CI.flockISocialWebService,
    CI.flockIRichContentDropHandler
  ]
  );

// FlockXPCOMUtils.genericModule() categories
flockTwitterService.prototype._xpcom_categories = [
  { category: "wsm-startup" },
  { category: "flockWebService", entry: CLASS_SHORT_NAME },
  { category: "flockRichContentHandler", entry: CLASS_SHORT_NAME }  
];


/*************************************************************************
 * flockTwitterService: Private Data and Functions
 *************************************************************************/
 
// Member variables.
flockTwitterService.prototype._init =
function fts_init() {
  FlockSvcUtils.getLogger(this);
  this._logger.debug(".init()");

  // Determine whether this service has been disabled, and unregister if so.
  var prefService = CC["@mozilla.org/preferences-service;1"]
                   .getService(CI.nsIPrefBranch);
  if (prefService.getPrefType(SERVICE_ENABLED_PREF) &&
     !prefService.getBoolPref(SERVICE_ENABLED_PREF))
  {
    this._logger.debug(SERVICE_ENABLED_PREF + " is FALSE! Not initializing.");
    this.deleteCategories();
    return;
  }

  var profiler = CC["@flock.com/profiler;1"].getService(CI.flockIProfiler);
  var evtID = profiler.profileEventStart("twitter-init");

  var obs = CC["@mozilla.org/observer-service;1"]
            .getService(CI.nsIObserverService);
  obs.addObserver(this, "xpcom-shutdown", false);

  this._accountClassCtor = flockTwitterAccount;

  FlockSvcUtils.getCoop(this);

  this._baseUrn = "urn:twitter";
  this._serviceUrn = this._baseUrn + ":service";

  if (this._coop.Service.exists(this._serviceUrn)) {
    this._c_svc = this._coop.get(this._serviceUrn);
  } else {
    this._c_svc = new this._coop.Service(this._serviceUrn, {
      name: CLASS_SHORT_NAME,
      desc: CLASS_TITLE
    });
  }
  this._c_svc.serviceId = CONTRACT_ID;
  this._c_svc.logoutOption = true;

  // Note that using FlockSvcUtils.getWD here adds the "_WebDetective"
  // property to the service.
  this._c_svc.domains = FlockSvcUtils.getWD(this)
                        .getString("twitter", "domains", "twitter.com");
  this._c_svc.loginURL = FlockSvcUtils.getWD(this)
                         .getString("twitter",
                                    "userlogin",
                                    "https://twitter.com/login/");

  // Initialize API
  gApi = new flockTwitterAPI();

  // Update auth states
  try {
    if (this._WebDetective.detectCookies("twitter", "loggedout", null)) {
      this._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
    }
  } catch (ex) {
    this._logger.error("ERROR updating auth states for Twitter: " + ex);
  }

  profiler.profileEventEnd(evtID, "");
}


flockTwitterService.prototype._lightPeopleIcon =
function flockTwitterService__lightPeopleIcon() {
  this._logger.debug("._lightPeopleIcon()");
  var obs = CC["@mozilla.org/observer-service;1"]
            .getService(CI.nsIObserverService);
  obs.notifyObservers(null, "new-people-notification", null);
}


flockTwitterService.prototype._refreshAccount =
function fts__refreshAccount(aUrn, aListener) {
  var inst = this;
  var refreshItem = this._coop.get(aUrn);
  var lastUpdate = refreshItem.lastUpdateDate;
  var numberOfAnswers = 0;

  // Ensures that both listeners have completed before sending our caller a
  // response.  Also sets the next refresh time to be soon if the people
  // sidebar is open.
  function onIndividualSuccess() {
    numberOfAnswers++;
    if (numberOfAnswers >= 3) {
      if (inst._acUtils.isPeopleSidebarOpen()) {
        refreshItem.nextRefresh = new Date(Date.now() + SHORT_INTERVAL * 1000);
      }
      if (aListener) {
        aListener.onResult();
      }
    }
  }

  /**
   * {
   *   "status": {
   *     "created_at": "Thu May 17 19:19:39 +0000 2007",
   *     "text": "attending the full staff meeting",
   *     "id": 67511202
   *   },
   *   "url": null,
   *   "followers_count": 0,
   *   "friends_count": 0,
   *   "profile_background_color": "9ae4e8",
   *   "name": "Matthew Willis",
   *   "favourites_count": 0,
   *   "profile_text_color": "000000",
   *   "statuses_count": 1,
   *   "profile_link_color": "0000ff",
   *   "description": null,
   *   "profile_sidebar_fill_color": "e0ff92",
   *   "location": null,
   *   "profile_image_url": "http:\/\/assets0.twitter.com\/images\/default_image.gif?1189634879",
   *   "id": 6117302,
   *   "utc_offset": -14400,
   *   "profile_sidebar_border_color": "87bc44",
   *   "screen_name": "lilmatt",
   *   "protected": false
   * }
   */

  // This listener handles getting the account owner's information.
  var userShowListener = {
    onSuccess: function userShow_onSuccess(aResult) {
      inst._logger.debug("Success for userShow");

      if (aResult.name) {
        inst._logger.debug("userShow: name: " + aResult.name);
        refreshItem.name = aResult.name;
      }

      if (aResult.screen_name) {
        inst._logger.debug("userShow: screen_name: " + aResult.screen_name);
        refreshItem.screenName = aResult.screen_name;
        refreshItem.URL = inst._WebDetective
                              .getString(CLASS_SHORT_NAME, "userprofile", null)
                              .replace("%accountid%", aResult.screen_name);
      }

      if (aResult.profile_image_url) {
        inst._logger.debug("userShow: avatar: " + aResult.profile_image_url);
        // If avatar returned is the default image, set coop.Account.avatar
        // to null and let the people sidebar code set the Flock common image.
        if (inst._hasDefaultAvatar(aResult.profile_image_url)) {
          inst._logger.debug("No avatar for account. Setting to null.");
          refreshItem.avatar = null;
        } else {
          refreshItem.avatar = aResult.profile_image_url;
        }
      }

      if (aResult.status &&
          aResult.status.text &&
          aResult.status.text != refreshItem.statusMessage)
      {
        inst._logger.debug("userShow: status: " + aResult.status.text);
        refreshItem.statusMessage = aResult.status.text;

        var dateString = aResult.status.created_at;
        refreshItem.lastProfileUpdate = inst._parseTwitterDate(dateString);
      }

      onIndividualSuccess();
    },
    onError: function userShow_onError(aError) {
      inst._logger.error("Error on userShow");
      aListener.onError(aError);
    }
  };

  // This listener handles getting the user's friends' information.
  var friendsListener = {
    onSuccess: function friends_onSuccess(aResult) {
      inst._logger.debug("friendsListener onSuccess");

      // Now that we have all the results we need, update the RDF.
      function myWorker(aShouldYield) {
        // ADD or update existing people
        for (var uid in aResult) {
          inst._addPerson(aResult[uid], refreshItem);
          if (aShouldYield()) {
            yield;
          }
        }
        // REMOVE locally people removed on the server
        var localEnum = refreshItem.friendsList.children.enumerate();
        while (localEnum.hasMoreElements()) {
          var identity = localEnum.getNext();
          if (!aResult[identity.accountId]) {
            inst._logger.info("Friend " + identity.accountId
                              + " has been deleted on the server");
            refreshItem.friendsList.children.remove(identity);
            identity.destroy();
          }
        }
        onIndividualSuccess();
      }
      FlockScheduler.schedule(null, 0.05, 10, myWorker);
    },
    onError: function friends_onError(aError) {
      inst._logger.error("friendsListener onError");
      aListener.onError(aError);
    }
  };

  // This listener handles getting a count of the user's direct messages.
  var messageCountListener = {
    onSuccess: function messages_onSuccess(aResult) {
      inst._logger.debug("messageCountListener onSuccess");
      inst._logger.debug("Message count: " + aResult);
      refreshItem.accountMessages = aResult;

      // Trigger the people icon highlight if the user has messages
      if (aResult > 0) {
        inst._lightPeopleIcon();
      }

      onIndividualSuccess();
    },
    onError: function messages_onError(aError) {
      inst._logger.error("messageCountListener onError");
      aListener.onError(aError);
    }
  };

  gApi.userShow(refreshItem.accountId, userShowListener);
  gApi.getFriendsStatus(null, friendsListener);
  gApi.getTotalMessageCount(messageCountListener);
}


flockTwitterService.prototype._addPerson =
function fts__addPerson(aPerson, aCoopAccount) {
  // We include the current accountId in the identity urn to prevent friend
  // collisions if multiple service accounts have the same friend.
  var identityUrn = this._getIdentityUrn(aCoopAccount.accountId, aPerson.id);
  var updating = this._coop.Identity.exists(identityUrn);
  var identity;

  var lastUpdate = this._parseTwitterDate(aPerson.status.created_at);
  this._logger.debug("Adding person: " + aPerson.id + " - " + aPerson.name);

  // XXX: Since Twitter only timestamps status changes, we need to add
  //      property-wise profile comparison here.
  var lastUpdateType = "status";

  var avatarUrl = null;
  if (!this._hasDefaultAvatar(aPerson.profile_image_url)) {
    avatarUrl = aPerson.profile_image_url;
  }

  if (updating) {
    // Update data of the coop.Identity object
    identity = this._coop.get(identityUrn);
    if (lastUpdate > identity.lastUpdate) {
      identity.name = aPerson.name;
      identity.avatar = avatarUrl;
      identity.screenName = aPerson.screen_name;
      identity.statusMessage = aPerson.status.text;
      identity.lastUpdate = lastUpdate;
      identity.lastUpdateType = lastUpdateType;
    }
  } else {
    identity = new this._coop.Identity(
      identityUrn,
      {
        name: aPerson.name,
        serviceId: this.contractId,
        accountId: aPerson.id,
        avatar: avatarUrl,
        screenName: aPerson.screen_name,
        statusMessage: aPerson.status.text,
        lastUpdate: lastUpdate,
        lastUpdateType: lastUpdateType
      }
    );
    aCoopAccount.friendsList.children.add(identity);
  }
}


flockTwitterService.prototype._getIdentityUrn =
function fts__getIdentityUrn(aAccountId, aUid) {
  var prefix = "urn:flock:identity:" + CLASS_SHORT_NAME;
  return prefix + aAccountId + ":" + aUid;
}


/**
 * Helper function to parse Twitter's date string into seconds since epoch.
 * @param  aDateString  A string formatted as: Wed Jan 31 00:16:35 +0000 2007
 * @return  The number of seconds since the epoch.
 */
flockTwitterService.prototype._parseTwitterDate =
function fts__parseTwitterDate(aDateString) {
  this._logger.debug("_parseTwitterDate: in: " + aDateString);
  if (!aDateString) {
    return 0;
  }
  // Date.parse() returns milliseconds since epoch. Divide by 1000 for seconds.
  return (Date.parse(aDateString) / 1000);
}


/**
 * Helper function to determine if the user has customized their avatar based
 * on the passed in URL.
 * @param  aUrl  A string containing the contents of the Twitter user's
 *               "profile_image_url" property.
 * @return  true if the user is still using the default avatar, else false
 */
flockTwitterService.prototype._hasDefaultAvatar =
function fts_hasDefaultAvatar(aUrl) {
  this._logger.debug("_hasDefaultAvatar(" + aUrl + ")");
  var defaultUrl = this._WebDetective.getString("twitter", "noAvatar", "");
  return (aUrl.indexOf(defaultUrl) != -1);
}


/*************************************************************************
 * flockTwitterService: flockIWebService Implementation
 *************************************************************************/

// readonly attribute AString contractId;
// ALMOST duplicated by nsIClassInfo::contractID
// with different case: contractId != contractID
flockTwitterService.prototype.contractId = CONTRACT_ID;

// readonly attribute AString icon;
flockTwitterService.prototype.icon = FAVICON;

// readonly attribute boolean needPassword;
flockTwitterService.prototype.needPassword = true;

// readonly attribute AString shortName;
flockTwitterService.prototype.shortName = CLASS_SHORT_NAME;

// readonly attribute long status;
flockTwitterService.prototype.status = CI.flockIWebService.STATUS_UNKNOWN;

// readonly attribute AString title;
flockTwitterService.prototype.title = CLASS_TITLE;

// readonly attribute AString url;
flockTwitterService.prototype.url = TWITTER_URL;

// readonly attribute AString urn;
flockTwitterService.prototype.urn = "urn:" + CLASS_SHORT_NAME + ":service";

/**
 * @see flockIWebService#addAccountById
 */
flockTwitterService.prototype.addAccountById =
function fts_addAccountById(aAccountId, aIsTransient, aListener) {
  this._logger.debug(".addAccountById('"
                     + aAccountId + "', " + aIsTransient + ", aListener)");

  if (!aAccountId) {
    aListener.onError();
    return;
  }

  var pw = this._acUtils.getPassword(this.urn + ":" + aAccountId);
  var name = (pw) ? pw.user : aAccountId;

  var accountUrn = "urn:flock:" + this.shortName + ":account:" + aAccountId;
  var account = new this._coop.Account(
    accountUrn, {
      name: name,
      serviceId: this.contractId,
      service: this._c_svc,
      accountId: aAccountId,
      isPollable: false,
      isTransient: aIsTransient,
      URL: this._WebDetective.getString(CLASS_SHORT_NAME, "userprofile", null)
               .replace("%accountid%", aAccountId),
      refreshInterval: REFRESH_INTERVAL,
      favicon: FAVICON
    });
  this._coop.accounts_root.children.add(account);

  var friendsListUrn = accountUrn + ":friends";
  var friendsList = new this._coop.FriendsList(
    friendsListUrn,
    {
      account: account
    });
  account.friendsList = friendsList;

  // Instantiate account component
  var acct = this.getAccount(account.id());
  if (aListener) {
    aListener.onSuccess(acct, "addAccount");
  }
  return acct;
}

/**
 * @see flockIWebService#removeAccount
 */
flockTwitterService.prototype.removeAccount =
function fts_removeAccount(aUrn) {
  this._logger.debug(".removeAccount(" + aUrn + ")");
  this._acUtils.removeAccount(aUrn);
}


/*************************************************************************
 * flockTwitterService: flockIManageableWebService Implementation
 *************************************************************************/

// DEFAULT: boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument aDocument);
// DEFAULT: AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: nsIPassword getCredentialsFromForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);

/**
 * void updateAccountStatusFromDocument(in nsIDOMHTMLDocument aDocument);
 *
 * @see flockIManageableWebService#updateAccountStatusFromDocument
 */
flockTwitterService.prototype.updateAccountStatusFromDocument =
function fts_updateAccountStatusFromDocument(aDocument) {
  this._logger.debug(".updateAccountStatusFromDocument(aDocument)");
  if (this._WebDetective.detect(this.shortName, "loggedout", aDocument, null) ||
      this._WebDetective.detectCookies(this.shortName, "loggedout", null))
  {
    this._logger.debug("We're logged out!");
    this._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
  } else if (this._WebDetective.detect(this.shortName,
                                       "loggedin",
                                       aDocument,
                                       null))
  {
    this._logger.debug("We're NOT logged out!  Let's try to get some creds!");
    var results = FlockSvcUtils.newResults();
    if (this._WebDetective.detect(this.shortName,
                                  "accountinfo",
                                  aDocument,
                                  results))
    {
      var acctId = results.getPropertyAsAString("accountid");
      var acctUrn = this._acUtils.getAccountURNById(this.urn, acctId);
      var acct = this._coop.get(acctUrn);
      if (!acct.isAuthenticated) {
        acct.isAuthenticated = true;
      }
    }
  } else {
    this._logger.debug("We don't match 'loggedout' or 'logged in'");
  }
}

/*************************************************************************
 * flockTwitterService: flockISocialWebService implementation
 *************************************************************************/

flockTwitterService.prototype.maxStatusLength = TWITTER_MAX_STATUS_LENGTH;


/*************************************************************************
 * flockTwitterService: flockIPollingService Implementation
 *************************************************************************/
/**
 * @see flockIPollingService#refresh
 */
flockTwitterService.prototype.refresh =
function fts_refresh(aUrn, aListener) {
  this._logger.debug(".refresh(" + aUrn + ")");
  var refreshItem = this._coop.get(aUrn);

  if (refreshItem instanceof this._coop.Account) {
    this._logger.debug("refreshing an Account");
    if (refreshItem.isAuthenticated) {
      this._refreshAccount(aUrn, aListener);
    } else {
      // If the user is not logged in, return a success without
      // refreshing anything
      aListener.onResult();
    }
  } else {
    this._logger.error("can't refresh " + aUrn + " (unsupported type)");
    aListener.onError(null);
  }
}


/**************************************************************************
 * flockTwitterService: nsIObserver Implementation
 **************************************************************************/

flockTwitterService.prototype.observe =
function fts_observe(aSubject, aTopic, aState) {
  switch (aTopic) {
    case "xpcom-shutdown":
      var obs = CC["@mozilla.org/observer-service;1"]
                .getService(CI.nsIObserverService);
      obs.removeObserver(this, "xpcom-shutdown");
      break;
  }
}


/**************************************************************************
 * flockTwitterService: flockIRichContentDropHandler Implementation
 **************************************************************************/

flockTwitterService.prototype.handleDrop =
function fts_handleDrop(aFlavours, aTextarea) {
  var inst = this;
  var dropCallback = function facebook_dropCallback(aFlav) {
    // Get URL from dropped text
    var dataObj = {}, len = {};
    aFlavours.getTransferData(aFlav, dataObj, len);
    var text = dataObj.value.QueryInterface(CI.nsISupportsString).data;
    var textParts = text.split(": ");
    var url = (textParts.length == 2) ? textParts[1] : text;

    // Find position
    var caretPos = aTextarea.selectionEnd;
    var currentValue = aTextarea.value;

    // Add a trailing space so that we don't mangle the url
    var nextChar = currentValue.charAt(caretPos);
    var trailingSpace = ((nextChar == "") ||
                         (nextChar == " ") ||
                         (nextChar == "\n"))
                      ? ""
                      : " ";

    // Put it all together to drop the text into the selection. Note: no
    // breadcrumb due to twitter length constraint.
    aTextarea.value = currentValue.substring(0, caretPos)
                    + url
                    + trailingSpace
                    + currentValue.substring(caretPos);
  };

  return this._handleTextareaDrop(CLASS_SHORT_NAME, this._c_svc.domains,
                                  aTextarea, dropCallback);
}


/*************************************************************************
 * Component: flockTwitterAPI
 *************************************************************************/
function flockTwitterAPI() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("twitterAPI");

  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);

  this._logger.debug("constructor");
}


/*************************************************************************
 * flockTwitterAPI: XPCOM Component Creation
 *************************************************************************/

// Use genericComponent() for the goodness it provides (QI, nsIClassInfo, etc),
// but do NOT add this component to the list of constructors passed to
// FlockXPCOMUtils.genericModule().
flockTwitterAPI.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " API",
  "",
  "",
  flockTwitterAPI,
  0,
  []
  );


/*************************************************************************
 * flockTwitterAPI: Private data and functions
 *************************************************************************/
/**
 * Actually do the API call.
 * @param  aListener
 * @param  aMethod
 * @param  aParams
 * @param  aRequestType  "GET" or "POST"
 * @param  aPostVars  Array of JS objects to include in the POST body
 * @param  aCount  A variable to increment when a 401 error occurs before
 *                 trying again.  This is handled internally by this
 *                 function.  External callers should set this to null.
 */
flockTwitterAPI.prototype._call =
function api__call(aListener, aMethod, aParams, aRequestType, aPostVars,
                   aCount)
{
  var requestType = aRequestType.toUpperCase();
  var responseFormat = ".json";
  var url = "https://twitter.com/" + aMethod + responseFormat;

  var idx = 0;
  for (var p in aParams) {
    // Only use "?" for the first param.  Use "&" after.
    url += (idx == 0) ? "?" : "&";
    url += p + "=" + aParams[p];
    idx++;
  }

  this._logger.debug("._call() url: " + url);

  var req = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
            .createInstance(CI.nsIXMLHttpRequest)
            .QueryInterface(CI.nsIJSXMLHttpRequest);

  // Don't pop nsIAuthPrompts if auth fails
  req.backgroundRequest = true;

  var coopAccountUrn = this._acUtils
                           .getFirstAuthenticatedAccountForService(CONTRACT_ID);
  var coopAccount = this._coop.get(coopAccountUrn);
  var passwordUrn = "urn:twitter:service:" + coopAccount.accountId;
  var creds = this._acUtils.getPassword(passwordUrn);

  req.open(requestType, url, true, creds.user, creds.password);

  if (requestType == "POST") {
    req.setRequestHeader("Content-Type",
                         "application/x-www-form-urlencoded; charset=UTF-8");
  }

  var inst = this;
  req.onreadystatechange = function ORSC(aEvent) {
    inst._logger.debug("._call() ReadyState: " + req.readyState);

    if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      try {
        inst._logger.debug("._call() Status: " + req.status);

        if (req.status/100 == 2) {
          inst._logger.debug("._call() response:\n" + req.responseText);
          var s = new CU.Sandbox("about:blank");
          var result = CU.evalInSandbox("(" + req.responseText + ")", s);

          if (result) {
            aListener.onSuccess(result);
          } else {
            inst._logger.debug("._call() error: no result");
            aListener.onError();
          }

        } else if (req.status == 401) {
          // XXX: Workaround for 401 errors we're seeing with Twitter.
          // Loop call if necessary up to TWITTER_MAX_AUTH_ATTEMPTS times.

          // Increment counter
          var count;
          if (aCount == null) {
            count = 1;
          } else {
            count = aCount + 1;
          }

          if (count < TWITTER_MAX_AUTH_ATTEMPTS) {
            // Try, try again
            inst._logger.debug("._call() Got a 401. Will try "
                               + (TWITTER_MAX_AUTH_ATTEMPTS - count)
                               + " more time(s).");
            inst._call(aListener, aMethod, aParams, aRequestType, aPostVars,
                       count);
          } else {
            // Ok. Give up.
            inst._logger.debug("._call() HTTP error");
            aListener.onError(/*req.status*/);
          }

        } else {
          // HTTP errors
          inst._logger.debug("._call() HTTP error");
          aListener.onError(/*req.status*/);
        }
      } catch (ex) {
        // XMLHTTPERROR (connection lost)
        inst._logger.debug("._call() exception: " + ex);
        aListener.onError(/*inst.getHTTPError("9001")*/);
      }
    }
  }

  var postBody = "";
  if (aPostVars) {
    for (var v in aPostVars) {
      if (postBody.length) {
        postBody += "&";
      }
      postBody += v + "=" + encodeURIComponent(aPostVars[v]);
    }
  }

  if ((requestType == "POST") && postBody && postBody.length) {
    this._logger.debug("._call() postBody: " + postBody);
    req.send(postBody);
  } else {
    req.send(null);
  }
}


/**
 * Get info (including status) about a user.
 * @param  aUid  Twitter uid of the user to query.
 * @param  aListener
 */
flockTwitterAPI.prototype.userShow =
function api_userShow(aUid, aListener) {
  this._logger.debug(".userShow(" + aUid + ", aListener)");

  var url = "users/show/" + aUid;
  gApi._call(aListener, url, null, "GET", null, null);
}


/**
 * direct_messages
 * Returns a list of the 20 most recent direct messages sent to the
 * authenticating user.  The XML and JSON versions include detailed
 * information about the sending and recipient users.
 *
 * URL: http://twitter.com/direct_messages.format
 * Formats: xml, json, rss, atom
 * Parameters:
 *   since     Optional.  Narrows the resulting list of direct messages to just
 *             those sent after the specified HTTP-formatted date.  The same
 *             behavior is available by setting the If-Modified-Since parameter
 *             in your HTTP request.
 *             Ex: http://twitter.com/direct_messages.atom?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
 *   since_id  Optional.  Returns only direct messages with an ID greater than
 *             (that is, more recent than) the specified ID.
 *             Ex: http://twitter.com/direct_messages.xml?since_id=12345
 *   page      Optional.  Retrieves the 20 next most recent direct messages.
 *             Ex: http://twitter.com/direct_messages.xml?page=3
 */
flockTwitterAPI.prototype.directMessages =
function api_directMessages(aSince, aSinceId, aPage, aListener) {
  this._logger.debug(".directMessages(" + aSince + ", "
                                        + aSinceId + ", "
                                        + aPage + ", "
                                        + "aListener)");
  var params = {};

  if (aSince) {
    params.since = aSince;
  }

  if (aSinceId) {
    params.since_id = aSinceId;
  }

  if (aPage) {
    params.page = aPage;
  }

  var url = "direct_messages";
  gApi._call(aListener, url, params, "GET", null, null);
}


/**
 * Get a count of all messages for the authenticated user
 * @param  aListener
 */
flockTwitterAPI.prototype.getTotalMessageCount =
function api_getTotalMessageCount(aListener) {
  this._logger.debug(".getTotalMessageCount(aListener)");

  // We start off by asking for page 1 of messages.
  var page = 1;

  // Message counter
  var count = 0;

  var inst = this;
  var getMessagesListener = {
    onSuccess: function L_onSuccess(aResult) {
      // If a full page is returned then go grab another
      inst._logger.debug("Twitter messages returned: " + aResult.length);
      count += aResult.length;
      if (aResult.length == TWITTER_MESSAGES_PAGE_SIZE) {
        inst._logger.debug("Fetching more messages");
        page++;
        inst.directMessages(null, null, page, getMessagesListener);
      } else {
        inst._logger.debug("Fetching messages complete");
        aListener.onSuccess(count);
      }
    },
    onError: function L_onError(aError) {
      inst._logger.debug(".getMessagesListener() error: " + aError);
    }
  }

  this.directMessages(null, null, page, getMessagesListener);
}


/**
 * Get a user's friends' updates.
 * @param  aUid  Twitter uid of the user whose friends to view or null to
 *               view the currently authenticated user's friends.
 * @param  aListener
 */
flockTwitterAPI.prototype.getFriendsStatus =
function api_getFriendsStatus(aUid, aListener) {
  this._logger.debug(".getFriendsStatus(" + aUid + ", aListener)");

  var peopleHash = {};
  var inst = this;

  var params = {};
  // We start off by asking for page 1 of friends.
  params.page = 1;

  var getFriendsListener = {
    onSuccess: function L_onSuccess(aResult) {
      for (var i in aResult) {
        var id = aResult[i].id;
        peopleHash[id] = aResult[i];

        // If there is no status item then stub one in
        if (!peopleHash[id].status) {
          peopleHash[id].status = {
            created_at: 0,
            text: "",
            id: null
          }
        }

        inst._logger.debug("Got Twitter person: " + peopleHash[id].name);
      }

      // If a full page is returned then go grab another
      inst._logger.debug("Twitter friends returned: " + aResult.length);
      if (aResult.length == TWITTER_FRIENDS_PAGE_SIZE) {
        params.page++;
        inst._logger.debug("Fetching more friends, now getting page "
                           + params.page);
        gApi._call(getFriendsListener, url, params, "GET", null, null);
      } else {
        inst._logger.debug("Fetching friends complete");
        aListener.onSuccess(peopleHash);
      }
    },
    onError: function L_onError(aError) {
      inst._logger.debug(".getFriendsStatus() error: " + aError);
    }
  }

  var url = "statuses/friends";
  if (aUid) {
    url += "/" + aUid;
  }
  gApi._call(getFriendsListener, url, null, "GET", null, null);
}


/**
 * Set the user's status
 * @param  aStatusMessage  a string containing the message to set
 * @param  aListener
 *
 * Notes from Twitter API documentation:
 * -----------------------------------------------------------------------
 * Request must be a POST.
 * URL: http://twitter.com/statuses/update.format
 * Formats: xml, json.
 *   Returns the posted status in requested format when successful.
 * Parameters:
 *   status  Required  The text of your status update.
 *                     Be sure to URL encode as necessary.
 *                     Must not be more than 160 characters and should not
 *                     be more than 140 characters to ensure optimal display
 */
flockTwitterAPI.prototype.setStatus =
function api_setStatus(aStatusMessage, aListener) {
  this._logger.debug(".setStatus(" + aStatusMessage + ", aListener)");

  // substring() starts at 0 while TWITTER_MAX_STATUS_LENGTH counts from 1.
  var message = aStatusMessage.substring(0, (TWITTER_MAX_STATUS_LENGTH - 1));

  var postVars = {
    "source": "flock", // This value specified by Alex Payne at Twitter.
    "status": message
  };
  var url = "statuses/update";
  gApi._call(aListener, url, null, "POST", postVars, null);
}


/*************************************************************************
 * Component: flockTwitterAccount
 *************************************************************************/

function flockTwitterAccount() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("twitterAccount");

  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);
  // XXX: I should be able to use FlockSvcUtils.getWD() here, but it can
  //      only be called by the service.
  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);

  FlockSvcUtils.flockIWebServiceAccount.addDefaultMethod(this, "deactivate");
  FlockSvcUtils.flockIWebServiceAccount.addDefaultMethod(this, "login");
  FlockSvcUtils.flockIWebServiceAccount.addDefaultMethod(this, "logout");
  FlockSvcUtils.flockIWebServiceAccount.addDefaultMethod(this, "remove");

  FlockSvcUtils.flockISocialWebServiceAccount.addDefaultMethod(this, "getFriendCount");

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  this._bundle = sbs.createBundle(TWITTER_PROPERTIES);
}


/*************************************************************************
 * flockTwitterAccount: XPCOM Component Creation
 *************************************************************************/

// Use genericComponent() for the goodness it provides (QI, nsIClassInfo, etc),
// but do NOT add this component to the list of constructors passed to
// FlockXPCOMUtils.genericModule().
flockTwitterAccount.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " Account",
  "",
  "",
  flockTwitterAccount,
  0,
  [
    CI.flockIWebServiceAccount,
    CI.flockISocialWebServiceAccount
  ]
  );


/*************************************************************************
 * flockTwitterAccount: flockIWebServiceAccount Implementation
 *************************************************************************/

// readonly attribute AString urn;
flockTwitterAccount.prototype.urn = "";

// void activate(in flockIListener aListener);
flockTwitterAccount.prototype.activate =
function fta_activate(aListener) {
  this._logger.debug(".activate()");
  var acctCoopObj = this._coop.get(this.urn);
  acctCoopObj.isAuthenticated = true;
  acctCoopObj.nextRefresh = new Date(0);
  acctCoopObj.isPollable = true;

  if (aListener) {
    aListener.onSuccess(this, "activate");
  }
}

// DEFAULT: void deactivate(in flockIListener aListener);
// DEFAULT: void login(in flockIListener aListener);
// DEFAULT: void logout(in flockIListener aListener);

// void keep();
flockTwitterAccount.prototype.keep =
function fta_keep() {
  this._logger.debug(".keep()");
  this._coop.get(this.urn).isTransient = false;
  var urn = this.urn.replace("account:", "service:").replace("flock:", "");
  this._acUtils.makeTempPasswordPermanent(urn);
}

// DEFAULT: void remove();


/*************************************************************************
 * flockTwitterAccount: flockISocialWebServiceAccount Implementation
 *************************************************************************/
// readonly attribute boolean hasFriendActions;
flockTwitterAccount.prototype.hasFriendActions = true;

// readonly attribute boolean isPostLinkSupported;
flockTwitterAccount.prototype.isPostLinkSupported = true;

// readonly attribute boolean isMyMediaFavoritesSupported;
flockTwitterAccount.prototype.isMyMediaFavoritesSupported = false;

// readonly attribute boolean isStatusSupported;
flockTwitterAccount.prototype.isStatusSupported = true;

// readonly attribute boolean isStatusEditable;
flockTwitterAccount.prototype.isStatusEditable = true;


// AString formatStatusForDisplay(in AString aStatusMessage);
flockTwitterAccount.prototype.formatStatusForDisplay =
function fma_formatStatusForDisplay(aStatusMessage) {
  this._logger.debug(".formatStatusForDisplay(" + aStatusMessage + ")");

  var message = (aStatusMessage) ? aStatusMessage : "";

  // Responses from Twitter contain these HTML entities.  Order is important!
  message = message.replace(/&amp;/g, "&")
                   .replace(/&gt;/g, ">")
                   .replace(/&lt;/g, "<")
                   .replace(/&quot;/g, '"');
  return message;
}


// AString getProfileURLForFriend(in AString aFriendUrn);
flockTwitterAccount.prototype.getProfileURLForFriend =
function fta_getProfileURLForFriend(aFriendUrn) {
  this._logger.debug(".getProfileURLForFriend('" + aFriendUrn + "')");

  var url = "";
  var coopFriend = this._coop.get(aFriendUrn);
  var screenName = coopFriend.screenName;

  if (screenName) {
    url = this._WebDetective.getString(CLASS_SHORT_NAME, "friendProfile", "")
                            .replace("%screenName%", screenName);
  }

  this._logger.debug(".getProfileURLForFriend()  url: " + url);
  return url;
}


// void setStatus(in AString aStatusMessage, in flockIListener aListener);
flockTwitterAccount.prototype.setStatus =
function fma_setStatus(aStatusMessage, aListener) {
  this._logger.debug(".setStatus(" + aStatusMessage + ")");

  var inst = this;
  var statusListener = {
    onSuccess: function L_onSuccess(aResult) {
      // If the API call succeeded, also set the coop.Account status.
      inst._coop.get(inst.urn).statusMessage = aStatusMessage;
      if (aListener) {
        aListener.onSuccess(aResult, null);
      }
    },
    onError: function L_onError(aError) {
      if (aListener) {
        aListener.onError(null, null, aError);
      }
    }
  }
  gApi.setStatus(aStatusMessage, statusListener);
}


// AString getEditableStatus();
flockTwitterAccount.prototype.getEditableStatus =
function fma_getEditableStatus() {
  this._logger.debug(".getEditableStatus()");
  var message = this._coop.get(this.urn).statusMessage;
  return this.formatStatusForDisplay(message);
}


// AString getMeNotifications();
flockTwitterAccount.prototype.getMeNotifications =
function fma_getMeNotifications() {
  this._logger.debug(".getMeNotifications()");

  var noties = [];
  var inst = this;
  function _addNotie(aType, aCount) {
    var stringName = "flock." + CLASS_SHORT_NAME + ".noties." + aType + "."
                   + ((parseInt(aCount) <= 0) ? "none" : "some");

    inst._logger.debug("aType: " + aType
                       + " aCount: " + aCount
                       + " name: " + stringName);
    noties.push({
      class: aType,
      tooltip: inst._bundle.GetStringFromName(stringName),
      count: aCount,
      URL: inst._WebDetective.getString(CLASS_SHORT_NAME, aType + "_URL", "")
    });
  }
  var coopAccount = this._coop.get(this.urn);
  _addNotie("meMessages", coopAccount.accountMessages);

  return JSON.toString(noties);
}

flockTwitterAccount.prototype.markAllMeNotificationsSeen =
function flockTwitterAccount_markAllMeNotificationsSeen(aType) {
  this._logger.debug(".markAllMeNotificationsSeen('" + aType + "')");
  var c_acct = this._coop.get(this.urn);
  switch (aType) {
    case "meMessages":
      c_acct.accountMessages = 0;
      break;
    default:
      break;
  }
}

// AString getFriendActions(in AString aFriendUrn);
flockTwitterAccount.prototype.getFriendActions =
function fma_getFriendActions(aFriendUrn) {
  this._logger.debug(".getFriendActions('" + aFriendUrn + "')");

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

  var actions = [];
  var coopFriend = this._coop.get(aFriendUrn);
  if (coopFriend) {
    var coopAccount = this._coop.get(this.urn);
    for each (var action in actionNames) {
      actions.push({
        label: this._bundle.GetStringFromName("flock."
                                              + CLASS_SHORT_NAME
                                              + ".actions." + action),
        class: action,
        spec: this._WebDetective.getString(CLASS_SHORT_NAME, action, "")
                  .replace("%accountid%", coopAccount.accountId)
                  .replace("%friendid%", coopFriend.accountId)
      });
    }
  }

  return JSON.toString(actions);
}


// AString getSharingAction(in AString aFriendUrn,
//                          in nsITransferable aTransferable);
flockTwitterAccount.prototype.getSharingAction =
function fma_getSharingAction(aFriendUrn, aTransferable) {
  this._logger.debug(".getSharingAction('" + aFriendUrn + "')");

  var sharingAction = "";
  var coopFriend = this._coop.get(aFriendUrn);
  if (!coopFriend) {
    return sharingAction;
  }

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

  var message = CC[FLOCK_RICH_DND_CONTRACTID]
                .getService(CI.flockIRichDNDService)
                .getMessageFromTransferable(aTransferable,
                                            flavours.length,
                                            flavours);
  if (!message.body) {
    return sharingAction;
  }

  sharingAction = this._WebDetective
                      .getString(CLASS_SHORT_NAME,
                                 "shareAction_directMessage", "")
                      .replace("%friendid%", coopFriend.accountId)
                      .replace("%message%", encodeURIComponent(message.body));

  this._logger.debug(".getSharingAction(): " + sharingAction);
  return sharingAction;
}


flockTwitterAccount.prototype.getPostLinkAction =
function fta_getPostLinkAction(aTransferable) {
  var postLinkAction = "";
  var url = "";

  if (aTransferable) {
    // Something was dropped onto the "Post Link" button: get the URL from the
    // transferable
    var flavours = ["text/x-flock-media",
                    "text/x-moz-url",
                    "text/unicode"];

    var message = CC[FLOCK_RICH_DND_CONTRACTID]
                  .getService(CI.flockIRichDNDService)
                  .getMessageFromTransferable(aTransferable,
                                              flavours.length,
                                              flavours);

    url = message.body;
  } else {
    // The "Post Link" button was clicked: get the current tab's URL
    var win = CC["@mozilla.org/appshell/window-mediator;1"]
              .getService(CI.nsIWindowMediator)
              .getMostRecentWindow("navigator:browser");
    if (win) {
      url = win.gBrowser.currentURI.spec;
    }
  }

  if (url) {
    postLinkAction = this._WebDetective
                         .getString(CLASS_SHORT_NAME,
                                    "shareAction_publicMessage",
                                    "")
                         .replace("%message%", encodeURIComponent(url));
  }

  return postLinkAction;
}


/*************************************************************************
 * flockTwitterAccount Private Data and Functions
 *************************************************************************/


/*************************************************************************
 * XPCOM Support - Module Construction
 *************************************************************************/

// Create array of components.
var componentsArray = [flockTwitterService];

// Generate a module for XPCOM to find.
var NSGetModule = FlockXPCOMUtils.generateNSGetModule(MODULE_NAME,
                                                      componentsArray);
