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

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

const 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 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#";

/* 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...");

    var inst = this;
    var timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
    var timerFunc = {
        notify: function(aTimer) {
            inst.updateStates();
        },
    }
    timer.initWithCallback(timerFunc, 5 * 1000, 1);
}

//flockIMigratable

flockPhotoAPIManager.prototype.__defineGetter__("migrationName",
function getter_migrationName() {
  return "Photo Accounts";
});

flockPhotoAPIManager.prototype.needsMigration =
function needsMigration(oldVersion) {
  this.mLogger.info(".needsMigration('" + oldVersion + "')\n");
  
  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\n");
      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\n");
    return true;
  } else if (oldVersion.substr(0, 4) == "1.0_" ||
             oldVersion.substr(0, 5) == "1.0.1" ||
             oldVersion.substr(0, 5) == "1.0RC")
  {
    this.mLogger.info("needs migration from 1.0 or 1.0.1");
    return true;
  } else if (oldVersion.substr(0, 3) == "1.0") { // migration from 1.0.X
    this.mLogger.info("needs migration from 1.0.X");
    return true;
  } else {
    return false;
  }
}

flockPhotoAPIManager.prototype.startMigration =
function startMigration(oldVersion, aFlockMigrationProgressListener) {
  this.mLogger.info(".startMigration('" + oldVersion + "', 'aFlockMigrationProgressListener')\n");
  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") {
    if (oldVersion == "0.9") {
      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.info("._migrateMediaQueries()\n");
  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;

  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 predicate = RDFS.GetResource(FLOCK_NS+"latestSeq");
    var latestSeqInt = dataFileDS.GetTarget(mediaQuery, predicate, true);
    if (latestSeqInt instanceof Components.interfaces.nsIRDFInt) {
      var latestSeqString = RDFS.GetLiteral(latestSeqInt.Value);
      dataFileDS.Change(mediaQuery, predicate, 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);
    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();
      }
    }
  }
}

//////////////////////////////////////////////////////////////////////////////
// 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 (aListener, aQuery, aCount, aPage) {
  // 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;
                             
  // JMC - If it's a page =1 request, perform the search
  // Otherwise, return the next batch from the cached results
  if (aPage > 1) {
    aListener.onSearchResult(this.myUnseenEnum);
  } 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;
       });
    var inst = this;   
    this.myUnseenEnum = {
      hasMoreElements : function() {
          return this.mEnum.hasMoreElements();
      },
      getNext : function() {
        var child = this.mEnum.getNext();
        // JMC - Get the real flockIPhoto from the coop obj.
        var svcName = child.getParent().service;
        return inst.getAPIFromShortname(svcName).getPhotoFromRDFNode(child.id());
      },
    };
    this.myUnseenEnum.mEnum = aggUnSeen;   
    aListener.onSearchResult(this.myUnseenEnum);
  }
}

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(aUrn, 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(aUrn);
  if (!myStream || this.processingQueue[aUrn]) {
    return; 
  }  
  this.processingQueue[aUrn] = true;
  
  // JMC - Pruning run, save to destroy children later
  var myStreamEnum = myStream.children.enumerateBackwards();
  var oldChildren = [];
  while (myStreamEnum.hasMoreElements())
  {
    var child = myStreamEnum.getNext();
    if (child) {
      oldChildren.push(child);
      myStream.children.remove(child);
    }
  }
 
  var biggestSeq = myStream.latestSeq;
  var latestDate = myStream.latestDate;
  var inst = this;
  var photoIndex = 1;
  var newContent = false;
  function myWorker(shouldYield) {
    while (aEnum.hasMoreElements() && photoIndex <= MAX_MEDIAQUERY_CHILDREN) {
      var photo = aEnum.getNext();
      photo.QueryInterface(Components.interfaces.flockIPhoto);
      
      // For streams with no uploadDate, we use now. Is that good?
      var photoDate = (photo.uploadDate) ? new Date(parseInt(photo.uploadDate)) : new Date();
      if (photoDate > latestDate) {
        latestDate = photoDate;
        biggestSeq = photo.id;
      }

      var urn = 'urn:flock:item:photo:'+myStream.service+':'+photo.id;
      var photoFav;
      photoFav = faves_coop.get(urn);
      var alreadyIn = (photoFav != null);
      if (!alreadyIn) {
        newContent = true;
      }
        
      // if it doesn't exist, create it
      if (!photoFav) {
        photoFav = new faves_coop.RichPhoto(urn, {
          datevalue: photoDate,
          URL: photo.webPageUrl,
          thumbnail: photo.thumbnail,
          cachedThumbnail: "",
          is_public: photo.is_public,
          is_video: photo.is_video,
          midSizePhoto: photo.midSizePhoto,
          name: photo.title,
          username: photo.username,
          userid: photo.userid,
          photoid: photo.id,
          largeSizePhoto: photo.largeSizePhoto
        });
        // add new photostream to myworld service queue
        var myworldService = Cc[MYWORLD_SERVICE_CONTRACTID].getService(Ci.flockIMyworldService);
        myworldService.queueResource(urn);
      }

      myStream.children.addOnce(photoFav);
      
      if (!myStream.firstRefresh && !alreadyIn && bMarkUnseen) {
        photoFav.unseen = true;
      }
      photoIndex++;
      if (shouldYield()) yield;
    }
    
    // if the old children aren't new children, and they're orphans, then destroy them
    for(var i =0; i < oldChildren.length; i++)
    {
      var child = oldChildren[i];
      if (child.getParents().length < 1)
      {
        child.destroy();
      }
    }
    // If we added a newer photo
    if (newContent) {
      myStream.latestSeq = biggestSeq;
      myStream.latestDate = latestDate;
      if (!myStream.firstRefresh && bMarkUnseen) {
        // JMC - Shouldn't this propagate?
        myStream.hasUnseenItems = true;
        var parent = myStream.getParent();
        if (parent)
        {
          parent.hasUnseenItems = true;
        }
      }
    }
    myStream.firstRefresh = false;                         
    if (myStream.latestDate != latestDate) {
      myStream.latestDate = latestDate;
    }

    inst.processingQueue[aUrn] = false;
  }
  schedule(0.3, 30, myWorker);
}

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.onSearchResult = function(aEnum) {
    photoAPIManager.processPhotoStream(aUrn, aEnum, true);
    this.mListener.onResult(null, null);
  };

  listener.prototype.onError = function(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; 
  }

  theApi.search(new listener(myStream, aListener), myStream.query, 5, null);
  return;

}


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



// a utility for async scheduling:

// cheap scheduler for javascript tasks

// aSlice is the maximum timeslice in seconds. 0.5 or less is a good value

// aPercent is the percent of wall-clock time that should be spent executing
// the task - eg: 20 = one fifth of the time

// aTask is defined to be a generator taking an function that evaluates
// to true if it should yield like:
// function task (should_yield) {
//   while (some_condition) {
//     do some work...
//     if (should_yield()) yield;
//   }
// }
// NOTE: should_yield() is fairly cheap but in a tight inner loop it might
//       make sense to call it every N cycles

var timers = [];
function schedule (aSlice, aPercent, aTask) {
  // convert percent to ratio
  var ratio = aPercent / 100.0;

  // convert slice to milliseconds
  var slice = aSlice * 1000.0;

  function milliseconds () { return (new Date ()).getTime (); }
  var wall_start = milliseconds ();
  var process_time = 0;

   
  var process_start = 0;
  function should_yield () {
    var now = milliseconds ();
    var so_far = now - process_start;
    var wall_time = now - wall_start;
    return so_far >= slice || ((wall_time * ratio) < (process_time + so_far));
  }

  var generator = aTask (should_yield);

  var timer = Components.classes['@mozilla.org/timer;1']
    .createInstance (Components.interfaces.nsITimer);
  var callback = {
    notify: function () {
      var wall_time = milliseconds () - wall_start;

      if ((wall_time * ratio) < process_time) {
        // if we've spent more time than we're supposed to
        // don't run this cycle
      }

      process_start = milliseconds ();
      try {
        generator.next ();
      } catch (err if err instanceof StopIteration) {
        // the task is complete
        timer.cancel ();
        for (var i=0; i<timers.length; i++) { 
          if (timers[i] == timer) {
            timers.splice (i,1);
          }
        }
        timer = null;
      }
      process_time += (milliseconds () - process_start);
    }
  }

  timer.initWithCallback (callback, Math.round (slice/ratio),
      Components.interfaces.nsITimer.TYPE_REPEATING_SLACK)
  timers.push (timer);
}
