// 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:///modules/FlockXMLUtils.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 FLOCK_ERROR_CONTRACTID = "@flock.com/error;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, "isHidden");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getAuthenticatedAccount");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getAccounts");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "removeAccount");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "logout");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getStringBundle");

  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.flockIManageableWebService
               .addDefaultMethod(this, "getSessionValue");

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

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

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

  profiler.profileEventEnd(evtID, "");
};


flockTwitterService.prototype._lightPeopleIcon =
function fts__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, aPollingListener) {
  var inst = this;
  var refreshItem = this._coop.get(aUrn);

  var pplRefreshListener = CC["@flock.com/people-refresh-listener;1"]
                           .createInstance(CI.flockIPeopleRefreshListener);
  pplRefreshListener.init(3, aUrn, aPollingListener, SHORT_INTERVAL);

  /**
   * {
   *   "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 userShowApiListener = {
    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;
        var lastStatusMessageUpdateDate = inst._parseTwitterDate(dateString);
        refreshItem.lastProfileUpdate = lastStatusMessageUpdateDate;
        refreshItem.lastStatusMessageUpdateDate = lastStatusMessageUpdateDate;
      }

      pplRefreshListener.onSuccess(null, "userShowApiListener");
    },
    onError: function userShow_onError(aError) {
      inst._logger.error("userShowApiListener onError");
      pplRefreshListener.onError(null, "userShowApiListener", aError);
    }
  };

  // This listener handles getting the user's friends' information.
  var friendsApiListener = {
    onSuccess: function friends_onSuccess(aResult) {
      inst._logger.debug("friendsApiListener 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();
          }
        }
        pplRefreshListener.onSuccess(null, "friendsApiListener");
      }
      FlockScheduler.schedule(null, 0.05, 10, myWorker);
    },
    onError: function friends_onError(aError) {
      inst._logger.error("friendsApiListener onError");
      pplRefreshListener.onError(null, "friendsApiListener", aError);
    }
  };

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

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

      pplRefreshListener.onSuccess(null, "messageCountApiListener");
    },
    onError: function messages_onError(aError) {
      inst._logger.error("messageCountApiListener onError");
      pplRefreshListener.onError(null, "messageCountApiListener", aError);
    }
  };

  gApi.userShow(refreshItem.accountId, userShowApiListener);
  gApi.getFriendsStatus(null, friendsApiListener);
  gApi.getTotalMessageCount(messageCountApiListener);
};


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

  var profileURL = this._WebDetective
                       .getString(CLASS_SHORT_NAME, "userprofile", null)
                       .replace("%accountid%", aPerson.screen_name);

  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;
      identity.URL = profileURL;
    }
  } 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,
        URL: profileURL
      }
    );
    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#addAccount
 */
flockTwitterService.prototype.addAccount =
function fts_addAccount(aAccountId, aIsTransient, aListener) {
  this._logger.debug(".addAccount('"
                     + aAccountId + "', " + aIsTransient + ", aListener)");

  if (!aAccountId) {
    if (aListener) {
      var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
      // XXX See bug 10749 - flockIError cleanup
      error.errorCode = 9999;
      error.errorString = "No Account ID provided";
      aListener.onError(this, "addAccount", error);
    }
    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;
};

// DEFAULT: flockIWebServiceAccount getAccount(in AString aUrn);
// DEFAULT: nsISimpleEnumerator getAccounts();
// DEFAULT: void removeAccount(in AString aUrn);
// DEFAULT: void logout();


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

// readonly attribute AString domains;
flockTwitterService.prototype.__defineGetter__("domains",
function fts_getdomains() {
  this._logger.debug("Getting property: domains");
  return this._c_svc.domains;
});

// readonly attribute AString loginURL;
flockTwitterService.prototype.__defineGetter__("loginURL",
function fts_getloginURL() {
  this._logger.debug("Getting property: loginURL");
  return this._c_svc.loginURL;
});

// 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, aAcctURN) {
  this._logger.debug("updateAccountStatusFromDocument('" + aAcctURN + "')");
  if (aAcctURN) {
    this._logger.debug("We're logged in!");
    this._acUtils.ensureOnlyAuthenticatedAccount(aAcctURN);
  } else 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 {
    this._logger.debug("We don't match 'loggedout' or 'logged in'");
  }
};

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

// void markAllMediaSeen(in AString aPersonUrn);
flockTwitterService.prototype.markAllMediaSeen =
function fts_markAllMediaSeen(aPersonUrn) {
  // Twitter has no media, so this does nothing and shouldn't be called.
  throw(NS_ERROR_NOT_IMPLEMENTED);
};

flockTwitterService.prototype.maxStatusLength = TWITTER_MAX_STATUS_LENGTH;


/*************************************************************************
 * flockTwitterService: flockIPollingService Implementation
 *************************************************************************/
/**
 * @see flockIPollingService#refresh
 */
flockTwitterService.prototype.refresh =
function fts_refresh(aUrn, aPollingListener) {
  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, aPollingListener);
    } else {
      // If the user is not logged in, return a success without
      // refreshing anything
      aPollingListener.onResult();
    }
  } else {
    this._logger.error("can't refresh " + aUrn + " (unsupported type)");
    aPollingListener.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 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: TwitterAPI
 *************************************************************************/
function TwitterAPI() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("twitterAPI");

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

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


/*************************************************************************
 * TwitterAPI: 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().
TwitterAPI.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " API",
  "",
  "",
  TwitterAPI,
  0,
  []
  );


/*************************************************************************
 * TwitterAPI: Private data and functions
 *************************************************************************/
/**
 * Call the specified Twitter API method.
 * @param  aApiListener
 *    An object (twitterApiListener) with the following two methods:
 *      void function onSuccess(in object aResult)
 *      void function onError(in flockIError aError)
 * @param  aMethod
 *    One of the API methods; "users/show/", etc.
 * @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.
 */
TwitterAPI.prototype._call =
function api__call(aApiListener, aMethod, aParams, aRequestType, aPostVars,
                   aCount)
{
  var requestType = aRequestType.toUpperCase();
  var responseFormat = ".json";
  var url = "https://twitter.com/" + aMethod + responseFormat;
  var error = {};

  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) {
            aApiListener.onSuccess(result);
          } else {
            inst._logger.debug("._call() error: no result");
            error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
            // XXX See bug 10749 - flockIError cleanup
            error.errorCode = CI.flockIError.INVALID_RESPONSE_XML;
            error.errorString = "responseText did not produce an object";
            aApiListener.onError(error);
          }

        } 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) {
            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(aApiListener, aMethod, aParams, aRequestType, aPostVars,
                       count);
          } else {
            // Ok. Give up.
            inst._logger.debug("._call() HTTP error");
            error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
            error.serviceErrorCode = req.status;
            error.serviceErrorString = req.responseText;
            aApiListener.onError(error);
          }

        } else {
          // HTTP errors
          inst._logger.debug("._call() HTTP error");
          error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
          error.serviceErrorCode = req.status;
          error.serviceErrorString = req.responseText;
          aApiListener.onError(error);
        }
      } catch (ex) {
        // XMLHTTPERROR (connection lost)
        inst._logger.debug("._call() exception: " + ex);
        error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
        error.serviceErrorCode = CI.flockIError.XML_HTTP_REQUEST_ERROR;
        error.serviceErrorString = "connection lost";
        aApiListener.onError(error);
      }
    }
  };

  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  aApiListener
 */
TwitterAPI.prototype.userShow =
function api_userShow(aUid, aApiListener) {
  this._logger.debug(".userShow(" + aUid + ", aApiListener)");

  var url = "users/show/" + aUid;
  gApi._call(aApiListener, 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
 */
TwitterAPI.prototype.directMessages =
function api_directMessages(aSince, aSinceId, aPage, aApiListener) {
  this._logger.debug(".directMessages(" + aSince + ", "
                                        + aSinceId + ", "
                                        + aPage + ", "
                                        + "aApiListener)");
  var params = {};

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

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

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

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


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

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

  // Message counter
  var count = 0;

  var inst = this;
  var messageLoopApiListener = {
    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, messageLoopApiListener);
      } else {
        inst._logger.debug("messageLoopApiListener() Fetching messages done");
        result = { count: count };
        aApiListener.onSuccess(result);
      }
    },
    onError: function L_onError(aError) {
      inst._logger.debug("messageLoopApiListener() onError");
      aApiListener.onError(aError);
    }
  };

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


/**
 * 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  aApiListener
 */
TwitterAPI.prototype.getFriendsStatus =
function api_getFriendsStatus(aUid, aApiListener) {
  this._logger.debug(".getFriendsStatus(" + aUid + ", aApiListener)");

  var peopleHash = {};
  var inst = this;

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

  var url = "statuses/friends";
  var friendsLoopApiListener = {
    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(friendsLoopApiListener, url, params, "GET", null, null);
      } else {
        inst._logger.debug("friendsLoopApiListener() Fetching friends done");
        aApiListener.onSuccess(peopleHash);
      }
    },
    onError: function L_onError(aError) {
      inst._logger.debug("friendsLoopApiListener() onError");
      aApiListener.onError(aError);
    }
  };

  if (aUid) {
    url += "/" + aUid;
  }
  gApi._call(friendsLoopApiListener, url, null, "GET", null, null);
};


/**
 * Set the user's status
 * @param  aStatusMessage  a string containing the message to set
 * @param  aApiListener
 *
 * 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
 */
TwitterAPI.prototype.setStatus =
function api_setStatus(aStatusMessage, aApiListener) {
  this._logger.debug(".setStatus(" + aStatusMessage + ", aApiListener)");

  // 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(aApiListener, 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);

  var wsa = FlockSvcUtils.flockIWebServiceAccount;
  wsa.addDefaultMethod(this, "deactivate");
  wsa.addDefaultMethod(this, "getService");
  wsa.addDefaultMethod(this, "getParam");
  wsa.addDefaultMethod(this, "setParam");
  wsa.addDefaultMethod(this, "getCustomParam");
  wsa.addDefaultMethod(this, "getAllCustomParams");
  wsa.addDefaultMethod(this, "setCustomParam");
  wsa.addDefaultMethod(this, "isAuthenticated");

  FlockSvcUtils.flockISocialWebServiceAccount
               .addDefaultMethod(this, "getFriendCount");
  FlockSvcUtils.flockISocialWebServiceAccount
               .addDefaultMethod(this, "enumerateFriends");
  FlockSvcUtils.flockISocialWebServiceAccount
               .addDefaultMethod(this, "login");
  FlockSvcUtils.flockISocialWebServiceAccount
               .addDefaultMethod(this, "logout");

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


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

flockTwitterAccount.prototype.formatFriendActivityForDisplay =
function flockTwitterAccount_formatFriendActivityForDisplay(aFriendActivityUrn)
{
  this._logger.info(".formatFriendActivityForDisplay()");

  var friendActivity = this._coop.get(aFriendActivityUrn);

  if (friendActivity.updateType == "profile") {
    return this._bundle
               .GetStringFromName("flock.friendFeed.lastUpdateTypePretty.profile");
  } else if (friendActivity.updateType == "status") {
    if (friendActivity.updateValue == "") {
      return this._bundle
                 .GetStringFromName("flock.friendFeed.lastUpdateTypePretty.statusCleared");
    } else {
      return this._bundle
                 .GetStringFromName("flock.friendFeed.lastUpdateTypePretty.statusUpdated.prefix")
                                    + " " + friendActivity.updateValue + ".";
    }
  }

  return "";
}

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

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

  // Responses from Twitter contain HTML entities.
  message = flockXMLDecode(message);

  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 fta_setStatus(aStatusMessage, aListener) {
  this._logger.debug(".setStatus(" + aStatusMessage + ")");

  var inst = this;
  var statusApiListener = {
    onSuccess: function L_onSuccess(aResult) {
      // If the API call succeeded, also set the coop.Account status.
      inst._coop.get(inst.urn).statusMessage = aStatusMessage;

      // Save the current time in seconds
      var now = new Date().getTime();
      now = Math.round(now / 1000);
      inst._coop.get(inst.urn).lastStatusMessageUpdateDate = now;

      if (aListener) {
        aListener.onSuccess(aResult, "setStatus");
      }
    },
    onError: function L_onError(aError) {
      if (aListener) {
        aListener.onError(null, "setStatus", aError);
      }
    }
  };
  gApi.setStatus(aStatusMessage, statusApiListener);
};

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

// AString getMeNotifications();
flockTwitterAccount.prototype.getMeNotifications =
function fta_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);
};

// void markAllMeNotificationsSeen(in AString aType);
flockTwitterAccount.prototype.markAllMeNotificationsSeen =
function fta_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 fta_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);
};

// DEFAULT: long getFriendCount();

// AString getSharingAction(in AString aFriendUrn,
//                          in nsITransferable aTransferable);
flockTwitterAccount.prototype.getSharingAction =
function fta_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;
};

// AString getPostLinkAction(in nsITransferable aTransferable);
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);
