// vim: ts=2 sw=2 expandtab cindent
//
// 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

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

const ENABLE_DEBUG = true; // switch to turn off slow debug code for production
function DEBUG(x) { if (ENABLE_DEBUG) debug("flockYouTubeService: "+x+"\n"); }

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const CU = Components.utils;

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

const YT_DEVID = "1aiV9CsTs3o";
const YT_DEVSECRET = "flock_is_open_source";
const YOUTUBE_CID = Components.ID('{DCB6A01E-7D4A-4C30-AB3D-9CC98C02F617}');
const YOUTUBE_CONTRACTID = '@flock.com/?photo-api-youtube;1';
const YOUTUBE_FAVICON = "chrome://flock/content/services/youtube/favicon.png";
const YOUTUBE_TITLE = "YouTube Web Service";
const SERVICE_ENABLED_PREF          = "flock.service.youtube.enabled";
const CATEGORY_COMPONENT_NAME       = "YouTube JS Component"
const CATEGORY_ENTRY_NAME           = "youtube"
const FRIENDS_AVATAR_TIMEOUT = 5000;  // in milliseconds
const FRIENDS_ACTIVITY_TIMEOUT = 500;  // in milliseconds

const FAV_CHANNELS_ROOT = "urn:media:favorites";

const PROPERTY_BAG_CONTRACTID = "@mozilla.org/hash-property-bag;1";
const XHR_CONTRACTID = "@mozilla.org/xmlextras/xmlhttprequest;1";
const FLOCK_RDDS_CONTRACTID = "@flock.com/rich-dnd-service;1";

const OBS_TOPIC_XPCOMSHUTDOWN = "xpcom-shutdown";
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";

// 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
// The delay between two refreshes when the sidebar is open (in seconds)
const YOUTUBE_SHORT_INTERVAL = 3600; // 60 minutes


var gCompTK;
function getCompTK() {
  if (!gCompTK) {
    gCompTK = Cc["@flock.com/singleton;1"]
                .getService(Ci.flockISingleton)
                .getSingleton("chrome://browser/content/flock/services/common/load-compTK.js")
                .wrappedJSObject;
  }
  return gCompTK;
}

function _getIdentityUrn(aAccountId, aUid) {
  var result = YOUTUBE_IDENTITY_URN_PREFIX
             + aAccountId + ":"
             + aUid;
  return result;
}

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

var gTimers = [];  // For use with the scheduler

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

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 = null;
  if (aMediaItem.length_seconds) {
    extras = <label>Length: {aMediaItem.length_seconds} seconds</label>;
  }
  if (aMediaItem.rating_avg) {
    extras += <label>Rating: {aMediaItem.rating_avg} ({aMediaItem.rating_count} times)</label>;
  }
  if (aMediaItem.view_count) {
    extras += <label>Views: {aMediaItem.view_count}</label>;
  }
  extras += <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>
        <vbox anonid="ttUserTxt">{extras}</vbox>
      </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;
  }
};


// ================================================
// ========== BEGIN youtubeService class ==========
// ================================================

function youtubeService()
{
  this.obs = Components.classes["@mozilla.org/observer-service;1"]
                       .getService(Components.interfaces.nsIObserverService);
  this.obs.addObserver(this, OBS_TOPIC_XPCOMSHUTDOWN, false);
  this.acUtils = Components.classes["@flock.com/account-utils;1"]
                           .getService(Components.interfaces.flockIAccountUtils);
  this.status = Components.interfaces.flockIWebService.STATUS_UNKNOWN;
  this.url = "http://www.youtube.com";
  this.mIsInitialized = false;
  this._currentQueryExhausted = false;
  this._ctk = {
    interfaces: [
      "nsISupports",
      "nsISupportsCString",
      "nsIClassInfo",
      "nsIObserver",
      "flockIWebService",
      "flockIMediaWebService",
      "flockIMediaEmbedWebService",
      "flockISocialWebService",
      "flockIYoutubeService",
      "flockIManageableWebService",
      "flockIPollingService",
      "flockIRichContentDropHandler"
    ],
    shortName: "youtube",
    fullName: "YouTube",
    description: YOUTUBE_TITLE,
    favicon: YOUTUBE_FAVICON,
    CID: YOUTUBE_CID,
    contractID: YOUTUBE_CONTRACTID,
    accountClass: youtubeAccount,
    needPassword: false
  };
  FlockSvcUtils.flockIMediaEmbedWebService
               .addDefaultMethod(this, "getSharingContent");

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

  this._logger = Cc['@flock.com/logger;1'].createInstance(Ci.flockILogger);
  this._logger.init('youtube');
  this._profiler = Cc["@flock.com/profiler;1"].getService(Ci.flockIProfiler);
  this.init();
}


// BEGIN nsIObserver interface
youtubeService.prototype.observe =
function youtubeService_observe(subject, topic, state)
{
  switch (topic) {
    case OBS_TOPIC_XPCOMSHUTDOWN:
    {
      this.obs.removeObserver(this, OBS_TOPIC_XPCOMSHUTDOWN);
    }; break;
  }
}
// END nsIObserver interface


youtubeService.prototype.init =
function youtubeService_init()
{
  DEBUG(".init()");

  // Prevent re-entry
  if (this.mIsInitialized) return;
  this.mIsInitialized = true;

  var evtID = this._profiler.profileEventStart("youtube-init");

  this.prefService = Components.classes["@mozilla.org/preferences-service;1"]
                               .getService(Components.interfaces.nsIPrefBranch);
  if ( this.prefService.getPrefType(SERVICE_ENABLED_PREF) &&
       !this.prefService.getBoolPref(SERVICE_ENABLED_PREF) )
  {
    DEBUG("Pref "+SERVICE_ENABLED_PREF+" set to FALSE... not initializing.");
    var catMgr = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
    catMgr.deleteCategoryEntry("wsm-startup", CATEGORY_COMPONENT_NAME, true);
    catMgr.deleteCategoryEntry("flockWebService", CATEGORY_ENTRY_NAME, true);
    catMgr.deleteCategoryEntry("flockMediaProvider", CATEGORY_ENTRY_NAME, true);
    return;
  }

  this.faves_coop = Components.classes['@flock.com/singleton;1']
                              .getService(Components.interfaces.flockISingleton)
                              .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                              .wrappedJSObject;
  this.account_root = this.faves_coop.accounts_root;

  this.ytService = new this.faves_coop.Service(
    "urn:youtube:service",
    {
      name: "youtube",
      desc: "The YouTube Service",
      loginURL: gStrings["userlogin"]
    }
  );
  this.ytService.serviceId = YOUTUBE_CONTRACTID;

  // Load Web Detective file
  this.webDetective = this.acUtils.useWebDetective("youtube.xml");
  for (var s in gStrings) {
    gStrings[s] = this.webDetective.getString("youtube", s, gStrings[s]);
  }
  this.ytService.domains = gStrings["domains"];

  this.urn = this.ytService.id();

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

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

  this._profiler.profileEventEnd(evtID, "");
}

youtubeService.prototype.getError =
function youtubeService_getError (aErrorType, aXML, aHTTPErrorCode) {
  var error = Components.classes["@flock.com/error;1"].createInstance(Ci.flockIError);
  if  (aErrorType == "HTTP_ERROR") {
    error.errorCode = aHTTPErrorCode;
  } else if (aErrorType == "SERVICE_ERROR") {
    var errorCode;
    var errorMessage;
    var serviceErrorMessage;
    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.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.call =
function youtubeService_call(aListener, aMethod, aParams)
{
  var url = "http://www.youtube.com/api2_rest?method=" + aMethod + "&dev_id=" + YT_DEVID;
  for (p in aParams) {
    url += "&" + p + "=" + aParams[p];
  }
  var hr = Components.classes['@mozilla.org/xmlextras/xmlhttprequest;1']
                     .createInstance(Components.interfaces.nsIXMLHttpRequest)
                     .QueryInterface(Components.interfaces.nsIJSXMLHttpRequest);

  var inst = this;
  hr.onreadystatechange = function (aEvt) {
    if (hr.readyState == 4) {
      try {
        if (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);
            aListener.onError(error);
          }
          else {
            aListener.onResult(hr.responseXML);
          }
        }
        else {
          // http errors
          aListener.onError(inst.getError("HTTP_ERROR", null, hr.status));
        }
      } catch(e) {
        // XMHTTPERROR (connection lost)
        inst._logger.error(e);
        aListener.onError(inst.getError("HTTP_ERROR", null, "9001"));
      }
    }
  };

  hr.backgroundRequest = true;
  hr.open('GET',  url,true);
  hr.send(null);
}

youtubeService.prototype.getFeed =
function youtubeService_getFeed(aListener, aFeedURL)
{
  ios = Components.classes["@mozilla.org/network/io-service;1"]
    .getService(Components.interfaces.nsIIOService);
  var uri = ios.newURI(aFeedURL, null, null);

  var ytServ = this;
  var feedListener = {
    onGetFeedComplete: function oncomplete (feed) {
      ytServ.handleFeedResult(aListener, feed);
    },
    onError: aListener.onError
  };

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

youtubeService.prototype.createAlbum =
function youtubeService_createAlbum(aListener, aAlbumName)
{
  var error = Cc["@flock.com/error;1"].createInstance(Ci.flockIError);
  error.errorCode = error.PHOTOSERVICE_UNKNOWN_ERROR;
  error.errorString = "NS_ERROR_NOT_IMPLEMENTED";
  aListener.onError(null, null, error);
}

youtubeService.prototype.findByUsername =
function youtubeService_findByUsername(aListener, aUsername)
{
  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      inst._logger.debug("Auth XML is:\n" + aXML + "\n");
      // XXX ERWAN: why are we making a call to the server, if we don't
      // use the xml we get as result??
      // XXX marcus: Debugging shows that aXML is empty.
      var newUserObj = Cc["@flock.com/person;1"]
                       .createInstance(Ci.flockIPerson);
      newUserObj.serviceId = inst.contractID;
      newUserObj.accountId = aUsername;
      newUserObj.name = aUsername;
      newUserObj.totalMedia = -1; // See mediabar.js
      aListener.onSuccess(newUserObj, "");
    },
    onError: function (aXML) {
      aListener.onError(null, null, null);
    }
  }
  var params = {};
  params.user = aUsername;
  this.call(myListener, "youtube.users.get_profile", params);
}

youtubeService.prototype.getAlbums = function(aListener, aUsername) { 
  aListener.onError(null, null, null);
}

youtubeService.prototype.login = function(aAccountURN, aListener) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; }

youtubeService.prototype.queryChannel =
function youtubeService_queryChannel(aListener,
                                     aQueryString,
                                     aCount,
                                     aPage,
                                     aRequestId)
{
  var aQuery = new queryHelper(aQueryString);
  // return; // XXX TODO FIXME: This is killing perf

  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      var rval = inst.handlePhotosResult(aXML);

      if (rval.length < aCount) {
        // 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;
        }
      }
      aListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function (aError) {
      aListener.onError(null, aRequestId, aError);
    }
  }

  var params = {}
  if (aQuery.search) {
    params.tag = aQuery.search;
    params.page = aPage;
    params.per_page = aCount;
    this.call(myListener, 'youtube.videos.list_by_tag', 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(aListener, channel.feed);
    else
      this.call(myListener, "youtube.videos.list_featured", params);
  }
}

youtubeService.prototype.search =
function youtubeService_search(aListener,
                               aQueryString,
                               aCount,
                               aPage,
                               aRequestId)
{
  if (aPage == 1) {
    // Same search refreshed or new search
    this._currentQueryExhausted = false;
  }

  if (this._currentQueryExhausted) {
    // No need to search as all pages for this query have been returned
    return;
  }

  var aQuery = new queryHelper(aQueryString);
  if(aQuery.favorites && !aQuery.user)
  {
    aQuery.user = aQuery.favorites;
  }
  if (!aQuery.user && aQuery.special != "favorites") {
    this.queryChannel(aListener, aQueryString, aCount, aPage, aRequestId);
    return;
  }

  var aUserid = aQuery.user;

  var params = {
    user: aUserid
  };

  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      var rval = inst.handlePhotosResult(aXML, aUserid);

      if (rval.length < aCount) {
        // 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;
        }
      }
      aListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function (aError) {
      aListener.onError(null, aRequestId, aError);
    }
  }

  if (aQuery.favorites) {
    this.call(myListener, "youtube.users.list_favorite_videos", params);
  } else {
    params.page = aPage;
    params.per_page = aCount;
    this.call(myListener, "youtube.videos.list_by_user", params);
  }
}

// flockIMediaWebService.getMediaItemFormatter()
youtubeService.prototype.getMediaItemFormatter =
function youtubeService_getMediaItemFormatter() {
  return flockMediaItemFormatter;
}

youtubeService.prototype.handlePhotosResult =
function youtubeService_handlePhotoResult(aXML, aUserid)
{
  var rval = [];
  var nextIdx = 0;
  var photoList = aXML.getElementsByTagName("video");
  for (var i = 0; i < photoList.length; i++) {
    var photo = photoList[i];
    var newMediaItem = Cc["@flock.com/photo;1"]
                       .createInstance(Ci.flockIMediaItem);
    newMediaItem.init(this.shortName, flockMediaItemFormatter);
    newMediaItem.has_miniView = "true";
    for (var j = 0; j < photo.childNodes.length; j++)
    {
      var child = photo.childNodes[j];
      var contentNode = child.childNodes[0];
      var childContent = "";
      if (contentNode) childContent = contentNode.nodeValue;
      switch (child.tagName)
      {
        case "author":
          newMediaItem.username = childContent;
          newMediaItem.userid = childContent;
          break;
        // case "id":
          // newMediaItem.id = childContent;
          // break;
        case "title":
          newMediaItem.title = childContent;
          break;
        case "upload_time":
          newMediaItem.uploadDate = childContent * 1000;
          newMediaItem.id = "" + parseInt(childContent) + nextIdx++;
          break;
        case "length_seconds":
          newMediaItem.length_seconds = childContent;
          break
        case "rating_avg":
          newMediaItem.rating_avg = childContent;
          break
        case "description":
          newMediaItem.description = childContent;
          break
        case "comment_count":
          newMediaItem.comment_count = childContent;
          break
        case "view_count":
          newMediaItem.view_count = childContent;
          break
        case "rating_count":
          newMediaItem.rating_count = childContent;
          break
        case "url":
          newMediaItem.webPageUrl = childContent;
          break;
        case "thumbnail_url":
          newMediaItem.thumbnail = childContent;
          newMediaItem.midSizePhoto = childContent;
          newMediaItem.largeSizePhoto = childContent;
          break;
      }
    }

    newMediaItem.is_public = "true";
    newMediaItem.is_video = true;
    rval.push(newMediaItem);
  }
  return rval;
}

youtubeService.prototype.handleFeedResult =
function youtubeService_handleFeedResult(aListener, aFeed)
{
  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();
    // 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;
    }
  }
  aListener.onSuccess(resultsEnum, "");
}

youtubeService.prototype.refresh =
function youtubeService_refresh(aURN, aListener)
{
  DEBUG("refresh with aURN of " + aURN);
  var refreshItem = this.faves_coop.get(aURN);

  if (refreshItem.isInstanceOf(this.faves_coop.Account)) {
    this._logger.debug("refreshing an Account");
    if (refreshItem.isAuthenticated) {
      this.refreshAccount(aURN, aListener);
    } else {
      // If the user is not logged in, return a success without
      // refreshing anything
      aListener.onResult();
    }
  } else {
    throw Components.results.NS_ERROR_ABORT;
  }
}

youtubeService.prototype.refreshAccount =
function youtubeService_refreshAccount(aURN, aListener)
{
  if (!aURN) return;
  var inst = this;
  var coopAcct = this.faves_coop.get(aURN);
  var individualSuccess = {};
  individualSuccess.numberOfAnswers  = 0;
  individualSuccess.listener = aListener;
  individualSuccess.coopAcct = coopAcct;
  individualSuccess.refreshNumber = 3;

  // get new message count
  var hr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
           .createInstance(Ci.nsIXMLHttpRequest);
  hr.onreadystatechange = function updateUserAccount_onreadystatechange(aEvent) {
    if (hr.readyState == 4) {
      var results = Cc["@mozilla.org/hash-property-bag;1"]
                    .createInstance(Ci.nsIWritablePropertyBag2);
      if (inst.webDetective.detectNoDOM("youtube", "accountinfo", "",
                                        hr.responseText, results))
      {
        // Set the message count
        var messages = "0";
        try {
          messages = results.getPropertyAsAString("messages");
          if (!messages || !messages.length) {
            messages = "0";
          }
        } catch (ex) {}
        
        inst._logger.debug("coopAcct.accountMessages " + messages);
        if (coopAcct.accountMessages < messages) {
          inst._lightPeopleIcon();
        }
        coopAcct.accountMessages = messages;

        // Set the avatar for the mecard.
        // If avatar returned as YT no avatar, then set coop avatar to
        // null and let people sidebar code set the Flock no avatar img.
        var fixedAvatar = null;
        try {
          var avatar = results.getPropertyAsAString("avatar");
          if (avatar.match(gStrings["noavatarimage"])) {
            avatar = null;
            inst._logger.debug("no avatar for account, setting to null\n");
          } else {
            fixedAvatar = inst._fixRelativeAvatarUrl(avatar);
            inst._logger.debug("coopAcct.accountAvatar " + coopAcct.avatar);
          }
        } catch (ex) {}
        coopAcct.avatar = fixedAvatar;
      } else {
        coopAcct.accountMessages = 0;
      }
      inst._onIndividualSuccess(individualSuccess);
    }
  }
  hr.open("GET", gStrings["userprofile"].replace("%accountid%", coopAcct.id()));

  // get friends
  var getFriendsListener = {
    onResult: function getFriendsListener_onResult(aXML) {
      if (coopAcct.isAuthenticated) {
        inst._handleFriendsResult(aXML, coopAcct, individualSuccess);
        hr.send(null);
      }
    },
    onError: function getFriendsListener_onError(aXML) {
      inst._logger.error("Error on getFriendsListener");
    }
  }
  var params = {};
  params.user = coopAcct.accountId;
  this.call(getFriendsListener, "youtube.users.list_friends", params);
}

youtubeService.prototype._onIndividualSuccess =
function youtubeService__onIndividualSuccess(aObj) {
  aObj.numberOfAnswers++;
  if (aObj.numberOfAnswers >= aObj.refreshNumber) {
    if (this.acUtils.isPeopleSidebarOpen()) {
      aObj.coopAcct.nextRefresh = new Date(Date.now()
                                           + YOUTUBE_SHORT_INTERVAL * 1000);
    }
    if (aObj.listener) {
      aObj.listener.onResult();
    }
  }
}

youtubeService.prototype.markAllMediaSeen =
function youtubeService_markAllMediaSeen(aIdentityUrn) {
  var identity = this.faves_coop.get(aIdentityUrn);
  identity.unseenMedia = 0;
} 

youtubeService.prototype._lightPeopleIcon =
function youtubeService__lightPeopleIcon() {
  this._logger.debug("._lightPeopleIcon()");
  this.obs.notifyObservers(null, "new-people-notification", null);
}

youtubeService.prototype._updateFriendAvatars =
function youtubeService__updateFriendAvatars(aAccountURN, aInvidualSuccessObj) {
  this._logger.info("._updateFriendAvatars");

  if (this._updateFriendAvatarsComplete || !aAccountURN) {
    return;
  }

  // Array of friends accounts (coop objects, not URNs) used to get avatars
  var friendsList = [];

  // Retrieve the account's friends list and add friends currently without an
  // avatar to a list to be updated at a later time
  var c_account = this.faves_coop.get(aAccountURN);
  var friendsEnum = c_account.friendsList.children.enumerate();
  while (friendsEnum.hasMoreElements()) {
    var friend = friendsEnum.getNext();
    if (friend.avatar == "") {
      this._logger.debug("adding friend with missing avatar for updating: "
                         + friend.accountId + "\n");
      friendsList.push(friend);
      // Increment number count that onIndividualSucess needs to count to
      aInvidualSuccessObj.refreshNumber++;
    }
  }

  var inst = this;

  var friendsTimerCallback = {
    notify: function friendNotify(aTimer) {
      // Scrape the friend page for their avatar
      if (friendsList.length > 0 && c_account.isAuthenticated) {
        var friend = friendsList.shift();
        var page = gStrings["userprofile"]
                   .replace("%accountid%", friend.accountId);
        var xhr = Cc[XHR_CONTRACTID].createInstance(Ci.nsIXMLHttpRequest);
        xhr.open("GET", page, true);

        xhr.onreadystatechange = function friend_OnReadyStateChange(aEvent) {
          if (xhr.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
            if (xhr.status == HTTP_CODE_OK) {
              var results = Cc[PROPERTY_BAG_CONTRACTID]
                            .createInstance(Ci.nsIWritablePropertyBag2);
              if (c_account.isAuthenticated &&
                  inst.webDetective.detectNoDOM("youtube", "page", "",
                                                xhr.responseText, results))
              {
                // If avatar returned as YT no avatar, then set coop avatar to
                // null and let people sidebar code set the Flock no avatar img.
                if (results.getPropertyAsAString("avatar")
                           .match(gStrings["noavatarimage"])) {
                  friend.avatar = null;
                  inst._logger.debug("no avatar for friend: " + friend.accountId + "\n");
                } else {
                  friend.avatar = results.getPropertyAsAString("avatar");
                  friend.avatar = inst._fixRelativeAvatarUrl(friend.avatar);
                  inst._logger.debug("found friend's avatar: " + friend.accountId
                                     + ", " + friend.avatar + "\n");
                }
              }
              inst._onIndividualSuccess(aInvidualSuccessObj);
            }
          }
        }

        inst._logger.debug("requesting avatar for: " + page + "\n");
        xhr.send(null);

      } else {
        // Kill the timer if there's nothing left to update in the array or if
        // the account is no longer authenticated
        inst._logger.debug("no friends without avatars left to update, "
                           + "kill the timer");
        inst._friendsTimer.cancel();
        inst._friendsTimer = null;
      }
    }
  };

  // Start the timer to update the account's friends avatars
  if (!this._friendsTimer && friendsList.length > 0) {
    this._logger.debug("found friends without avatars to update, "
                       + "start the timer");
    this._friendsTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    this._friendsTimer.initWithCallback(friendsTimerCallback,
                                        FRIENDS_AVATAR_TIMEOUT,
                                        Ci.nsITimer.TYPE_REPEATING_SLACK);

    // Once we've started updating friends without avatars, we only want to
    // check once per session so as not to incur a performance hit
    this._updateFriendAvatarsComplete = true;
  }
}

youtubeService.prototype._updateFriendAvatar =
function youtubeService__updateFriendAvatar(aDocument, aAccountID) {
  if (aDocument && aAccountID) {
    var results = Cc[PROPERTY_BAG_CONTRACTID]
                  .createInstance(Ci.nsIWritablePropertyBag2);

    if (this.webDetective.detect("youtube", "page", aDocument, results)) {
      var friendID = results.getPropertyAsAString("friend");

      // Only need to update the friend avatar if we're visiting the friend's page
      if (aAccountID != friendID) {
        var friendURN = _getIdentityUrn(aAccountID, friendID);
        if (friendURN) {
          var friend = this.faves_coop.get(friendURN);
          var avatar = results.getPropertyAsAString("avatar");
          avatar = this._fixRelativeAvatarUrl(avatar);

          // Only need to update the friend avatar if it's changed
          if (friend && friend.avatar != avatar) {
            friend.avatar = avatar;
          }
        }
      }
    }
  }
}

/**
 * The avatar URL we scraped may be a relative URL. If so, prepend
 * "http://www.youtube.com/" to it.
 */
youtubeService.prototype._fixRelativeAvatarUrl = 
function youtubeService__fixRelativeAvatarUrl(aUrl) {
  var url = aUrl;

  if (url.indexOf("http") != 0) {
    url = "http://www.youtube.com";
    // Do we need to add a leading slash?
    if (aUrl.indexOf("/") != 0) {
      url += "/";
    }
    url += aUrl;
  }

  return url;
}

youtubeService.prototype._personUpdateRequired =
function youtubeService__personUpdateRequired(aCoopPerson, aPerson) {
  return (aCoopPerson.name != aPerson.accountId);
}

youtubeService.prototype.addCoopPerson =
function youtubeService_addCoopPerson(aPhotoPerson, aCoopAccount)
{
  var person = aPhotoPerson;
  var personURL = this.webDetective.getString("youtube", "friendprofile", "")
                                   .replace("%accountid%", person.accountId);
  var identityUrn = _getIdentityUrn(aCoopAccount.accountId,
                                    person.accountId);
  var updating = this.faves_coop.Identity.exists(identityUrn);
  var identity;
  if (updating) {
    identity = this.faves_coop.get(identityUrn);
    if (this._personUpdateRequired(aCoopAccount,person)) {
      // Update data of the identity coop obj here
      identity.name = person.accountId;
      identity.URL = personURL;
      this._incrementMedia(identity, person);
    }
  } else {
    identity = new this.faves_coop.Identity(
      identityUrn,
      {
        name: person.accountId,
        serviceId: YOUTUBE_CONTRACTID,
        accountId: person.accountId,
        avatar: "",
        statusMessage: "",
        lastUpdateType: "media",
        lastUploadedMedia: person.videoCount,
        URL: personURL
      }
    );
    aCoopAccount.friendsList.children.add(identity);
    this._addIdentityToActivityArray(identity);
  }
}

youtubeService.prototype._addIdentityToActivityArray =
function youtubeService__addIdentityToActivityArray(aIdentity) {
  this._logger.info("._addIdentityToActivityArray('"+ aIdentity.name + "')");
  
   //only want to add to array when friendsActivity timer is ready
  if (!this._updateFriendActivityComplete) {
    this._friendActivityArray.push(aIdentity);
  }
}

youtubeService.prototype._incrementMedia =
function youtubeService__incrementMedia(identity, person) {
  this._logger.info("._incrementMedia('"+identity.id() + "," + person.accountId +"')");
  
  if (identity.lastUploadedMedia != person.videoCount) {
    identity.lastUploadedMedia = person.videoCount;
    identity.unseenMedia = 1;
    this._addIdentityToActivityArray(identity);
  }
}

youtubeService.prototype._updateFriendRecentActivity =
function youtubeService__updateFriendRecentActivity(aIndividualSuccess) {
  this._logger.info("._updateFriendRecentActivity");
  var success = aIndividualSuccess;
  if (this._updateFriendActivityComplete ||
     this._friendActivityArray.length == 0)
  {
    this._onIndividualSuccess(success);
    return;
  }
  // We are successful for refresh after each friend in _friendActivityArray
  // list is updated
  success.refreshNumber += (this._friendActivityArray.length -1);
  var inst = this;

  var friendsTimerCallback = {
    notify: function friendNotify(aTimer) {
      inst._updateFriendActivityComplete = true;
      while(inst._friendActivityArray.length != 0)
      {
        var identity = inst._friendActivityArray.pop();
        inst._updateLastUpdateDate(identity, success);
        inst._logger.info("updating recent activity for " + identity.name);
      }
      inst._logger.debug("no friends without avatars left to update, "
                          + "kill the timer");
      inst._friendsActivityTimer.cancel();
      inst._friendsActivityTimer = null;
      inst._updateFriendActivityComplete = false;
      inst._friendActivityArray = null;
      inst._friendActivityArray = [];
      inst._logger.info("done updating recent activity");
    }
  };

  // Start the timer to update the account's friends avatars
  if (!this._friendsActivityTimer) {
    this._logger.debug("found friends without avatars to update, "
                       + "start the timer");
    this._friendsActivityTimer = Cc["@mozilla.org/timer;1"]
                                   .createInstance(Ci.nsITimer);
    this._friendsActivityTimer.initWithCallback(friendsTimerCallback,
                                        FRIENDS_ACTIVITY_TIMEOUT,
                                        Ci.nsITimer.TYPE_REPEATING_SLACK);
    this._updateFriendActivityComplete = false;
  }
}

youtubeService.prototype._updateLastUpdateDate =
function youtubeService__updateLastUpdateDate(aIdentity, aInvidualSuccessObj)
{
  this._logger.info("._updateLastUpdateDate('"+ aIdentity.name + "')");
  
  var identity = aIdentity;
  var params = {
    user: identity.name,
    page: 1,
    per_page: 1,
  };

  var inst = this;
  var myListener = {
    onResult: function (aXML) {
      inst._logger.info("onResult");
      var video = inst.handlePhotosResult(aXML);
      if (video.length == 1) {
        video = video[0];
        //Youtube gives us UNIX time in seconds, change to miliseconds
        var time = video.uploadDate / 1000;
        identity.lastUpdate = time;
      }
      inst._onIndividualSuccess(aInvidualSuccessObj);
    },
    onError: function (aError) {
      if (!identity.lastUpdate) {
        identity.lastUpdate = inst._numberFromString(identity.name);
      }
    }
  };

  this.call(myListener, "youtube.videos.list_by_user", params);
}

youtubeService.prototype._handleFriendsResult =
function youtubeService__handleFriendsResult(aXML, aAccount,
                                             aInvidualSuccessObj)
{
  DEBUG("._handleFriendsResult(aXML, aAccount)");
  
  var friendList = aXML.getElementsByTagName("friend");
  var inst = this;
  var peopleHash = [];
  DEBUG(" - found "+friendList.length+" friends");
  for (var i = 0; i < friendList.length; i++) {
    var friend = friendList[i];
    var friendObj = {};
    for (var j = 0; j < friend.childNodes.length; j++)
    {
      var child = friend.childNodes[j];
      var contentNode = child.childNodes[0];
      var childContent = "";
      if (contentNode) childContent = contentNode.nodeValue;
      switch (child.tagName)
      {
        case "user":
          friendObj.accountId = childContent;
          friendObj.name = childContent;
          break;
        case "video_upload_count":
          friendObj.videoCount = childContent;
          break;
      }
    }
    peopleHash[friendObj.accountId] = friendObj;
    peopleHash[friendObj.accountId].media = {
      count: 1,
      latest: friendObj.videoCount
    };
  }
  function myWorker(aShouldYield) {
    // ADD or update existing people
    for (var uid in peopleHash) {
      if (!aAccount.isAuthenticated) {
        // If this account is no longer authenticated, bail out.
        return;
      }
      inst.addCoopPerson(peopleHash[uid], aAccount);
      if (aShouldYield()) {
        yield;
      }
    }
    
    // REMOVE locally people removed on the server
    var localEnum = aAccount.friendsList.children.enumerate();
    while (localEnum.hasMoreElements()) {
      var identity = localEnum.getNext();
      if (!peopleHash[identity.accountId]) {
        inst._logger.info("Friend " + identity.accountId
                                    + " has been deleted on the server");
        aAccount.friendsList.children.remove(identity);
        identity.destroy();
      }
    }
    inst._onIndividualSuccess(aInvidualSuccessObj);
    inst._updateFriendRecentActivity(aInvidualSuccessObj);
    inst._updateFriendAvatars(aAccount.id(), aInvidualSuccessObj);
  } // end myWorker()
    
  FlockScheduler.schedule(null, 0.05, 10, myWorker);
}

var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
                       .getService(Components.interfaces.mozIJSSubScriptLoader);
loader.loadSubScript("chrome://browser/content/utilityOverlay.js");

// BEGIN flockIWebService interface
youtubeService.prototype.addAccount =
function youtubeService_addAccount(aAccountID, aIsTransient, aListener)
{
  DEBUG("{flockIWebService}.addAccount('"+aAccountID+"', "+aIsTransient+", aListener)");

  var url = gStrings["userprofile"].replace("%accountid%", aAccountID);
  var accountURN = this.acUtils.createAccount(this,
                                              aAccountID,
                                              aAccountID,
                                              url,
                                              aIsTransient);
  var account = this.faves_coop.get(accountURN);
  this.USER = accountURN;

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

  // Instanciate account component
  var acct = this.getAccount(account.id());
  if (aListener) aListener.onSuccess(acct, "addAccount");
  return acct;
}
// END flockIWebService interface


// BEGIN flockIManageableWebService interface
youtubeService.prototype.updateAccountStatusFromDocument =
function youtubeService_updateAccountStatusFromDocument(aDocument, aAcctURN)
{
  this._logger.info("updateAccountStatusFromDocument('" + aAcctURN + "')");
  if (aAcctURN) {
    // We're logged in to this account
    var c_acct = this.faves_coop.get(aAcctURN);
    if (!c_acct.isAuthenticated) {
      var inst = this; 
      var loginListener = {
        onSuccess: function loginListener_onSuccess() {
          inst._logger.debug(".updateAccountStatusFromDocument(): "
                             + "loginListener: onSuccess()");
          c_acct.isAuthenticated = true;
          inst._updateFriendAvatarsComplete = false;
        },
        onError: function loginListener_onError(aError) {
          inst._logger.debug(".updateAccountStatusFromDocument(): "
                             + "loginListener: onError()");
          inst._logger.debug(aError ? (aError.errorString) : "No details");
        }
      };
      this.getAccount(aAcctURN).login(loginListener);
    }
  } else if (this.webDetective
                 .detect("youtube", "loggedout", aDocument, null))
  {
    // We're logged out (of all accounts)
    this.acUtils.markAllAccountsAsLoggedOut(YOUTUBE_CONTRACTID);
  }
}
// END flockIManageableWebService interface


// BEGIN flockISocialWebService interface
// END flockISocialWebService interface


// BEGIN flockIMediaWebService interface
// readonly attribute boolean supportsUsers;
youtubeService.prototype.supportsUsers = true;

youtubeService.prototype.migrateAccount =
function youtubeService_migrateAccount(aId, aUsername) {
  this.init();
  this.addAccount(aId, false, null);
}

youtubeService.prototype.decorateForMedia =
function youtubeService_decorateForMedia(aDocument)
{
  DEBUG("{flockIMediaWebService}.decorateForMedia(aDocument)");
  aDocument.QueryInterface(Components.interfaces.nsIDOMHTMLDocument);
  var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                          .createInstance(Components.interfaces.nsIWritablePropertyBag2);
  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);
    var media = {
      name: userid,
      query: 'favorites:' + userid,
      label: favname,
      favicon: this.icon,
      service: this.shortName
    }
    mediaArr.push(media);

    if (!aDocument._flock_decorations) {
      aDocument._flock_decorations = {};
    }
    aDocument._flock_decorations.mediaArr = mediaArr;
    this.obs.notifyObservers(aDocument, 'media', 'media:update');
  }
}

// END flockIMediaWebService interface

// BEGIN flockIMediaEmbedWebService interface

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

youtubeService.prototype.getMediaQueryFromURL =
function youTubeService_getMediaQueryFromURL(aUrl, aListener)
{
  var videoId = this.getVideoIDFromUrl(aUrl);
  if (videoId) {
    var myListener = {
      onResult: function (aXML) {
        var userID = aXML.getElementsByTagName('author')[0].firstChild.nodeValue;
        var results = Components.classes["@mozilla.org/hash-property-bag;1"]
                                .createInstance(Components.interfaces.nsIWritablePropertyBag2);
        results.setPropertyAsAString("query", "user:" + userID + "|username:" + userID);
        // Note: The "title" property is currently not used by the media bar.
        results.setPropertyAsAString("title", userID);
        aListener.onSuccess(results, "query");
      },
      onError: function (aError) {
       aListener.onError(null, aError, null);
      }
    }
    var params = {};
    params.video_id = videoId;
    this.call(myListener, "youtube.videos.get_details", params);
  } else {
    aListener.onError(null, "Unable to get user.", null);
  }
}

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

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 detectResults = Cc["@mozilla.org/hash-property-bag;1"]
                      .createInstance(Ci.nsIWritablePropertyBag2);
  if (this.webDetective.detectNoDOM("youtube",
                                    "videoId",
                                    "",
                                    aUrl,
                                    detectResults))
  {
    videoId = detectResults.getPropertyAsAString("videoId");
  }
  return videoId;
}

// Checks if the TEXTAREA is drag and droppable
youtubeService.prototype._isDnDableTextarea =
function youtubeService__isDnDableTextarea(aDocument, aXPath, aTextarea)
{
  if (aDocument && aXPath && aTextarea) {
    var xpath = this.webDetective.getString("youtube", aXPath, "");
    var results = aDocument.evaluate(xpath, aDocument, null,
                                     Ci.nsIDOMXPathResult.ANY_TYPE, null);
    if (results && results.iterateNext() == aTextarea) {
      return true;
    }
  }
  return false;
}


// BEGIN flockIRichContentDropHandler
youtubeService.prototype.handleDrop =
function youtubeService_handleDrop(aFlavours, aTextarea)
{
  this._logger.info(".handleDrop()");

  var dropCallback = function youtube_dropCallback(aFlav) {
    var data = {}, len = {};
    aFlavours.getTransferData(aFlav, data, len);
    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"))
                      ? ""
                      : " ";
    // Only add a breadcrumb if the insertion point is at the end of
    // the text so that we don't duplicate breadcrumbs
    var breadcrumb = (aTextarea.value.length == aTextarea.selectionEnd)
                   ? Cc[FLOCK_RDDS_CONTRACTID]
                       .getService(Ci.flockIRichDNDService)
                       .getBreadcrumb("plain")
                   : "";

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

  return this._handleTextareaDrop(CATEGORY_ENTRY_NAME, this.ytService.domains,
                                  aTextarea, dropCallback);
}
// END flockIRichContentDropHandler

youtubeService.prototype.maxStatusLength = 0;

// ========== END youtubeService class ==========



// ================================================
// ========== BEGIN youtubeAccount class ==========
// ================================================

function youtubeAccount()
{
  this.acUtils = Components.classes["@flock.com/account-utils;1"]
                           .getService(Components.interfaces.flockIAccountUtils);
  this.service = Components.classes[YOUTUBE_CONTRACTID]
                           .getService(Components.interfaces.flockIWebService)
                           .QueryInterface(Components.interfaces.flockISocialWebService);
  this._coop = Components.classes["@flock.com/singleton;1"]
                         .getService(Components.interfaces.flockISingleton)
                         .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                         .wrappedJSObject;
  this.webDetective = Cc["@flock.com/web-detective;1"]
                      .getService(Ci.flockIWebDetective);
  this._ctk = {
    interfaces: [
      "nsISupports",
      "flockIWebServiceAccount",
      "flockIMediaWebServiceAccount",
      "flockISocialWebServiceAccount",
      "flockIYoutubeAccount"
    ]
  };
  getCompTK().addAllInterfaces(this);

  this._logger = Cc["@flock.com/logger;1"].createInstance(Ci.flockILogger);
  this._logger.init("youtubeAccount");
}

// BEGIN 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 id = FAV_CHANNELS_ROOT + ":" + this.service.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: this.service.shortName,
    favicon: this.service.icon,
    isPollable: true,
    hasUnseenItems: false
  });

  this._coop.media_favorites.children.addOnce(item);
}
// END account helper

// BEGIN flockIWebServiceAccount interface
youtubeAccount.prototype.urn = "";
youtubeAccount.prototype.username = "";
youtubeAccount.prototype.status = "";

youtubeAccount.prototype.activate =
function youtubeAccount_activate(aListener)
{
  DEBUG("{flockIWebServiceAccount}.activate()");
  var acctCoopObj = this._coop.get(this.urn);
  acctCoopObj.nextRefresh = new Date(0);
  acctCoopObj.isPollable = true;
  this.addMediaToFavorites(acctCoopObj.accountId);
  if (aListener) {
    aListener.onSuccess(this, "accountAuthorized");
  }
}

youtubeAccount.prototype.login =
function youtubeAccount_login(aListener)
{
  DEBUG("{flockIWebServiceAccount}.login()");
  this.acUtils.ensureOnlyAuthenticatedAccount(this.urn);
  this.setParam("isPollable", true);
  // force refresh on login
  var pollerSvc = Cc["@flock.com/poller-service;1"]
                  .getService(Ci.flockIPollerService);
  pollerSvc.forceRefresh(this.urn);
  aListener.onSuccess();
}
// END flockIWebServiceAccount interface


// BEGIN flockISocialWebServiceAccount interface
youtubeAccount.prototype.hasFriendActions = true;
youtubeAccount.prototype.isStatusSupported = false;
youtubeAccount.prototype.isStatusEditable  = false;
youtubeAccount.prototype.isPostLinkSupported = false;
youtubeAccount.prototype.isMyMediaFavoritesSupported = true;

youtubeAccount.prototype.setStatus =
function youtubeAccount_setStatus(aStatusMessage, aListener)
{
  this._logger.info("{flockISocialWebServiceAccount}.setStatus('"+aStatusMessage+"',"+
                                                              "'"+aListener+"')");
}

youtubeAccount.prototype.getEditableStatus =
function youtubeAccount_getEditableStatus()
{
  this._logger.info("{flockISocialWebServiceAccount}.getEditableStatus()");
  return "";
}

youtubeAccount.prototype.formatStatusForDisplay =
function youtubeAccount_formatStatusForDisplay(aStatusMessage)
{
  return "";
}

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.info("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", "")
    });
  }
  var c_acct = this._coop.get(this.urn);
  _addNotie("meMessages", c_acct.accountMessages);
  return JSON.toString(noties);
}

youtubeAccount.prototype.markAllMeNotificationsSeen =
function youtubeAccount_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;
  }
}

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) {
    var c_acct = this._coop.get(this.urn);
    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%", c_acct.accountId)
                  .replace("%friendid%", c_friend.accountId)
      });
    }
  }
  return JSON.toString(actions);
}

youtubeAccount.prototype.getSharingAction =
function youtubeAccount_getSharingAction(aFriendURN, aTransferable)
{
  this._logger.info(".getSharingAction('" + aFriendURN + "')");

  var sharingAction = "";
  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_RDDS_CONTRACTID]
                  .getService(Ci.flockIRichDNDService)
                  .getMessageFromTransferable(aTransferable,
                                              flavors.length,
                                              flavors);
    if (!message.body) {
      return sharingAction;
    } else {
      var videoId = this.service.getVideoIDFromUrl(message.body);
      if (videoId) {
        sharingAction = this.webDetective.getString("youtube",
                                                    "shareAction_sharevideo",
                                                    "");
      } else {
        sharingAction = this.webDetective.getString("youtube",
                                                    "shareAction_privatemessage",
                                                    "");
      }
    }
  }
  this._logger.info(sharingAction);
  return sharingAction;
}

youtubeAccount.prototype.getProfileURLForFriend =
function youtubeAccount_getProfileURLForFriend(aFriendURN)
{
  this._logger.info(".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;
}

youtubeAccount.prototype.getPostLinkAction =
function youtubeAccount_getPostLinkAction(aTransferable)
{
  return "";
}
// END flockISocialWebServiceAccount interface

// BEGIN flockIYoutubeAccount interface
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);
}

youtubeAccount.prototype.youtubePrivateMessage =
function YoutubeAccount_youtubePrivateMessage(aFriendURN, aTransferable)
{
  this._logger.info(".youtubePrivateMessage('" + aFriendURN + "')");

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

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

youtubeAccount.prototype.youtubeShareVideo =
function youtubeAccount_youtubeShareVideo(aFriendURN, aTransferable)
{
  this._logger.info(".youtubeShareVideo('" + aFriendURN + "')");

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

  var message = Cc[FLOCK_RDDS_CONTRACTID]
                .getService(Ci.flockIRichDNDService)
                .getMessageFromTransferable(aTransferable,
                                            flavors.length,
                                            flavors);
  if (message.body) {
    var inst = this;
    var share_url = inst.webDetective.getString("youtube",
                                                "shareIframeURL", "");
    var obs = Cc["@mozilla.org/observer-service;1"]
                .getService(Ci.nsIObserverService);
    var videoId = this.service.getVideoIDFromUrl(message.body);
    var url = this.webDetective.getString("youtube", "youtubeShare_URL", "")
                  .replace("%videoid%", videoId);
    var wm = Cc["@mozilla.org/appshell/window-mediator;1"]
               .getService(Ci.nsIWindowMediator);
    var win = wm.getMostRecentWindow("navigator:browser");
    var dimen = inst.webDetective.getString("youtube",
                                            "shareAction_sharevideo_features",
                                            "");
    win.open(url, "Share", dimen);
    win = wm.getMostRecentWindow("navigator:browser");
    flockDocumentReadyObserver = {
      observe: function openShareVideo_FormFill (aSubject, aTopic, aData) {
        if (aData == share_url) {
          obs.removeObserver(this, "FlockDocumentReady");
          var contentWindow = win.gBrowser.docShell
                                 .QueryInterface(Ci.nsIInterfaceRequestor)
                                 .getInterface(Ci.nsIDOMWindow);

          function insertContent(aDoc, aXpathquery, aAction, aMessage) {
            var formItems = aDoc.evaluate(aXpathquery, aDoc, null,
                                          Ci.nsIDOMXPathResult.ANY_TYPE, null);
            if (formItems) {
              var formItem = formItems.iterateNext();
              if (!formItem) {
                return;
              }
              switch (aAction) {
                case "click":
                  formItem.click();
                  break;

                case "username":
                  if (formItem.hasAttribute("value")) {
                    formItem.setAttribute("value", aMessage);
                  }
                  break;

                case "message":
                  var textNode = doc.createTextNode(aMessage);
                  formItem.appendChild(textNode);
                  break;
              
                default:
                  inst._logger.debug("no action associated with " + aAction);
              }
            }
          }
          var doc = contentWindow.document;
          // Insert Flock bread crumb
          var breadcrumb = Cc[FLOCK_RDDS_CONTRACTID]
                             .getService(Ci.flockIRichDNDService)
                             .getBreadcrumb("plain");
          if (breadcrumb) {
            var webDicName = "youtubeshareVideo_message";
            var xpathquery = inst.webDetective.getString("youtube",
                                                         webDicName, "");
            insertContent(doc, xpathquery, "message", breadcrumb);
          }

          // Insert friend trying to save to into iframe
          webDicName = "youtubeshareVideo_IframeXPath";
          xpathquery = inst.webDetective.getString("youtube",
                                                   webDicName, "");
          var formItems = doc.evaluate(xpathquery, doc, null,
                                       Ci.nsIDOMXPathResult.ANY_TYPE, null);
          if (formItems) {
            var iframe = formItems.iterateNext();
            if (iframe) {
              var c_friend = inst._coop.get(aFriendURN);
              webDicName = "youtubeshareVideo_friendCheckXPath";
              xpathquery = inst.webDetective.getString("youtube", webDicName, "")
                                            .replace("%friendname%", c_friend.name);
              insertContent(iframe.contentDocument, xpathquery, "click");
            }
          }
        }
      }
    };
    obs.addObserver(flockDocumentReadyObserver, 'FlockDocumentReady', false);
  }
}

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();
           if (formItem.hasAttribute("value")) {
             formItem.setAttribute("value", aMessage);
           } else {
             var textNode = doc.createTextNode(aMessage);
             formItem.appendChild(textNode);
             inst._logger.info("aMessage " + aMessage);
           }
         }
       }
       if (contentWindow == aContent) {
         obs.removeObserver(this, "EndDocumentLoad");
         insertContent("youtubemessage_subjectXPath", subject);
         if (addBreadCrumb) {
           // Add breadcrumb to message body
            var breadcrumb = Cc[FLOCK_RDDS_CONTRACTID]
                               .getService(Ci.flockIRichDNDService)
                               .getBreadcrumb("plain");
           if (breadcrumb) {
             body += breadcrumb;
           }
        }
         insertContent("youtubemessage_bodyXPath", body);
       }
     }
   };
   obs.addObserver(observer, "EndDocumentLoad", false);
  }
}
// END flockIYoutubeAccount interface

// ========== END youtubeAccount class ==========



// ==============================================
// ========== BEGIN XPCOM registration ==========
// ==============================================

function createModule(aParams) {
  return {
    registerSelf: function (aCompMgr, aFileSpec, aLocation, aType) {
      aCompMgr.QueryInterface(Ci.nsIComponentRegistrar);
      aCompMgr.registerFactoryLocation( aParams.CID, aParams.componentName,
                                        aParams.contractID, aFileSpec,
                                        aLocation, aType );
      var catMgr = Cc["@mozilla.org/categorymanager;1"]
        .getService(Ci.nsICategoryManager);
      if (!aParams.categories) { aParams.categories = []; }
      for (var i = 0; i < aParams.categories.length; i++) {
        var cat = aParams.categories[i];
        catMgr.addCategoryEntry( cat.category, cat.entry,
                                 cat.value, true, true );
      }
    },
    getClassObject: function (aCompMgr, aCID, aIID) {
      if (!aCID.equals(aParams.CID)) { throw Cr.NS_ERROR_NO_INTERFACE; }
      if (!aIID.equals(Ci.nsIFactory)) { throw Cr.NS_ERROR_NOT_IMPLEMENTED; }
      return { // Factory
        createInstance: function (aOuter, aIID) {
          if (aOuter != null) { throw Cr.NS_ERROR_NO_AGGREGATION; }
          var comp = new aParams.componentClass();
          if (aParams.implementationFunc) { aParams.implementationFunc(comp); }
          return comp.QueryInterface(aIID);
        }
      };
    },
    canUnload: function (aCompMgr) { return true; }
  };
}

// NS Module entrypoint
function NSGetModule(aCompMgr, aFileSpec) {
  return createModule({
    componentClass: youtubeService,
    CID: YOUTUBE_CID,
    contractID: YOUTUBE_CONTRACTID,
    componentName: CATEGORY_COMPONENT_NAME,
    implementationFunc: function (aComp) { getCompTK().addAllInterfaces(aComp); },
    categories: [
      { category: "wsm-startup", entry: CATEGORY_COMPONENT_NAME, value: YOUTUBE_CONTRACTID },
      { category: "flockWebService", entry: CATEGORY_ENTRY_NAME, value: YOUTUBE_CONTRACTID },
      { category: "flockMediaProvider", entry: CATEGORY_ENTRY_NAME, value: YOUTUBE_CONTRACTID },
      { category: "flockRichContentHandler", entry: CATEGORY_ENTRY_NAME, value: YOUTUBE_CONTRACTID }
    ]
  });
}

// ========== END XPCOM registration ==========



// HELPER STUFF

function loadSubScript(spec)
{
  var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
                         .getService(Components.interfaces.mozIJSSubScriptLoader);
  var context = {};
  loader.loadSubScript(spec, context);
  return context;
}

function loadLibraryFromSpec(aSpec)
{
  var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
                         .getService(Components.interfaces.mozIJSSubScriptLoader);
  loader.loadSubScript(aSpec);
}
