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

// XPConnect Helpers
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cr = Components.results;

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

// Constants 
const FAVORITES_RDF_FILE           = "flock-data.rdf";
const OLD_FAVORITES_RDF_FILE       = "flock_favorites.rdf";
const OLD_FAVORITES_RDF_FILE_RELIC = "flock_favorites_old.rdf";
const FAVORITES_CID                = Components.ID("{3606f63a-f874-42fa-8174-cc9a0dd0a712}");

// Contract IDs
const FAVORITES_CONTRACTID     = "@flock.com/favorites-service;1";
const CONTAINER_CONTRACTID     = "@mozilla.org/rdf/container;1";
const DIRECTORY_SVC_CONTRACTID = "@mozilla.org/file/directory_service;1";
const LOCAL_FILE_CONTRACTID    = "@mozilla.org/file/local;1";

// Service handles
const RDFS = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService);
const RDFCU = Cc["@mozilla.org/rdf/container-utils;1"].getService(Ci.nsIRDFContainerUtils);
const PREFS = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
const IOS = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);

const FLOCK_NS = "http://flock.com/rdf#";
const WEB_NS   = "http://home.netscape.com/WEB-rdf#";

const LASTVISITDATE =   WEB_NS + "LastVisitDate";
const LATESTDATE    = FLOCK_NS + "latestDate";
const UPDATEDATE    = FLOCK_NS + "updateDate";
const DATEVALUE     = FLOCK_NS + "datevalue";
// Namespaces Helpers
function Namespace(ns) { return function (arg) { return RDFS.GetResource(ns+arg); } }
const W3RDF  = Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#");
const NSRDF  = Namespace("http://home.netscape.com/NC-rdf#");
const WBRDF  = Namespace(WEB_NS);
const FLRDF  = Namespace(FLOCK_NS);
const COLRDF = Namespace("urn:flock:collection:");

// RDF Properties
const RDF_URL              = NSRDF("URL");
const RDF_TAGS             = FLRDF("tags");
const RDF_TAG              = FLRDF("tag");
const RDF_WSID             = FLRDF("wsid");
const RDF_NAME             = NSRDF("Name");
const RDF_PERMISSIONS      = FLRDF("permissions");
const RDF_TYPE             = W3RDF("type");
const RDF_FAVORITE         = FLRDF("Favorite");
const RDF_COLLECTION       = FLRDF("Collection");
const RDF_FOLDER           = FLRDF("Folder");
const RDF_LIVEMARK         = NSRDF("Livemark");
const RDF_FEEDURL          = NSRDF("FeedURL");
const RDF_FEEDENABLED      = FLRDF("feedEnabled");
const RDF_SHORTCUTURL      = NSRDF("ShortcutURL");
const RDF_DESCRIPTION      = NSRDF("Description");
const RDF_NSYNC            = FLRDF("nsync");
const RDF_LOCAL            = FLRDF("local");
const RDF_LASTMODIFIEDDATE = WBRDF("LastModifiedDate");
const RDF_LASTVISITDATE    = WBRDF("LastVisitDate");
const RDF_ADDEDDATE        = FLRDF("FavoriteAddDate");
const RDF_FAVORITESROOT    = FLRDF("BookmarksRoot");
const RDF_COLLECTIONSROOT  = FLRDF("CollectionsRoot");
const RDF_FOLDERSROOT      = FLRDF("FoldersRoot");
const RDF_TAGSROOT         = FLRDF("TagsRoot");
const RDF_INSTANCEOF       = W3RDF("instanceOf");
const RDF_SEQ              = W3RDF("Seq");
const RDF_FEED             = FLRDF("feed");
const RDF_TOOLBAR          = FLRDF("toolbar");
const RDF_STORE            = FLRDF("store");
const RDF_VERSION          = FLRDF("version");
const RDF_HASFEEDS         = FLRDF("hasFeeds");
const RDF_ISFEED           = FLRDF("isFeed");
const RDF_COOPTYPE         = FLRDF("CoopType");

// RDF Helpers
var rdfLiteral = RDFS.GetLiteral;
var rdfDate    = RDFS.GetDateLiteral;
var rdfInt     = RDFS.GetIntLiteral;
function rdfBoolean(b) { return rdfLiteral(b ? "true" : "false"); }
const RDF_TRUE  = rdfBoolean(true);
const RDF_FALSE = rdfBoolean(false);

// Prefs
const PREF_INDEXED_INITIAL_FAVORITES = "flock.favorites.indexedInitialFavorites";
const PREF_FIRSTRUN_COMPLETED        = "flock.firstrun.components.browser_startup.completed";
const PREF_MYWORLD_TOPSITES_COUNT    = "flock.myworld.topsites.count";
const PREF_MYWORLD_TOPFEEDS_COUNT    = "flock.myworld.topfeeds.count";
const PREF_MYWORLD_TOPMEDIA_COUNT    = "flock.myworld.topmedia.count";
const PREF_MYWORLD_TOPFRIENDS_COUNT  = "flock.myworld.topfriends.count";
const PREF_MYWORLD_FRIEND_ACTIVITY_EXCLUDE_CRITERIA = "flock.myworld.friendActivity.excludecriteria";
const PREF_MYWORLD_TOPSITES_COUNT_DEFAULT = 25;
const PREF_MYWORLD_TOPFEEDS_COUNT_DEFAULT = 25;
const PREF_MYWORLD_TOPMEDIA_COUNT_DEFAULT = 25;
const PREF_MYWORLD_TOPFRIENDS_COUNT_DEFAULT = 25;

const RECOUNT_INTERVAL = 500; // half-second delay in propagating count changes

const SYNC_TIMEOUT = 250; // wait 250 ms for the RDF node to fully populate

// Cardinal migration
const CARDINAL_BM_TOOLBAR_DEFAULT_NAME = "Favorites Toolbar";

function getObserverService() {
  return Cc['@mozilla.org/observer-service;1']
    .getService(Ci.nsIObserverService);
}

// Remove Top Site
function removeSite(aFolderURN, oldFav, aCoopDS) {
  if (!oldFav) return;
  var c_Folder = aCoopDS.get(aFolderURN);
  c_Folder.children.remove(oldFav);
}
// Bubble Sorting Routine
//  aFolderURN urn of the top folder (e.g urn:flock:topsites)
//  aDateCriteria  - field to sort items on
//  aNewFav - favorite item to be sorted
//  aCoopDS - Coop data source
//  aMaxCount -  max number of items in the top folder (default 25)
function bubbleSort(aFolderURN, aNewFav, aDateCriteria, aCoopDS, aMaxCount) 
{
  var c_Folder = aCoopDS.get(aFolderURN, true);
  return c_Folder.children.insertSortedOn(aDateCriteria, aNewFav, aMaxCount);
}

// This data structure contains all the information we need to determine and
// maintain the list of "top" faves in each of the supported categories (Sites,
// Feeds, Media).  The associative array keys are URNs of the folder nodes.
//  'criteria' - array of predicate-object pairs used to identify the type
//  'triggers' - array of type-source-predicate-object tuples (arc property
//               operations) that will trigger the observer
//  'observer' - callback for triggers, refreshes folder contents
var gTopFaves = [
  {
    urn: "urn:flock:topsites",
    transient: false,
    criteria: [[FLRDF("flockType"), rdfLiteral("bookmark")]],
    triggers: [[Ci.flockIRDFObserver.TYPE_ALL, null, WBRDF("LastVisitDate"), null]
              ],
    maxNumberOfItems: 0,
    observer: {
      rdfChanged: function (aDS, aType, aSource, aPredicate, aTarget, aOldTarget) {
        
        if (!aSource) return;
        var id = aSource.ValueUTF8;

        var obs = getObserverService();
        var fav = this.svc.faves_coop.get_from_resource(aSource, id, true);
        var topic = "refresh-myworld-topsites";

        if (aType == Ci.flockIRDFObserver.TYPE_UNASSERT) {
          // Remove this from topsites
          removeSite(this.ft.urn, fav, this.svc.faves_coop);
          obs.notifyObservers(null, topic, id);
        } else {
          var replacedUrn = bubbleSort(this.ft.urn, fav,
                                       LASTVISITDATE,
                                       this.svc.faves_coop,
                                       this.ft.maxNumberOfItems);
          if (replacedUrn) {
            obs.notifyObservers(aSource, topic, replacedUrn);
          }
        }
      }
    },
    initialize: function (ft,svc,coopObj)
    {
      if (coopObj == null) {
        coopObj = Cc["@flock.com/singleton;1"]
                  .getService(Ci.flockISingleton)
                  .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                  .wrappedJSObject
                  .bookmarks_root;
      }
      
      var container = Cc["@mozilla.org/rdf/container;1"]
                      .createInstance(Ci.flockIRDFContainer);
      container.Init(svc.faves_coop.datasource, coopObj.resource());
      var e = container.GetElementsBackwards();

      while (e.hasMoreElements()){
        var next = e.getNext().QueryInterface(Ci.nsIRDFResource);
        var entry = svc.faves_coop.get_from_resource(next, null, true);
        if (!entry) {
          // Fix old profiles corrupted with dangling refs to bookmarks.
          container.RemoveElement(next, false);
          continue;
        }
        var flockType = entry.get("flockType");

        if (flockType == "bookmark") {
          if (!entry.get("LastVisitDate")) {
            entry.set("LastVisitDate", new Date(0));
          }
          if (!bubbleSort(ft.urn, entry,
                          LASTVISITDATE,
                          svc.faves_coop,
                          ft.maxNumberOfItems)) {
            break; //max reached
          }
        } else if (flockType == "folder") {
          this.initialize(ft,svc,entry);
        }
      }
    },
    setup: function topSites_setup(aFaveType) {

      if (!PREFS.getPrefType(PREF_MYWORLD_TOPSITES_COUNT)) {
        PREFS.setIntPref(PREF_MYWORLD_TOPSITES_COUNT,
                         PREF_MYWORLD_TOPSITES_COUNT_DEFAULT);
      }

      aFaveType.maxNumberOfItems =
        PREFS.getIntPref(PREF_MYWORLD_TOPSITES_COUNT);
    }
  },
  {
    urn: "urn:flock:topfeeds",
    transient: false,
    criteria: [[FLRDF("flockType"), rdfLiteral("feed")]],
    triggers: [[Ci.flockIRDFObserver.WATCH_TYPES, null, FLRDF("unseenItems"), null]],
    maxNumberOfItems: 0,
    observer: {
      rdfChanged: function (aDS, aType, aSource, aPredicate, aTarget, aOldTarget) {
        
        if (!aSource) return;

        var id = aSource.ValueUTF8;
        var inst = this;

        var sync = {  // Check if it's a feed
          notify: function syncTopFeeds(aTimer) {
            if (aTimer) {
              delete inst.svc.syncTimers[id];
            }

            if (!checkCriteria(inst.ft.criteria,aSource,inst.svc.dataSource)) {
              return; 
            }

            // Check parent for feed subscription
            var c_Feed = inst.svc.faves_coop.get_from_resource(aSource, id, true);
            var parents = c_Feed.getParents();
            for each (var parent in parents) {
              var node = parent;
              while (!node.isInstanceOf(inst.svc.faves_coop.FeedContext)) {
                var nodeParents = node.getParents();
                if (nodeParents.length)
                  node = nodeParents[0];
                else
                  break;
              }
              if (node.id() == "urn:flock:feedcontext:news") {
                var obs = getObserverService();
                if (aTarget.QueryInterface(Ci.nsIRDFLiteral).Value == "0") {
                  // all feed items have been read. Remove from top feeds
                  inst.svc.faves_coop.get(inst.ft.urn).children.remove(c_Feed);
                  obs.notifyObservers(null, "refresh-myworld-topfeeds", id);
                } else {
                  var replacedUrn = bubbleSort(inst.ft.urn,
                                               c_Feed,
                                               DATEVALUE,
                                               inst.svc.faves_coop,
                                               inst.ft.maxNumberOfItems);
                  if (replacedUrn) {
                    // the feed is new enough to be in the top feeds.
                    obs.notifyObservers(aSource,
                                        "refresh-myworld-topfeeds",
                                        replacedUrn);
                  }
                }
                break;
              }
            }
          }
        }

        var timer = this.svc.syncTimers[id];
        if (timer) {
          timer.cancel();
        } else {
          timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
          this.svc.syncTimers[id] = timer;
        }
        timer.initWithCallback(sync, SYNC_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
      }
    },
    initialize: function (ft,svc,coopObj)
    {
      if(coopObj == null) {
        coopObj = Cc["@flock.com/singleton;1"]
                  .getService(Ci.flockISingleton)
                  .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                  .wrappedJSObject
                  .get("urn:flock:feedcontext:news");
      }

      // If we are still null, forget it
      if(coopObj == null)
        return;

      var e = coopObj.children.enumerate();

      while (e.hasMoreElements()){
        var entry = e.getNext();
        if(entry.flockType == "feed"){
          bubbleSort(ft.urn,
                     entry,
                     DATEVALUE,
                     svc.faves_coop,
                     ft.maxNumberOfItems);
        } else if (entry.flockType == "folder") {
          this.initialize(ft,svc,entry);
        }
      }
    },
    setup: function topFeeds_setup(aFaveType) {

      if (!PREFS.getPrefType(PREF_MYWORLD_TOPFEEDS_COUNT)) {
        PREFS.setIntPref(PREF_MYWORLD_TOPFEEDS_COUNT,
                         PREF_MYWORLD_TOPFEEDS_COUNT_DEFAULT);
      }

      aFaveType.maxNumberOfItems =
        PREFS.getIntPref(PREF_MYWORLD_TOPFEEDS_COUNT);
    }
  },
  {
    urn: "urn:flock:topmedia",
    transient: false,
    criteria: [[W3RDF("type"), NSRDF("MediaQuery")]],
    triggers: [[Ci.flockIRDFObserver.WATCH_TYPES, null, FLRDF("latestDate"), null]],
    maxNumberOfItems: 0,
    observer: {
      rdfChanged: function (aDS, aType, aSource, aPredicate, aTarget, aOldTarget) {
        
        if (!aSource) return;

        var id = aSource.ValueUTF8;

        if (!checkCriteria(this.ft.criteria, aSource, this.svc.dataSource)) {
          return;
        }

        // Check parent for media subscription
        var c_Media = this.svc.faves_coop.get_from_resource(aSource, id, true);
        var parents = c_Media.getParents();
        for (var i in parents) {
          var parent = parents[i];
          if (parent.id() == "urn:media:favorites") {
            var replacedUrn = bubbleSort(this.ft.urn,
                                         c_Media,
                                         LATESTDATE,
                                         this.svc.faves_coop,
                                         this.ft.maxNumberOfItems);
            if (replacedUrn) {
              // the media stream is new enough to be in the top media.
              getObserverService().notifyObservers(aSource,
                                                   "refresh-myworld-topmedia",
                                                   replacedUrn);
            }
            break;
          }
        }
      }
    },
    initialize: function (ft,svc,coopObj)
    {
      if(coopObj == null) {
        coopObj = Cc["@flock.com/singleton;1"]
                  .getService(Ci.flockISingleton)
                  .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                  .wrappedJSObject
                  .get("urn:media:favorites");
      }

      // If we are still null, forget it
      if(coopObj == null)
        return;

      var e = coopObj.children.enumerate();

      while (e.hasMoreElements()){
        var entry = e.getNext();
        // JMC - MediaQuery is no longer CountedObject
        if(entry.flockType == "media"){
          bubbleSort(ft.urn,
                     entry,
                     LATESTDATE,
                     svc.faves_coop,
                     ft.maxNumberOfItems);
        } else if (entry.flockType == "folder") {
          this.initialize(ft,svc,entry);
        }
      }
    },
    setup: function topMedia_setup(aFaveType) {

      if (!PREFS.getPrefType(PREF_MYWORLD_TOPMEDIA_COUNT)) {
        PREFS.setIntPref(PREF_MYWORLD_TOPMEDIA_COUNT,
                         PREF_MYWORLD_TOPMEDIA_COUNT_DEFAULT);
      }

      aFaveType.maxNumberOfItems =
        PREFS.getIntPref(PREF_MYWORLD_TOPMEDIA_COUNT);
    }
  },
  {
    urn: "urn:flock:aggregatedFriendActivityLog",
    transient: true,
    criteria: [[FLRDF("flockType"), rdfLiteral("identity")],
               [FLRDF("flockType"), rdfLiteral("Account")],
               [FLRDF("flockType"), rdfLiteral("FriendActivity")]],
    triggers: [[Ci.flockIRDFObserver.WATCH_TYPES, null, FLRDF("lastUpdate"), null],
               [Ci.flockIRDFObserver.WATCH_TYPES, null, FLRDF("lastUpdateType"), null],
               [Ci.flockIRDFObserver.WATCH_TYPES, null, FLRDF("isAuthenticated"), RDF_FALSE],
               [Ci.flockIRDFObserver.WATCH_TYPES, null, FLRDF("isRefreshing"), RDF_FALSE],
               [Ci.flockIRDFObserver.TYPE_UNASSERT, null, FLRDF("Identity"), null]],
    maxNumberOfItems: 0,
    excludeCriteria: null,
    observer: {
      rdfChanged: function aggregatedFriendActivityLog_rdfChanged(aDS,
                                                                  aType,
                                                                  aSource,
                                                                  aPredicate,
                                                                  aTarget,
                                                                  aOldTarget)
      {
        this._logger.debug("RDF Changed event ");
        this._logger.debug("aType =  " + aType);
        this._logger.debug("aPredicate.Value =  " + aPredicate.Value);

        if (!aSource) {
          return;
        }
        var id = aSource.ValueUTF8;

        var coopObj = this.svc.faves_coop.get_from_resource(aSource, id);
        if (!coopObj) {
          return;
        }

        var inst = this;
        var sync = {
          notify: function syncNotify() {

            // Handle FriendActivity related changes
            if (coopObj.isInstanceOf(inst.svc.faves_coop.FriendActivity)) {

              // The Identity reference to this FriendActivity is being removed
              // so remove it
              if ((aType == Ci.flockIRDFObserver.TYPE_UNASSERT) &&
                  (aPredicate.Value == "http://flock.com/rdf#Identity"))
              {
                coopObj.destroy();
                var obs = getObserverService();
                obs.notifyObservers(null, "refresh-myworld-topfriends", false);
              }

            // Handle Identity related changes  
            } else if (coopObj.isInstanceOf(inst.svc.faves_coop.Identity)) {
              // If the last update type is not set we cant do anything
              // with this update
              if (!coopObj.lastUpdateType) {
                return;
              }

              var account = coopObj.getAccount();

              inst._logger.debug("identity for account id " + account.id());

              // We only want to process for accounts that are
              // authenticated
              if (!account.isAuthenticated) {
                return;
              }

              // Ensure that this update type is the kind we are interested in
              if (!inst.ft.isValid(coopObj)) {
                return;
              }

              // Create the friend activity log for this account
              // if it has not been created already
              if (!account.friendActivityLog) {
                inst._logger.debug("creating friendActivityLog object");
                account.friendActivityLog = new inst.svc
                                                    .faves_coop
                                                    .FriendActivityLog();
              }

              // Get a reference to the friend activity log for this account
              var friendActivityLog = account.friendActivityLog;
              inst._logger.debug("friendActivityLog.id =" +
                                 friendActivityLog.id());

              // Create a new friendActivity
              var friendActivity = new inst.svc.faves_coop.FriendActivity();
              friendActivity.updateType = coopObj.lastUpdateType;

              // Convert to date type, lastUpdate in identity is stored as
              // epoch seconds
              friendActivity.updateDate = new Date(coopObj.lastUpdate * 1000);
              friendActivity.updateValue = coopObj.statusMessage;
              friendActivity.identity = coopObj;

              // Only add friend activities into the list if it has a valid
              // updated date associated with it.  Otherwise we will end up
              // showing activity on friends that are in fact not active, they
              // just exist.
              if (friendActivity.updateDate.getTime() > 0) {
                // Bubble sort the friend activity into both the account's
                // friend activity log and the aggregated friend activity log
                bubbleSort(friendActivityLog.id(),
                           friendActivity,
                           UPDATEDATE,
                           inst.svc.faves_coop,
                           inst.ft.maxNumberOfItems);
              }

            // Handle account related changes
            } else if (coopObj.isInstanceOf(inst.svc.faves_coop.Account)) {

              // Find out what type of account it is
              var webServAccount = null;
              if (coopObj.serviceId) {
                webServAccount = Cc[coopObj.serviceId]
                                           .getService(Ci.flockIWebService)
                                           .getAccount(coopObj.id());
              }

              // We only care about flockISocialWebServiceAccount accounts

              // FIXME: this test does not do anything. This opens a can of
              // worms and it will be fixed shortly in a different patch.
              if (!webServAccount instanceof Ci.flockISocialWebServiceAccount) {
                return;
              }

              // Create the friend activity log for the account if it
              // doesn't have one yet
              if (!coopObj.friendActivityLog) {
                inst._logger.debug("creating friendActivityLog object");
                coopObj.friendActivityLog = new inst.svc
                                                    .faves_coop
                                                    .FriendActivityLog();
              }

              var aggregatedFriendActivityLog = inst.svc
                                                    .faves_coop
                                                    .get(inst.ft.urn);

              // Handler for accounts finishing refreshing
              if (aPredicate.Value == "http://flock.com/rdf#isRefreshing") {

                // We only care about accounts that are authenticated
                if (!coopObj.isAuthenticated) {
                  return;
                }

                // The account has completed refreshing so refresh
                // the aggregated activity log with friend activities
                // from this account's friend activity log
                var childEnum = coopObj.friendActivityLog.children
                                                          .enumerateBackwards();
                while (childEnum.hasMoreElements()) {
                  var child = childEnum.getNext();

                  var inserted = bubbleSort(aggregatedFriendActivityLog.id(),
                                            child,
                                            UPDATEDATE,
                                            inst.svc.faves_coop,
                                            inst.ft.maxNumberOfItems);

                  if (!inserted) {
                    // No need to continue processing items as the last one
                    // didn't make it into the aggregated friend activity
                    // log which is ordered by date
                    break;
                  }
                }

                // Notify observers
                inst._logger.debug("notifying myworld observers");
                var obs = getObserverService();
                obs.notifyObservers(null, "refresh-myworld-topfriends", false);

              // Handler for accounts logging off
              } else if (aPredicate.Value == "http://flock.com/rdf#isAuthenticated") {
                inst._logger.debug("account logging off");

                // When an account logs off, we must recreate the
                // aggregated friend activity log using friend activity
                // logs for all logged in accounts

                // Remove all the items in the aggregated friend activity
                // log as we are going to recreate it
                inst._logger.debug("clearing aggregated friend activity log");
                aggregatedFriendActivityLog.children.empty();

                var accounts = inst.svc.faves_coop.accounts_root.children.enumerate();
                while (accounts.hasMoreElements()) {
                  var account = accounts.getNext();
                  inst._logger.debug("account = " + account.id());

                  // We only want logged in accounts
                  if (!account.isAuthenticated) {
                    inst._logger.debug("account is logged out so ignoring it");
                    continue;
                  }

                  // We only want flockISocialWebService type accounts
                  var testServiceType = Cc[account.serviceId]
                                        .getService(Ci.flockIWebService);
                  if (!(testServiceType instanceof Ci.flockISocialWebService)) {
                    inst._logger
                        .debug("Not a flockISocialWebServiceAccount so ignoring");
                    continue;
                  }

                  // Create the friend activity log for the account if it
                  // doesn't have one yet
                  if (!account.friendActivityLog) {
                    inst._logger.debug("creating friendActivityLog object");
                    account.friendActivityLog = new inst.svc
                                                        .faves_coop
                                                        .FriendActivityLog();
                  }

                  // Loop through the friend activity log for this account and
                  // bubble sort insert into the aggregated friend activity log
                  var childEnum = account.friendActivityLog.children
                                                           .enumerateBackwards();
                  while (childEnum.hasMoreElements()) {
                    var child = childEnum.getNext();
                    inst._logger.debug("insert friendActivity with id = " +
                                       child.id());
                    var inserted = bubbleSort(aggregatedFriendActivityLog.id(),
                                              child,
                                              UPDATEDATE,
                                              inst.svc.faves_coop,
                                              inst.ft.maxNumberOfItems);

                    if (!inserted) {
                      // No need to continue processing items as the last one
                      // didn't make it into the aggregated friend activity
                      // log which is ordered by date
                      break;
                    }
                  }
                }

                // Notify observers
                inst._logger.debug("notifying myworld observers");
                var obs = getObserverService();
                obs.notifyObservers(null, "refresh-myworld-topfriends", false);
              }
            }
          }
        }

        var timer = this.svc.syncTimers[id];
        if (timer) {
          timer.cancel();
        } else {
          timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
          this.svc.syncTimers[id] = timer;
        }
        timer.initWithCallback(sync, SYNC_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
      }
    },
    initialize: function aggregatedFriendActivityLog_initialize(ft,
                                                                svc,
                                                                coopObj)
    {
      // XXX: TODO
    },
    setup: function aggregatedFriendActivityLog_setup(aFaveType) {
      if (PREFS.getPrefType(PREF_MYWORLD_FRIEND_ACTIVITY_EXCLUDE_CRITERIA)) {
        aFaveType.excludeCriteria =
          PREFS.getCharPref(PREF_MYWORLD_FRIEND_ACTIVITY_EXCLUDE_CRITERIA);
      }

      if (!PREFS.getPrefType(PREF_MYWORLD_TOPFRIENDS_COUNT)) {
        PREFS.setIntPref(PREF_MYWORLD_TOPFRIENDS_COUNT,
                         PREF_MYWORLD_TOPFRIENDS_COUNT_DEFAULT);
      }

      aFaveType.maxNumberOfItems =
        PREFS.getIntPref(PREF_MYWORLD_TOPSITES_COUNT);
    },
    isValid: function aggregatedFriendActivityLog_isValid(aCoopObj) {

      // The isValid function returns true if aCoopObj does not match
      // any of the exclude criteria specified in a pref.
      // The required format for the exclude criteria pref is shown in the
      // example below and is a JSON string that represents sets of criteria.
      // The example shows a single criteria set which will match on
      // any identity where the last update was to a status and the status is
      // empty.
      // [[{"field":"lastUpdateType","value":"status"},
      //   {"field":"statusMessage","value":"flockempty"}]]';
      // Note: flockempty is used to represent empty string

      if (!this.excludeCriteria) {
        // No exclude criteria set so nothing to do
        return true;
      }

      var excludeCriteriaSets = eval(this.excludeCriteria);
      // Loop through each criteria set
      for each (var criteriaSet in excludeCriteriaSets) {
        var exclude = true;
        // Loop through each criterion
        for each (var criterion in criteriaSet) {
          // Special check for null
          var isMatchForNull = (aCoopObj[criterion.field] == "" &&
                                criterion.value == "flockempty");
          if ((aCoopObj[criterion.field] != criterion.value) && !isMatchForNull) {
            exclude = false;
            break;
          }
        }

        if (exclude) {
          // If exclude is still true at this point we know that this identity
          // matched at least one set of exclude criteria
          return false;
        }
      }
      return true;
    }
  }
];

// checks incoming criteria for a match
function checkCriteria(aCriteriaArr,aSource,aDataSource) 
{
  aSource.QueryInterface(Components.interfaces.nsIRDFResource);


  for (var i=0;i<aCriteriaArr.length;i++) {
    var prop = aCriteriaArr[i][0].QueryInterface(Ci.nsIRDFResource); 
    var value = aCriteriaArr[i][1].QueryInterface(Ci.nsIRDFNode); 

    if(!aDataSource.HasAssertion(aSource, prop, value, true)) {
      return false;
    }
  }

  return true;
}

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

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


// nsISimpleEnumerator implementation 
function simpleEnumerator(aArray)
{
  aArray.hasMoreElements = function () { 
    return this.length != 0; 
  }
  aArray.getNext = function () { 
    return this.shift(); 
  }
  return aArray;
}


function makeNSIArray(aArray) {
  aArray.queryElementAt = function (aIndex, aIIDRef) {
    return aArray[aIndex].QueryInterface(aIIDRef);
  }
  aArray.enumerate = function () { return simpleEnumerator(this); }
  aArray._indexOf = aArray.indexOf;
  aArray.indexOf = function (aStartIndex, aElement) { return this._indexOf(aElement, aStartIndex); }
  return aArray;
}


function getProfDirFile()
{
  var dirService = Cc[DIRECTORY_SVC_CONTRACTID].getService(Ci.nsIProperties);
  

  // create a datasource in the profile directory
  var profd = dirService.get("ProfD", Ci.nsIFile);
  var file = Cc[LOCAL_FILE_CONTRACTID].createInstance(Ci.nsILocalFile);
  file.initWithPath(profd.path);
  return file;
}


function base64Encode(aInput) {
  var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  var output = "";
  while (aInput.length > 0) {
    output += chars[aInput.charCodeAt(0) >> 2];
    output += chars[((aInput.charCodeAt(0) & 0x03) << 4) | 
      (aInput.length > 1 ? ((aInput.charCodeAt(1) & 0xF0) >> 4) : 0)];
    output += chars[aInput.length > 1 ?
      ((aInput.charCodeAt(1) & 0x0F) << 2) | 
      (aInput.length > 2 ? ((aInput.charCodeAt(2) & 0xC0) >> 6) : 0) : 64];
    output += chars[aInput.length > 2 ?
      (aInput.charCodeAt(2) & 0x3F) : 64];
    if (aInput.length > 3) {
      aInput = aInput.substr(3);
    } else {
      break;
    }
  }
  return output;
}



// ======================================================
// ========== BEGIN favoritesService Component ==========
// ======================================================


// BEGIN Constructor
function favoritesService()
{
  this._logger = Cc["@flock.com/logger;1"].createInstance(Ci.flockILogger);
  this._logger.init("favoritesService");
  this._logger.info("Created Favorites Service Object");

  // JMC - Delay init until the DIRService Exists
  this.loaded = false;
}
// END Constructor


// BEGIN nsISupports
favoritesService.prototype.QueryInterface =
function (aIID)
{
  if ( !aIID.equals(Ci.nsIBookmarksService) && 
       !aIID.equals(Ci.flockIFavoritesService) &&
       !aIID.equals(Ci.flockIFeedContextObserver) &&
       !aIID.equals(Ci.flockIMigratable) &&
       !aIID.equals(Ci.nsICharsetResolver) &&
       !aIID.equals(Ci.nsIClassInfo) &&
       !aIID.equals(Ci.nsIObserver) &&
       !aIID.equals(Ci.nsISupports) )
  {
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
  return this;
}
// END nsISupports


// BEGIN nsIClassInfo
favoritesService.prototype.flags = Ci.nsIClassInfo.SINGLETON;
favoritesService.prototype.classDescription = "Flock Favorites Service";
favoritesService.prototype.getInterfaces = function (count) {
  var interfaceList = [ Ci.nsIBookmarksService, Ci.flockIFavoritesService,
                        Ci.nsICharsetResolver,
                        Ci.flockIMigratable, Ci.nsIClassInfo ];
  count.value = interfaceList.length;
  return interfaceList;
}
favoritesService.prototype.getHelperForLanguage = function (count) { return null; }
// END nsIClassInfo


// BEGIN nsIObserver
favoritesService.prototype.observe =
function favoritesService_observe(subject, topic, data)
{
  if ( topic == "nsPref:changed" &&
       data == PREF_FIRSTRUN_COMPLETED &&
       PREFS.getBoolPref(PREF_FIRSTRUN_COMPLETED) )
  {
    for (var folderURN in gTopFaves) {
      var faveType = gTopFaves[folderURN];

      // start gTopFaves init function
      faveType.initialize(faveType,this,null);
    }

    if (!PREFS.getBoolPref(PREF_INDEXED_INITIAL_FAVORITES)) {
      var index_initial_callback = {};
      index_initial_callback.svc = this;
      index_initial_callback.notify = function (timer) {
        var e = this.svc.getAllFavorites();
        while (e.hasMoreElements()) {
          var u = e.getNext().QueryInterface(Ci.nsIRDFResource).ValueUTF8;
          //this.svc.luceneIndexer.AddDocument(u);
        }
        PREFS.setBoolPref(PREF_INDEXED_INITIAL_FAVORITES, true);
      }
    }
  }

  if (topic == "flock-shutdown") {
    this.obs.removeObserver(this, "flock-shutdown");
    this._shutdown();
  }
}
// END nsIObserver


// BEGIN flockIFeedContextObserver
favoritesService.prototype.onSubscribe = function(context, feed)
{
  var id = this.faves_coop.Feed.get_id({ URL: feed.getURL().spec });
  var obj = this.faves_coop.get(id);

  if (!PREFS.getPrefType(PREF_MYWORLD_TOPFEEDS_COUNT)) {
    PREFS.setIntPref(PREF_MYWORLD_TOPFEEDS_COUNT,
                     PREF_MYWORLD_TOPFEEDS_COUNT_DEFAULT);
  }

  bubbleSort("urn:flock:topfeeds",
             obj,
             DATEVALUE,
             this.faves_coop,
             PREFS.getIntPref(PREF_MYWORLD_TOPFEEDS_COUNT));
}

favoritesService.prototype.onUnsubscribe = function(context, feed)
{
  var id = this.faves_coop.Feed.get_id({ URL: feed.getURL().spec });
  var obj = this.faves_coop.get(id);
  removeSite("urn:flock:topfeeds", obj, this.faves_coop);
}

favoritesService.prototype.onUnreadCountChange = function(context, unreadCount)
{
}
// END flockIFeedContextObserver


// BEGIN flockIFavoritesService
favoritesService.prototype.setupDefaultFavorites =
function favoritesService_setupDefaultFavorites()
{ 
  this._logger.debug(".setupDefaultFavorites()");
  var faves_coop = this.faves_coop;
  var favSrv = this;
  var context = {coop: faves_coop, service: favSrv, dump: dump};
  var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
    .getService(Ci.mozIJSSubScriptLoader);

  this._logger.info("setting up default favorites");
  loader.loadSubScript("chrome://flock/locale/favorites/defaultFavorites.js", context);
  this._logger.info("finished setting up default favorites");

  this._logger.info("setting up default media");
  loader.loadSubScript("chrome://flock/locale/photo/defaultMedia.js", context);
  this._logger.info("finished setting up default media");
}

favoritesService.prototype.init = 
function favoritesService_init(aDataSource)
{
  if (this.loaded) return;

  aDataSource.QueryInterface(Ci.flockIRDFObservable);

  this.dataSource = aDataSource;
  this.faves_coop = Cc["@flock.com/singleton;1"]
                    .getService(Ci.flockISingleton)
                    .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                    .wrappedJSObject;

  this.syncTimers = {};

  var prefsService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService);
  var favesBranch = prefsService.getBranch("flock.favorites.");

  // Convert 0.9.0.x LastVisitDate/LastModifiedDate from strings to real dates
  var migratedStringDates;
  try {
    migratedStringDates = favesBranch.getBoolPref("migratedStringDates");
  } catch (ex) {
    migratedStringDates = false;
  }

  if (!migratedStringDates) {
    var inst = this;

    function convertOldDate(aResource, aProp) {
      var target = inst.dataSource.GetTarget(aResource, aProp, true);
      if (target && target instanceof Ci.nsIRDFLiteral) {
        var dateStr = target.QueryInterface(Ci.nsIRDFLiteral).Value;
        var time = Date.parse(dateStr);
        if (!isNaN(time)) {
          inst.dataSource.Change(aResource, aProp, target,
                                 RDFS.GetDateLiteral(1000 * time));
        } else {
          inst.dataSource.Unassert(aResource, aProp, target);
        }
      }
    }

    var resources = this.faves_coop.Bookmark.all_ids();
    while (resources.hasMoreElements()) {
      var resource = resources.getNext();
      convertOldDate(resource, RDF_LASTVISITDATE);
      convertOldDate(resource, RDF_LASTMODIFIEDDATE);
    }

    favesBranch.setBoolPref("migratedStringDates", true);
  }

  // Make sure no favorite has more than one parent (only once)
  var uniqueId = false;
  try {
    uniqueId = favesBranch.getBoolPref("uniqueid");
  } catch(e) { /* The pref is not set yet */ }
  if (!uniqueId) {
    for (var i in gTopFaves) {
      // Change the top favorites from the Folder type to TopFavesFolder
      try {
        var res = RDFS.GetResource(gTopFaves[i].urn);
        var oldtype = this.dataSource.GetTarget(res, W3RDF("type"), true);
        var oldcooptype = this.dataSource.GetTarget(res, FLRDF("CoopType"), true);
        if (oldtype)
          this.dataSource.Change(res, W3RDF("type"), oldtype, FLRDF("TopFavesFolder"));
        if (oldcooptype)
          this.dataSource.Change(res, FLRDF("CoopType"), oldcooptype, FLRDF("TopFavesFolder"));
      }
      catch (e) {}
    }
  
    var allFaves = this.faves_coop.Bookmark.all();
    var nonUnique = [];
    while (allFaves.hasMoreElements()) {
      var fave = allFaves.getNext();
      // Assuming all bookmarks with an id starting with "urn:" are online
      // faves, and thus should not be transformed.
      var urn = fave.id();
      if (urn.substr(0, 4) == "urn:") {
        continue;
      }
      var parentCount = 0;
      for each (var parent in fave.getParents()) {
        if (parent instanceof this.faves_coop.Folder)
          parentCount++;
      }
      if (parentCount > 1) {
        nonUnique.push(fave);
      }
    }
    
    for each (var fave in nonUnique) {
      // This favorite has more than one parent.
      // We need to clone it so each favorite has only one parent.
      var firstparent = true;
      var faveRes = RDFS.GetResource(fave.id());
      for each (var parent in fave.getParents()) {
        if (parent instanceof this.faves_coop.Folder) {
          if (firstparent) {
            // The first parent keeps the original favorite
            firstparent = false;
          }
          else {
            // Other parents get a clone
            var newRes = this.cloneResource(faveRes);
            var newFave = this.faves_coop.get(newRes.Value);

            var position = parent.children.indexOf(fave);
            parent.children.insertAt(newFave, position);
            parent.children.remove(fave);
          }
        }
        else {
          // MyWorld's "Top Faves" keeps the original
        }
      }
    }
    favesBranch.setBoolPref ("uniqueid", true);
  }
  
  this._obsObsrv = {};
  this._obsObsrv._logger = this._logger;
  this._obsObsrv.observe = function main_observer(aSubject, aTopic, aVerb) {
    
    if( aTopic == "myworld-enableDisable-observers" ) {
      if( aVerb == "true" ) {
        // Re-enable the arc observers
        for (var folderURN in this.gtf) {
          // Make sure the sorting folders exist for each type
          var faveType = this.gtf[folderURN];
          faveType.observer.ft = faveType;
          faveType.observer.svc = this.svc;
          faveType.observer._logger = this._logger;
          faveType.root = this.fc.get(faveType.urn);
          if (!faveType.root) {
            faveType.root = new this.fc.TopFavesFolder(faveType.urn);
            faveType.root.isTransient = faveType.transient;
          }

          // Call setup routine
          faveType.setup(faveType);

          // Start watching for the appropriate arc changes
          for (var t in faveType.triggers) {
            var trig = faveType.triggers[t];
            this.ds.addArcObserver(trig[0], trig[1], trig[2], trig[3], faveType.observer);
          }
        }

        var feedManager = Cc["@flock.com/feed-manager;1"].getService(Ci.flockIFeedManager);
        var newsFeedContext = feedManager.getFeedContext("news");
        newsFeedContext.addObserver(this.svc);
      } else {
        // remove the arc observers
        for (var folderURN in this.gtf) {
          // Make sure the sorting folders exist for each type
          var faveType = this.gtf[folderURN];
          faveType.observer.ft = faveType;
          faveType.observer.svc = this.svc;
          faveType.root = this.fc.get(faveType.urn);
          if (!faveType.root) { faveType.root = new this.fc.TopFavesFolder(faveType.urn); }
          // Start watching for the appropriate arc changes
          for (var t in faveType.triggers) {
            var trig = faveType.triggers[t];
            this.ds.removeArcObserver(trig[0], trig[1], trig[2], trig[3], faveType.observer);
          }
        }

        var feedManager = Cc["@flock.com/feed-manager;1"].getService(Ci.flockIFeedManager);
        var newsFeedContext = feedManager.getFeedContext("news");
        newsFeedContext.removeObserver(this.svc);
      }
    }
  }
  this._obsObsrv.fc   = this.faves_coop;
  this._obsObsrv.svc  = this;
  this._obsObsrv.ds   = this.dataSource;
  this._obsObsrv.gtf  = gTopFaves;

  this._obsObsrv.observe(null, "myworld-enableDisable-observers", "true");

  this.obs = Cc["@mozilla.org/observer-service;1"]
    .getService(Ci.nsIObserverService);
    
  this.obs.addObserver(this._obsObsrv, "myworld-enableDisable-observers", false);
  this.obs.addObserver(this, "flock-shutdown", false);

  // set up a prefs observer for the webservice id
  var prefBranch2 = PREFS.QueryInterface(Ci.nsIPrefBranch2);
  prefBranch2.addObserver(PREF_FIRSTRUN_COMPLETED, this, false);

  this.deleteTransients();
  this.loaded = true;
  this._logger.info("flock favorites data source set up");    
}

favoritesService.prototype._shutdown =
function favoritesService__shutdown()
{
  for (let [id, timer] in Iterator(this.syncTimers)) {
    var sync = timer.callback;
    timer.cancel();
    if (sync && sync.notify) {
      sync.notify(null);
    }
  }

  this.syncTimers = {};

  var prefBranch2 = PREFS.QueryInterface(Ci.nsIPrefBranch2);
  prefBranch2.removeObserver(PREF_FIRSTRUN_COMPLETED, this);

  try {
    this.deleteTransients();
  }
  catch (ex) {
    this._logger.debug("ERROR deleting transient data!");
    this._logger.debug(ex);
  }

  this.obs.removeObserver(this._obsObsrv, "myworld-enableDisable-observers");

  this.loaded = false;
}

favoritesService.prototype.createBookmarkWithTags =
function (aName, aURL, aShortcutURL, aDescription, aDocCharSet, aPostData, aTags)
{
  var bookmark = this.createNewBookmarkWithTags(aName, aURL, aShortcutURL, aDescription,
                                                aDocCharSet, aPostData, aTags);
  this.faves_coop.bookmarks_root.children.addOnce(bookmark);
  return RDFS.GetResource(bookmark.id())
}

favoritesService.prototype.addBookmarkToFolder =
function (aFolderName, aName, aURL, aShortcutURL, aDescription, aDocCharSet, aPostData, aTags)
{
  var folder = this.faves_coop.get(aFolderName);
  var bookmark = this.createNewBookmarkWithTags(aName, aURL, aShortcutURL, aDescription,
                                                aDocCharSet, aPostData, aTags);
  folder.children.addOnce(bookmark);
  return RDFS.GetResource(bookmark.id());
}

favoritesService.prototype.writeBookmarks =
function (aBookmarksFile)
{
  var bsvc = Cc["@mozilla.org/browser/bookmarks-service;1"].getService(Ci.flockIBookmarksService);
  var dirService = Cc[DIRECTORY_SVC_CONTRACTID].getService(Ci.nsIProperties);
  var ds = RDFS.GetDataSource("rdf:flock-favorites").QueryInterface(Ci.nsIRDFDataSource);
  var resource = RDF_FOLDERSROOT;
  bsvc.WriteBookmarks(aBookmarksFile, ds, resource);
}

favoritesService.prototype.importBookmarks =
function favoritesService_importBookmarks(aBookmarksFile)
{
  // Make sure the file exists and is readable
  aBookmarksFile.QueryInterface(Ci.nsIFile);
  if ((!aBookmarksFile.exists()) || (!aBookmarksFile.isReadable())) {
    throw Cr.NS_ERROR_UNEXPECTED;
  }

  // Load the contents of the file into 'lines' array
  var fis = Cc["@mozilla.org/network/file-input-stream;1"]
    .createInstance(Ci.nsIFileInputStream);
  fis.init(aBookmarksFile, 1, 0, 0);
  fis.QueryInterface(Ci.nsILineInputStream);
  var lines = []; var line = {};
  while (fis.readLine(line)) { lines.push(line.value); }

  // Local functions to import bookmarks and folders
  var inst = this;
  var decodeEntities = function (str) {
    return str.replace(/&lt;/g, "<")
              .replace(/&gt;/g, ">")
              .replace(/&amp;/g, "&")
              .replace(/&quot;/g, "\"")
              .replace(/&#39;/g, "'");
  };
  var importBookmark = function () {
    var url, lastVisit, lastMod, id, name, desc, feedurl;
    if (lines[i].match(/ HREF="([^"]*)"/)) { url = RegExp.$1; }
    if (lines[i].match(/ FEEDURL="([^"]*)"/)) { feedurl = RegExp.$1; }
    if (lines[i].match(/ LAST_VISIT="([^"]*)"/)) { lastVisit = RegExp.$1; }
    if (lines[i].match(/ LAST_MODIFIED="([^"]*)"/)) { lastMod = RegExp.$1; }
    if (lines[i].match(/ ID="([^"]*)"/)) { id = RegExp.$1; }
    if (lines[i].match(/">(.*)<\/A>/)) { name = decodeEntities(RegExp.$1); }
    if (lines[i+1].match(/^ *<DD>(.*)$/)) { desc = decodeEntities(RegExp.$1); i++; }
    var bmRes;
    if (feedurl) {
      bmRes = inst.createLivemark(name, url, feedurl, desc);
    }
    else if (url == undefined) {
      bmRes = inst.createSeparator();
    }
    else {
      bmRes = inst.createBookmarkInternal(name, url, desc, new Date(lastVisit), new Date(lastMod));
    }
    return inst.faves_coop.get(bmRes.Value);
  };
  var importFolder = function () {
    lines[i++].match(/">(.*)<\/H3/i);
    var folderRes = inst.createFolder(RegExp.$1);
    var c_folder = inst.faves_coop.get(folderRes.Value);
    inst.faves_coop.bookmarks_root.children.remove(c_folder);
    if (lines[i].match(/^ *<DD>(.*)$/i)) {
      c_folder.description = decodeEntities(RegExp.$1);
      i++;
    }
    if (lines[i].match(/^ *<DL><p>/i)) {
      // This folder contains stuff
      importContents(c_folder);
    }
    return c_folder;
  };
  var importContents = function (aParentFolder) {
    i++; // Increment past <DL><p>
    while (i < lines.length) {
//      inst._logger.warn("\n Parent: "+aParentFolder.Value+" line: "+lines[i]);
      var child = null;
      if (lines[i].match(/^ *<DT><A /i)) {
        child = importBookmark();
      } else if (lines[i].match(/^ *<DT><H3/i)) {
        child = importFolder();
      } else if (lines[i].match(/^ *<\/DL><p>/i)) {
        return;
      }
      if (child) {
        aParentFolder.children.addOnce(child);
      }
      i++;
    }
  };

  // Begin parsing the file
  var i = 0;
  while ((!lines[i].match(/^ *<DL><p>/i)) && (i < lines.length)) { i++; }
  var folder = this.faves_coop.get(this.createFolder("Imported Bookmarks").Value);
  importContents(folder);
}
// END flockIFavoritesService



// BEGIN nsIBookmarksService
favoritesService.prototype.addBookmarkImmediately = function (aURI, aTitle, bmType, aDocCharSet) 
{ 
  return this.createBookmark(aName, aURI, aURI, "", aDocCharSet, null);
}

favoritesService.prototype.cloneResource = function (aSource) 
{
  var newResource = RDFS.GetAnonymousResource();

  // We need to create CoopType first or the indexer complains
  var coopType = this.dataSource.GetTarget(aSource, RDF_COOPTYPE, true);
  if (coopType)
    this.dataSource.Assert(newResource, RDF_COOPTYPE, coopType, true);

  var arcs = this.dataSource.ArcLabelsOut(aSource);

  while (arcs.hasMoreElements()) {
    var property = arcs.getNext();
    property = property.QueryInterface(Ci.nsIRDFResource);
    if (property.EqualsNode(this.getBookmarksToolbarFolder())) continue;
    if (property.EqualsNode(RDF_COOPTYPE)) continue; // Already created
    if (property.EqualsNode(FLRDF('isIndexable'))) continue; // Will be created later
    
    var target = this.dataSource.GetTarget(aSource, property, true);
    
    // Test if the arc points to a child.
    if (RDFCU.IsOrdinalProperty(property)) {
      var oldChild = target.QueryInterface(Ci.nsIRDFResource);
      var newChild = this.cloneResource(oldChild);
      this.dataSource.Assert(newResource, property, newChild, true);
    } else {
      this.dataSource.Assert(newResource, property, target, true);
    }
  }
  
  // Create isIndexable last to avoid firing the indexer too often
  var isIndexable = this.dataSource.GetTarget(aSource, FLRDF('isIndexable'), true);
  if (isIndexable)
    this.dataSource.Assert(newResource, FLRDF('isIndexable'), isIndexable, true);
  
  return newResource;
}

favoritesService.prototype.createBookmark =
function (aName, aURL, aShortcutURL, aDescription, aDocCharSet, aPostData)
{
  if (!aURL || aURL == null) aURL = "about:blank";

  var date = new Date();
  var bookmark = new this.faves_coop.Bookmark(
   {
      name: aName,
      URL: aURL,
      description: aDescription,
      BookmarkAddDate: date,
      LastVisitDate: date
    }
  );
  return RDFS.GetResource(bookmark.id());
}

favoritesService.prototype.createBookmarkInternal =
function (aName, aURL, aDescription, aLastVisit, aLastMod)
{
  if (!aURL || aURL == null) aURL = "about:blank";

  var bookmark = new this.faves_coop.Bookmark(
     {
      name: aName,
      URL: aURL,
      description: aDescription,
      LastVisitDate: aLastVisit,
      LastModifiedDate: aLastMod
    }
  );
  return RDFS.GetResource(bookmark.id());
}

favoritesService.prototype.createFolder = function (aName) 
{
  if (!aName || aName == null) aName = "New Folder";
  var folder = new this.faves_coop.Folder({
    BookmarkAddDate: new Date(),
    name: aName
  });
  this.faves_coop.bookmarks_root.children.addOnce(folder);
  return RDFS.GetResource(folder.id());
}

favoritesService.prototype.createFolderInContainer =
function (aName, aParentFolder, aIndex)
{
  var folder = new this.faves_coop.Folder({
    BookmarkAddDate: new Date(),
    name: aName
  });

  // default to the favorites_root folder...
  var parent = this.faves_coop.favorites_root;
  if (aParentFolder) {
    // FIXME: get should be able to take an nsIRDFResource as an id
    parent = this.faves_coop.get(aParentFolder.ValueUTF8)
  }
  if (aIndex < 0)
    parent.children.add(folder);
  else
    parent.children.insertAt(folder, aIndex);

  return RDFS.GetResource(folder.id());
}

favoritesService.prototype.createLivemark =
function (aName, aURL, aRSSURL, aDescription)
{
  var feedService = Cc["@flock.com/feed-manager;1"].getService(Ci.flockIFeedManager);
  var livemarkContext = feedService.getFeedContext("livemarks");
  var feed = livemarkContext.getRoot().subscribeURL(aRSSURL, aName);
  return RDFS.GetResource(feed.id());
}

favoritesService.prototype.createLivemarkInContainer =
function FS_createLivemarkInContainer(aName, aURL, aRSSURL, aDescription, aFolder, aIndex)
{
  var feedService = Cc["@flock.com/feed-manager;1"].getService(Ci.flockIFeedManager);
  var livemarkContext = feedService.getFeedContext("livemarks");
 
  parent_ = this.faves_coop.get(aFolder.ValueUTF8)
  if (!parent_) { // Invalid folder
    throw Components.Exception('createLivemarkInContainer: invalid folder '+aFolder.ValueUTF8,
                                Cr.NS_ERROR_UNEXPECTED);
  }

  var feed = livemarkContext.getRoot().subscribeURL(aRSSURL, aName);
  var coopFeed = this.faves_coop.get(feed.id());

  if (aIndex < 0) {
    parent_.children.add(coopFeed);
  }
  else {
    parent_.children.insertAt(coopFeed, aIndex);
  }
}

favoritesService.prototype.createSeparator = function ()
{ 
  var newSeparator = new this.faves_coop.Separator();
  return RDFS.GetResource(newSeparator.id());
}

favoritesService.prototype.getBookmarksToolbarFolder = function ()
{
  if (this.faves_coop.toolbar.folder) {
    return RDFS.GetResource(this.faves_coop.toolbar.folder.id());
  } else {
    // JMC - Always have a toolbar folder
    var favesToolbar = new this.faves_coop.Folder({name: "Favorites Toolbar"});
    this.faves_coop.toolbar.folder = favesToolbar;
    this.faves_coop.bookmarks_root.children.addOnce(this.faves_coop.toolbar.folder);
    return this.getBookmarksToolbarFolder();
  }
}

favoritesService.prototype.getLastCharset = function (aURL) { return null; }

favoritesService.prototype.getParent = function (aSource) 
{ 
  if (!aSource || !aSource.QueryInterface || !aSource.QueryInterface(Ci.nsIRDFNode)) return false;

  var arcs = this.dataSource.ArcLabelsIn(aSource);
  while (arcs && arcs.hasMoreElements()) {
    var resource = arcs.getNext();
    if (resource && resource.QueryInterface) {
      resource = resource.QueryInterface(Ci.nsIRDFResource);
    }
    if (resource && resource.QueryInterface && RDFCU.IsOrdinalProperty(resource)) {
      var parent = this.dataSource.GetSource(resource, aSource, true);
      return parent;
    }
  }
  return null;
}

favoritesService.prototype.getParentChain = function (aSource) 
{ 
  var chain = [];
  var current = this.getParent(aSource);
  while (current) {
    this._logger.info("parentChain: "+current);
    chain.push(current);
    current = this.getParent(current);
  }
  chain.reverse();
  return makeNSIArray(chain); 
}

favoritesService.prototype.isBookmarked = function (aURL)
{
  return this.faves_coop.Bookmark.get_from_url(aURL) != null;
}

favoritesService.prototype.isBookmarkedResource = function (aResource)
{
  return this.faves_coop.Bookmark.exists(aResource.Value);
}

favoritesService.prototype.readBookmarks = function () { return; }

favoritesService.prototype.removeBookmarkIcon = function (aURL) { return; }

favoritesService.prototype.resolveKeyword = function (aName, aPostData) 
{
  var keywordArray = this.faves_coop.Bookmark.find({ShortcutURL: aName})
  var keyword = null;
  if (keywordArray && keywordArray.length == 1) {
    keyword = keywordArray[0].URL;
  }
  return keyword; 
}

favoritesService.prototype.updateBookmarkIcon =
function (aURL, aIconMIMEtype, aIconData, aIconDataLen) 
{
  var data = "data:";
  if (aIconData) {
    iconData = String.fromCharCode.apply(null, aIconData);
    data += aIconMIMEtype;
    data += ";base64,";
    data += base64Encode(iconData);
  }

  var faves = this.faves_coop.Bookmark.find({URL: aURL});
  for (var i = 0; i < faves.length; i++) {
    var fav = faves[i];
    fav.favicon = data;
  }
  return;
}

favoritesService.prototype.updateLastVisitedDate = function (aURL, docCharset) {
  if (aURL == '') return;

  var literal = RDFS.GetLiteral(aURL);
  var sources = this.dataSource.GetSources(RDF_URL, literal, true);
  var bookmark = null;
  while (sources && sources.hasMoreElements()) {
    var source = sources.getNext().QueryInterface(Ci.nsIRDFResource);

    var ctRes = this.dataSource.GetTarget(source, FLRDF("CoopType"), true);
    if (ctRes) {
      ctRes.QueryInterface(Ci.nsIRDFResource);
      if (ctRes.Value == "http://flock.com/rdf#Bookmark") {
        bookmark = this.faves_coop.get_from_resource(source);
        break;
      }
    }
  }

  if (bookmark) {
    bookmark.LastVisitDate = new Date();
  }
  return;
}


//check if string is an integer
favoritesService.prototype.isInt = function (str) {
	var i = parseInt (str);

	if (isNaN (i))
		return false;

	i = i . toString ();
	if (i != str)
		return false;

	return true;
}
// END nsIBookmarksService



// BEGIN flockIMigratable
favoritesService.prototype.needsMigration =
function favoritesService_needsMigration(oldVersion)
{
  oldVersion = oldVersion.substr(0, 3);
  if (oldVersion == "0.9" || oldVersion == "1.0") {
    return true;
  }
  var oldFavoritesFile = Cc["@mozilla.org/file/directory_service;1"] 
    .getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
  oldFavoritesFile.append(OLD_FAVORITES_RDF_FILE);

  var relicFavoritesFile = Cc["@mozilla.org/file/directory_service;1"] 
    .getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
  relicFavoritesFile.append(OLD_FAVORITES_RDF_FILE_RELIC);

  // only migrate if the old favorites rdf file exists
  // FIXME: after the flock_favorites.rdf file is renamed to flock_favorites_old.rdf
  // flock_favorites.rdf is created again in the same dir. so we need the 2nd
  // condition in the IF to stop migration from happening again
  if (oldFavoritesFile.exists() && !relicFavoritesFile.exists()) {
    return true;
  }
  return false;
}

favoritesService.prototype.startMigration = 
function (oldVersion, aFlockMigrationProgressListener)
{
  // Tell myworld to stop observing.
  var obs = getObserverService();
  obs.notifyObservers(null, 'myworld-enableDisable-observers', false);
  
  var oldFavoritesFile = null;
  if(oldVersion.substr(0, 3) == "0.7") {
    oldFavoritesFile = Cc["@mozilla.org/file/directory_service;1"] 
                       .getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
    oldFavoritesFile.append(OLD_FAVORITES_RDF_FILE);
  }

  var ctxt = {
    oldVersion: oldVersion,
    listener: aFlockMigrationProgressListener,
    oldFavoritesFile: oldFavoritesFile
  };

  if (oldFavoritesFile && oldFavoritesFile.exists()) {
    ctxt.listener.onUpdate(0, 'Migrating favorites');
  }
  return { wrappedJSObject: ctxt };
}

favoritesService.prototype.doMigrationWork =
function (ctxtWrapper)
{
  var ctxt = ctxtWrapper.wrappedJSObject;

  if (!ctxt.oldFavoritesFile || !ctxt.oldFavoritesFile.exists())
    return false;

  if (!ctxt.favoritesMigrator)
    ctxt.favoritesMigrator = this._migrateFavorites(ctxt);
  if (ctxt.favoritesMigrator.next())
    ctxt.favoritesMigrator = null;

  return Boolean(ctxt.favoritesMigrator);
}

favoritesService.prototype.finishMigration = function (ctxtWrapper) { 
  // Re-init the topfaves
  var oldVersion = ctxtWrapper.wrappedJSObject.oldVersion.substr(0, 3);
  for each (var faveType in gTopFaves) {
    var coopObj = null;
    if ((oldVersion == "0.9" || oldVersion == "1.0")
       && faveType.urn != "urn:flock:aggregatedFriendActivityLog") {
      // the sort order has changed in 1.1 -> resort tops
      coopObj = this.faves_coop.get(faveType.urn);
    }
    // start gTopFaves init function
    faveType.initialize(faveType, this, coopObj);
  }  
  
  // Tell myworld to resume rdf observation
  var obs = getObserverService();
  obs.notifyObservers(null, 'myworld-enableDisable-observers', true);
}
// END flockIMigratable


favoritesService.prototype.deleteTransients =
function favoritesService_deleteTransients()
{
  this._logger.info(".deleteTransients()");
  var types = ["OnlineBookmarksStream", "MediaQuery"];
  for (var t in types) {
    var type = types[t];
    this._logger.info(" deleting transients of type '"+type+"'");
    var transientArray = this.faves_coop[type].find({isTransient: true});
    for (var i = 0; i < transientArray.length; i++) {
      var transient_ = transientArray[i];
      this._logger.info('gonna remove transient named ' + transient_.name);
      var transientParents = transient_.getParents();
      for (var j = 0; j < transientParents.length; j++) {
        transientParents[j].children.remove(transient_);
      }
      if (transient_.children) {
        var childEnum = transient_.children.enumerate();
        while (childEnum.hasMoreElements()) {
          var child = childEnum.getNext();
          transient_.children.remove(child);
          if (!child.getParent()) child.destroy(); 
        }
      }
      transient_.destroy();
    }
  }

  // Delete transient accounts
  var transientAccounts = this.faves_coop.Account.find({isTransient: true});
  for (var j = 0; j < transientAccounts.length; j++) {
    var acct = transientAccounts[j];
    this._logger.info("Removing transient Account named " + acct.id());
    Cc[acct.serviceId].getService(Ci.flockIWebService)
                      .removeAccount(acct.id());
  }

  // Delete transient topfaves
  var transFavs = this.faves_coop.TopFavesFolder.find({isTransient: true});
  for each (var transFav in transFavs) {
    var childEnum = transFav.children.enumerate();
    while (childEnum.hasMoreElements()) {
      var child = childEnum.getNext();
      transFav.children.remove(child);
    }
  }
}

favoritesService.prototype.Seq = function (node) {
  // make the resource a sequence if its not and return a container object
  if (!this.isSeq(node)) {
    return RDFCU.MakeSeq(this.dataSource, node);
  }
  var container = Cc[CONTAINER_CONTRACTID].createInstance(Ci.nsIRDFContainer);
  container.Init(this.dataSource, node);
  return container;
}

favoritesService.prototype._Seq = favoritesService.prototype.Seq;

favoritesService.prototype.isSeq = function (node) {
  // this._logger.info ("Called isSeq with node " + node + "\n");
  // is the node a sequence
  return RDFCU.IsSeq(this.dataSource, node);
}


favoritesService.prototype.getContainedActions = function (aURL)
{
  return simpleEnumerator(this._getContainedActions(aURL, null));
}

favoritesService.prototype._getContainedActions =
function (aURL, aArrIdentityActions)
{
  var getIdentityActions = function (aIdentity, identityActions) {
    var enabledActions = aIdentity.enabledAction.enumerate();
    while (enabledActions.hasMoreElements()) {
      var action = enabledActions.getNext();
      this._logger.info("adding action " + action.name + " to identityActions ");
      identityActions.push({action: action, actionName: action.name, identity: aIdentity});
    }
  };
  if (!aArrIdentityActions) {
    aArrIdentityActions = [];
  }
  var container = this.faves_coop.get(aURL);
  if (container instanceof this.faves_coop.Folder) {
    // Recurse for each contained Person
    var persons = container.children.enumerate();
    while (persons.hasMoreElements()) {
      this._getContainedActions(aChild.id(), aArrIdentityActions);
    }
  }
  if (container instanceof this.faves_coop.Person) {
    var identities = container.children.enumerate();
    while (identities.hasMoreElements()) {
      var identity = identities.getNext();
      getIdentityActions(identity, aArrIdentityActions);
    }
  }
  // return an enumerator of actions?
  return aArrIdentityActions;
}

favoritesService.prototype.createSeparatorInContainer =
function (aParentFolder, aIndex)
{
  var parentFolder = this.faves_coop.get_from_resource(aParentFolder);
  if (parentFolder instanceof this.faves_coop.Stream) throw NS_RDF_ASSERTION_REJECTED;
  var newSeparator = new this.faves_coop.Separator();
  parentFolder.children.insertAt(newSeparator, aIndex);
  return RDFS.GetResource(newSeparator.id());
}


favoritesService.prototype.createNewBookmarkWithTags =
function (aName, aURL, aShortcutURL, aDescription, aDocCharSet, aPostData, aTags)
{
  var now = new Date();
  
  if (!aURL || aURL == null) aURL = "about:blank";

  var shared = false;
  if ( PREFS.getPrefType("flock.bookmarks.shouldnotshare") &&
       PREFS.getBoolPref("flock.bookmarks.shouldnotshare") == false )
  {
    shared = true;
  }
  return new this.faves_coop.Bookmark(
      {
      name: aName,
      URL: IOS.newURI(aURL, null, null).spec,
      description: aDescription,
      tags: aTags,
      shared: shared,
      ShortcutURL: aShortcutURL,
      BookmarkAddDate: now,
      LastModifiedDate: now,
      LastVisitDate: now,
      LastCharset: aDocCharSet
    }
  );
}

favoritesService.prototype.setBookmarksToolbarFolder = function (aSource)
{
  var folder = this.faves_coop.get_from_resource(aSource);
  if (folder) {
    this.faves_coop.toolbar.folder = folder;
  }
}

favoritesService.prototype.removeFolder = function (aFolder)
{
  // verify the folderness
  if (!this.isFolderResource(aFolder)) {
    return false;
  }
  return true;
  // FIXME: remove all the children
  // FIXME: gc the children as they're removed
  // FIXME: remove the name and the type
  // FIXME: remove the Seq-ness
  // FIXME: remove the folder from it's parent
  // FIXME: return success
}

favoritesService.prototype.renameFolder =
function (aFolder, aNewName)
{
  // verify the folderness
  if (!this.isFolderResource(aFolder)) {
    return false;
  }
  return true;
  // FIXME: get the old name...
  // FIXME: ds.Change RDF_NAME
  // FIXME: return success
}

favoritesService.prototype.moveFolder =
function (aFolder, aNewParent)
{
  // verify the folderness
  if (!this.isFolderResource(aFolder)) {
      return false;
  }
  var folder = this.faves_coop.get(aFolder.ValueUTF8);
  var newparent = this.faves_coop.favorites_root;
  if (aNewParent != null) {
    newparent = this.faves_coop.get(aNewParent.ValueUTF8);
  }
  return true;
  // FIXME: get old parent folder
  // FIXME: remove from old parent folder
  // FIXME: add to new parent folder
  // FIXME: return success
}

favoritesService.prototype.getFolderName = function (aFolder)
{
  // verify the folderness
  if (!this.isFolderResource(aFolder)) {
    return null;
  }
  var folder = this.faves_coop.get(aFolder.ValueUTF8);
  return folder.name;
}

favoritesService.prototype.createBookmarkInContainer =
function (aName, aURL, aShortcutURL, aDescription, aDocCharSet, aPostData, aFolder, aIndex)
{
  var bookmarkResource = this.createBookmark(aName, aURL, aShortcutURL, aDescription, aDocCharSet, aPostData);
  var bookmark = this.faves_coop.get(bookmarkResource.Value);
  var folder = this.faves_coop.get(aFolder.ValueUTF8);

  // FIXME: use index?
  folder.children.add(bookmark);

  //??? this.removeFavoriteFromFolder(aURL, this.favoritesRoot);
}

favoritesService.prototype.__defineGetter__("migrationName",
function favoritesService_getter_migrationName() {
  return flockGetString("common/migrationNames", "migration.name.favorites");
});

favoritesService.prototype._migrateFavorites = function (ctxt)
{
  // a coop version of the bookmarks_root folder
  var realBookmarksRootFolder = this.faves_coop.bookmarks_root.children;
  // read into in-memory rdf
  var ds = RDFS.GetDataSourceBlocking(IOS.newFileURI(ctxt.oldFavoritesFile).spec);
  // coopify the rdf graph
  var resourceCount = 0;
  var resources = ds.GetAllResources();

  // helper function
  function _addBookmarkToFolderHelper(aScope, aFolder, aFav) {
    aScope.addBookmarkToFolder(aFolder, aFav.Name, aFav.URL, null,
                               aFav.description, null, null, aFav.tags);
  }

  while (resources.hasMoreElements()) {
    var resource = resources.getNext();
    var type_target = ds.GetTarget(resource, RDF_TYPE, true);
    if (type_target) {
      ds.Assert(resource, RDF_COOPTYPE, type_target, true);
      resourceCount++;
    }
  }
  var Coop = loadSubScript("chrome://flock/content/common/coop.js").Coop;
  var migration_coop = new Coop(ds, "chrome://flock/content/common/migration-coop.js");
  var folderRes;
  var favEnum = migration_coop.CardinalFavorite.all();
  var favesCount = 0;
  var folderID = this.faves_coop.bookmarks_root.id();
  while (favEnum.hasMoreElements()) {
    var fave = favEnum.getNext();
    if (!fave.Name) continue;
    if (fave.URL == "http://flock.com/rdf#FavoritesRoot") continue;
    var favParents = fave.getParents();
    if (favParents) {
      // fave.getParents() possibly has an array of null parents. These are
      // the bookmarks that are not put into a collection or bookmarks toolbar
      // in Cardinal. If there is only 1 entry in array, we need to add
      // that bookmark to the bookmarks root.
      if (favParents.length == 1 && !favParents[0]) {
        _addBookmarkToFolderHelper(this, folderID, fave);
      }
    }
    favesCount++;
    var percentage = Math.round(100 * (favesCount / resourceCount));
    ctxt.listener.onUpdate(percentage, "Migrating favorites");
    yield false;
  }

  // migrate collections
  var collEnum = migration_coop.CardinalCollection.all();
  var folderRes = RDFS.GetResource(this.faves_coop.bookmarks_root.id());
  while (collEnum.hasMoreElements()) {
    var coll = collEnum.getNext();
    var folderIndex = -1;
    if (!coll) {
      // We have to do this since anything that doesn't match the
      // migrations-coop schema is returned as null.
      continue;
    }
    if (!coll.Name) {
      continue;
    }
    if (coll.toolbar) {
      folderIndex = 1;
    }
    var folder = this.createFolderInContainer(coll.Name, folderRes, folderIndex);
    var collContents = coll.children.enumerate();
    while (collContents.hasMoreElements()) {
      fave = collContents.getNext();
      if (!fave) {
        // We have to do this since anything that doesn't match the
        // migrations-coop schema is returned as null.
        continue;
      }
      // Add bookmark into new folder from collection in Cardinal.
      _addBookmarkToFolderHelper(this, folder.ValueUTF8, fave);
    }
    if (coll.toolbar) {
      // Get the new folder's coop representation.
      var newFolder = this.faves_coop.get(folder.ValueUTF8);
      
      // get the bm toolbar folder name from Cormorant
      var bmToolbarId = this.getBookmarksToolbarFolder().Value;
      var bmToolbarName = this.faves_coop.get(bmToolbarId).name;

      // delete the default bookmark toolbar folder
      this.faves_coop.get(bmToolbarId).destroy();

      this.faves_coop.toolbar.folder = newFolder;
      
      // Change the toolbar folder name to Cormorant's folder name.
      this.faves_coop.toolbar.folder.name = bmToolbarName;
    }
  }

  // rename the file and delete the old copy
  RDFS.UnregisterDataSource(ds);
  ds = null;
  migration_coop.datasource = null;
  migration_coop = null;
  
  ctxt.oldFavoritesFile.moveTo(null, OLD_FAVORITES_RDF_FILE_RELIC);
  yield true;
}


favoritesService.prototype.notifyResolvedCharset =
function FS_notifyResolvedCharset (aCharset, aClosure) {
  throw Components.Exception("Unexpected call to NotifyResolvedCharset -- we never set aWantCharset to true!",
                             Cr.NS_ERROR_UNEXPECTED);
}

favoritesService.prototype.requestCharset =
function FS_requestCharset (aWebNavigation, aChannel, aWantCharset, aClosure) {
  aWantCharset.value = false;
  aClosure.value = null;
  
  var url = aChannel.URI.spec;

  var fav = this.faves_coop.get(url);
  if (fav && fav.LastCharset) {
    return fav.LastCharset;
  } else {
    return "";
  } 
}


// ================================================
// ========== BEGIN XPCOM Module support ==========
// ================================================

function createModule(aParams) {
  return {
    registerSelf: function (aCompMgr, aFileSpec, aLocation, aType) {
      var aCompMgr = aCompMgr.QueryInterface(Ci.nsIComponentRegistrar);
      aCompMgr.registerFactoryLocation( aParams.CID, aParams.componentName,
                                        aParams.contractID, aFileSpec,
                                        aLocation, aType );
      aCompMgr.registerFactoryLocation( aParams.CID, aParams.componentName,
                                        "@mozilla.org/embeddor.implemented/bookmark-charset-resolver;1", 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: favoritesService,
    CID: FAVORITES_CID,
    contractID: FAVORITES_CONTRACTID,
    componentName: "Favorites JS Component",
    categories: [
      { category: "flockMigratable", entry: "favorites", value: FAVORITES_CONTRACTID }
    ]
  });

}

// ========== END XPCOM Module support ==========
