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

const Cc = Components.classes;
const Ci = Components.interfaces;

Components.utils.import("resource:///modules/FlockStringBundleHelpers.jsm");
Components.utils.import("resource:///modules/FlockScheduler.jsm");

const FLOCK_PHOTO_API_MANAGER_CID               = Components.ID('{18e27f2e-c99c-4915-9907-1a9fc0780ed6}');

const nsISupports                   = Components.interfaces.nsISupports;
const nsIClassInfo                  = Components.interfaces.nsIClassInfo;
const nsIFactory                    = Components.interfaces.nsIFactory;
const nsIProperties                 = Components.interfaces.nsIProperties;
const nsILocalFile                  = Components.interfaces.nsILocalFile;
const nsIFile                       = Components.interfaces.nsIFile;
const nsIIOService                  = Components.interfaces.nsIIOService;
const nsIFileProtocolHandler        = Components.interfaces.nsIFileProtocolHandler;
const nsIPreferenceService          = Components.interfaces.nsIPrefBranch;
const flockIPhotoAPIManager         = Components.interfaces.flockIPhotoAPIManager;
const flockIPhotoPeopleService      = Components.interfaces.flockIPhotoPeopleService;
const flockIPollingService          = Components.interfaces.flockIPollingService;
const flockIMigratable              = Components.interfaces.flockIMigratable;

const FLOCK_PHOTO_API_MANAGER_CONTRACTID        = '@flock.com/photo-api-manager;1?';
const FLOCK_PHOTO_CONTRACTID        = '@flock.com/photo;1';
const DIRECTORY_SERVICE_CONTRACTID  = '@mozilla.org/file/directory_service;1';
const LOCAL_FILE_CONTRACTID         = '@mozilla.org/file/local;1';
const PREFERENCES_CONTRACTID        = '@mozilla.org/preferences-service;1';
const IO_SERVICE_CONTRACTID         = '@mozilla.org/network/io-service;1';
const RDFCU_CONTRACTID              = '@mozilla.org/rdf/container-utils;1';
const MYWORLD_SERVICE_CONTRACTID    = '@flock.com/myworld-service;1';
const FLOCK_CATEGORY_MANAGER_CONTRACT_ID = "@mozilla.org/categorymanager;1";

const RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
const FLOCK_NS = "http://flock.com/rdf#";
const NC_NS = "http://home.netscape.com/NC-rdf#";
const FLOCK_SHARE_CONTENT_HANDLER = "flockShareContentHandler";

/* for Cardinal migration */
const OLD_PEOPLE_PHOTO_RDF_FILE = 'flock_people_photo.rdf';
const OLD_PEOPLE_PHOTO_RDF_FILE_RELIC = 'flock_people_photo_old.rdf';
const LIST_ACCOUNTS_ROOT = 'urn:flock:people:photo:lists:accounts';
const MEDIA_FAVES_ROOT = 'urn:media:favorites';
const FLICKR_STRING_BUNDLE = "services/flickr";

const RDFS = Components.classes['@mozilla.org/rdf/rdf-service;1']
           .getService (Components.interfaces.nsIRDFService);
const IOS = Components.classes[IO_SERVICE_CONTRACTID]
          .getService(Components.interfaces.nsIIOService);
const RDFCU = Components.classes[RDFCU_CONTRACTID]
            .getService(Components.interfaces.nsIRDFContainerUtils);

function flockPhotoAPIManager() {
    this.processingQueue = {};
    this.mPrefService = Components.classes[PREFERENCES_CONTRACTID].getService(nsIPreferenceService);

    this.mLogger = Components.classes["@flock.com/logger;1"].createInstance(Components.interfaces.flockILogger);
    this.mLogger.init("photo");
    this.mLogger.info("starting up...");
    this._coop = Cc["@flock.com/singleton;1"].getService(Ci.flockISingleton)
                 .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                 .wrappedJSObject;
    this.mLastSearchRequestId = new Date().getTime();
    var inst = this;
    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    var cb = {
      notify: function() {
        inst.updateStates();
      }
    }
    timer.initWithCallback(cb, 5 * 1000, Ci.nsITimer.TYPE_REPEATING_SLACK);
}

//flockIMigratable

flockPhotoAPIManager.prototype.__defineGetter__("migrationName",
function getter_migrationName() {
  return flockGetString("common/migrationNames", "migration.name.photoAPI");
});

flockPhotoAPIManager.prototype.needsMigration =
function needsMigration(oldVersion) {
  this.mLogger.debug(".needsMigration('" + oldVersion + "')");
  
  if (oldVersion.substr(0, 3) == "0.7") { // migration from Cardinal (0.7.x)
    var oldPeoplePhotoFile = Components.classes["@mozilla.org/file/directory_service;1"]
                           .getService(Components.interfaces.nsIProperties)
                           .get("ProfD", Components.interfaces.nsIFile);
    oldPeoplePhotoFile.append(OLD_PEOPLE_PHOTO_RDF_FILE);

    if (oldPeoplePhotoFile.exists()) {
      this.mLogger.info("needs migration from 0.7.x");
      return true;
    }
    return false;
  } else if (oldVersion.substr(0, 3) == "0.9") { // migration from 0.9.X
    this.mLogger.info("needs migration from 0.9.X");
    return true;
  } else if (oldVersion.substr(0, 4) == "1.0." ||
             oldVersion.substr(0, 5) == "1.0RC")
  {
    this.mLogger.info("needs migration from 1.0 or 1.0 point releases");
    return true;
  } else {
    return false;
  }
}

flockPhotoAPIManager.prototype.startMigration =
function startMigration(oldVersion, aFlockMigrationProgressListener) {
  this.mLogger.debug(".startMigration('" + oldVersion + "', 'aFlockMigrationProgressListener')");
  var ctxt = {
    oldVersion: oldVersion,
    listener: aFlockMigrationProgressListener,
    needMediaQueryIDFixup: false
  };

  if (oldVersion.substr(0, 3) == "0.7") { // migration from Cardinal (0.7.x)
    var oldPeoplePhotoFile = Components.classes["@mozilla.org/file/directory_service;1"]
                                       .getService(Components.interfaces.nsIProperties)
                                       .get("ProfD", Components.interfaces.nsIFile);
    oldPeoplePhotoFile.append(OLD_PEOPLE_PHOTO_RDF_FILE);

    ctxt.oldPeoplePhotoFile = oldPeoplePhotoFile;

    if (oldPeoplePhotoFile.exists())
      ctxt.listener.onUpdate(0, "Migrating photo people");

  } else if (oldVersion.substr(0, 3) == "0.9") { // Migration from 0.9.x
    ctxt.listener.onUpdate(0, "Migrating account and media queries");
    ctxt.needMediaQueryIDFixup = true;
  } else if (oldVersion.substr(0, 4) == "1.0_" ||
             oldVersion.substr(0, 5) == "1.0.1" ||
             oldVersion.substr(0, 5) == "1.0RC")
  {
    ctxt.needMediaQueryIDFixup = true;
  }

  return { wrappedJSObject: ctxt };
}

flockPhotoAPIManager.prototype.finishMigration =
function finishMigration(ctxtWrapper) {
}

flockPhotoAPIManager.prototype.doMigrationWork =
function doMigrationWork(ctxtWrapper) {
  var ctxt = ctxtWrapper.wrappedJSObject;
  var oldVersion = ctxt.oldVersion.substr(0, 3);

  if (oldVersion == "0.7") {
    if (!ctxt.oldPeoplePhotoFile.exists()) {
      return false;
    }

    if (!ctxt.photoMigrator) {
      ctxt.photoMigrator = this._migratePhotoPeople(ctxt);
    }
    if (ctxt.photoMigrator.next()) {
      ctxt.photoMigrator = null;
    }

    return Boolean(ctxt.photoMigrator);
  } else if (oldVersion == "0.9" || oldVersion == "1.0") {
    this._migrate09xPhotoAccount();
    this._migrateMediaQueries(oldVersion);
    this._sanitizePublicPhotosStream();
  }

  if (ctxt.needMediaQueryIDFixup) {
    this._fixupMediaQueryIDs();
  }

  return false;
}

// Clean up any user streams that don't have userid in the urn.
// i.e. urn's ending with "user:"
// These corrupted mediaqueries were created by buggy Cardinal migration code.
// c.f. https://bugzilla.flock.com/show_bug.cgi?id=11053
flockPhotoAPIManager.prototype._sanitizePublicPhotosStream =
function _sanitizePublicPhotosStream()
{
  const BAD_ID_POSTFIX = ":user:";

  var coop = Cc["@flock.com/singleton;1"]
             .getService(Components.interfaces.flockISingleton)
             .getSingleton("chrome://flock/content/common/load-faves-coop.js")
             .wrappedJSObject;

  var resources = coop.MediaQuery.all_ids();
  while (resources.hasMoreElements()) {
    var resource = resources.getNext();

    var id = resource.ValueUTF8;
    var urn = id.split("|")[0];
    if (urn.substr(urn.length - BAD_ID_POSTFIX.length) == BAD_ID_POSTFIX) {
      var oldQuery = coop.get_from_resource(resource);

      for each (var parent in oldQuery.getParents()) {
        parent.children.remove(oldQuery);
      }
      oldQuery.destroy();
    }
  }
}

// josh changed latestSeq property of mediaqueries from
// an int to a string - we need to migrate this type change
flockPhotoAPIManager.prototype._migrateMediaQueries =
function _migrateMediaQueries(aOldVersion)
{
  this.mLogger.debug("._migrateMediaQueries()");
  var dataFileDS = RDFS.GetDataSource("rdf:flock-favorites");

  // get all media query rdf nodes
  var mqResource = RDFS.GetResource(FLOCK_NS+"CoopType");
  var mqLiteral = RDFS.GetResource(NC_NS+"MediaQuery");
  var mediaQueries = dataFileDS.GetSources(mqResource, mqLiteral, true);

  function getTargetLiteral(aSource, aArcName) {
    var target = dataFileDS.GetTarget(aSource,
                                      RDFS.GetResource(FLOCK_NS+aArcName),
                                      true);
    target instanceof Components.interfaces.nsIRDFLiteral;
    return target;
  };
  function getTargetDate(aSource, aArcName) {
    var target = dataFileDS.GetTarget(aSource,
                                      RDFS.GetResource(FLOCK_NS+aArcName),
                                      true);
    target instanceof Components.interfaces.nsIRDFDate;
    return target;
  };
  function fixUnseenItems(aNode, aUnseenItems, aUnseenItemsPred) {
    var oldUnseenItems = dataFileDS.GetTarget(aNode, aUnseenItemsPred, true)
                                   .QueryInterface(Ci.nsIRDFLiteral);
    var unseenItems = RDFS.GetLiteral(aUnseenItems);
    if (unseenItems != oldUnseenItems) {
      dataFileDS.Change(aNode, aUnseenItemsPred, oldUnseenItems, unseenItems);
      var hasUnseenItemsPred = RDFS.GetResource(FLOCK_NS+"hasUnseenItems");
      var oldHasUnseen = dataFileDS.GetTarget(aNode, hasUnseenItemsPred, true)
                                   .QueryInterface(Ci.nsIRDFLiteral);
      var hasUnseenItems = RDFS.GetLiteral(aUnseenItems? "true" : "false");
      dataFileDS.Change(aNode, hasUnseenItemsPred, oldHasUnseen, hasUnseenItems);
    }
  };
  
  
  var unseenItemsPred = RDFS.GetResource(FLOCK_NS+"unseenItems");
  var totalUnseenItems = 0;

  var lSeqPred = RDFS.GetResource(FLOCK_NS+"latestSeq");
  while (mediaQueries && mediaQueries.hasMoreElements()) {
    var mediaQuery = mediaQueries.getNext();
    mediaQuery.QueryInterface(Components.interfaces.nsIRDFResource);
    // For latestSeq, make sure it is not the old int value (0.9.0)
    var latestSeqInt = dataFileDS.GetTarget(mediaQuery, lSeqPred, true);
    if (latestSeqInt instanceof Ci.nsIRDFInt) {
      var latestSeqString = RDFS.GetLiteral(latestSeqInt.Value);
      dataFileDS.Change(mediaQuery, lSeqPred, latestSeqInt, latestSeqString);
    }
    var unseenItems = dataFileDS.GetTarget(mediaQuery, unseenItemsPred, true);
    unseenItems.QueryInterface(Ci.nsIRDFLiteral);
    totalUnseenItems += parseInt(unseenItems.Value);
    unseenItems = 0; // now calculate the real count for that query.
    var picsContainer = Cc["@mozilla.org/rdf/container;1"]
                        .createInstance(Ci.flockIRDFContainer);
    picsContainer.Init(dataFileDS, mediaQuery);
    var pics = picsContainer.GetElements();
    while (pics.hasMoreElements()) 
    {
      var unseen = getTargetLiteral(pics.getNext(), "unseen");
      if (unseen && unseen.Value == "true") {
        unseenItems++;
      }
    }
    fixUnseenItems(mediaQuery, unseenItems, unseenItemsPred)
    if (aOldVersion == "1.0") {
      continue;
    }
    // Migrate the favicon url
    var iconPred = RDFS.GetResource(FLOCK_NS+"favicon"); 
    var faviconLit = getTargetLiteral(mediaQuery, "favicon");
    var svcShortNameLit = getTargetLiteral(mediaQuery, "svc");
    var icon = this.getAPIFromShortname(svcShortNameLit.Value).icon;
    var iconLit = RDFS.GetLiteral(icon);
    // Don't overwrite binary hardcoded favicons
    if (faviconLit.Value.match(/^chrome:/)) {
      dataFileDS.Change(mediaQuery, iconPred, faviconLit, iconLit);
    }

    var isPollable = getTargetLiteral(mediaQuery, "isPollable");
    if (isPollable.Value == "true") {
      // Force refresh on media query
      dataFileDS.Change(mediaQuery,
                        RDFS.GetResource(FLOCK_NS + "nextRefresh"),
                        getTargetDate(mediaQuery, "nextRefresh"),
                        RDFS.GetDateLiteral((new Date()).getTime()*1000));
    }
  }
  // First correction of the top count (StreamCountProgator will do the rest)
  var mediaFaves = RDFS.GetResource(MEDIA_FAVES_ROOT);
  fixUnseenItems(mediaFaves, totalUnseenItems, unseenItemsPred);
}

flockPhotoAPIManager.prototype._migrate09xPhotoAccount =
function flockPhotoAPIManager__migrate09xPhotoAccount()
{
  this.mLogger.info("._migrate09xPhotoAccount()");
  
  var acUtils = Cc["@flock.com/account-utils;1"]
                   .getService(Ci.flockIAccountUtils);
  var _coopfile = "chrome://flock/content/common/load-faves-coop.js";
  var _coop = Cc["@flock.com/singleton;1"]
                .getService(Ci.flockISingleton)
                .getSingleton(_coopfile)
                .wrappedJSObject;
  var accounts = acUtils.getAccountsByInterface("flockIMediaWebService");
  while (accounts.hasMoreElements()) {
    var account = accounts.getNext();
    account = account.QueryInterface(Ci.flockIWebServiceAccount);
    this.mLogger.debug("_migrate09xPhotoAccount -- migrating " + account.urn);
    var c_acct = _coop.get(account.urn);
    var service = Cc[c_acct.serviceId].getService(Ci.flockIWebService);

    // Check if account URN is using old syntax
    //   - urn:flock:<servicename>:<accountId>
    // If so change all appropriate URNs to new syntax
    //   - urn:flock:<servicename>:account:<accountId>
    if (c_acct.id() == "urn:flock:"+service.shortName+":"+c_acct.accountId) {
      var friendsList = c_acct.friendsList;
      // Insert ":account:" into the urn.
      var newAcctUrn = "urn:flock:" + service.shortName
                     + ":account:" + c_acct.accountId;
      c_acct.changeId(newAcctUrn);

      if(friendsList) {
        var newFriendsListUrn = c_acct.id() + ":friends";
        friendsList.changeId(newFriendsListUrn);
      }
    }

    service.migrateAccount(c_acct.accountId, c_acct.name);
  }
}
flockPhotoAPIManager.prototype._migratePhotoPeople =
function _migratePhotoPeople(ctxt)
{
  // Load old datasource
  var oldPhotoPeepsDS = RDFS.GetDataSourceBlocking(IOS.newFileURI(ctxt.oldPeoplePhotoFile).spec);
  var acctsRoot = RDFS.GetResource(LIST_ACCOUNTS_ROOT);
  var streamsRoot = RDFS.GetResource("urn:flock:people:photo:lists:watched");
  var getValue = function (aSource, aArcName) {
    var target = oldPhotoPeepsDS.GetTarget(aSource, RDFS.GetResource(FLOCK_NS+aArcName), true);
    target instanceof Components.interfaces.nsIRDFLiteral;
    return target.Value;
  };

  // Migrate accounts
  var accounts = RDFCU.MakeSeq(oldPhotoPeepsDS, acctsRoot).GetElements(); 
  while (accounts && accounts.hasMoreElements()) {
    var account = accounts.getNext();
    account.QueryInterface(Components.interfaces.nsIRDFResource);
    var username = getValue(account, "username");
    var apiShortName = getValue(account, "apiShortName");
    var id = getValue(account, "id");
    var api = this.getAPIFromShortname(apiShortName);
    api.migrateAccount(id, username);
  }

  // Load new datasource
  var _coop = Components.classes["@flock.com/singleton;1"]
                        .getService(Components.interfaces.flockISingleton)
                        .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                        .wrappedJSObject;
  var mediaFavesURN = MEDIA_FAVES_ROOT;
  var mediaFaves = _coop.get(mediaFavesURN);
  if (!mediaFaves) {
    mediaFaves = new _coop.Folder(mediaFavesURN);
    _coop.favorites_root.children.add(mediaFaves);
  }

  // Migrate favorite photo streams
  var streams = RDFCU.MakeSeq(oldPhotoPeepsDS, streamsRoot).GetElements();
  while (streams && streams.hasMoreElements()) {
    var stream = streams.getNext();
    stream.QueryInterface(Components.interfaces.nsIRDFResource);
    var apiShortName = getValue(stream, "apiShortName");
    var id = getValue(stream, "id");
    if (id) {
      var mediaqueryURN = MEDIA_FAVES_ROOT+":"+apiShortName+":user:"+id+"|username:"+getValue(stream, "username");
      var mediaquery = new _coop.MediaQuery(
        mediaqueryURN,
        {
          serviceId: FLOCK_PHOTO_API_MANAGER_CONTRACTID,
          service: apiShortName,
          favicon: this.getAPIFromShortname(apiShortName).icon,
          query: "user:"+id+"|username:"+getValue(stream, "username"),
          name: getValue(stream, "username"),
          isPollable: true
        }
      );
      mediaFaves.children.add(mediaquery);
    } else {
      // syntax used for Flickr/PB recent photo streams are different from 0.7.x
      // to 0.9.x. If the stream doesn't have an id, test to see if it is the
      // Cardinal version of recent photos stream - if so, add a new stream
      // using the 0.9.x syntax.
      // c.f. https://bugzilla.flock.com/show_bug.cgi?id=11053
      const BAD_FLICKR_PUBLIC_PHOTOS_ID = "urn:flock:people:photo:flickr:";
      const BAD_PB_PUBLIC_PHOTOS_ID = "urn:flock:people:photo:photobucket:";

      var about = stream.ValueUTF8;
      if (about == BAD_FLICKR_PUBLIC_PHOTOS_ID
           || about == BAD_PB_PUBLIC_PHOTOS_ID)
      {
        var mediaqueryURN = MEDIA_FAVES_ROOT + ":"
                                             + getValue(stream, "apiShortName")
                                             + ":special:recent";

        var mediaquery = new _coop.MediaQuery(
          mediaqueryURN,
          {
            serviceId: FLOCK_PHOTO_API_MANAGER_CONTRACTID,
            service: apiShortName,
            favicon: this.getAPIFromShortname(apiShortName).icon,
            query: "special:recent",
            name: flockGetString(FLICKR_STRING_BUNDLE, "flock.flickr.title.recent"),
            isPollable: true
          }
        );
        mediaFaves.children.add(mediaquery);
      }
    }
  }

  // rename the old people photo file
  ctxt.oldPeoplePhotoFile.moveTo(null, OLD_PEOPLE_PHOTO_RDF_FILE_RELIC);
  yield true;
}

// This fixes up objects created by earlier botched code in _migratePhotoPeople
flockPhotoAPIManager.prototype._fixupMediaQueryIDs =
function _fixupMediaQueryIDs(ctxt)
{
  const BAD_IDS = ["urn:media:favoritesflickr",
                   "urn:media:favoritesphotobucket"];

  var coop = Cc["@flock.com/singleton;1"]
             .getService(Components.interfaces.flockISingleton)
             .getSingleton("chrome://flock/content/common/load-faves-coop.js")
             .wrappedJSObject;

  var resources = coop.MediaQuery.all_ids();
  while (resources.hasMoreElements()) {
    var resource = resources.getNext();

    var id = resource.ValueUTF8;
    for each (var badID in BAD_IDS) {
      if (id.substr(0, badID.length) == badID) {
        var newID = id.replace("urn:media:favorites", "urn:media:favorites:");

        var oldQuery = coop.get_from_resource(resource);
        var newQuery = new coop.MediaQuery(
          newID,
          {
            serviceId: oldQuery.serviceId,
            service: oldQuery.service,
            favicon: this.getAPIFromShortname(oldQuery.service).icon,
            query: oldQuery.query,
            name: oldQuery.name,
            isPollable: oldQuery.isPollable,
            firstRefresh: oldQuery.firstRefesh,
            latestSeq: oldQuery.latestSeq
          }
        );

        if (oldQuery.children) {
          var children = oldQuery.children.enumerate();
          while (children.hasMoreElements()) {
            newQuery.children.add(children.getNext());
          }
        }

        for each (var parent in oldQuery.getParents()) {
          parent.children.remove(oldQuery);
          parent.children.add(newQuery);
        }

        oldQuery.destroy();
      }
    }
  }
}

flockPhotoAPIManager.prototype._getPhotoFromRDFNode =
function flockPhotoAPIManager__getPhotoFromRDFNode(aService, aRDFId)
{
  var coopPhoto = this._coop.get(aRDFId);
  var newMediaItem = Cc["@flock.com/photo;1"]
                     .createInstance(Ci.flockIMediaItem);
  newMediaItem.init(aService.shortName, aService.getMediaItemFormatter());
  newMediaItem.webPageUrl = coopPhoto.URL;
  newMediaItem.thumbnail = coopPhoto.thumbnail;
  newMediaItem.midSizePhoto = coopPhoto.midSizePhoto;
  newMediaItem.largeSizePhoto = coopPhoto.largeSizePhoto;
  newMediaItem.username = coopPhoto.username;
  newMediaItem.userid = coopPhoto.userid;
  newMediaItem.title = coopPhoto.name;
  newMediaItem.id = coopPhoto.photoid;
  newMediaItem.icon = coopPhoto.favicon;
  newMediaItem.uploadDate = coopPhoto.datevalue;
  newMediaItem.is_public = coopPhoto.is_public;
  newMediaItem.is_video = coopPhoto.is_video;
  return newMediaItem;
}

//////////////////////////////////////////////////////////////////////////////
// Implementation
//////////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////////

flockPhotoAPIManager.prototype.mStates = {};
flockPhotoAPIManager.prototype.mListeners = new Array();

flockPhotoAPIManager.prototype.addListener = function(aListener) {
    if (aListener == null) {
        throw  "Listener is null!!";
    }
    this.mListeners.push(aListener);
}

flockPhotoAPIManager.prototype.updateStates = function() {
    for(var p in this.mStates) {
        var shortName = p;
        var state = this.mStates[p];
        var api = this.getAPIFromShortname(p);
        var newState = api.authState;
        if(newState != state) {
            this.notify(api);
        }
        this.mStates[p] = newState;
    }
}

flockPhotoAPIManager.prototype.notify = function(aService) {
    for(var i=0;i<this.mListeners.length;++i) {
            this.mListeners[i].onAPIStateChange(aService);
    }
}

flockPhotoAPIManager.prototype.doLogin = function(aAccountURN) {
    // TODO: MAYBE FIXME (CDC):
    // I changed the param for this function from aShortname to aAccountURN
    // ... but I don't think this is actually called from anywhere so for now
    // I'm gonna just throw NOT_IMPLEMENTED instead of fixing it to handle the
    // new parameter.  If it breaks things, I owe the first person who notices
    // it a bubble tea.
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;

    var inst = this;
    var myListener = {
        onAuth: function() {
            inst.notify(service);
            inst.setDefaultService(service.shortName);
        },
        onError: function(aError) {
           for (var i=0; i < inst.mListeners.length; i++) {
               aError.QueryInterface(Components.interfaces.flockIError);
               try {
                    var theError = {};
                    theError.errorCode = aError.errorCode;
                    theError.serviceErrorCode = aError.serviceErrorCode;
                    theError.serviceErrorString = aError.serviceErrorString;
                    theError.errorString = aError.errorString;
                    inst.mListeners[i].onError(theError);
               } catch (ex) {
                    this.mLogger.error(ex);
               }
            }
            
        },
    }
    var service = this.getAPIFromShortname(aShortname);
    service.login(null, myListener);
    this.notify(service);
}

flockPhotoAPIManager.prototype.doLogout = function(aShortname) {
    var service = this.getAPIFromShortname(aShortname);
    service.logout();

    // TODO - maybe change the default service to something that is enabled ???
    
    this.notify(service);
}


flockPhotoAPIManager.prototype.removeListener = function(aListener) {
    for(var i=0;i<this.mListeners.length;++i) {
        if(aListener==this.mListeners[i]) {
            this.mListeners.splice(i,1);
            break;
        }
    }
}


flockPhotoAPIManager.prototype.getAPIFromShortname = function(aShortname) {
  // JMC - Special case, me!
  if (aShortname == "preview") {
    return this;
  }

  var catman = Cc["@mozilla.org/categorymanager;1"]
               .getService(Ci.nsICategoryManager);
  var webServices = [];
  var e = catman.enumerateCategory("flockMediaProvider");
  while (e.hasMoreElements()) {
    var entry = e.getNext().QueryInterface(Ci.nsISupportsCString);
    if (!entry) {
      continue;
    }
    var contractID = catman.getCategoryEntry("flockMediaProvider", entry.data);
    var svc = Cc[contractID].getService(Ci.flockIWebService);
    if (svc.shortName == aShortname) {
      return svc;
    }
  }
  return null;
}


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

loadLibraryFromSpec("chrome://flock/content/common/aggregate.js");

flockPhotoAPIManager.prototype.supportsSearch = function (aQuery) {
  return false;
}

flockPhotoAPIManager.prototype.search = function PAM_search (aListener,
                                                             aQuery,
                                                             aCount,
                                                             aPage,
                                                             aRequestId)
{
  // JMC - PQ of all mediaquery objects with new stuff
  var faves_coop = Components.classes['@flock.com/singleton;1']
                             .getService(Components.interfaces.flockISingleton)
                             .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                             .wrappedJSObject;
  var inst = this;
  this.myUnseenEnum = {
    hasMoreElements: function myUnseenEnum_hasMoreElements() {
      return this.mEnum.hasMoreElements();
    },
    getNext: function myUnseenEnum_getNext() {
      var child = this.mEnum.getNext();
      var svcName = child.getParent().service;
      var svc = inst.getAPIFromShortname(svcName);
      return inst._getPhotoFromRDFNode(svc, child.id());
    },
    QueryInterface: function myUnseenEnum_QueryInterface(aIID) {
      if (aIID.equals(Ci.nsISimpleEnumerator)) {
        return this;
      }
      throw Cr.NS_ERROR_NO_INTERFACE;
    }
  };

  // JMC - If it's a page =1 request, perform the search
  // Otherwise, return the next batch from the cached results
  if (aPage > 1) {
    aListener.onSuccess(this.myUnseenEnum, aRequestId);
  } else {
    var mediaQueries = faves_coop.MediaQuery.find({hasUnseenItems: true});
    var streams = [];
    for (x in mediaQueries) {
      var mediaquery = mediaQueries[x];
      streams.push(mediaquery.children.enumerate());
    }
    var aggUnSeen = filterStream( aggregatePhotoStreams( streams ), function(item) {
         return item && item.unseen;
       });
    this.myUnseenEnum.mEnum = aggUnSeen;
    aListener.onSuccess(this.myUnseenEnum, aRequestId);
  }
}

flockPhotoAPIManager.prototype.__defineGetter__("services", function () {
    var ar = new Array();
    var catman = Cc["@mozilla.org/categorymanager;1"]
                 .getService(Ci.nsICategoryManager);
    var webServices = [];
    var e = catman.enumerateCategory("flockMediaProvider");
    while (e.hasMoreElements()) {
      var entry = e.getNext().QueryInterface(Ci.nsISupportsCString);
      if (!entry) {
        continue;
      }
      var contractID = catman.getCategoryEntry("flockMediaProvider", entry.data);
      var svc = Cc[contractID]
                .getService(Ci.flockIWebService);
      if (svc instanceof Ci.flockIMediaWebService) {
        ar.push(svc);
      }
    }

    var rval = {
        getNext: function() {
            var rval = ar.shift();
            return rval;
        },
        hasMoreElements: function() {
            return (ar.length>0);
        },
    }
    return rval;
})

flockPhotoAPIManager.prototype.getPreferredServices = function() {
    try {
        var pref = this.mPrefService.getCharPref("flock.photo.preferredServices");
        var ar = pref.split(",");
        var rval = [];
        for(var i=0;i<ar.length;++i) {
            if(ar[i].length) rval.push(ar[i]); 
        }
        return rval;
    } catch(e) {
        //debug(e);
        debug("getPreferredServices: No preferred services found, returning an empty set\n");
        return [];
    }
}

flockPhotoAPIManager.prototype.setPreferredServices = function(aArray) {
    var val = aArray.join(",");
    var pref = this.mPrefService.setCharPref("flock.photo.preferredServices", val);
}

flockPhotoAPIManager.prototype.getDefaultService = function() {
    try {
        var pref = this.mPrefService.getCharPref("flock.photo.lastService");
        if (pref && pref.length)
            return this.getAPIFromShortname(pref);
    } catch(e) {
        return null;
    }
}

flockPhotoAPIManager.prototype.setDefaultService = function(aShortname) {
    try {
        this.mPrefService.setCharPref("flock.photo.lastService", aShortname);
    } catch(e) {
        debug(e);
    }
}

flockPhotoAPIManager.prototype.__defineGetter__('preferredServices', function () { 
try {
    var inst = this;
    var ar = this.getPreferredServices();
    var rval = {
        getNext: function() {
            var shortName = ar.shift();
            var api = inst.getAPIFromShortname(shortName);
            return api;
        },
        hasMoreElements: function() {
            return (ar.length>0);
        },
    }
    return rval;
    } catch(e) {
        debug(e);
    }
})


flockPhotoAPIManager.prototype.__defineGetter__("hasNewMedia", function () {
  var faves_coop = Components.classes['@flock.com/singleton;1']
                             .getService(Components.interfaces.flockISingleton)
                             .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                             .wrappedJSObject;
  var mediaFavsFolder = faves_coop.get(MEDIA_FAVES_ROOT);
  return mediaFavsFolder.hasUnseenItems;
})

// This method has three responsibilities,
// and two entry points.
// First, it ensures that the mediaquery 
// contains the most recent 5 RichPhoto items.
// Second, it ensures that RichPhoto items that are being
// freshly created are marked as "unseen".
// Finally, it does NOT mark them as unseen on first refresh.

// The enum is required to be sorted, newest first

flockPhotoAPIManager.prototype.processPhotoStream =
function photoAPI_processPhotoStream(aQueryUrn, aEnum, bMarkUnseen)
{
  var MAX_MEDIAQUERY_CHILDREN = 5;
  var faves_coop = Components.classes['@flock.com/singleton;1']
                              .getService(Components.interfaces.flockISingleton)
                              .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                              .wrappedJSObject;
                              
  var myStream = faves_coop.get(aQueryUrn);
  if (!myStream || this.processingQueue[aQueryUrn]) {
    return; 
  }  
  this.processingQueue[aQueryUrn] = true;
  
  // Destroy old children later
  var newPhotos = [];
 
  var biggestSeq = null;
  var latestDate = null;
  var inst = this;
  var previousIndex = 0;
  function myWorker(shouldYield) {
    var refreshTopMedia = false;
    while (aEnum.hasMoreElements() && newPhotos.length < MAX_MEDIAQUERY_CHILDREN) {
      var mediaItem = aEnum.getNext();
      mediaItem.QueryInterface(Ci.flockIMediaItem);

      // For streams with no uploadDate, we use now. Is that good?
      var mediaItemDate = (mediaItem.uploadDate)
                          ? new Date(parseInt(mediaItem.uploadDate))
                          : new Date();
      if (mediaItemDate > latestDate) {
        latestDate = mediaItemDate;
        biggestSeq = mediaItem.id;
      }

      var urn = "urn:flock:item:photo:"
              + myStream.service
              + ":"
              + mediaItem.id;
      var mediaItemFav = faves_coop.get(urn);
      var photoIndex = -1;
      if (mediaItemFav) {
        photoIndex = myStream.children.indexOf(mediaItemFav);
      } else {
        // if it doesn't exist, create it
        mediaItemFav = new faves_coop.RichPhoto(urn, {
          datevalue: mediaItemDate,
          URL: mediaItem.webPageUrl,
          thumbnail: mediaItem.thumbnail,
          is_public: mediaItem.is_public,
          is_video: mediaItem.is_video,
          midSizePhoto: mediaItem.midSizePhoto,
          name: mediaItem.title,
          username: mediaItem.username,
          userid: mediaItem.userid,
          photoid: mediaItem.id,
          largeSizePhoto: mediaItem.largeSizePhoto
        });
        if (!myStream.firstRefresh && bMarkUnseen) {
          mediaItemFav.unseen = true;
        }
      }

      if (photoIndex < 0) {
        // a photo can be shared by several queries
        previousIndex++;
        myStream.children.insertAt(mediaItemFav, previousIndex);
        refreshTopMedia = true;
      } else {
        previousIndex = photoIndex;
      }
      newPhotos.push(urn);
      if (shouldYield()) yield;
    }
    if (!refreshTopMedia && newPhotos.length < MAX_MEDIAQUERY_CHILDREN) {
      var oldNumPhotos = 0;
      var photosEnum = myStream.children.enumerate();
      while (photosEnum.hasMoreElements()) {
        photosEnum.getNext();
        oldNumPhotos++;
      }
      refreshTopMedia = oldNumPhotos != newPhotos.length;
    }

    if (refreshTopMedia) {
      // If the user added a newer photo or removed one
      inst._destroyOldPhotos(myStream, newPhotos);
      if (biggestSeq != myStream.latestSeq) {
        // If the old children aren't new children, and no other query uses them,
        // then destroy them
        myStream.latestSeq = biggestSeq;
        myStream.latestDate = latestDate;
      } else {
        // the user just removed a photo. No need to bubble sort
        var os = Cc['@mozilla.org/observer-service;1']
                 .getService(Ci.nsIObserverService);
        os.notifyObservers(myStream.resource(),
                           "refresh-myworld-topmedia",
                           aQueryUrn);
      }
    }
    myStream.firstRefresh = false;                         

    delete inst.processingQueue[aQueryUrn];
  }
  FlockScheduler.schedule(null, 0.3, 30, myWorker);
}

flockPhotoAPIManager.prototype.destroyPhotos = function(aQueryUrn) {
  var ok = !this.processingQueue[aQueryUrn];
  if (ok) {
    var _coop = Cc["@flock.com/singleton;1"].getService(Ci.flockISingleton)
                .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                .wrappedJSObject;
    this._destroyOldPhotos(_coop.get(aQueryUrn));
  }
  return ok;
}

flockPhotoAPIManager.prototype._destroyOldPhotos = function(aMediaQuery, aNewPhotos) {
  var queryPhotos = aMediaQuery.children;
  var queryPhotosEnum = queryPhotos.enumerateBackwards();
  while (queryPhotosEnum.hasMoreElements()) {
    var photo = queryPhotosEnum.getNext();
    if (!aNewPhotos || aNewPhotos.indexOf(photo.id()) == -1) {
      queryPhotos.remove(photo);
      if (!photo.getParent()) {
        photo.destroy();
      }
    }
  }
}
 

flockPhotoAPIManager.prototype.refresh = function(aUrn, aListener) {
 
  var faves_coop = Components.classes['@flock.com/singleton;1']
                             .getService(Components.interfaces.flockISingleton)
                             .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                             .wrappedJSObject;
  var photoAPIManager = this;
  var listener = function(aStream, aSearchListener) {
    this.mStream = aStream;
    this.mListener = aSearchListener;
  }
  
  var myStream = faves_coop.get(aUrn);
  if (!myStream) {
    aListener.onError();
    return; 
  }

  listener.prototype.onStart =
  function PAMRefresh_onStart(aSubject, aTopic) {
  };

  listener.prototype.onSuccess =
  function PAMRefresh_onSuccess(aSubject, aTopic) {
    aSubject.QueryInterface(Ci.nsISimpleEnumerator);
    photoAPIManager.processPhotoStream(aUrn, aSubject, true);
    this.mListener.onResult(null, this.mLastSearchRequestId);
  };

  listener.prototype.onError =
  function PAMRefresh_onError(aSubject, aTopic, aError) {
    photoAPIManager.mLogger.error(aError);
    if (this.mListener) {
      this.mListener.onError(aError);
    }
  };

  var theApi = this.getAPIFromShortname(myStream.service);
  if (!theApi) {
    aListener.onResult(null, null);
    return; 
  }

  this.mLastSearchRequestId = new Date().getTime();
  theApi.search(new listener(myStream, aListener),
                myStream.query,
                5,
                null,
                this.mLastSearchRequestId);
  return;

}

flockPhotoAPIManager.prototype.getSharingContent =
function getSharingContent(aSrc, aProp) {
  // Find the appropriate supported service to retrieve the element's url and
  // title
  var catMgr = Cc[FLOCK_CATEGORY_MANAGER_CONTRACT_ID]
               .getService(Ci.nsICategoryManager);
  var svcEnum = catMgr.enumerateCategory(FLOCK_SHARE_CONTENT_HANDLER);
  if (svcEnum) {
    while (svcEnum.hasMoreElements()) {
      var entry = svcEnum.getNext().QueryInterface(Ci.nsISupportsCString);
      if (entry) {
        var contractID = catMgr.getCategoryEntry(FLOCK_SHARE_CONTENT_HANDLER,
                                                 entry.data);
        var service = Cc[contractID].getService(Ci.flockIMediaEmbedWebService);
        if (service.getSharingContent(aSrc, aProp)) {
          return true;
        }
      }
    }
  }
  return false;
}

flockPhotoAPIManager.prototype.flags = nsIClassInfo.SINGLETON;
flockPhotoAPIManager.prototype.classDescription = "Flock Photo People Service";
flockPhotoAPIManager.prototype.getInterfaces = function (count) {
    var interfaceList = [Components.interfaces.flockISocialWebService, flockIPhotoAPIManager, flockIPollingService,
                         flockIMigratable, nsIClassInfo];
    count.value = interfaceList.length;
    return interfaceList;
}

flockPhotoAPIManager.prototype.getHelperForLanguage = function (count) {return null;}

// JMC - Making this a social photo service for the preview stream
flockPhotoAPIManager.prototype.QueryInterface =
function (iid) {
    if (!iid.equals(Components.interfaces.flockISocialWebService) &&
        !iid.equals(flockIPhotoAPIManager) && 
        !iid.equals(flockIPollingService) &&
        !iid.equals(flockIMigratable) &&
        !iid.equals(nsIClassInfo) &&
        !iid.equals(nsISupports))
        throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
}

// Module implementation
var PhotoPeopleModule = new Object();

PhotoPeopleModule.registerSelf =
function (compMgr, fileSpec, location, type)
{
    compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);

    compMgr.registerFactoryLocation(FLOCK_PHOTO_API_MANAGER_CID, 
                                    "Flock Photo API Manager JS Component",
                                    FLOCK_PHOTO_API_MANAGER_CONTRACTID, 
                                    fileSpec, 
                                    location,
                                    type);
    var catman = Components.classes['@mozilla.org/categorymanager;1']
      .getService(Components.interfaces.nsICategoryManager);
     
    catman.addCategoryEntry('flockMigratable', 'photoservice', FLOCK_PHOTO_API_MANAGER_CONTRACTID, true, true);
}

PhotoPeopleModule.getClassObject =
function (compMgr, cid, iid) {
    if (!cid.equals(FLOCK_PHOTO_API_MANAGER_CID))
        throw Components.results.NS_ERROR_NO_INTERFACE;
    
    if (!iid.equals(Components.interfaces.nsIFactory))
        throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
    
    return PhotoAPIManagerFactory;
}

PhotoPeopleModule.canUnload =
function(compMgr)
{
    return true;
}
    
/* factory object */
var PhotoAPIManagerFactory = new Object();

PhotoAPIManagerFactory.createInstance =
function (outer, iid) {
    if (outer != null)
        throw Components.results.NS_ERROR_NO_AGGREGATION;

    return (new flockPhotoAPIManager()).QueryInterface(iid);
}

/* entrypoint */
function NSGetModule(compMgr, fileSpec) {
    return PhotoPeopleModule;
}
