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


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

// 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("http://home.netscape.com/WEB-rdf#");
const FLRDF  = Namespace("http://flock.com/rdf#");
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_TOPSITES_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
function bubbleSort(aFolderURN, newFav, aCompareFunction, aCoopDS) 
{
  var bInserted = false;

  var c_Folder = aCoopDS.get(aFolderURN);

  // Get Children of c_Folder
  var childEnum = c_Folder.children.enumerate();

  var aTopSiteCount = 0;

  var bAlreadySeen = false;

  var iMaxCount = PREF_MYWORLD_TOPSITES_COUNT_DEFAULT;
  if (PREFS.getPrefType(PREF_MYWORLD_TOPSITES_COUNT)) {
    iMaxCount = PREFS.getIntPref(PREF_MYWORLD_TOPSITES_COUNT);
  } else {
    PREFS.setIntPref(PREF_MYWORLD_TOPSITES_COUNT, iMaxCount);
  }

  while (childEnum.hasMoreElements()) {
    aTopSiteCount++;
  
    var child = childEnum.getNext();   

    // If we have NOT inserted, and this child is the same as what we want to insert
    // BAIL out.
    if (!bInserted && checkDupe(aFolderURN, newFav, child)) {
      return;
    }

    // If we have NOT inserted yet, check for an appropriate spot
    if (!bInserted && (aCompareFunction(newFav,child) >= 0)) {
      var aIndex = c_Folder.children.indexOf(child);

      while(c_Folder.children.has(newFav)) {
        c_Folder.children.remove(newFav);
      }

      c_Folder.children.insertAt(newFav,aIndex);

      bInserted = true;
      break;
    }
  }

  // Never inserted
  // no items were in the collection before OR it is the oldest item.  
  // Insert only if there's room
  if (!bInserted && (aTopSiteCount+1 <= iMaxCount)) {
    c_Folder.children.insertAt(newFav,aTopSiteCount+1);
  }


  // Go through the list again and make sure we remove the max elements
  var e = c_Folder.children.enumerate();

  var iIndex = 0;
  while(e.hasMoreElements()) {
    var maxchild = e.getNext();
    iIndex++;

    // We're over our allotted amount.. remove!
    if (iIndex > iMaxCount && maxchild) {
      c_Folder.children.remove(maxchild);
    } 
  }
}

function checkDupe(aFolderURN, newFav, child) {
  switch(aFolderURN) {
    case "urn:flock:topsites": {
      if(newFav.URL == child.URL)
        return true;
    } break;
    case "urn:flock:topfeeds": {
      if(newFav.URL == child.URL)
        return true;
    } break;
    case "urn:flock:topmedia": {
      if(newFav.name == child.name
          && newFav.service == child.service) {
        return true;
      }
    }
  }

  return false;
}


// 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
//  'compare'  - function used to sort items of that type
var gTopFaves = [
  {
    urn: "urn:flock:topsites",
    criteria: [[FLRDF("flockType"), rdfLiteral("bookmark")]],
    triggers: [[Ci.flockIRDFObserver.WATCH_TYPES, null, WBRDF("LastModifiedDate"), null],
               [Ci.flockIRDFObserver.TYPE_ALL, null, WBRDF("LastVisitDate"), null]
              ],
    observer: {
      rdfChanged: function (aDS, aType, aSource, aPredicate, aTarget, aOldTarget) {
        
        if (!aSource) return;

        var fav = this.svc.faves_coop.get_from_resource(aSource);

        if (aType == Ci.flockIRDFObserver.TYPE_UNASSERT) {
          // Remove this from topsites
          removeSite(this.ft.urn, fav, this.svc.faves_coop);
        } else {
          bubbleSort(this.ft.urn, fav, this.ft.compare, this.svc.faves_coop);
        }
        
        var obs = getObserverService();
        obs.notifyObservers(null, 'refresh-myworld-topsites', false);
      }
    },
    compare: function (aResourceA, aResourceB) {
      // Compare the two resource's timestamp
      // -1 = A older than B
      // 0  = equivalent
      // 1  = A newer than B
      return aResourceA.LastVisitDate - aResourceB.LastVisitDate;
    },
    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 e = coopObj.children.enumerate();

      while (e.hasMoreElements()){
        var entry = e.getNext();

        if (entry.flockType == "bookmark") {
          bubbleSort(ft.urn, entry, ft.compare, svc.faves_coop);
        } else if (entry.flockType == "folder") {
          this.initialize(ft,svc,entry);
        }
      }
    }
  },
  {
    urn: "urn:flock:topfeeds",
    criteria: [[FLRDF("flockType"), rdfLiteral("feed")]],
    triggers: [[Ci.flockIRDFObserver.TYPE_ALL, null, FLRDF("datevalue"), null],
               [Ci.flockIRDFObserver.TYPE_ALL, null, FLRDF("unseenItems"), null]
              ],
    observer: {
      rdfChanged: function (aDS, aType, aSource, aPredicate, aTarget, aOldTarget) {
        
        if (!aSource) return;

        var id = aSource.ValueUTF8;

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

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

              // Check parent for feed subscription
              var c_Feed = this.svc.faves_coop.get_from_resource(aSource);
              var parents = c_Feed.getParents();
              for each (var parent in parents) {
                var node = parent;
                while (!node.isInstanceOf(this.svc.faves_coop.FeedContext)) {
                  var nodeParents = node.getParents();
                  if (nodeParents.length)
                    node = nodeParents[0];
                  else
                    break;
                }
                if (node.id() == "urn:flock:feedcontext:news") {
                  bubbleSort(this.ft.urn, c_Feed, this.ft.compare, this.svc.faves_coop);
                  break;
                }
              }
            }
          }

        sync.ft = this.ft;
        sync.svc = this.svc;

        if (this.svc.syncTimers[id]) {
          this.svc.syncTimers[id].cancel();
        }

        var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
        timer.initWithCallback(sync, SYNC_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
        this.svc.syncTimers[id] = timer;
      }
    },
    compare: function (aResourceA, aResourceB) {
      // A == newFav
      return aResourceA.datevalue - aResourceB.datevalue;
    },
    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, ft.compare, svc.faves_coop);
        } else if (entry.flockType == "folder") {
          this.initialize(ft,svc,entry);
        }
      }
    }
  },
  {
    urn: "urn:flock:topmedia",
    criteria: [[W3RDF("type"), NSRDF("MediaQuery")]],
    triggers: [[Ci.flockIRDFObserver.TYPE_ALL, null, FLRDF("latestDate"), null]],
    observer: {
      rdfChanged: function (aDS, aType, aSource, aPredicate, aTarget, aOldTarget) {
        
        if (!aSource) return;

        if(aType == Ci.flockIRDFObserver.TYPE_UNASSERT) {
          // Don't process
          return; 
        }

        var id = aSource.ValueUTF8;

        var sync = {  // Check if it's a media obj
            notify: function syncTopMedia(timer) {
              if (timer) {
                delete this.svc.syncTimers[id];
              }
        
              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);
              var parents = c_Media.getParents();
              for (var i in parents) {
                var parent = parents[i];
                if (parent.id() == "urn:media:favorites") {
                  bubbleSort(this.ft.urn, c_Media, this.ft.compare, this.svc.faves_coop);
                  break;
                }
              }
            }
          }

        sync.ft = this.ft;
        sync.svc = this.svc;

        if (this.svc.syncTimers[id]) {
          this.svc.syncTimers[id].cancel();
        }

        var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
        timer.initWithCallback(sync, SYNC_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
        this.svc.syncTimers[id] = timer;
      }
    },
    compare: function (aResourceA, aResourceB) {
      // A == newFav
      return aResourceA.latestDate - aResourceB.latestDate;
    },
    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, ft.compare, svc.faves_coop);
        } else if (entry.flockType == "folder") {
          this.initialize(ft,svc,entry);
        }
      }
    }
  }
];

// 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)
{
  function compare(a, b) {
    return a.datevalue - b.datevalue;
  }
  var id = this.faves_coop.Feed.get_id({ URL: feed.getURL().spec });
  var obj = this.faves_coop.get(id);
  bubbleSort("urn:flock:topfeeds", obj, compare, this.faves_coop);
}

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 ()
{ 
  this._logger.info("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);
  loader.loadSubScript("chrome://flock/locale/favorites/defaultFavorites.js", context);
  
  // Load up the defaul media favs too.
  loader.loadSubScript("chrome://flock/locale/photo/defaultMedia.js", context);
}

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.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.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.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();
    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)
{
  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 = Cc["@mozilla.org/file/directory_service;1"] 
    .getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
  oldFavoritesFile.append(OLD_FAVORITES_RDF_FILE);

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

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

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

  if (!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
  for (var folderURN in gTopFaves) {
    var faveType = gTopFaves[folderURN];

    // start gTopFaves init function
    faveType.initialize(faveType,this,null);
  }  
  
  // 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());
  }
}  

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: aURL,
      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 "Bookmarks";
});

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