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

// Developer ID = 1aiV9CsTs3o
// Developer Secret = flock_is_open_source

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

CU.import("resource:///modules/FlockScheduler.jsm");
CU.import("resource:///modules/FlockSvcUtils.jsm");
CU.import("resource:///modules/FlockXPCOMUtils.jsm");
FlockXPCOMUtils.debug = false;
CU.import("resource:///modules/FlockGoogleAuth.jsm");
CU.import("resource:///modules/FlockMediaUtils.jsm");
CU.import("resource://gre/modules/ISO8601DateUtils.jsm");

const YT_DEVID = "1aiV9CsTs3o";
const YT_DEVSECRET = "flock_is_open_source";

const API_CLASS_NAME = "Flock Youtube API";
const API_CLASS_ID = Components.ID("{524f2f40-47a5-11dd-ae16-0800200c9a66}");
const API_CONTRACT_ID = "@flock.com/webservice/api/youtube;1";

const MODULE_NAME = "Youtube";
const CLASS_ID = Components.ID("{DCB6A01E-7D4A-4C30-AB3D-9CC98C02F617}");
const CONTRACT_ID = "@flock.com/?photo-api-youtube;1";
const CLASS_NAME = "YouTube";
const CLASS_SHORT_NAME = "youtube";

const YOUTUBE_FAVICON = "chrome://flock/content/services/youtube/favicon.png";
const SERVICE_ENABLED_PREF = "flock.service.youtube.enabled";
const FRIENDS_AVATAR_TIMEOUT = 5000;  // in milliseconds
const FRIENDS_ACTIVITY_TIMEOUT = 500;  // in milliseconds

const GPHOTO_NAMESPACE = "http://search.yahoo.com/mrss/";
const GDATA_NAMESPACE = "http://schemas.google.com/g/2005";
const YT_NAMESPACE = "http://gdata.youtube.com/schemas/2007";
const OPENSEARCH_NAMESPACE = "http://a9.com/-/spec/opensearchrss/1.0/"
const FAV_CHANNELS_ROOT = "urn:media:favorites";

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

const YOUTUBE_IDENTITY_URN_PREFIX = "urn:flock:identity:youtube:";
const PEOPLE_PROPERTIES = "chrome://flock/locale/people/people.properties";
const YOUTUBE_STRING_BUNDLE = "chrome://flock/locale/services/youtube.properties";
const LOGIN_URL = "https://www.google.com/youtube/accounts/ClientLogin";
const YOUTUBE_API_GET_USER_INFO_URL = "http://gdata.youtube.com/feeds/api/users/";

// From nsIXMLHttpRequest.idl
// 4: COMPLETED     Finished with all operations.
const XMLHTTPREQUEST_READYSTATE_COMPLETED = 4;

const HTTP_CODE_OK = 200;

// The delay between two refreshes when the sidebar is closed (in seconds)
const YOUTUBE_REFRESH_INTERVAL = 3600; // 60 minutes
const YAHOO_PHOTO_MAX_RESULT = 25;

var gYoutubeAPI = null;

// String defaults... may be updated later through Web Detective
var gStrings = {
  "domains": "youtube.com",
  "userprofile": "http://www.youtube.com/profile?user=%accountid%",
  "editprofile": "http://youtube.com/my_profile",
  "inbox": "http://youtube.com/my_messages"
};

var flockMediaItemFormatter = {
  canBuildTooltip: true,
  canBuildHTML: true,
  canBuildLargeHTML: true,
  canBuildBBCode: true,
  canBuildMiniPage: true,

  buildTooltip: function fmif_buildTooltip(aMediaItem) {

    default xml namespace =
      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

    var extras = <vbox anonid="ttUserTxt" />;
    if (aMediaItem.length_seconds) {
      extras.appendChild(<label>Length: {aMediaItem.length_seconds} seconds</label>);
    }
    if (aMediaItem.rating_avg) {
      extras.appendChild(<label>Rating: {aMediaItem.rating_avg}
                         ({aMediaItem.rating_count} times)</label>);
    }
    if (aMediaItem.view_count) {
      extras.appendChild(<label>Views: {aMediaItem.view_count}</label>);
    }

    extras.appendChild(<label class="user">{aMediaItem.username}</label>);

    var xml =
      <vbox>
        <hbox>
          <image src={aMediaItem.midSizePhoto} style="margin-bottom: 2px;" />
          <spacer flex="1" />
        </hbox>
        <vbox anonid="ttInfoContainer">
          <label anonid="ttTitleTxt">{aMediaItem.title}</label>
          {extras}
        </vbox>
      </vbox>;

    return xml;
  },

  buildHTML: function fmif_buildHTML(aMediaItem) {
    var flashUrl = aMediaItem.webPageUrl.replace(/\/\?v\=/,"/v/");
    var html = '<object width="425" height="350">'
         + '<param name="movie" value="'
         + flashUrl
         + '"/>'
         + '<embed src="'
         + flashUrl
         + '" type="application/x-shockwave-flash" width="425" height="350"/>'
         + aMediaItem.webPageUrl
         + '</object>';
    return html;
  },

  buildLargeHTML: function fmif_buildLargeHTML(aMediaItem) {
    return this.buildHTML(aMediaItem);
  },

  buildBBCode: function fmif_buildBBCode(aMediaItem) {
    var flashUrl = aMediaItem.webPageUrl.replace(/\/\?v\=/,"/watch?v=");
    var bbCode = "[youtube]" + flashUrl + "[/youtube]";
    return bbCode;
  },

  buildMiniPage: function fmif_buildMiniPage(aMediaItem) {
    var flashUrl = aMediaItem.webPageUrl.replace(/\/\?v\=/,"/v/");
    var html = '<html><head><title>'
             + aMediaItem.title
             + ' (' + aMediaItem.username + ')</title></head>'
             + '<body><center><object width="425" height="350">'
             + '<param name="movie" value="'
             + flashUrl
             + '"/>'
             + '<embed src="'
             + flashUrl
             + '&autoplay=1" type="application/x-shockwave-flash"'
             + ' width="425" height="350"/></object></center></body></html>';
    return html;
  }
};


/*************************************************************************
 * Component: YoutubeService
 *************************************************************************/

function YoutubeService() {
  FlockSvcUtils.getLogger(this);

  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "isHidden");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getStringBundle");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "url");

  FlockSvcUtils.flockILoginWebService.addDefaultMethod(this, "loginURL");
  FlockSvcUtils.flockILoginWebService.addDefaultMethod(this, "tooltip");
  FlockSvcUtils.flockILoginWebService.addDefaultMethod(this, "getAccount");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getAuthenticatedAccount");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getAccountUrns");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "removeAccount");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getAccountIDFromDocument");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getCredentialsFromForm");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "ownsDocument");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getSessionValue");
  FlockSvcUtils.flockIMediaWebService
               .addDefaultMethod(this, "getChannel");
  FlockSvcUtils.flockIMediaWebService
               .addDefaultMethod(this, "enumerateChannels");
  FlockSvcUtils.flockIMediaWebService
               .addDefaultMethod(this, "getIconForQuery");

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

  FlockSvcUtils.flockIMediaEmbedWebService
               .addDefaultMethod(this, "getSharingContent");

  var sws = FlockSvcUtils.flockISocialWebService;
  sws.addDefaultMethod(this, "parseStatus");

  this._obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);

  this._ppSvc = CC["@flock.com/people-service;1"]
                .getService(CI.flockIPeopleService);
  this._currentQueryExhausted = false;
  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(YOUTUBE_STRING_BUNDLE);

  this._channels = {
    "special:added": {
      title: bundle.GetStringFromName("flock.youtube.title.added"),
      supportsSearch: false,
      feed: "http://youtube.com/rss/global/recently_added.rss"
    },
    "special:featured": {
      title: bundle.GetStringFromName("flock.youtube.title.featured"),
      supportsSearch: false
    },
    "special:top_favorites": {
      title: bundle.GetStringFromName("flock.youtube.title.topfav"),
      supportsSearch: false,
      feed: "http://youtube.com/rss/global/top_favorites.rss"
    },
    "special:rated": {
      title: bundle.GetStringFromName("flock.youtube.title.rated"),
      supportsSearch: false,
      feed: "http://youtube.com/rss/global/top_rated.rss"
    }
  };

  var prefs = CC["@mozilla.org/preferences-service;1"]
              .getService(CI.nsIPrefBranch);
  if (prefs.getPrefType(SERVICE_ENABLED_PREF) &&
     !prefs.getBoolPref(SERVICE_ENABLED_PREF))
  {
    this._logger.debug(SERVICE_ENABLED_PREF + " is FALSE! Not initializing.");
    this.deleteCategories();
    return;
  }

  this._accountClassCtor = YoutubeAccount;

  // getWD() also adds the "_acUtils" property.
  FlockSvcUtils.getWD(this);
  for (var s in gStrings) {
    gStrings[s] = this._WebDetective.getString("youtube", s, gStrings[s]);
  }
  FlockSvcUtils.initService(this);
  if (!gYoutubeAPI) {
    gYoutubeAPI = new YoutubeAPI();
  }
  this.api = gYoutubeAPI;
  this.api.svc = this;

  this._updateFriendActivityComplete = false;
  this._friendActivityArray = [];

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

  var query = {serviceId: CONTRACT_ID, isAuthenticated: true};
  var authenticatedUrns = this._coop.Account.find(query);
  if (authenticatedUrns.length != 0) {
    this.api.setAuthAccount(this._coop.Account.get(authenticatedUrns[0]));
  }
}

YoutubeService.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME,
  CLASS_ID,
  CONTRACT_ID,
  YoutubeService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.flockILoginWebService,
    CI.flockIMediaEmbedWebService,
    CI.flockIMediaWebService,
    CI.flockIPollingService,
    CI.flockIRichContentDropHandler,
    CI.flockISocialWebService,
    CI.flockIWebService,
    CI.flockIYoutubeService
  ]
);

YoutubeService.prototype._xpcom_categories = [
  { category: "wsm-startup" },
  { category: "flockWebService", entry: CLASS_SHORT_NAME },
  { category: "flockMediaProvider", entry: CLASS_SHORT_NAME },
  { category: "flockRichContentHandler", entry: CLASS_SHORT_NAME }  
];

/**************************************************************************
 * YoutubeService: flockILoginWebService Implementation
 **************************************************************************/

// readonly attribute AString domains;
YoutubeService.prototype.__defineGetter__("domains",
function YoutubeService_getdomains() {
  this._logger.debug("Getting property: domains");
  return gStrings["domains"];
});

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

/**
 * flockIWebServiceAccount addAccount(in AString aAccountId,
 *                                    in boolean aIsTransient,
 *                                    in flockIListener aFlockListener);
 * @see flockILoginWebService#addAccount
 */
YoutubeService.prototype.addAccount =
function YoutubeService_addAccount(aAccountId, aIsTransient, aFlockListener) {
  this._logger.debug(".addAccount('" + aAccountId + "', "
                                     + aIsTransient + ", ...)");

  var url = gStrings["userprofile"].replace("%accountid%", aAccountId);
  var accountURN = this._acUtils.createAccount(this,
                                               aAccountId,
                                               aAccountId,
                                               url,
                                               aIsTransient);
  var account = this.getAccount(accountURN);
  account.setParam("service", this._coopObj);

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

  // Instantiate account component
  account._addMediaToFavorites(aAccountId);
  if (aFlockListener) {
    aFlockListener.onSuccess(account, "addAccount");
  }
  return account;
};

YoutubeService.prototype.ownsLoginForm =
function YoutubeService_ownsLoginForm(aForm) {
  this._logger.debug(".ownsLoginForm()");
  aForm.QueryInterface(CI.nsIDOMHTMLFormElement);
  if (this._WebDetective.detectForm(this.shortName,
                                    "login",
                                    aForm,
                                    FlockSvcUtils.newResults()))
  {
    return true;
  }
  if (this._WebDetective.detectForm(this.shortName,
                                    "signup",
                                    aForm,
                                    FlockSvcUtils.newResults()))
  {
    return true;
  }
  if (this._WebDetective.detectForm(this.shortName,
                                    "changepassword",
                                    aForm,
                                    FlockSvcUtils.newResults()))
  {
    return true;
  }
  return false;
};

// DEFAULT: boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument aDocument);
// DEFAULT: AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: nsIStringEnumerator getAccountUrns();
// DEFAULT: flockIWebServiceAccount getAccount(in AString aUrn);
// DEFAULT: flockIWebServiceAccount getAuthenticatedAccount();
// DEFAULT: flockILoginInfo getCredentialsFromForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: AString getSessionValue();

// DEFAULT: boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: void removeAccount(in AString aUrn);

YoutubeService.prototype.logout =
function YoutubeService_logout() {
  this._logger.debug(".logout()");
  this.api.deauthenticate();
  this._acUtils.markAllAccountsAsLoggedOut(this);
  this._acUtils.removeCookies(this._WebDetective.getSessionCookies("youtube"));
}

/**
 * void updateAccountStatusFromDocument(in nsIDOMHTMLDocument aDocument,
 *                                      flockIWebServiceAccount aAccount,
 *                                      in flockIListener aFlockListener);
 * @see flockILoginWebService#updateAccountStatusFromDocument
 */
YoutubeService.prototype.updateAccountStatusFromDocument =
function YoutubeService_updateAccountStatusFromDocument(aDocument,
                                                        aAccount,
                                                        aFlockListener)
{
  if (aAccount) {
    // We're logged in to this account
    if (!aAccount.isAuthenticated()) {
      aAccount.login(aFlockListener);
    }
  } else if (this._WebDetective
                 .detect("youtube", "loggedout", aDocument, null))
  {
    // We're logged out (of all accounts)
    this.logout();
  }
};


/**************************************************************************
 * YoutubeService: flockIMediaEmbedWebService Implementation
 **************************************************************************/

/**
 * boolean checkIsStreamUrl(in AString aUrl);
 * @see flockIMediaEmbedWebService#checkIsStreamUrl
 */
YoutubeService.prototype.checkIsStreamUrl =
function YoutubeService_checkIsStreamUrl(aUrl) {
  if (this._WebDetective.detectNoDOM("youtube", "isStreamUrl", "", aUrl, null)) {
    this._logger.debug("Checking if url is youtube stream: YES: " + aUrl);
    return true;
  }
  this._logger.debug("Checking if url is youtube stream: NO: " + aUrl);
  return false;
};

/**
 * void getMediaQueryFromURL(in AString aUrl, in flockIListener aFlockListener);
 * @see flockIMediaEmbedWebService#getMediaQueryFromURL
 */
YoutubeService.prototype.getMediaQueryFromURL =
function youTubeService_getMediaQueryFromURL(aUrl, aFlockListener) {
  var videoId = this.getVideoIDFromUrl(aUrl);
  if (videoId) {
    var inst = this;
    var myListener = {
      onResult: function getMediaQueryFromURL_onResult(aXML) {
        var userID = aXML.getElementsByTagName('author')[0].firstChild.nodeValue;
        var results = CC["@mozilla.org/hash-property-bag;1"]
                      .createInstance(CI.nsIWritablePropertyBag2);
        results.setPropertyAsAString("query", "user:" + userID + "|username:" + userID);
        // Note: The "title" property is currently not used by the media bar.
        results.setPropertyAsAString("title", userID);
        aFlockListener.onSuccess(results, "query");
      },
      onError: function getMediaQueryFromURL_onError(aFlockError) {
        inst._logger.error(".getMediaQueryFromURL() myListener.onError()");
        aFlockListener.onError(aFlockError, null);
      }
    };
    var params = {};
    params.video_id = videoId;
    this.call(myListener, "youtube.videos.get_details", params);
  } else {
    this._logger.error(".getMediaQueryFromURL() Unable to get user.");
    aFlockListener.onError(null, "Unable to get user.");
  }
};

// DEFAULT: boolean getSharingContent(in nsIDOMHTMLElement aSrc,
//                                    in nsIWritablePropertyBag2 aProp);


/**************************************************************************
 * YoutubeService: flockIMediaWebService Implementation
 **************************************************************************/

// readonly attribute boolean supportsUsers;
YoutubeService.prototype.supportsUsers = true;

// DEFAULT: flockIMediaChannel getChannel(in AString aChannelId);

/**
 * void decorateForMedia(in nsIDOMHTMLDocument aDocument,
 *                       in flockIWebServiceAccount aAccount);
 * @see flockIMediaWebService#decorateForMedia
 */
YoutubeService.prototype.decorateForMedia =
function YoutubeService_decorateForMedia(aDocument, aAccount) {
  aDocument.QueryInterface(Components.interfaces.nsIDOMHTMLDocument);
  this._logger.debug(".decorateForMedia('" + aDocument.URL + "')");
  var results = FlockSvcUtils.newResults();
  if (this._WebDetective.detect("youtube", "media", aDocument, results)) {
    var sbs = CC["@mozilla.org/intl/stringbundle;1"]
              .getService(CI.nsIStringBundleService);
    var bundle = sbs.createBundle(YOUTUBE_STRING_BUNDLE);

    var mediaArr = [];
    var userid = results.getPropertyAsAString("userid");

    // media item for user videos
    var vidname = bundle.formatStringFromName("flock.youtube.videosUC",
                                              [userid], 1);
    var media = {
      name: userid,
      query: "user:" + userid + "|username:" + userid,
      label: vidname,
      favicon: this.icon,
      service: this.shortName
    };
    mediaArr.push(media);

    // media item for user favorites
    var favname = bundle.formatStringFromName("flock.youtube.favoritesUC",
                                              [userid], 1);
    media = {
      name: userid,
      query: "favorites:" + userid,
      label: favname,
      favicon: this.icon,
      service: this.shortName
    };
    mediaArr.push(media);

    if (!aDocument._flock_decorations) {
      aDocument._flock_decorations = {};
    }

    if (aDocument._flock_decorations.mediaArr) {
      aDocument._flock_decorations.mediaArr =
        aDocument._flock_decorations.mediaArr.concat(mediaArr);
    } else {
      aDocument._flock_decorations.mediaArr = mediaArr;
    }

    this._obs.notifyObservers(aDocument, "media", "media:update");
  }
};

// DEFAULT: nsISimpleEnumerator enumerateChannels();

/**
 * void findByUsername(in flockIListener aFlockListener,
 *                     in AString aUsername);
 * @see flockIMediaWebService#findByUsername
 */
YoutubeService.prototype.findByUsername =
function YoutubeService_findByUsername(aFlockListener, aUsername) {
  this._logger.debug("findByUsername ");
  var inst = this;
  var isUserListener = {
    onResult: function isUser_onResult(aXML) {
      var newUserObj = {};
      newUserObj.serviceId = this.contractID;
      newUserObj.accountId = aUsername;
      newUserObj.name = aUsername;
      newUserObj.totalMedia = -1; // See mediabar.js
      var wrappedUser = {
        wrappedJSObject: { newUserObj: newUserObj }
      };
      aFlockListener.onSuccess(wrappedUser, "findByUsername");
    },
    onError: function isUser_onError(aFlockError) {
      inst._logger.error(".getMediaQueryFromURL() Unable to get user.");
      aFlockListener.onError(aFlockError, aUsername);
    }
  };
  this.api.doCall(isUserListener,
                  YOUTUBE_API_GET_USER_INFO_URL + aUsername,
                  null);
};

/**
 * void getAlbums(in flockIListener aFlockListener,
 *                in AString aUsername);
 * @see flockIMediaWebService#getAlbums
 */
YoutubeService.prototype.getAlbums =
function YoutubeService_getAlbums(aFlockListener, aUsername) { 
  aFlockListener.onError(null, null);
};

/**
 * flockIMediaItemFormatter getMediaItemFormatter();
 * @see flockIMediaWebService#getMediaItemFormatter
 */
YoutubeService.prototype.getMediaItemFormatter =
function YoutubeService_getMediaItemFormatter() {
  return flockMediaItemFormatter;
};

/**
 * void migrateAccount(in AString aId, in AString aUsername);
 * @see flockIMediaWebService#migrateAccount
 */
YoutubeService.prototype.migrateAccount =
function YoutubeService_migrateAccount(aId, aUsername) {
  this.addAccount(aId, false, null);
};

/**
 * void search(in flockIListener aFlockListener,
 *             in AString aQuery,
 *             in long aCount,
 *             in long aPage,
 *             in AString aRequestId);
 * @see flockIMediaWebService#search
 */
YoutubeService.prototype.search =
function YoutubeService_search(aFlockListener,
                               aQueryString,
                               aCount,
                               aPage,
                               aRequestId)
{
  var aQuery = new QueryHelper(aQueryString);
  if (this._currentQueryExhausted) {
    // No need to search as all pages for this query have been returned
    if (aPage == 1) {
      this._currentQueryExhausted = false;
    } else {
      return;
    }
  }
  if (aQuery.favorites && !aQuery.user) {
    aQuery.user = aQuery.favorites;
  }
  if (!aQuery.user && aQuery.special != "favorites") {
    this._queryChannel(aFlockListener, aQueryString, aPage, aRequestId);
    return;
  }

  var aUserid = aQuery.user;
  var params = {};
  params["start-index"] =
    (aPage == 1) ? aPage : (aPage - 1) * YAHOO_PHOTO_MAX_RESULT + 1;
  params["max-results"] = YAHOO_PHOTO_MAX_RESULT;

  var inst = this;
  var myListener = {
    onResult: function search_onResult(aXML) {
      inst._logger.debug(".search() myListener.onResult()");
      var rval = inst._handlePhotosResult(aXML, aUserid);
      if (rval.length < YAHOO_PHOTO_MAX_RESULT) {
        // All results for the current query have been returned 
        inst._currentQueryExhausted = true;
      }

      var resultsEnum = {
        hasMoreElements: function resultsEnum_hasMoreElements() {
          return (rval.length > 0);
        },
        getNext: function resultsEnum_getNext() {
          return rval.shift();
        },
        QueryInterface: function resultsEnum_QueryInterface(aIID) {
          if (aIID.equals(CI.nsISimpleEnumerator)) {
            return this;
          }
          throw CR.NS_ERROR_NO_INTERFACE;
        }
      };
      aFlockListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function search_onError(aFlockError) {
      inst._logger.error(".search() myListener.onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };
  var link = "http://gdata.youtube.com/feeds/api/users/"
           + aUserid + (aQuery.favorites ? "/favorites" : "/uploads");
  this.api.doCall(myListener, link, params);
};

/**
 * boolean supportsSearch(in AString aQuery);
 * @see flockIMediaWebService#supportsSearch
 */
YoutubeService.prototype.supportsSearch =
function YoutubeService_supportsSearch(aQueryString) {
  var aQuery = new QueryHelper(aQueryString);

  if (aQuery.special) {
    var channel = this._channels["special:" + aQuery.special];
    if (channel) {
      return channel.supportsSearch;
    }
  }

  // None of the other apis/feeds support search
  return false;
};

YoutubeService.prototype._getFeed =
function YoutubeService__getFeed(aFlockListener, aFeedURL, aRequestId) {
  var ios = CC["@mozilla.org/network/io-service;1"].getService(CI.nsIIOService);
  var uri = ios.newURI(aFeedURL, null, null);

  var inst = this;
  var feedListener = {
    onGetFeedComplete: function oncomplete(feed) {
      inst._handleFeedResult(aFlockListener, feed, aRequestId);
    },
    onError: function onError(aFlockError) {
      inst._logger.error("._getFeed() feedListener.onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  var fm = Components.classes["@flock.com/feed-manager;1"]
    .getService(Components.interfaces.flockIFeedManager);
  fm.getFeedBypassCache(uri, feedListener, false);
};

YoutubeService.prototype._handleFeedResult =
function YoutubeService__handleFeedResult(aFlockListener, aFeed, aRequestId) {
  var photos = [];
  var nextIdx = 0;

  var items = aFeed.getItems();
  while (items && items.hasMoreElements()) {
    var item = items.getNext();

    var newMediaItem = CC["@flock.com/photo;1"]
                       .createInstance(CI.flockIMediaItem);
    newMediaItem.init(this.shortName, flockMediaItemFormatter);
    newMediaItem.has_miniView = "true";

    newMediaItem.title = item.getTitle();
    newMediaItem.webPageUrl = item.getLink().spec;

    newMediaItem.uploadDate = item.getPubDate() / 1000;
    // Undo the scaling applied in _handlePhotosResult() and ensure that the ID
    // is truly unique so tooltips work properly.
    newMediaItem.id = "" + (newMediaItem.uploadDate / 1000) + nextIdx++;

    var author = item.getAuthor();
    newMediaItem.username = author;
    newMediaItem.userid = author;

    var desc = item.getContent();
    var re = /<img [^>]*src="([^"]*)"/i;
    var thumbnail = desc.match(re)[1];
    newMediaItem.thumbnail = thumbnail;
    newMediaItem.midSizePhoto = thumbnail;
    newMediaItem.largeSizePhoto = thumbnail;

    newMediaItem.is_public = "true";
    newMediaItem.is_video = true;

    photos.push(newMediaItem);
  }

  var resultsEnum = {
    hasMoreElements: function resultsEnum_hasMoreElements() {
      return (photos.length > 0);
    },
    getNext: function resultsEnum_getNext() {
      return photos.shift();
    },
    QueryInterface: function resultsEnum_QueryInterface(aIID) {
      if (aIID.equals(CI.nsISimpleEnumerator)) {
        return this;
      }
      throw CR.NS_ERROR_NO_INTERFACE;
    }
  };
  aFlockListener.onSuccess(resultsEnum, aRequestId);
};

YoutubeService.prototype._handlePhotosResult =
function YoutubeService__handlePhotoResult(aXML, aUserid) {
  var rval = [];
  var nextIdx = 0;
  var photoList = aXML.getElementsByTagName("entry");
  // Extract all photo information from photo XML result and add them to
  // flockMediaItem
  for (var i = 0; i < photoList.length; i++) {
    var photo = photoList[i];
    var newMediaItem = CC["@flock.com/photo;1"]
                       .createInstance(CI.flockIMediaItem);
    newMediaItem.webPageUrl = photo.getElementsByTagName("link")[0]
                                   .getAttribute("href");
    // If this media item is not valid, continue to next item
    if (this._WebDetective.detectNoDOM("youtube",
                                       "mediabarDud",
                                       "",
                                       newMediaItem.webPageUrl,
                                       null))
    {
      continue;
    }
    newMediaItem.init(this.shortName, flockMediaItemFormatter);
    newMediaItem.has_miniView = "true";
    var author = photo.getElementsByTagName("author")[0]
                      .firstChild.firstChild.nodeValue;
    newMediaItem.username = author;
    newMediaItem.userid = author;
    newMediaItem.title = photo.getElementsByTagName("title")[0]
                              .firstChild.nodeValue;
    newMediaItem.description
      = (photo.getElementsByTagName("content")[0].firstChild)
      ? photo.getElementsByTagName("content")[0].firstChild.nodeValue
      : "";
    var published = photo.getElementsByTagName("published")[0]
                         .firstChild.nodeValue;
    var date = ISO8601DateUtils.parse(published);
    newMediaItem.uploadDate = date.getTime();
    newMediaItem.id = "" + newMediaItem.uploadDate + nextIdx++;
    var video_id = photo.getElementsByTagName("id")[0]
                        .firstChild.nodeValue;
    video_id = this.getVideoIDFromUrl(video_id);
    newMediaItem.webPageUrl =
    this._WebDetective.getString("youtube", "youtubeMedia_URL", "")
        .replace("%VIDEO_ID%", video_id);
    var mediagroup = photo.getElementsByTagNameNS(GPHOTO_NAMESPACE, "group")[0];
    var duration =
      mediagroup.getElementsByTagNameNS(YT_NAMESPACE, "duration")[0]
                .getAttribute("seconds");
    newMediaItem.length_seconds = duration;
    var thumb =
      mediagroup.getElementsByTagNameNS(GPHOTO_NAMESPACE, "thumbnail")[0]
                .getAttribute("url");
    newMediaItem.thumbnail = thumb;
    newMediaItem.midSizePhoto = thumb;
    newMediaItem.largeSizePhoto = thumb;
    var comments = photo.getElementsByTagNameNS(GDATA_NAMESPACE, "comments")[0];
    newMediaItem.comment_count = comments
                               ? comments.firstChild.getAttribute("countHint")
                               : 0;
    var rating = photo.getElementsByTagNameNS(GDATA_NAMESPACE, "rating")[0];
    newMediaItem.rating_count = rating ? rating.getAttribute("average") : 0;
    var view = photo.getElementsByTagNameNS(YT_NAMESPACE, "statistics")[0];
    newMediaItem.view_count = view ? view.getAttribute("viewCount") : 0;
    newMediaItem.is_public = "true";
    newMediaItem.is_video = true;
    rval.push(newMediaItem);
  }
  return rval;
};

YoutubeService.prototype._queryChannel =
function YoutubeService__queryChannel(aFlockListener,
                                      aQueryString,
                                      aPage,
                                      aRequestId)
{
  var aQuery = new QueryHelper(aQueryString);
  // return; // XXX TODO FIXME: This is killing perf

  var inst = this;
  var myListener = {
    onResult: function _queryChannel_onResult(aXML) {
      var rval = inst._handlePhotosResult(aXML);
      var totalPhotosViewed = rval.length * aPage;
      if (rval.length < YAHOO_PHOTO_MAX_RESULT) {
        // All results for the current query have been returned
        inst._currentQueryExhausted = true;
      }

      var resultsEnum = {
        hasMoreElements: function resultsEnum_hasMoreElements() {
          return (rval.length > 0);
        },
        getNext: function resultsEnum_getNext() {
          return rval.shift();
        },
        QueryInterface: function resultsEnum_QueryInterface(aIID) {
          if (aIID.equals(CI.nsISimpleEnumerator)) {
            return this;
          }
          throw CR.NS_ERROR_NO_INTERFACE;
        }
      };
      aFlockListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function _queryChannel_onError(aFlockError) {
      inst._logger.error("._queryChannel() myListener.onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  var params = {};
  if (aQuery.search) {
    params.vq = aQuery.search;
    params.start_index =
      (aPage == 1) ? aPage : (aPage - 1) * YAHOO_PHOTO_MAX_RESULT + 1;
    params.per_page = YAHOO_PHOTO_MAX_RESULT;
    var link = "http://gdata.youtube.com/feeds/api/videos/";
    this.api.doCall(myListener, link, params);
  } else {
    // This API call doesn't support pagination and only shows most recent 25 items
    if (aPage > 1) {
      return;
    }
    var channel = this._channels[aQuery.stringVal];
    if (!channel) {
      return;
    }
    if (channel.feed) {
      this._getFeed(aFlockListener, channel.feed, aRequestId);
    } else {
      var link ="http://gdata.youtube.com/feeds/api/standardfeeds/"
               + "recently_featured";
      this.api.doCall(myListener, link, params);
    }
  }
};


/**************************************************************************
 * YoutubeService: flockIPollingService Implementation
 **************************************************************************/

/**
 * void refresh(in AString aURN, in flockIPollingListener aPollingListener);
 * @see flockIPollingService#refresh
 */
YoutubeService.prototype.refresh =
function YoutubeService_refresh(aURN, aPollingListener) {
  this._logger.info(".refresh('" + aURN + "', ...)");
  var acct = this.getAccount(aURN).QueryInterface(CI.flockISocialAccount);

  if (acct) {
    this._logger.debug("refreshing an Account");
    if (acct.isAuthenticated()) {
      this._refreshAccount(acct, aPollingListener);
    } else {
      // If the user is not logged in, return a success without
      // refreshing anything
      aPollingListener.onResult();
    }
  } else {
    throw CR.NS_ERROR_ABORT;
  }
};

YoutubeService.prototype._refreshAccount =
function YoutubeService__refreshAccount(aAccount, aPollingListener) {
  var inst = this;

  var accountListener = {
    onResult: function accountListener_onResult(aXml) {
      inst._logger.debug("Account info retrieved successfully");
      var entry = aXml.getElementsByTagName("entry")[0];
      if (entry) {
        // update the name and avatar if they have changed.
        // flockRDFSourceUtils does not write them to the RDF if they haven't.
        var nameXML = entry.getElementsByTagName("name");
        if (nameXML && nameXML[0]) {
          aAccount.setParam("name", nameXML[0].firstChild.nodeValue);
        }

        var thumbnailXML = entry.getElementsByTagNameNS(GPHOTO_NAMESPACE,
                                                        "thumbnail");
        if (thumbnailXML && thumbnailXML[0]) {
          aAccount.setParam("avatar", thumbnailXML[0].getAttribute("url"));
        }
      }
      var mailListener = {
        onSuccess: function mailListener_onSuccess(aSubject) {
          inst._logger.debug("mailListener_onSuccess");
          inst.api.updateFriends(aAccount, aPollingListener);
          inst._obs.notifyObservers(aAccount,
                                    "flock-acct-refresh",
                                    "user-info");
        },
        onError: function mailListener_onError(aFlockError) {
          inst._logger.error("._refreshAccount() mailListener.onError()");
          aPollingListener.onError(aFlockError, "mailListener");
        }
      };
      inst.api._getMail(aAccount, mailListener);
    },
    onError: function accountListener_onError(aFlockError) {
      inst._logger.error("._refreshAccount() accountListener.onError()");
      aPollingListener.onError(aFlockError);
    }
  };

  this.api.doCall(accountListener,
                  "http://gdata.youtube.com/feeds/api/users/"
                  + aAccount.getParam("accountId"),
                  null);
};

/**************************************************************************
 * YoutubeService: flockIRichContentDropHandler Implementation
 **************************************************************************/

// boolean handleDrop(in nsITransferable aFlavours,
//                    in nsIDOMHTMLElement aTargetElement);
YoutubeService.prototype.handleDrop =
function YoutubeService_handleDrop(aFlavours, aTargetElement) {
  this._logger.info(".handleDrop()");

  // Handle textarea drops
  if (aTargetElement instanceof CI.nsIDOMHTMLTextAreaElement) {
    var dropCallback = function youtube_dropCallback(aFlav) {
      var data = {};
      var len = {};
      aFlavours.getTransferData(aFlav, data, len);
      var caretPos = aTargetElement.selectionEnd;
      var currentValue = aTargetElement.value;

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

      // We'll only add a breadcrumb if there isn't one already present
      var breadcrumb = "";
      var richDnDSvc = CC[FLOCK_RDNDS_CONTRACTID]
                       .getService(CI.flockIRichDNDService);
      if (!richDnDSvc.hasBreadcrumb(aTargetElement)) {
        breadcrumb = richDnDSvc.getBreadcrumb("plain");
      }

      aTargetElement.value = currentValue.substring(0, caretPos)
                           + data.value.QueryInterface(CI.nsISupportsString)
                                 .data.replace(/: /, ":\n")
                           + trailingSpace
                           + currentValue.substring(caretPos)
                           + breadcrumb;
    };

    return this._handleTextareaDrop(CLASS_SHORT_NAME,
                                    this._coopObj.domains,
                                    aTargetElement,
                                    dropCallback);
  }

  // Default handling otherwise
  return false;
};

/**************************************************************************
 * YoutubeService: API methods
 **************************************************************************/

YoutubeService.prototype.call =
function YoutubeService_call(aListener, aMethod, aParams) {
  var url = "http://www.youtube.com/api2_rest?method=" + aMethod + "&dev_id=" + YT_DEVID;
  for (var p in aParams) {
    url += "&" + p + "=" + aParams[p];
  }
  var hr = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
           .createInstance(CI.nsIXMLHttpRequest)
           .QueryInterface(CI.nsIJSXMLHttpRequest);

  var inst = this;
  hr.onreadystatechange = function (aEvt) {
    if (hr.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      if (Math.floor(hr.status / 100) == 2) {
        var rsp = hr.responseXML.getElementsByTagName("ut_response")[0];
        var stat = rsp.getAttribute("status");
        if (stat != "ok") {
          var error = inst.getError('SERVICE_ERROR', hr.responseXML, null);
          inst._logger.error(".call(..., '" + aMethod + "', ...) "
                             + "status is NOT OK; status = " + stat);
          aListener.onError(error);
        } else {
          aListener.onResult(hr.responseXML);
        }
      } else {
        // HTTP errors (0 for connection lost)
        inst._logger.error(".call(..., '" + aMethod + "', ...) "
                           + "HTTP Error; status = " + hr.status);
        aListener.onError(inst.getError("HTTP_ERROR", null, hr.status));
      }
    }
  };

  hr.mozBackgroundRequest = true;
  hr.open('GET',  url,true);
  this._logger.debug(".call(..., '" + aMethod + "', ...) url = " + url);
  hr.send(null);
};

YoutubeService.prototype.getError =
function YoutubeService_getError(aErrorType, aXML, aHTTPErrorCode) {
  var errorCode;
  var serviceErrorMessage;
  var error = CC["@flock.com/error;1"].createInstance(CI.flockIError);
  if (aErrorType == "HTTP_ERROR") {
    error.errorCode = aHTTPErrorCode;
  } else if (aErrorType == "SERVICE_ERROR") {
    try {
      errorCode = aXML.getElementsByTagName("error")[0].getAttribute('code');
      serviceErrorMessage = aXML.getElementsByTagName("error")[0].getAttribute('description');
    } catch (ex) {
      errorCode = "999"; // in case the error xml is invalid
    }

    switch (errorCode) { // http://www.youtube.com/dev_error_codes
      case "1":
        error.errorCode = error.HTTP_INTERNAL_SERVER_ERROR;
      break;

      case "2": // These errors are due to Flock sending a bad request
      case "3":
      case "4":
      case "5":
      case "6":
        error.errorCode = error.PHOTOSERVICE_INVALID_QUERY;
      break;

      case "7":
      case "8":
        error.errorCode = error.PHOTOSERVICE_INVALID_API_KEY;
        break;

      case "999":
        error.errorCode = error.PHOTOSERVICE_UNKNOWN_ERROR;
      break;

      default:
        error.errorCode = error.PHOTOSERVICE_UNKNOWN_ERROR;
      break;
    }
  }
  error.serviceErrorCode = errorCode;
  error.serviceErrorString = serviceErrorMessage;
  this._logger.error(error.errorString);
  return error;
};


/**************************************************************************
 * YoutubeService: flockISocialWebService Implementation
 **************************************************************************/

// readonly attribute long maxStatusLength;
YoutubeService.prototype.maxStatusLength = 0;

/**
 * void markAllMediaSeen(in AString aPersonUrn);
 * @see flockISocialWebService#markAllMediaSeen
 */
YoutubeService.prototype.markAllMediaSeen =
function YoutubeService_markAllMediaSeen(aIdentityUrn) {
  var identity = this._coop.get(aIdentityUrn);
  identity.unseenMedia = 0;
}; 


/**************************************************************************
 * YoutubeService: flockIWebService Implementation
 **************************************************************************/

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

// readonly attribute AString icon;
YoutubeService.prototype.icon = YOUTUBE_FAVICON;

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

// readonly attribute AString title;
YoutubeService.prototype.title = CLASS_NAME;

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


/**************************************************************************
 * YoutubeService: flockIYoutubeService Implementation
 **************************************************************************/

/**
 * AString getVideoIDFromUrl(in AString aUrl);
 * @see flockIYoutubeService#getVideoIDFromUrl
 */
YoutubeService.prototype.getVideoIDFromUrl =
function youTubeService_getVideoIDFromUrl(aUrl) {
  var videoId = null;
  //http://www.youtube.com/v/lp_daXRkOH0
  //http://www.youtube.com/watch/v/YP2rgi978tw
  //http://www.youtube.com/watch?v=BjfbS_Kj-J0
  // /player2.swf?video_id=xPxDw7ajfGE&....
  var results = FlockSvcUtils.newResults();
  if (this._WebDetective.detectNoDOM("youtube",
                                     "videoId",
                                     "",
                                     aUrl,
                                     results))
  {
    videoId = results.getPropertyAsAString("videoId");
  }
  return videoId;
};


/*************************************************************************
 * Component: YoutubeAccount
 *************************************************************************/

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

  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);
  this.api = gYoutubeAPI;
  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);

  var sa = FlockSvcUtils.flockISocialAccount;
  sa.addDefaultMethod(this, "enumerateFriends");
  sa.addDefaultMethod(this, "getInviteFriendsURL");
  sa.addDefaultMethod(this, "getFormattedFriendUpdateType");
  sa.addDefaultMethod(this, "getFriendCount");

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

YoutubeAccount.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " Account",
  "",
  "",
  YoutubeAccount,
  0,
  [
    CI.flockIWebServiceAccount,
    CI.flockISocialAccount,
    CI.flockIMediaAccount
  ]
);


/**************************************************************************
 * YoutubeAccount: flockISocialAccount Implementation
 **************************************************************************/

// readonly attribute boolean hasFriendActions;
YoutubeAccount.prototype.hasFriendActions = true;

// readonly attribute boolean isMyMediaFavoritesSupported;
YoutubeAccount.prototype.isMyMediaFavoritesSupported = true;

// readonly attribute boolean isPostLinkSupported;
YoutubeAccount.prototype.isPostLinkSupported = false;

// readonly attribute boolean isMeStatusSupported;
YoutubeAccount.prototype.isMeStatusSupported = false;

// readonly attribute boolean isFriendStatusSupported;
YoutubeAccount.prototype.isFriendStatusSupported = false;

// readonly attribute boolean isStatusEditable;
YoutubeAccount.prototype.isStatusEditable = false;

// DEFAULT: nsISimpleEnumerator enumerateFriends();

/**
 * AString formatFriendActivityForDisplay(in AString aFriendActivityUrn);
 * @see flockISocialAccount#formatFriendActivityForDisplay
 */
YoutubeAccount.prototype.formatFriendActivityForDisplay =
function YoutubeAccount_formatFriendActivityForDisplay(aFriendActivityUrn) {
  var friendActivity = FlockSvcUtils.getCoop(this)
                                    .get(aFriendActivityUrn);
  var bundle = this.getService().getStringBundle();

  switch (friendActivity.updateType) {
   case "media":
     var name = "flock.friendFeed.lastUpdateTypePretty.mediaUpdated";
     return bundle.GetStringFromName(name);
  }
  return "";
}

/**
 * AString formatStatusForDisplay(in AString aStatusMessage);
 * @see flockISocialAccount#formatStatusForDisplay
 */
YoutubeAccount.prototype.formatStatusForDisplay =
function YoutubeAccount_formatStatusForDisplay(aStatusMessage) {
  return aStatusMessage;
};

/**
 * AString getEditableStatus();
 * @see flockISocialAccount#getEditableStatus
 */
YoutubeAccount.prototype.getEditableStatus =
function YoutubeAccount_getEditableStatus() {
  return "";
};

/**
 * AString getFriendActions(in AString aFriendURN);
 * @see flockISocialAccount#getFriendActions
 */
YoutubeAccount.prototype.getFriendActions =
function YoutubeAccount_getFriendActions(aFriendURN) {
  this._logger.info(".getFriendActions('" + aFriendURN + "')");

  var actionNames = ["friendMessage",
                     "friendViewProfile",
                     "friendMediaFave",
                     "friendShareFlock"];

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(YOUTUBE_STRING_BUNDLE);

  var actions = [];
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    for each (var i in actionNames) {
      actions.push({
        label: bundle.GetStringFromName("flock.youtube.actions." + i),
        class: i,
        spec: this._WebDetective.getString("youtube", i, "")
                  .replace("%accountid%", this.coopObj.accountId)
                  .replace("%friendid%", c_friend.accountId)
      });
    }
  }
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(actions);
};

// DEFAULT: long getFriendCount();

/**
 * AString getMeNotifications();
 * @see flockISocialAccount#getMeNotifications
 */
YoutubeAccount.prototype.getMeNotifications =
function YoutubeAccount_getMeNotifications() {
  this._logger.info(".getMeNotifications()");

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(YOUTUBE_STRING_BUNDLE);

  var noties = [];
  var inst = this;
  function _addNotie(aType, aCount) {
    var stringName = "flock.youtube.noties."
                   + aType + "."
                   + ((parseInt(aCount) <= 0) ? "none" : "some");
    inst._logger.debug("aType " + aType
                       + " aCount " + aCount
                       + " name " + stringName);
    noties.push({
      class: aType,
      tooltip: bundle.GetStringFromName(stringName),
      metricsName: aType,
      count: aCount,
      URL: inst._WebDetective.getString("youtube", aType + "_URL", "")
    });
  }
  _addNotie("meMessages", this.coopObj.accountMessages);
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(noties);
};

/**
 * void postLink(in nsITransferable aTransferable);
 * @see flockISocialAccount#postLink
 */
YoutubeAccount.prototype.postLink =
function YoutubeAccount_postLink(aTransferable) {
  // Not needed for Youtube.
};

/**
 * AString getProfileURLForFriend(in AString aFriendURN);
 * @see flockISocialAccount#getProfileURLForFriend
 */
YoutubeAccount.prototype.getProfileURLForFriend =
function YoutubeAccount_getProfileURLForFriend(aFriendURN) {
  this._logger.debug(".getProfileURLForFriend('" + aFriendURN + "')");

  var url = "";
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    url = this._WebDetective.getString("youtube", "friendprofile", "")
                            .replace("%accountid%", c_friend.accountId);
  }
  return url;
};

/**
 * void shareWithFriend(in AString aFriendURN,
 *                      in nsITransferable aTransferable);
 * @see flockISocialAccount#shareWithFriend
 */
YoutubeAccount.prototype.shareWithFriend =
function YoutubeAccount_shareWithFriend(aFriendURN, aTransferable) {
  this._logger.info(".shareWithFriend('" + aFriendURN + "')");

  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    var flavors = ["text/x-flock-media",
                   "text/x-moz-url",
                   "text/unicode",
                   "text/html"];

    var message = CC[FLOCK_RDNDS_CONTRACTID]
                  .getService(CI.flockIRichDNDService)
                  .getMessageFromTransferable(aTransferable,
                                              flavors.length,
                                              flavors);
    if (message.body) {
      this._composeMessage(aFriendURN, message.subject, message.body, true);
    }
  }
};

/**
 * void markAllMeNotificationsSeen(in AString aType);
 * @see flockISocialAccount#markAllMeNotificationsSeen
 */
YoutubeAccount.prototype.markAllMeNotificationsSeen =
function YoutubeAccount_markAllMeNotificationsSeen(aType) {
  this._logger.debug(".markAllMeNotificationsSeen('" + aType + "')");
  switch (aType) {
    case "meMessages":
      this.coopObj.accountMessages = 0;
      break;
    default:
      break;
  }
};

/**
 * void shareFlock(in AString aFriendURN);
 * @see flockISocialAccount#shareFlock
 */
YoutubeAccount.prototype.shareFlock =
function YoutubeAccount_shareFlock(aFriendURN) {
  this._logger.info(".shareFlock('" + aFriendURN + "')");

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
              .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(YOUTUBE_STRING_BUNDLE);
  var body = bundle.GetStringFromName("flock.youtube.friendShareFlock.message");
  var subj = bundle.GetStringFromName("flock.youtube.friendShareFlock.subject");
  this._composeMessage(aFriendURN, subj, body, false);
};

// void friendAction(in AString aAction, in AString aFriendURN, in AString..
YoutubeAccount.prototype.friendAction =
function YoutubeAccount_friendAction(aAction, aFriendURN, aActivityURN) {
  this._logger.error(".friendAction(): NS_ERROR_NOT_IMPLEMENTED");
  throw CR.NS_ERROR_NOT_IMPLEMENTED;
};

/**
 * void setStatus(in AString aStatusMessage, in flockIListener aFlockListener);
 * @see flockISocialAccount#setStatus
 */
YoutubeAccount.prototype.setStatus =
function YoutubeAccount_setStatus(aStatusMessage, aFlockListener) {
};


/**************************************************************************
 * YoutubeAccount: flockIWebServiceAccount Implementation
 **************************************************************************/

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

// DEFAULT: nsIPropertyBag getAllCustomParams();
// DEFAULT: nsIVariant getCustomParam(in AString aParamName);
// DEFAULT: nsIVariant getParam(in AString aParamName);
// DEFAULT: flockILoginWebService getService();
// DEFAULT: boolean isAuthenticated();

/**
 * void keep();
 * @see flockIWebServiceAccount#keep
 */
YoutubeAccount.prototype.keep =
function YoutubeAccount_keep() {
  this._logger.debug(".keep()");
  this.coopObj.isTransient = false;
  var key = this.getService().urn + ":" + this.coopObj.accountId;
  this._acUtils.makeTempPasswordPermanent(key);
};

/**
 * void login(in flockIListener aFlockListener);
 * @see flockIWebServiceAccount#login
 */
YoutubeAccount.prototype.login =
function YoutubeAccount_login(aFlockListener) {
  this._logger.debug(".login(...)");
  this._acUtils.ensureOnlyAuthenticatedAccount(this);
  this.api.setAuthAccount(this.coopObj);
  // Force refresh on login is done by the people sidebar
  if (aFlockListener) {
    aFlockListener.onSuccess(this, "login");
  }
};

// DEFAULT: void logout(in flockIListener aFlockListener);
// DEFAULT: void setCustomParam(in AString aParamName, in nsIVariant aValue);
// DEFAULT: void setParam(in AString aParamName, in nsIVariant aValue);

// account helper
YoutubeAccount.prototype._addMediaToFavorites =
function YoutubeAccount__addMediaToFavorites(aAccountId) {
  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(YOUTUBE_STRING_BUNDLE);
  
  var svc = this.getService();

  var id = FAV_CHANNELS_ROOT + ":" + svc.shortName
         + ":favorites:" + aAccountId;
  var name = bundle.formatStringFromName("flock.youtube.favoritesLC",
                                         [aAccountId], 1);
  var item = new this._coop.MediaQuery(id, {
    name: name,
    query: "favorites:" + aAccountId,
    service: svc.shortName,
    favicon: svc.icon,
    isPollable: true,
    hasUnseenItems: false
  });

  this._coop.media_favorites.children.addOnce(item);
};

YoutubeAccount.prototype._composeMessage =
function YoutubeAccount__composeMessage(aFriendURN,
                                        aSubject,
                                        aBody,
                                        addBreadCrumb)
{
  var body = aBody;
  var subject = aSubject;
  var c_friend = this._coop.get(aFriendURN);
  var url = this._WebDetective.getString("youtube", "youtubeMessage_URL", "")
                .replace("%friendid%", c_friend.accountId);
  var wm = CC["@mozilla.org/appshell/window-mediator;1"]
             .getService(CI.nsIWindowMediator);
  var win = wm.getMostRecentWindow("navigator:browser");
  if (win) {
   var browser = win.getBrowser();
   var newTab = browser.loadOneTab(url, null, null, null, false, false);
   var obs = CC["@mozilla.org/observer-service;1"]
             .getService(CI.nsIObserverService);
   var inst = this;
   var observer = {
     observe: function openSendMessageTabForFill_observer(aContent,
                                                          aTopic,
                                                          aContextUrl)
     {
       var contentWindow = newTab.linkedBrowser.docShell
                                 .QueryInterface(CI.nsIInterfaceRequestor)
                                 .getInterface(CI.nsIDOMWindow);
       function insertContent(aWebDicString, aMessage) {
         var xpathquery = inst._WebDetective.getString("youtube", aWebDicString, "");
         var doc = contentWindow.document;
         var formItems = doc.evaluate(xpathquery, doc, null,
                                      CI.nsIDOMXPathResult.ANY_TYPE, null);
         if (formItems) {
           var formItem = formItems.iterateNext();
           formItem.value = aMessage;
         }
       }
       if (contentWindow == aContent) {
         obs.removeObserver(this, "EndDocumentLoad");
         insertContent("youtubemessage_subjectXPath", subject);
         if (addBreadCrumb) {
           // Add breadcrumb to message body
            var breadcrumb = CC[FLOCK_RDNDS_CONTRACTID]
                               .getService(CI.flockIRichDNDService)
                               .getBreadcrumb("plain");
           if (breadcrumb) {
             body += breadcrumb;
           }
        }
         insertContent("youtubemessage_bodyXPath", body);
       }
     }
   };
   obs.addObserver(observer, "EndDocumentLoad", false);
  }
};

/*************************************************************************
 * Component: YoutubeAPI
 *************************************************************************/

function YoutubeAPI() {
  this._acUtils = CC["@flock.com/account-utils;1"]
                  .getService(CI.flockIAccountUtils);
  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);
  this._logger = CC["@flock.com/logger;1"].createInstance(CI.flockILogger);
  this._logger.init("youtubeAPI");

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  this._WebDetective = CC["@flock.com/web-detective;1"]
                      .getService(CI.flockIWebDetective);
  this._ppSvc = CC["@flock.com/people-service;1"]
                .getService(CI.flockIPeopleService);
  this._authKey = null;
  this._authAccount = null;
  this._flockGoogleAuth = new flockGoogleAuth("youtube");
};

YoutubeAPI.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " API",
  "",
  "",
  YoutubeAPI,
  0,
  [CI.flockIAuthenticatedAPI]
);

/*************************************************************************
 * YoutubeAPI: flockIAuthenticatedAPI Implementation
 *************************************************************************/

YoutubeAPI.prototype.setAuthAccount =
function YoutubeAPI_setAuthAccount(aCoopObj) {
  this._authAccount = aCoopObj;
};

/**
 * void deauthenticate();
 * @see flockIAuthenticatedAPI#deauthenticate
 */
YoutubeAPI.prototype.deauthenticate =
function YoutubeAPI_deauthenticate() {
  this._logger.debug("deauthenticate()");
  this._authKey = null;
  this._authAccount = null;
};

/**
 * void authenticate(in nsILoginInfo aCredentials,
 *                   in flockIListener aFlockListener);
 * @see flockIAuthenticatedAPI#authenticate
 */
YoutubeAPI.prototype.authenticate =
function YoutubeAPI_authenticate(aCredentials, aFlockListener) {
  var api = this;
  this._logger.debug("authenticate(" + aCredentials.username + ")");
  var dcListener = {
    onSuccess: function dcl_onSuccess(aAuthKey) {
      api._logger.debug("dcl_onSuccess() - " + aAuthKey);
      if (aAuthKey) {
        api._logger.debug("setting authKey = " + aAuthKey);
        api._authKey = aAuthKey;
        if (aFlockListener) {
          aFlockListener.onSuccess(null, null);
        }
      } else {
        if (aFlockListener) {
          var svc = CC[CONTRACT_ID].getService(CI.flockIWebService);
          api._acUtils.markAllAccountsAsLoggedOut(svc);
          aFlockListener.onError(null, null);
        }
      }
    },
    onError: function dcl_onError(aFlockError) {
      var svc = CC[CONTRACT_ID].getService(CI.flockIWebService);
      api._acUtils.markAllAccountsAsLoggedOut(this);
      api._logger.error(".authenticate() dcListener.onError()");
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, null);
      }
    }
  };
  this._flockGoogleAuth.authenticate(dcListener, aCredentials, LOGIN_URL);
};

YoutubeAPI.prototype.doCall =
function YoutubeAPI_doCall(aListener, aUrl, aParams) {
  this._logger.debug("doCall(" + aUrl + ")");
  this._logger.debug("_authKey = " + this._authKey);
  if (!this._authKey && this._authAccount && this._authAccount.accountParamList)
  {
    // We have an account but no auth key yet so go get one.
    this._logger.debug("We need to re-authorize");
    this._authNeeded(aListener, "doCall", aListener, aUrl, aParams);
    return;
  }
  this._flockGoogleAuth.doCall(this._authKey, aListener, aUrl, aParams);
};

YoutubeAPI.prototype._doXMLCall =
function YoutubeAPI__doXMLCall(aListener, aUrl, aParams, aXml, aMethod) {
  this._logger.debug("_doXMLCall()");
  if (!this._authKey && this._authAccount && this._authAccount.accountParamList)
  {
    // We have an account but no auth key yet so go get one.
    this._authNeeded(aUploadListener,
                     "_doXMLCall",
                     aUrl,
                     aParams,
                     aXml,
                     aMethod);
    return;
  }
  this._flockGoogleAuth.doXMLCall(this._authKey,
                                  aListener,
                                  aUrl,
                                  aParams,
                                  aXml,
                                  aMethod);
};

YoutubeAPI.prototype._authNeeded =
function YoutubeAPI__authNeeded(aListener, aMethod, aP1, aP2, aP3, aP4, aP5) {
  var api = this;
  var listener = {
    onSuccess: function doCall_onSucess(aSubject, aTopic) {
      api[aMethod](aP1, aP2, aP3, aP4, aP5);
    },
    onError: function doCall_onError(aError) {
      api._logger.error("._authNeeded() listener.onError()");
      aListener.onError(aError);
    }
  }
  var paramList = this._authAccount.accountParamList;
  // authAccountId is supposed to be set to loginName and username to accountId
  var credentials = {
    authAccountId: paramList.getCustomParam("loginName").value,
    username: this._authAccount.accountId,
    service: "youtube",
    urn: this.svc.urn
  };
  this.authenticate(credentials, listener);
};

YoutubeAPI.prototype._updatePerson =
function YoutubeAPI__updatePerson(aPersonXML, aAccount) {
  this._logger.debug("_updatePerson");
  var name = aPersonXML.getElementsByTagName("name")[0]
                       .firstChild.nodeValue;
  var person = aAccount.coopObj.addPerson({accountId: name, name: name});
  if (person.screenName == null) {
    person.screenName = name;
  }
  
  var thumbnailXML = aPersonXML.getElementsByTagNameNS(GPHOTO_NAMESPACE,
                                                       "thumbnail");
  if (thumbnailXML && thumbnailXML[0]) {
    person.avatar = thumbnailXML[0].getAttribute("url");
  }

  person.URL = aPersonXML.getElementsByTagName("link")[0]
                         .getAttribute("href");
  return person;
};

YoutubeAPI.prototype.updateFriends =
function YoutubeAPI_updateFriends(aAcct, aPollingListener) {
  this._logger.debug("updateFriends");
  var inst = this;
  var getFriendsListener = {
    onResult: function getFriendsListener_onResult(aXml) {
      var friends = aXml.getElementsByTagName("entry");
      var pplRefreshListener = CC["@flock.com/people-refresh-listener;1"]
                               .createInstance(CI.flockIPeopleRefreshListener);
      pplRefreshListener.init(friends.length, aAcct.urn, aPollingListener, 0);
      var realFriends = [];
      // We have retrieved our list of friends, but some friends have not been
      // confirmed yet. So we need to retrieve detailed friend information
      // (such as avatar) and media information only from the *real* friends.
      for (var f = 0; f < friends.length; f++) {
        var friend = friends[f];
        var friendStatus = friend
                           .getElementsByTagNameNS(YT_NAMESPACE,"status")[0];
        if (friendStatus.firstChild.nodeValue != "accepted") {
          pplRefreshListener.onSuccess(null, "");
          continue;
        }
        var name = friend.getElementsByTagName("title")[0].firstChild.nodeValue;
        realFriends.push(name);
        var apiLink = friend.getElementsByTagName("link")[0];

        var infoListener = {
          onResult: function infoListener_onSuccess(aXml) {
            inst._logger.debug("Successfully retrieved detailed friend info.");

            var xml = aXml.getElementsByTagName("entry")[0];
            // Create a coop Identity from the data.
            var person = inst._updatePerson(xml, aAcct);
            var serializer = CC["@mozilla.org/xmlextras/xmlserializer;1"]
                             .createInstance(CI.nsIDOMSerializer);
            var xmlStr = serializer.serializeToString(aXml.documentElement);
            var xmlData = new XML(xmlStr);
            var gd = new Namespace(GDATA_NAMESPACE);
            var usrUpload = inst._WebDetective
                                .getString("youtube",
                                           "youtubeUserUploadNamespace",
                                           "");
            var feedlink = xmlData.gd::feedLink
                                  .(@rel == YT_NAMESPACE + usrUpload);
            var totalMedia = feedlink.@countHint;

            if (parseInt(totalMedia, 10) > parseInt(person.totalMedia, 10)) {
              // Get the friend's media.
              var url = feedlink.@href;
              var mediaListener = {
                onResult: function m_onSuccess(aMediaXml) {
                  person.lastUpdateType = "media";
                  person.totalMedia = totalMedia;
                  person.unseenMedia++;
                  var entry = aMediaXml.getElementsByTagName("entry")[0];
                  var updated = entry.getElementsByTagName("updated")[0]
                                     .firstChild.nodeValue;
                  var date = ISO8601DateUtils.parse(updated);
                  person.lastUpdate = date.getTime() / 1000;
                  inst._logger.debug("Successfully updated media.");
                  pplRefreshListener.onSuccess(null, "friendsRetrieved_Listener");
                },
                onError: function m_onError(aFlockError) {
                  // Error occurred while grabbing new media
                  inst._logger.error(".updateFriends() mediaListener.onError()");
                  if (person.lastUpdate == null) {
                    // Trigger the RDF observers and update the side bar
                    person.lastUpdate = 0;
                  }
                  pplRefreshListener.onError(null, "friendsRetrieved_Listener");
                }
              }
              inst.doCall(mediaListener, url, null);
            } else {
              if (person.lastUpdate == null) {
                // Trigger the RDF observers and update the side bar
                person.lastUpdate = 0;
              }
              pplRefreshListener.onSuccess(null, "infoListener");
            }
          },
          onError: function infoListener_onError(aFlockError) {
            inst._logger.error(".updateFriends() infoListener.onError()");
            pplRefreshListener.onError(null, "infoListener");
          }
        };
        inst.doCall(infoListener, apiLink.getAttribute("href"), null);
      }
      // REMOVE locally people removed on the server
      var localFriends = aAcct.enumerateFriends();
      while (localFriends.hasMoreElements()) {
        var identity = localFriends.getNext();
        if (realFriends.indexOf(identity.accountId) == -1) {
          inst._logger.debug("Friend " + identity.accountId
                             + " has been deleted on the server");
          identity.destroy();
        }
      }
    },
    onError: function getFriendsListener_onError(aXML) {
      inst._logger.error(".updateFriends() getFriendsListener.onError()");
      aPollingListener.onError(null);
    }
  };
  this.doCall(getFriendsListener,
              "http://gdata.youtube.com/feeds/api/users/default/contacts",
              null);
};

YoutubeAPI.prototype._getMail =
function YoutubeAPI__getMail(aAcct, aListener) {
  this._logger.debug("_getMail " + aAcct.getParam("accountId"));
  // Get new message count
  var inst = this;
  var hr = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
           .createInstance(CI.nsIXMLHttpRequest);
  hr.mozBackgroundRequest = true
  hr.onreadystatechange = function updateMail_onreadystatechange(aEvent) {
    if (hr.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      var results = FlockSvcUtils.newResults();
      // Set the message count
      var messages = "0";
      if (inst._WebDetective.detectNoDOM("youtube",
                                         "messages",
                                         "",
                                         hr.responseText,
                                         results))
      {
        try {
          messages = results.getPropertyAsAString("messagecount");
          if (!messages || !messages.length) {
            messages = "0";
          }
        } catch (ex) {
          inst._logger.error("Exception getting mail: " + ex.message);
        }
        if (aAcct.getParam("accountMessages") < messages) {
          inst._ppSvc.togglePeopleIcon(true);
        }
      } else {
        inst._logger.error("Exception getting mail: no accountinfo found!");
      }
      aAcct.setParam("accountMessages", messages);
      aListener.onSuccess(null, "_getMail");
    }
  }
  var url = this._WebDetective.getString(CLASS_SHORT_NAME, "userprofile", "")
                .replace("%accountid%", aAcct.getParam("accountId"));
  hr.open("GET", url);
  hr.send(null);
};

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

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

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