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

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

const FEED_CONTRACTID = '@flock.com/feed;1';
const FEED_CLASSID    = Components.ID('2f7499f4-b47f-4350-a6c5-aee66520c720');
const FEED_CLASSNAME  = 'Flock Feed';

const ITEM_CONTRACTID = '@flock.com/feed-item;1';
const ITEM_CLASSID    = Components.ID('f97d9a24-2463-4e8d-b383-9e41f2bc60e8');
const ITEM_CLASSNAME  = 'Flock Feed Item';

const FC_CONTRACTID   = '@flock.com/feed-context;1';
const FC_CLASSID      = Components.ID('6e95c222-6707-42cf-aa1b-12baea3983e6');
const FC_CLASSNAME    = 'Flock Feed Context';

const FF_CONTRACTID   = '@flock.com/feed-folder;1';
const FF_CLASSID      = Components.ID('16b7ab70-c745-4f53-83a0-3375a031a113');
const FF_CLASSNAME    = 'Flock Feed Folder';

const FM_CONTRACTID   = '@flock.com/feed-manager;1';
const FM_CLASSID      = Components.ID('287848be-bbc9-42aa-953e-2ec916eb8d49');
const FM_CLASSNAME    = 'Flock Feed Manager';


const FEED_CONTENT_FILE      = 'feedcontent.sqlite';

const EXCERPT_MAX_WORDS      = 50;
const TITLE_TRIM_MAX_CHARS   = 30;

const PURGE_RUN_INTERVAL     = 500;
const PURGE_SLEEP_INTERVAL   = 1500;

const UNKNOWN_FEED_TYPE      = 'unknown';

const URI_FEED_PROPERTIES    = 'chrome://flock/locale/feeds/feeds.properties';

const URN_FEED_ROOT          = 'urn:flock:feedroot';

const NEWS_CONTEXT_NAME      = 'news';
const LIVEMARKS_CONTEXT_NAME = 'livemarks';

const ATTR_URI_LIST          = ['action', 'href', 'src', 'longdesc', 'usemap',
                                'cite'];


/* prefs */
const PREF_EXPIRATION_TIME_BRANCH      = 'flock.feeds.expiration_time';
const PREF_EXPIRATION_TIME             = '.subscriptions';
const PREF_METADATA_EXPIRATION_TIME    = '.metadata_only';

const DEFAULT_EXPIRATION_TIME          = 60;
const DEFAULT_METADATA_EXPIRATION_TIME = 24 * 60;

const FAVICON_EXPIRATION_TIME          = 24 * 60 * 60 * 1000;


/* cardinal to danphe migration */
const FLOCK_NS                   = 'http://flock.com/rdf#';

const OLD_FEEDS_RDF_FILE         = 'flock_subscriptions.rdf';
const OLD_FEEDS_RDF_FILE_RELIC   = 'flock_subscriptions_old.rdf';
const SUBSCRIPTIONS_RDF_ROOT     = 'urn:flock:feed:subscriptions';

const OLD_FLAGGED_RDF_FILE       = 'flock_feeds_flagged.rdf';
const OLD_FLAGGED_RDF_FILE_RELIC = 'flock_feeds_flagged_old.rdf';
const FLAGGED_RDF_ROOT           = 'urn:flock:feed';

const OLD_ROOT_RDF_FILE          = 'flock_feeds_root.rdf';
const OLD_DISCOVERY_RDF_FILE     = 'flock_feeds_discovery.rdf';

const OLD_FEED_DATA_DIR          = 'feeds';
const OLD_FEED_DATA_FILENAME     = 'feed.rdf';
const OLD_FEED_DATA_RDF_PREFIX   = 'urn:flock:feed:';
const OLD_FEED_DATA_POST_PREFIX  = OLD_FEED_DATA_RDF_PREFIX + 'post:';

const FORMAT_CONVERSIONS = { 'Atom 1.0': 'atom',
                             'Atom 0.3': 'atom03',
                             'RSS 2.0' : 'rss2',
                             'RSS 1.0' : 'rss1',
                             'RSS 0.9' : 'rss090',
                             'RSS 0.91': 'rss091',
                             'RSS 0.92': 'rss092',
                             'RSS 0.93': 'rss093',
                             'RSS 0.94': 'rss094',
                           };

const DATEVALUE  = FLOCK_NS + "datevalue";

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

/* from nspr's prio.h */
const PR_RDONLY      = 0x01;
const PR_WRONLY      = 0x02;
const PR_RDWR        = 0x04;
const PR_CREATE_FILE = 0x08;
const PR_APPEND      = 0x10;
const PR_TRUNCATE    = 0x20;
const PR_SYNC        = 0x40;
const PR_EXCL        = 0x80;


var gIOService     = null;
var gFeedStorage   = null;
var gReadMoreBlurb = null;


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


function excerptText(text, link) {
  var words = text.split(/\s+/);

  if (words.length <= EXCERPT_MAX_WORDS)
    return text;

  var excerpt = words.slice(0, EXCERPT_MAX_WORDS + 1).join(' ');

  if (link)
    excerpt += '&#133; <a href="' + link + '">' + gReadMoreBlurb + '</a>';

  return excerpt;
}


function checkEmpty(value) {
  if (value == null) return null;
  var str = value.toString();
  return str ? str : null;
}

function makeURI(uri) {
  try {
    return gIOService.newURI(uri, null, null);
  }
  catch (e) {
    return null;
  }
}


function getFeedNode(url, coop) {
  var urn = coop.Feed.get_id({ URL: url });
  return coop.get(urn);
}


function insertFavicon(feedNode) {
  var age = Date.now() - feedNode.lastFaviconFetch.getTime();
  if (age < FAVICON_EXPIRATION_TIME)
    return;

  var url = makeURI(feedNode.link);
  if (!url)
    return;

  if (!/^https?/.test(url.scheme))
    return;

  feedNode.lastFaviconFetch = new Date();

  var faviconURL = url.prePath + '/favicon.ico';

  var hr = Cc['@mozilla.org/xmlextras/xmlhttprequest;1']
    .createInstance(Ci.nsIXMLHttpRequest);

  hr.onload = function(evt) { 
    if (evt.target.status == 200)
      feedNode.favicon = faviconURL;
  };

  hr.backgroundRequest = true;
  hr.open('HEAD', faviconURL);
  hr.send(null);
}

function getContextsForFeed(feedNode, coop) {
  var contexts = [];
  var parents = feedNode.getParents();
  for each (var parent in parents) {
    var node = parent;
    while (!node.isInstanceOf(coop.FeedContext)) {
      var nodeParents = node.getParents();
      if (nodeParents.length)
        node = nodeParents[0];
      else
        break;
    }
    if (node.isInstanceOf(coop.FeedContext))
      contexts.push(node);
  }
  return contexts;
}

function contextHasObject(contextName, obj, coop) {
  if (obj.isInstanceOf(coop.FeedContext))
    return obj.name == contextName;

  var parents = obj.getParents();
  for each (var parent in parents) {
    var node = parent;
    while (!node.isInstanceOf(coop.FeedContext)) {
      var nodeParents = node.getParents();
      if (nodeParents.length)
        node = nodeParents[0];
      else
        break;
    }

    if (node.isInstanceOf(coop.FeedContext) && node.name == contextName)
      return true;
  }

  return false;
}

function setFeedIndexable(feedNode, indexable) {
  feedNode.isIndexable = indexable;

  var items = feedNode.children.enumerate();
  while (items.hasMoreElements()) {
    var item = items.getNext();
    item.isIndexable = indexable;
  }
}

function updateNextRefresh(feedNode, coop) {
  var contexts = getContextsForFeed(feedNode, coop);
  if (contexts.length == 0)
    return;

  var refreshInterval = contexts[0].refreshInterval;

  if (contexts.length > 1) {
    for (var i = 1; i < contexts.length; i++) {
      refreshInterval = Math.min(refreshInterval, contexts[i].refreshInterval);
    }
  }

  feedNode.nextRefresh = new Date(Date.now() + refreshInterval * 1000);
}

function feedItemSorter(a, b) {
  var res = a.datevalue - b.datevalue;
  if (res == 0)
    return b.indexvalue - a.indexvalue;
  else
    return res;
}

function createStatement(dbconn, sql) {
  var stmt = dbconn.createStatement(sql);
  var wrapper = Cc["@mozilla.org/storage/statement-wrapper;1"]
    .createInstance(Ci.mozIStorageStatementWrapper);

  wrapper.initialize(stmt);
  return wrapper;
}

function FeedStorage() {
  var dbfile = Cc['@mozilla.org/file/directory_service;1']
    .getService(Ci.nsIProperties).get('ProfD', Ci.nsIFile);
  dbfile.append(FEED_CONTENT_FILE);

  var storageService = Cc['@mozilla.org/storage/service;1']
    .getService(Ci.mozIStorageService);
  this._DBConn = storageService.openDatabase(dbfile);

  var schema = 'id STRING PRIMARY KEY, feed STRING, ' +
               'content STRING, excerpt STRING';

  try {
    this._DBConn.createTable('feed_content', schema);
  }
  catch (e) { }

  this._queryContent = createStatement(this._DBConn,
    'SELECT content FROM feed_content WHERE id = :id');
  this._queryExcerpt = createStatement(this._DBConn,
    'SELECT excerpt FROM feed_content WHERE id = :id');

  this._removeItem = createStatement(this._DBConn,
    'DELETE FROM feed_content where id = :id');
  this._insertItem = createStatement(this._DBConn,
    'INSERT INTO feed_content (id, feed, content, excerpt) ' +
    'VALUES (:id, :feed, :content, :excerpt)');

  this._removeFeed = createStatement(this._DBConn,
    'DELETE FROM feed_content where feed = :feed');
}

FeedStorage.prototype = {
  deleteItem: function FS_deleteItem(id) {
    this._removeItem.params.id = id;
    this._removeItem.step();
    this._removeItem.reset();
  },
  saveItem: function FS_saveItem(id, feed, content, excerpt) {
    this.deleteItem(id);

    var pp = this._insertItem.params;
    pp.id = id;
    pp.feed = feed;
    pp.content = content;
    pp.excerpt = excerpt;
    this._insertItem.step();
    this._insertItem.reset();
  },
  deleteFeed: function FS_deleteFeed(feed) {
    this._removeFeed.params.feed = feed;
    this._removeFeed.step();
    this._removeFeed.reset();
  },
  getContent: function FS_getContent(id) {
    return this._getData(id, this._queryContent, 'content');
  },
  getExcerpt: function FS_getExcerpt(id) {
    return this._getData(id, this._queryExcerpt, 'excerpt');
  },
  beginTransaction: function FS_beginTransation() {
    this._DBConn.beginTransaction();
  },
  commitTransaction: function FS_commitTransation() {
    this._DBConn.commitTransaction();
  },
  rollbackTransaction: function FS_rollbackTransation() {
    this._DBConn.rollbackTransaction();
  },
  _getData: function FS__getData(id, stmt, field) {
    stmt.reset();
    stmt.params.id = id;

    var data = null;
    if (stmt.step())
      data = stmt.row[field];
    stmt.reset();
    return data;
  }
}

function Feed(feedNode, context, coop) {
  this._feedNode = feedNode;
  this._context = context;
  this._coop = coop;
}

Feed.prototype = {
  getURL: function FEED_getURL() {
    return makeURI(this._feedNode.URL);
  },
  getFinalURL: function FEED_getFinalURL() {
    return makeURI(this._feedNode.finalURL);
  },
  getLink: function FEED_getLink() {
    return makeURI(this._feedNode.link);
  },
  getType: function FEED_getType() {
    return this._feedNode.format;
  },
  getTitle: function FEED_getTitle() {
    return checkEmpty(this._feedNode.name);
  },
  getSubtitle: function FEED_getSubtitle() {
    return checkEmpty(this._feedNode.subtitle);
  },
  getImage: function FEED_getImage() {
    return makeURI(this._feedNode.image);
  },
  getFavicon: function FEED_getFavicon() {
    return makeURI(this._feedNode.favicon);
  },
  getAuthor: function FEED_getAuthor() {
    return checkEmpty(this._feedNode.author);
  },
  getPubDate: function FEED_getPubDate() {
    return this._feedNode.datevalue;
  },
  getItemCount: function FEED_getItemCount() {
    return this._feedNode.count;
  },
  getItem: function FEED_getItem(index) {
    var items = this.getItems();
    var i = 0;
    while (items.hasMoreElements()) {
      var item = items.getNext();
      if (i == index)
        return item;
      i++;
    }
    return null;
  },
  getItems: function FEED_getItems() {
    var filter = function(feed, item, coop) {
      return new FeedItem(feed, item, coop);
    };
    return this._enumerateItems(filter);
  },
  refresh: function FEED_refresh() {
    this._feedNode.nextRefresh = new Date();
  },
  getContext: function FEED_getContext() {
    return this._context;
  },
  getUnreadCount: function FEED_getUnreadCount() {
    return this._feedNode.unseenItems;
  },
  getUnreadItems: function FEED_getUnreadItems() {
    var filter = function(feed, item, coop) {
      if (item.unseen)
        return new FeedItem(feed, item, coop);
    };
    return this._enumerateItems(filter);
  },
  getFlaggedItems: function FEED_getFlaggedItems() {
    var filter = function(feed, item, coop) {
      if (item.flagged)
        return new FeedItem(feed, item, coop);
    };
    return this._enumerateItems(filter);
  },
  setTitle: function FEED_setTitle(title) {
    this._feedNode.name = title;
  },
  markRead: function FEED_markRead() {
    var items = this._feedNode.children.enumerate();
    while (items.hasMoreElements()) {
      var item = items.getNext();
      item.unseen = false;
    }
    var countsPropagator = Cc['@flock.com/stream-counts-propagator;1']
      .getService(Ci.flockIStreamCountsPropagator);
    countsPropagator.syncCounts(this._feedNode.resource());
  },
  getFolder: function FEED_getFolder() {
    if (this._context) {
      var parents = this._feedNode.getParents();
      if (parents.length == 1) {
        return new FeedFolder(parents[0], this._context, this._coop);
      } else if (parents.length > 1) {
        var id = this._context._contextNode.id();
        for each (var parent in parents) {
          var node = parent;
          while (!node.isInstanceOf(this._coop.FeedContext)) {
            var nodeParents = node.getParents();
            if (nodeParents.length)
              node = nodeParents[0];
            else
              break;
          }
          if (node.isInstanceOf(this._coop.FeedContext) && node.id() == id)
            return new FeedFolder(parent, this._context, this._coop);
        }
      }
    }

    return null;
  },
  id: function FEED_id() {
    return this._feedNode.id();
  },

  _enumerateItems: function FEED__enumerateItems(filterFunc) {
    var feed = this;
    var coop = this._coop;
    var items = this._feedNode.children.enumerateBackwards();

    var filteredEnumerator = {
      next: null,
      hasMoreElements: function() {
        while (items.hasMoreElements()) {
          this.next = filterFunc(feed, items.getNext(), coop);
          if (this.next)
            return true;
        }
        return false;
      },
      getNext: function() {
        return this.next;
      },
    }

    return filteredEnumerator;
  },

  getInterfaces: function FEED_getInterfaces(countRef) {
    var interfaces = [Ci.flockIFeed, Ci.flockIFeedFolderItem,
                      Ci.flockICoopObject, Ci.nsIClassInfo, Ci.nsISupports];
    countRef.value = interfaces.length;
    return interfaces;
  },
  getHelperForLanguage: function FEED_getHelperForLanguage(language) {
    return null;
  },
  contractID: FEED_CONTRACTID,
  classDescription: FEED_CLASSNAME,
  classID: FEED_CLASSID,
  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,

  QueryInterface: function FEED_QueryInterface(iid) {
    if (iid.equals(Ci.flockIFeed) ||
        iid.equals(Ci.flockIFeedFolderItem) ||
        iid.equals(Ci.flockICoopObject) ||
        iid.equals(Ci.nsIClassInfo) ||
        iid.equals(Ci.nsISupports))
      return this;
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
}


function FeedItem(feed, itemNode, coop) {
  this._feed = feed;
  this._itemNode = itemNode;
  this._coop = coop;
}

FeedItem.prototype = {
  getItemID: function FI_getItemID() {
    return checkEmpty(this._itemNode.itemid);
  },
  getLink: function FI_getLink() {
    return makeURI(this._itemNode.URL);
  },
  getTitle: function FI_getTitle() {
    return checkEmpty(this._itemNode.name);
  },
  getPubDate: function FI_getPubDate() {
    return this._itemNode.pubdate;
  },
  getCorrectedPubDate: function FI_getCorrectedPubDate() {
    return this._itemNode.datevalue;
  },
  getAuthor: function FI_getAuthor() {
    return checkEmpty(this._itemNode.author);
  },
  getContent: function FI_getContent() {
    return gFeedStorage.getContent(this._itemNode.id());
  },
  getExcerpt: function FI_getExcerpt() {
    return gFeedStorage.getExcerpt(this._itemNode.id());
  },
  getContext: function FI_getContext() {
  },
  isRead: function FI_isRead() {
    return !this._itemNode.unseen;
  },
  isFlagged: function FI_isFlagged() {
    return this._itemNode.flagged;
  },
  setRead: function FI_setRead(read) {
    this._itemNode.unseen = !read;
  },
  setFlagged: function FI_setFlagged(flagged) {
    if (this._itemNode.flagged == flagged)
      return;

    this._itemNode.flagged = flagged;

    var feedNode = this._feed._feedNode;
    var contexts = getContextsForFeed(feedNode, this._coop);

    if (flagged) {
      for each (var context in contexts) {
        context.flaggedItems.children.insertSortedOn(DATEVALUE,
                                                     this._itemNode);
      }
    } else {
      for each (var context in contexts) {
        context.flaggedItems.children.remove(this._itemNode);
      }
    }
  },
  getFeed: function FI_getFeed() {
    return this._feed;
  },

  getInterfaces: function FI_getInterfaces(countRef) {
    var interfaces = [Ci.flockIFeedItem, Ci.nsIClassInfo, Ci.nsISupports];
    countRef.value = interfaces.length;
    return interfaces;
  },
  getHelperForLanguage: function FI_getHelperForLanguage(language) {
    return null;
  },
  contractID: ITEM_CONTRACTID,
  classDescription: ITEM_CLASSNAME,
  classID: ITEM_CLASSID,
  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,

  QueryInterface: function FI_QueryInterface(iid) {
    if (iid.equals(Ci.flockIFeedItem) ||
        iid.equals(Ci.nsIClassInfo) ||
        iid.equals(Ci.nsISupports))
      return this;
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
}


function FeedFolder(folderNode, context, coop) {
  this._logger = Cc['@flock.com/logger;1'].createInstance(Ci.flockILogger);
  this._logger.init('feedmanager');
  this._logger.info('created folder object');

  this._folderNode = folderNode;
  this._context = context;
  this._coop = coop;
}

FeedFolder.prototype = {
  getTitle: function FF_getTitle() {
    return this._folderNode.name;
  },
  getItemCount: function FF_getItemCount() {
  },
  getItems: function FF_getItems() {
  },
  setTitle: function FF_setTitle(title) {
    this._folderNode.name = title;
  },
  getUnreadCount: function FF_getUnreadCount() {
    return this._folderNode.unseenItems;
  },
  markRead: function FF_markRead() {
    var children = this.getChildren();
    while (children.hasMoreElements()) {
      var child = children.getNext().QueryInterface(Ci.flockIFeedFolderItem);
      child.markRead();
    }
  },
  subscribeURL: function FF_subscribeURL(url, title) {
    var feedObj = { URL: url, serviceId: FM_CONTRACTID };

    if (title) {
      title = title.replace(/[\r\n]+/g, ' ');
      this._logger.info('subscribing feed ' + url + ' with title ' + title);
      feedObj.name = title;
    } else {
      this._logger.info('subscribing feed ' + url);
    }

    var feedNode = new this._coop.Feed(feedObj);

    if (contextHasObject(NEWS_CONTEXT_NAME, this._folderNode, this._coop))
      setFeedIndexable(feedNode, true);

    this._folderNode.children.addOnce(feedNode);

    if (feedNode.nextRefresh)
      updateNextRefresh(feedNode, this._coop);    
    else
      feedNode.nextRefresh = new Date();

    feedNode.isPollable = true;

    var feed = new Feed(feedNode, null, this._coop);
    this._context.notifyOnSubscribe(feed);

    return feed;
  },
  subscribeFeed: function FF_subscribeFeed(feed) {
    var feedNode = this._subscriptionNode(feed, 'subscribing');

    if (contextHasObject(NEWS_CONTEXT_NAME, this._folderNode, this._coop))
      setFeedIndexable(feedNode, true);

    this._folderNode.children.addOnce(feedNode);

    updateNextRefresh(feedNode, this._coop);    
    feedNode.isPollable = true;

    this._context.notifyOnSubscribe(feed);
  },
  unsubscribeFeed: function FF_unsubscribeFeed(feed) {
    var feedNode = this._subscriptionNode(feed, 'unsubscribing');

    this._folderNode.children.remove(feedNode);
    var obs = getObserverService();
    obs.notifyObservers(null, "refresh-myworld-topfeeds", feedNode.id());

    var contexts = getContextsForFeed(feedNode, this._coop);
    if (contexts.length == 0)
      feedNode.isPollable = false;

    if (contexts.length == 0 ||
        !contextHasObject(NEWS_CONTEXT_NAME, feedNode, this._coop))
      setFeedIndexable(feedNode, false);

    this._context.notifyOnUnsubscribe(feed);
  },
  subscribeFeedWithPosition:
  function FF_subscribeFeedWithPosition(feed, target, orientation) {
    var coop = this._coop;
    var feedNode = this._subscriptionNode(feed, 'subscribing');

    var check, value;
    if (target instanceof Ci.flockIFeedFolder) {
      value = target.getTitle();
      check = function(child) {
        return child.isInstanceOf(coop.FeedFolder) && child.name == value;
      }
    } else {
      if (orientation == Ci.flockIFeedFolder.ORIENT_INSIDE)
        throw Cr.NS_ERROR_INVALID_ARG;

      value = target.getURL().spec;
      check = function(child) {
        return child.isInstanceOf(coop.Feed) && child.URL == value;
      }
    }

    var children = this._folderNode.children.enumerate();
    while (children.hasMoreElements()) {
      var node = children.getNext();
      if (check(node)) {
        switch (orientation) {
          case Ci.flockIFeedFolder.ORIENT_INSIDE:
             node.children.addOnce(feedNode);
             break;
          case Ci.flockIFeedFolder.ORIENT_ABOVE:
             var container = this._folderNode.children;
             container.insertAt(feedNode, container.indexOf(node));
             break;
          case Ci.flockIFeedFolder.ORIENT_BELOW:
             var container = this._folderNode.children;
             container.insertAt(feedNode, container.indexOf(node) + 1);
             break;
          default:
             throw Cr.NS_ERROR_INVALID_ARG;
             break;
        }

        updateNextRefresh(feedNode, this._coop);    
        feedNode.isPollable = true;

        if (contextHasObject(NEWS_CONTEXT_NAME, this._folderNode, this._coop))
          setFeedIndexable(feedNode, true);

        this._context.notifyOnSubscribe(feed);
        return;
      }
    }

    throw Components.Exception("Couldn't find feed");
  },
  addFolder: function FF_addFolder(title) {
    this._logger.info('adding folder ' + title);
    var children = this._folderNode.children.enumerate();
    while (children.hasMoreElements()) {
      var child = children.getNext();
      if (child.name == title) {
        var msg = 'Folder already exists: ' + title;
        this._logger.error(msg);
        throw Components.Exception(msg);
      }
    }
    var folder = new this._coop.FeedFolder({ name: title });
    this._folderNode.children.add(folder);
    return new FeedFolder(folder, this._context, this._coop);
  },
  removeFolder: function FF_removeFolder(folder) {
    var name = folder.getTitle();
    this._logger.info('removing folder ' + name);
    var children = this._folderNode.children.enumerate();
    while (children.hasMoreElements()) {
      var child = children.getNext();
      if (child.name == name && child.isInstanceOf(this._coop.FeedFolder)) {
        var childFolder = new FeedFolder(child, this._context, this._coop);
        childFolder._destroySelf();
        return;
      }
    }
    this._logger.warn('folder ' + name + ' not found');
  },
  getChildFolder: function FF_getChildFolder(title) {
    var children = this._folderNode.children.enumerate();
    while (children.hasMoreElements()) {
      var obj = children.getNext();
      if (obj.isInstanceOf(this._coop.FeedFolder) && obj.name == title)
        return new FeedFolder(obj, this._context, this._coop);
    }
    throw Components.Exception("Folder does not exist: " + title);
  },
  getChildren: function FF_getChildren() {
    var coop = this._coop;
    var context = this._context;

    var children = this._folderNode.children.enumerate();

    var enumerator = {
      hasMoreElements: function() {
        return children.hasMoreElements();
      },
      getNext: function() {
        var obj = children.getNext();
        if (obj.isInstanceOf(coop.FeedFolder))
          return new FeedFolder(obj, context, coop)
        else
          return new Feed(obj, context, coop)
      }
    };

    return enumerator;
  },
  getFolder: function FF_getFolder() {
    if (this._folderNode.isInstanceOf(this._coop.FeedContext))
      return null;

    var parents = this._folderNode.getParents();
    if (parents.length > 0)
      return new FeedFolder(parents[0], this._context, this._coop);

    return null;
  },

  _subscriptionNode: function FF___subscriptionNode(feed, action) {
    var url = feed.getURL().spec;
    this._logger.info(action + ' feed ' + url + ' from folder ' +
                      this.getTitle());

    var feedNode = getFeedNode(url, this._coop);
    if (!feedNode)
      throw Cr.NS_ERROR_FAILURE;

    return feedNode;
  },

  _destroySelf: function FF__destroySelf() {
    var feeds = [];
    var folders = [];

    var children = this._folderNode.children.enumerateBackwards();
    while (children.hasMoreElements()) {
      var child = children.getNext();
      if (child.isInstanceOf(this._coop.FeedFolder)) {
        var folder = new FeedFolder(child, this._context, this._coop);
        folders.push(folder);
      } else if (child.isInstanceOf(this._coop.Feed)) {
        var feed = new Feed(child, this._context, this._coop);
        feeds.push(feed);
      }
    }

    for each (var folder in folders)
      folder._destroySelf();

    for each (var feed in feeds)
      this.unsubscribeFeed(feed);

    var name = this._folderNode.name;
    this._folderNode.destroy();

    this._logger.info('folder ' + name + ' removed');
  },

  getInterfaces: function FF_getInterfaces(countRef) {
    var interfaces = [Ci.flockIFeedFolder, Ci.flockIFeedFolderItem,
                      Ci.nsIClassInfo, Ci.nsISupports];
    countRef.value = interfaces.length;
    return interfaces;
  },
  getHelperForLanguage: function FF_getHelperForLanguage(language) {
    return null;
  },
  contractID: FF_CONTRACTID,
  classDescription: FF_CLASSNAME,
  classID: FF_CLASSID,
  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,

  QueryInterface: function FF_QueryInterface(iid) {
    if (iid.equals(Ci.flockIFeedFolder) ||
        iid.equals(Ci.flockIFeedFolderItem) ||
        iid.equals(Ci.nsIClassInfo) ||
        iid.equals(Ci.nsISupports))
      return this;
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
}

function FeedContext(contextNode, coop) {
  this._logger = Cc['@flock.com/logger;1'].createInstance(Ci.flockILogger);
  this._logger.init('feedmanager');
  this._logger.info('created context for ' + contextNode.name);

  this._coop = coop;
  this._contextNode = contextNode;

  this._observers = [];
}

FeedContext.prototype = {
  getName: function FC_getName() {
    return this._contextNode.name;
  },

  getRoot: function FC_getRoot() {
    return new FeedFolder(this._contextNode, this, this._coop);
  },

  getSubscription: function FC_getSubscription(url) {
    if (!this.existsSubscription(url))
      return null;

    var feedNode = getFeedNode(url.spec, this._coop);
    return new Feed(feedNode, this, this._coop);
  },
  getSubscriptions: function FC_getSubscriptions() {
    var context = this;
    var coop = this._coop;
    var feeds = this._coop.Feed.all();

    var filteredEnumerator = {
      next: null,
      hasMoreElements: function() {
        while (feeds.hasMoreElements()) {
          var feedNode = feeds.getNext();
          if (contextHasObject(context._contextNode.name, feedNode, coop)) {
            this.next = new Feed(feedNode, context, coop);
            return true;
          }
        }
        return false;
      },
      getNext: function() {
        return this.next;
      }
    }
   
    return filteredEnumerator; 
  },
  existsSubscription: function FC_existsSubscription(url) {
    var feedNode = getFeedNode(url.spec, this._coop);
    if (!feedNode)
      return false;

    return contextHasObject(this._contextNode.name, feedNode, this._coop);
  },

  refresh: function FC_refresh() {
    var feeds = this.getSubscriptions();
    while (feeds.hasMoreElements()) {
      var feed = feeds.getNext().QueryInterface(Ci.flockIFeed);
      feed.refresh();
    }
  },

  getRefreshInterval: function FC_getRefreshInterval() {
    return this._contextNode.refreshInterval / 60;
  },
  setRefreshInterval: function FC_setRefreshInterval(minutes) {
    this._contextNode.refreshInterval = minutes * 60;
  },
  getFeedItemCap: function FC_getFeedItemCap() {
    return this._contextNode.maxItems ? this._contextNode.maxItems : 0;
  },
  setFeedItemCap: function FC_setFeedItemCap(maxItems) {
    this._contextNode.maxItems = maxItems;
    var streamHousekeeping = Cc["@flock.com/stream-housekeeping;1"]
                             .getService(Ci.flockIHousekeeping);
    streamHousekeeping.runHousekeeping();
  },

  addObserver: function FC_addObserver(observer) {
    function hasFunc(element, index, array) {
      return element == observer;
    }
    if (!this._observers.some(hasFunc)) {
      this._observers.push(observer);
      if (this._observers.length == 1)
        this._watchUnseenItems();
    }
  },
  removeObserver: function FC_removeObserver(observer) {
    function keepFunc(element, index, array) {
      return element != observer;
    }
    this._observers = this._observers.filter(keepFunc);
    if (this._observers.length == 0)
      this._unwatchUnseenItems();
  },

  _watchUnseenItems: function FC__watchUnseenItems() {
    var RDFS = Cc['@mozilla.org/rdf/rdf-service;1']
      .getService(Ci.nsIRDFService);
    var unseenItems = RDFS.GetResource('http://flock.com/rdf#unseenItems')

    var faves = Cc['@mozilla.org/rdf/datasource;1?name=flock-favorites']
      .getService(Ci.flockIRDFObservable);
    faves.addArcObserver(Ci.flockIRDFObserver.TYPE_CHANGE,
                         this._contextNode.resource(), unseenItems,
                         null, this); 
  },
  _unwatchUnseenItems: function FC__unwatchUnseenItems() {
    var RDFS = Cc['@mozilla.org/rdf/rdf-service;1']
      .getService(Ci.nsIRDFService);
    var unseenItems = RDFS.GetResource('http://flock.com/rdf#unseenItems')

    var faves = Cc['@mozilla.org/rdf/datasource;1?name=flock-favorites']
      .getService(Ci.flockIRDFObservable);
    faves.removeArcObserver(Ci.flockIRDFObserver.TYPE_CHANGE,
                            this._contextNode.resource(), unseenItems,
                            null, this); 
  },
  rdfChanged: function FC_rdfChanged(ds, type, rsrc, pred, obj, oldObj) {
    for each (var observer in this._observers) {
      observer.onUnreadCountChange(this, this._contextNode.unseenItems);
    }
  },

  notifyOnSubscribe: function FC__notifyOnSubscribe(feed) {
    for each (var observer in this._observers) {
      observer.onSubscribe(this, feed);
    }
  },
  notifyOnUnsubscribe: function FC__notifyOnUnsubscribe(feed) {
    for each (var observer in this._observers) {
      observer.onUnsubscribe(this, feed);
    }
  },
 
  getInterfaces: function FC_getInterfaces(countRef) {
    var interfaces = [Ci.flockIFeedContext, Ci.flockIRDFObserver,
                      Ci.nsIClassInfo, Ci.nsISupports];
    countRef.value = interfaces.length;
    return interfaces;
  },
  getHelperForLanguage: function FC_getHelperForLanguage(language) {
    return null;
  },
  contractID: FC_CONTRACTID,
  classDescription: FC_CLASSNAME,
  classID: FC_CLASSID,
  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,

  QueryInterface: function FC_QueryInterface(iid) {
    if (iid.equals(Ci.flockIFeedContext) ||
        iid.equals(Ci.flockIRDFObserver) ||
        iid.equals(Ci.nsIClassInfo) ||
        iid.equals(Ci.nsISupports))
      return this;
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
}


function FeedRequest(fm, url, coop, listener, use_feed_api, metadataOnly) {
  this._fm = fm;
  this._url = url;
  this._coop = coop;
  this._listener = listener;
  this._use_feed_api = use_feed_api;
  this._metadataOnly = metadataOnly;

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

  this._channel = gIOService.newChannelFromURI(url);

  try {
    this._httpChannel = this._channel.QueryInterface(Ci.nsIHttpChannel);
  }
  catch (e) {
    this._httpChannel = null;
  }
}

FeedRequest.prototype = {
  get: function FR_get() {
    if (this._httpChannel) {
      var feedNode = getFeedNode(this._url.spec, this._coop);
      if (feedNode && feedNode.lastModification)
        this._httpChannel.setRequestHeader('If-Modified-Since',
                                           feedNode.lastModification,
                                           false);
    }

    this._channel.asyncOpen(this, null);
  },

  onStartRequest: function FR_onStartRequest(request, context) {
    var channel = request.QueryInterface(Ci.nsIChannel);

    if (Components.isSuccessCode(request.status))
      channel.contentType = 'text/xml';

    this._processor = Cc['@mozilla.org/feed-processor;1']
      .createInstance(Ci.nsIFeedProcessor);
    this._processor.listener = this;

    if (this._metadataOnly) {
      this._processor.QueryInterface(Ci.flockIFeedProcessor);
      this._processor.parseFeedMetadataAsync(null, channel.URI);
    } else {
      this._processor.parseAsync(null, channel.URI);
    }

    this._processor.onStartRequest(request, context);
  },

  onStopRequest: function FR_onStopRequest(request, context, status) {
    if (this._processor) {
      this._processor.onStopRequest(request, context, status);
      this._processor = null;
    }
  },

  onDataAvailable: function FR_onDataAvailable(request, context, inputStream,
                                               sourceOffset, count) {
    if (this._processor)
      this._processor.onDataAvailable(request, context, inputStream,
                                      sourceOffset, count);
  },

  handleResult: function FR_handleResult(result) {
    var failed = true;

    if (result && result.doc) {
      var feed = this._fm.storeFeed(this._url, result);
      if (feed) {
        failed = false;

        if (this._httpChannel && !this._metadataOnly)
          this._saveLastModification(feed._feedNode);

        this._notifyListener(feed);
      }
    } else if (this._httpChannel &&
               Components.isSuccessCode(this._httpChannel.status) &&
               this._httpChannel.responseStatus == 304) {
      this._logger.info('Feed not modified: ' + this._url.spec);

      var feedNode = getFeedNode(this._url.spec, this._coop);
      if (feedNode) {
        updateNextRefresh(feedNode, this._coop);

        if (feedNode.state != 'failed') {
          feedNode.lastFetch = new Date();

          var feed = new Feed(feedNode, null, this._coop);
          this._notifyListener(feed);
        }
      }
      else if (this._listener)
        this._listener.onError(null);

      failed = false;
    }

    if (failed) {
      this._logger.warn('Error processing feed: ' + this._url.spec);

      if (!this._metadataOnly) {
        var feedNode = getFeedNode(this._url.spec, this._coop);
        if (feedNode) {
          feedNode.state = 'failed';
          updateNextRefresh(feedNode, this._coop);

          if (this._httpChannel)
            this._saveLastModification(feedNode);
        }
      }

      if (this._listener)
        this._listener.onError(null);
    }

    this._processor = null;
    this._listener = null;
  },

  _notifyListener: function FR__notifyListener(feed) {
    if (this._listener) {
      if (this._use_feed_api)
        this._listener.onGetFeedComplete(feed);
      else
        this._listener.onResult();
    }
  },
  _saveLastModification: function FR__saveLastModification(feedNode) {
    var lastModification = null;
    try {
      lastModification = this._httpChannel.getResponseHeader('Last-Modified');
    }
    catch (e) { }

    if (lastModification)
      feedNode.lastModification = lastModification;
  },

  QueryInterface: function FR_QueryInterface(iid) {
    if (iid.equals(Ci.nsIFeedResultListener) ||
        iid.equals(Ci.nsIStreamListener) ||
        iid.equals(Ci.nsIRequestObserver)||
        iid.equals(Ci.nsISupports))
      return this;
    throw Cr.NS_ERROR_NO_INTERFACE;
  },
}


function FeedManager() {
  var obs = getObserverService();
  obs.addObserver(this, 'xpcom-shutdown', false);

  this._start();
}

FeedManager.prototype = {
  _start: function FM__start() {
    this._profiler = Cc['@flock.com/profiler;1'].getService(Ci.flockIProfiler);
    var evtID = this._profiler.profileEventStart('feedmanager-init');

    this._logger = Cc['@flock.com/logger;1'].createInstance(Ci.flockILogger);
    this._logger.init('feedmanager');
    this._logger.info('starting up...');

    this._obsService = getObserverService();

    gIOService = Cc['@mozilla.org/network/io-service;1']
      .getService(Ci.nsIIOService);

    var sbs = Cc['@mozilla.org/intl/stringbundle;1']
      .getService(Ci.nsIStringBundleService);
    var bundle = sbs.createBundle(URI_FEED_PROPERTIES);
    gReadMoreBlurb = bundle.GetStringFromName('flock.feed.temp.readmore');

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

    gFeedStorage = new FeedStorage();

    var feedroot = new this._coop.Folder(URN_FEED_ROOT);

    this._feedContexts = {};
    this.createFeedContext(NEWS_CONTEXT_NAME);
    this.createFeedContext(LIVEMARKS_CONTEXT_NAME);

    this._coop.FeedItem.add_destroy_notifier(this._deleteItemContent);

    var prefService = Cc['@mozilla.org/preferences-service;1']
      .getService(Ci.nsIPrefBranch2);
    prefService.addObserver(PREF_EXPIRATION_TIME_BRANCH, this, false);

    this.observe(null, 'nsPref:changed', PREF_EXPIRATION_TIME);
    this.observe(null, 'nsPref:changed', PREF_METADATA_EXPIRATION_TIME);

    this._profiler.profileEventEnd(evtID, '');
  },
  _shutdown: function FM__shutdown() {
    var prefService = Cc['@mozilla.org/preferences-service;1']
      .getService(Ci.nsIPrefBranch2);
    prefService.removeObserver(PREF_EXPIRATION_TIME_BRANCH, this);

    this._coop.FeedItem.remove_destroy_notifier(this._deleteItemContent);

    gIOService   = null;
    gFeedStorage = null;

    this._obsService = null;
  },
  _updateExpirationTimes: function FM__updateExpirationTimes(topic, pref) {
    var prefService = Cc['@mozilla.org/preferences-service;1']
      .getService(Components.interfaces.nsIPrefService);
    var prefBranch = prefService.getBranch(PREF_EXPIRATION_TIME_BRANCH);

    var val = 0;
    try {
      val = prefBranch.getIntPref(pref);
    }
    catch (e) { }

    if (pref == PREF_EXPIRATION_TIME) {
      this._refreshExpire  = val > 0 ? val : DEFAULT_EXPIRATION_TIME;
      this._refreshExpire *= 60 * 1000;
    } else {
      this._metadataExpire = val > 0 ? val : DEFAULT_METADATA_EXPIRATION_TIME;
      this._metadataExpire *= 60 * 1000;
    }
  },

  observe: function FM_observe(subject, topic, state) {
    var obs = getObserverService();

    switch (topic) {
      case 'xpcom-shutdown':
        obs.removeObserver(this, 'xpcom-shutdown');
        this._shutdown();
        break;

      case 'nsPref:changed':
        this._updateExpirationTimes(topic, state);
        break;
    }
  },

  getFeed: function FM_getFeed(url, listener) {
    var feedNode = getFeedNode(url.spec, this._coop);
    if (feedNode && !feedNode.metadataOnly) {
      var age = Date.now() - feedNode.lastFetch.getTime();
      if (age < this._refreshExpire) {
        this._logger.info("getting feed from cache: " + url.spec);
        var feed = new Feed(feedNode, null, this._coop);
        if (listener)
          listener.onGetFeedComplete(feed);
        return;
      }
    }

    this.getFeedBypassCache(url, listener);
  },

  getFeedMetadata: function FM_getFeedMetadata(url, listener) {
    var feedNode = getFeedNode(url.spec, this._coop);
    if (feedNode) {
      if (!feedNode.metadataOnly) {
        this._logger.info("getting feed metadata from complete feed: " +
                          url.spec);
        this.getFeed(url, listener);
        return;
      } else {
        var age = Date.now() - feedNode.lastFetch.getTime();
        if (age < this._metadataExpire) {
          this._logger.info("getting feed metadata from cache: " + url.spec);
          var feed = new Feed(feedNode, null, this._coop);
          if (listener)
            listener.onGetFeedComplete(feed);
          return;
        }
      }
    }

    this._logger.info("getting feed metadata from network: " + url.spec);
    var req = new FeedRequest(this, url, this._coop, listener, true, true);
    req.get();
  },

  getFeedBypassCache: function FM_getFeedBypassCache(url, listener) {
    this._logger.info("getting feed from network: " + url.spec);
    var req = new FeedRequest(this, url, this._coop, listener, true, false);
    req.get();
  },

  createFeedContext: function FM_createFeedContext(name) {
    var ctxt = this._feedContexts[name];
    if (ctxt)
      return ctxt;

    var context = new this._coop.FeedContext({ name: name });
    context.flaggedItems = new this._coop.FeedFlaggedStream({ context: context });

    var feedroot = this._coop.get(URN_FEED_ROOT);
    feedroot.children.addOnce(context);

    ctxt = new FeedContext(context, this._coop);
    this._feedContexts[name] = ctxt;

    return ctxt;
  },

  getFeedContext: function FM_getFeedContext(name) {
    var ctxt = this._feedContexts[name];
    if (ctxt)
      return ctxt;

    var urn = this._coop.FeedContext.get_id({ name: name });
    var context = this._coop.get(urn);
    if (context) {
      ctxt = new FeedContext(context, this._coop);
      this._feedContexts[name] = ctxt;
      return ctxt;
    } else {
      throw Components.Exception("No feed context named " + name);
    }
  },

  getFeedContexts: function FM_getFeedContexts() {
    var contexts = this._coop.FeedContext.all();
    var fm = this;
    var ctor = function(contextNode, coop) {
      var ctxt = fm._feedContexts[contextNode.name];
      if (!ctxt) {
        ctxt = new FeedContext(contextNode, coop);
        fm.feedContexts[contextNode.name] = ctxt;
      }
      return ctxt;
    }
    return this._enumerateObjects(contexts, ctor);
  },

  deleteFeedContext: function FM_deleteFeedContext(name) {
    var urn = this._coop.FeedContext.get_id({ name: name });
    var context = this._coop.get(urn);
    if (!context)
      throw Components.Exception("No feed context named " + name);
    
    delete this._feedContexts[name];

    context.flaggedItems.destroy();
    // FIXME: delete folders as well
    context.destroy();
  },

  existsFeed: function FM_existsFeed(url) {
    var urn = this._coop.Feed.get_id({ URL: url.spec });
    return this._coop.Feed.exists(urn);
  },

  refresh: function FM_refresh(urn, listener) {
    this._logger.info('refreshing feed: ' + urn);
    var feedNode = this._coop.get(urn);
    if (feedNode) {
/*
      if (!feedNode.metadataOnly) {
        var age = Date.now() - feedNode.lastFetch.getTime();
        if (age < this._refreshExpire) {
          if (listener)
            listener.onResult();
          return;
        }
      }
*/

      var req = new FeedRequest(this, makeURI(feedNode.URL), this._coop,
                                listener, false, false);
      req.get();
    } else {
      var msg = 'could not get feed: ' + urn;
      this._logger.error(msg);
      throw Components.Exception(msg, Cr.NS_ERROR_UNEXPECTED);
    }
  },

  getFeedFolderItem: function FM_getFeedFolderItem(urn) {
    this._logger.info('getting feed folder item: ' + urn);
    var node = this._coop.get(urn);
    if (node) {
      var context = this.getFeedContext(NEWS_CONTEXT_NAME);
      if (node.isInstanceOf(this._coop.Feed)) {
        return new Feed(node, context, this._coop);
      } else if (node.isInstanceOf(this._coop.FeedFolder)) {
        return new FeedFolder(node, context, this._coop);
      }
    }

    this._logger.warn('could not get feed folder item: ' + urn);
    return null;
  },
  getFeedItem: function FM_getFeedItem(urn) {
    this._logger.info('getting feed item: ' + urn);
    var node = this._coop.get(urn);
    if (node && node.isInstanceOf(this._coop.FeedItem)) {
      var parents = node.getParents();
      for each (var parent in parents) {
        if (parent.isInstanceOf(this._coop.Feed)) {
          var context = this.getFeedContext(NEWS_CONTEXT_NAME);
          var feed = new Feed(parent, context, this._coop);
          return new FeedItem(feed, node, this._coop);
        }
      }
    }

    this._logger.warn('could not get feed item: ' + urn);
    return null;
  },

  getLibrary: function FM_getLibrary() {
    var feeds = this._coop.Feed.all();
    var ctor = function(feedNode, coop) {
      return new Feed(feedNode, null, coop);
    }
    return this._enumerateObjects(feed, ctor);
  },

  storeFeed: function FM_storeFeed(feedURL, feedResult) {
    if (feedResult.bozo)
      return null;

    var feed = feedResult.doc;
    feed.QueryInterface(Ci.nsIFeed);

    var flockFeed = feed.QueryInterface(Ci.flockIFeedContainer);
    if (flockFeed.metadataOnly) {
      var evtID = this._profiler.profileEventStart('feedmanager-store-feed-metadata');
      this._logger.debug('storing feed metadata: ' + feedURL.spec);
    } else {
      var evtID = this._profiler.profileEventStart('feedmanager-store-feed');
      this._logger.info('storing feed: ' + feedURL.spec);
    }

    var title    = feed.title    ? feed.title.plainText()    : null;
    var subtitle = feed.subtitle ? feed.subtitle.plainText() : null;

    var format = feedResult.version ? feedResult.version : UNKNOWN_FEED_TYPE;

    var author = null;
    if (feed.authors && feed.authors.length)
      author = feed.authors.queryElementAt(0, Ci.nsIFeedPerson).name;

    var image = null;
    try {
      image = makeURI(feed.image.getPropertyAsAString('url'));
    }
    catch (e) { }

    var existingFeed = getFeedNode(feedURL.spec, this._coop);
    if (existingFeed)
      title = existingFeed.name;

    var feedNode = this._newFeedNode(feedURL, feedResult.uri,
                                     title, subtitle, feed.link, author,
                                     format, image, flockFeed.metadataOnly);

    feedNode.lastFetch = new Date();

    if (flockFeed.metadataOnly) {
      this._profiler.profileEventEnd(evtID, feedURL.spec);
      return new Feed(feedNode, null, this._coop);
    }

    updateNextRefresh(feedNode, this._coop);

    insertFavicon(feedNode);

    var feedItems = feed.items;
    var numItems = feedItems.length;

    var items = [];

    for (var i = 0; i < numItems; i++) {
      var item = feedItems.queryElementAt(i, Ci.nsIFeedEntry);
      var datevalue = item.updated ? new Date(item.updated)
                                   : feedNode.datevalue;
      items.push({item: item, datevalue: datevalue, indexvalue: i});
    }

    items.sort(feedItemSorter);

    var refDate;
    var children = feedNode.children.enumerateBackwards();
    if (children.hasMoreElements())
      refDate = children.getNext().datevalue;
    else
      refDate = new Date(0);

    gFeedStorage.beginTransaction();

    try {
      var itemsAdded = 0;
      for each (var i in items) {
        var node = this._storeFeedEntry(i.item, feedNode, refDate, feed);
        if (node)
          itemsAdded++;
      }
    }
    finally {
      gFeedStorage.commitTransaction();
    }

    children = feedNode.children.enumerateBackwards();
    if (children.hasMoreElements())
      feedNode.datevalue = children.getNext().datevalue;
    else
      feedNode.datevalue = new Date();

    var feedObj = new Feed(feedNode, null, this._coop);

    if (itemsAdded && existingFeed)
      this._obsService.notifyObservers(feedObj, 'new-feed-items', null);

    this._profiler.profileEventEnd(evtID, feedURL.spec);

    return feedObj;
  },

  _newFeedNode: function FM__newFeedNode(feedURL, finalURL, title, subtitle,
                                         link, author, format, image,
                                         metadataOnly) {
    var feedInfo = { URL: feedURL.spec,
                     finalURL: finalURL ? finalURL.spec : feedURL.spec,
                     name: title ? title : feedURL.spec,
                     subtitle: subtitle,
                     link: link ? link.spec : feedURL.spec,
                     author: author ? author : null,
                     format: format ? format : UNKNOWN_FEED_TYPE,
                     image: image ? image.spec : null,
                     metadataOnly: metadataOnly,
                     state: 'ok',
                     serviceId: FM_CONTRACTID
                   };
    return new this._coop.Feed(feedInfo);
  },

  _storeFeedEntry: function FM__storeFeedEntry(item, feedNode, refDate, feed) {
    var title = null;
    if (item.title)
      title = item.title.plainText();

    var author = null;
    if (item.authors && item.authors.length)
      author = item.authors.queryElementAt(0, Ci.nsIFeedPerson).name;

    var pubdate = item.published;
    if (!pubdate)
      pubdate = item.updated;
    if (!pubdate)
      pubdate = feed.updated;

    var content = null;
    if (item.content)
      content = item.content;
    else if (item.summary)
      content = item.summary;

    var excerptSource = null;
    if (item.summary)
       excerptSource = item.summary.plainText();
    else if (item.content)
       excerptSource = item.content.plainText();

    return this._storeFeedItem(feedNode, item.id, title,
                               item.link, author, pubdate, refDate,
                               content, excerptSource,
                               false, false);
  },

  _storeFeedItem: function FM__storeFeedItem(feedNode, id, title, link, author,
                                             pubdate, refDate,
                                             content, excerptSource,
                                             read, flagged) {
    if (!id && link)
      id = link.spec;

    if (!link)
      link = makeURI(feedNode.URL);

    if (!title && excerptSource)
      title = excerptSource.substr(0, TITLE_TRIM_MAX_CHARS) + '...';

    if (title)
      title = title.replace(/[\r\n]+/g, ' ');
    else
      title = link.spec;

    if (pubdate)
       pubdate = new Date(pubdate);

    var itemInfo = { itemid: id,
                     URL: link.spec,
                     name: title,
                     author: author,
                     unseen: !read,
                     flagged: flagged,
                     parentfeed: feedNode.URL
                   };

    //TODO: we'll want to be more intelligent about this, but the goal should be to only update feed items that need updating
    var rdfId = this._coop.FeedItem.get_id(itemInfo);
    if (rdfId && this._coop.FeedItem.exists(rdfId))
      return null;

    itemInfo.pubdate = pubdate;

    if (!pubdate)
      itemInfo.datevalue = new Date();
    else if (pubdate.getTime() > Date.now())
      itemInfo.datevalue = new Date();
    else if (pubdate < refDate)
      itemInfo.datevalue = new Date();
    else
      itemInfo.datevalue = pubdate;

    itemInfo.isIndexable = feedNode.isIndexable;

    this._logger.info('storing feeditem: ' + itemInfo.name);

    var itemNode = new this._coop.FeedItem(itemInfo);

    feedNode.children.insertSortedOn(DATEVALUE, itemNode);

    if (content) {
      if (typeof(content) != 'string') {
        var parser = Cc['@mozilla.org/xmlextras/domparser;1']
          .createInstance(Ci.nsIDOMParser);
        var doc = parser.parseFromString('<div/>', 'application/xhtml+xml');

        var docElem = doc.documentElement;
        var fragment = content.createDocumentFragment(docElem);
        docElem.appendChild(fragment);

        this._filterFeedItemContent(doc, content.base);

        var serializer = Cc['@mozilla.org/xmlextras/xmlserializer;1']
          .createInstance(Ci.nsIDOMSerializer);
        var data = serializer.serializeToString(doc);
      } else {
        data = content;
      }

      var excerpt = excerptText(excerptSource, itemInfo.URL);
      gFeedStorage.saveItem(itemNode.id(), feedNode.URL, data, excerpt);
    } else {
      gFeedStorage.deleteItem(itemNode.id());
    }

    return itemNode;
  },
  _filterFeedItemContent: function FM__filterFeedItemContent(doc, baseURI) { 
    if (!baseURI)
      return;

    var tw = doc.createTreeWalker(doc.documentElement,
                                  Ci.nsIDOMNodeFilter.SHOW_ELEMENT,
                                  null, false);
    node = tw.nextNode();
    while (node) {
      for each (var attrName in ATTR_URI_LIST) {
        var attr = node.attributes.getNamedItem(attrName);
        if (attr) {
          var nval = attr.nodeValue;
          try {
            var newURI = gIOService.newURI(nval, null, baseURI);
            attr.nodeValue = newURI.spec;
          }
          catch (e) { }
        }
      }
      node = tw.nextNode();
    }
  },

  _deleteItemContent: function FM__deleteItemContent(item, coop) {
    gFeedStorage.deleteItem(item.id());
  },

  _purgeFeed: function FM__purgeFeed(feed) {
    this._logger.info('Purging ' + feed.id());

    var items = feed.children.enumerateBackwards();
    if (items.hasMoreElements()) {
      this._coop.FeedItem.remove_destroy_notifier(this._deleteItemContent);

      gFeedStorage.deleteFeed(feed.URL);

      while (items.hasMoreElements())
        items.getNext().destroy();

      this._coop.FeedItem.add_destroy_notifier(this._deleteItemContent);
    }

    feed.destroy();
  },

  _enumerateObjects: function FM__enumerateObjects(objects, ctor) {
    var coop = this._coop;

    var enumerator = {
      hasMoreElements: function() {
        return objects.hasMoreElements();
      },
      getNext: function() {
        return ctor(objects.getNext(), coop);
      }
    }

    return enumerator;
  },

  _purgeOrphanedFeeds: function FM__purgeOrphanedFeeds() {
    var feeds = this._coop.Feed.all();

    var fm = this;

    var purge = {
      notify: function(aTimer) {
        var start = Date.now();
        while (feeds.hasMoreElements()) {
          var feed = feeds.getNext();

          var contexts = getContextsForFeed(feed, fm._coop);
          if (contexts.length == 0)
            fm._purgeFeed(feed);

          if (Date.now() > start + PURGE_RUN_INTERVAL)
            return;
        }

        aTimer.cancel();
      }
    };

    var timer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer);
    timer.initWithCallback(purge, PURGE_SLEEP_INTERVAL,
                           Ci.nsITimer.TYPE_REPEATING_SLACK);

  },

  /* flockIHousekeeping interface */
  runHousekeeping: function FM_runHousekeeping() {
    this._logger.info('running housekeeping...');
    this._purgeOrphanedFeeds();
  },

  /* flockIStreamOwner interface */
  getMaxItemsForStream: function FM_getMaxItemsForStream(aURN) {
    this._logger.debug("getting maxItems for: " + aURN);
    var maxItems = 0;

    var node = this._coop.get(aURN);
    if (node && node.isInstanceOf(this._coop.Feed)) {
      var contexts = getContextsForFeed(node, this._coop);
      for each (var context in contexts) {
        if (context.maxItems > 0) {
          if (maxItems > 0) {
            maxItems = Math.min(maxItems, context.maxItems);
          } else {
            maxItems = context.maxItems;
          }
        }
      }
    }

    if (maxItems > 0) {
      this._logger.debug(maxItems + " maxItems for: " + aURN);
    } else {
      this._logger.debug("using default maxItems for: " + aURN);
    }

    return maxItems;
  },

  /* flockIMigratable interface */
  get migrationName() {
    return flockGetString("common/migrationNames", "migration.name.feeds");
  },

  needsMigration: function FM_needsMigration(oldVersion) {
    var oldFeedsFile = this._getOldFeedServiceFile(OLD_FEEDS_RDF_FILE);
    return oldFeedsFile.exists();
  },
  startMigration: function FM_startMigration(oldVersion, listener) {
    var obs = getObserverService();
    obs.notifyObservers(null, 'myworld-enableDisable-observers', false);
    
    this._start();

    gFeedStorage.beginTransaction();

    var ctxt = {
      listener: listener,

      oldFeedsFile     : this._getOldFeedServiceFile(OLD_FEEDS_RDF_FILE),
      oldFlaggedFile   : this._getOldFeedServiceFile(OLD_FLAGGED_RDF_FILE),
      oldRootFile      : this._getOldFeedServiceFile(OLD_ROOT_RDF_FILE),
      oldDiscoveryFile : this._getOldFeedServiceFile(OLD_DISCOVERY_RDF_FILE),
      oldFeedDir       : this._getOldFeedServiceFile(OLD_FEED_DATA_DIR),

      oldFeedsReported   : false,
      oldFlaggedReported : false,
      cleanupReported    : false,
    };

    return { wrappedJSObject: ctxt };
  },
  finishMigration: function FM_finishMigration(ctxtWrapper) {
    gFeedStorage.commitTransaction();
    
    var obs = getObserverService();
    obs.notifyObservers(null, 'myworld-enableDisable-observers', true);
  },
  doMigrationWork: function FM_doMigrationWork(ctxtWrapper) {
    var ctxt = ctxtWrapper.wrappedJSObject;

    if (!ctxt.oldFeedsReported) {
      ctxt.listener.onUpdate(0, 'Migrating subscriptions');
      ctxt.oldFeedsReported = true;
    } else if (ctxt.oldFeedsFile.exists()) {
      if (!ctxt.oldFeedsMigrator)
        ctxt.oldFeedsMigrator = this._migrateOldFeeds(ctxt);
      if (ctxt.oldFeedsMigrator.next())
        ctxt.oldFeedsMigrator = null;
    } else if (!ctxt.oldFlaggedReported) {
      ctxt.listener.onUpdate(-1, 'Migrating saved articles');
      ctxt.oldFlaggedReported = true;
    } else if (ctxt.oldFlaggedFile.exists()) {
      this._migrateOldFlagged(ctxt);
    } else if (!ctxt.cleanupReported) {
      ctxt.listener.onUpdate(-1, 'Cleaning up old data');
      ctxt.cleanupReported = true;
    } else if (ctxt.oldRootFile.exists()) {
      ctxt.oldRootFile.remove(false);
    } else if (ctxt.oldDiscoveryFile.exists()) {
      ctxt.oldDiscoveryFile.remove(false);
    } else if (ctxt.oldFeedDir.exists()) {
      ctxt.oldFeedDir.remove(true);
      return false;
    } else {
      return false;
    }

    return true;
  },

  _getOldFeedServiceFile: function FM__getOldFeedServiceFile(filename) {
    var oldFile = Cc['@mozilla.org/file/directory_service;1']
      .getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
    oldFile.append(filename)
    return oldFile;
  },
  _migrateOldFeeds: function FM__migrateOldFeeds(ctxt) {
    var RDFS = Cc['@mozilla.org/rdf/rdf-service;1']
      .getService(Ci.nsIRDFService);
    var RDFCU = Cc['@mozilla.org/rdf/container-utils;1']
      .getService(Ci.nsIRDFContainerUtils);

    var spec = gIOService.newFileURI(ctxt.oldFeedsFile).spec;
    var ds = RDFS.GetDataSourceBlocking(spec);
    var root = RDFS.GetResource(SUBSCRIPTIONS_RDF_ROOT);

    var context = this.getFeedContext(NEWS_CONTEXT_NAME);
    var news_root = context.getRoot();

    var titleProp = RDFS.GetResource(FLOCK_NS + 'title');

    var typeProp = RDFS.GetResource(FLOCK_NS + 'type');
    var feeds = ds.GetSources(typeProp, RDFS.GetLiteral('feed'), true);

    var numFeeds = 0;
    while (feeds && feeds.hasMoreElements()) {
      var feed = feeds.getNext();
      numFeeds++;
    }

    var children = RDFCU.MakeSeq(ds, root).GetElements(), i = 0;
    while (children && children.hasMoreElements()) {
      var child = children.getNext();
      child.QueryInterface(Ci.nsIRDFResource);

      if (RDFCU.IsContainer(ds, child)) {
        var title = ds.GetTarget(child, titleProp, true);
        if (!title)
          continue;

        title.QueryInterface(Ci.nsIRDFLiteral);
        if (!title.Value)
          continue;

        var subFolder = news_root.addFolder(title.Value);

        var subChildren = RDFCU.MakeSeq(ds, child).GetElements();
        while (subChildren && subChildren.hasMoreElements()) {
          var subChild = subChildren.getNext();
          subChild.QueryInterface(Ci.nsIRDFResource);

          var url = subChild.Value;
          var percent = Math.round(i / numFeeds * 100);
          ctxt.listener.onUpdate(percent, 'Migrating ' + url);
          yield false;

          i++;
          this._migrateFeed(url, subFolder);
        }
      } else {
        var url = child.Value;
        var percent = Math.round(i / numFeeds * 100);
        ctxt.listener.onUpdate(percent, 'Migrating ' + url);
        yield false;

        i++;
        this._migrateFeed(url, news_root);
      }
    }

    var oldFile = ctxt.oldFeedsFile.clone();
    oldFile.moveTo(null, OLD_FEEDS_RDF_FILE_RELIC);
    //ctxt.oldFeedsFile.remove(false);

    yield true;
  },
  _migrateFeed: function FM__migrateFeed(url, folder) {
    var RDFS = Cc['@mozilla.org/rdf/rdf-service;1']
      .getService(Ci.nsIRDFService);
    var RDFCU = Cc['@mozilla.org/rdf/container-utils;1']
      .getService(Ci.nsIRDFContainerUtils);

    var oldFeedFile = this._getOldFeedServiceFile(OLD_FEED_DATA_DIR);
    oldFeedFile.append(FlockCryptoHash.md5(url));
    oldFeedFile.append(OLD_FEED_DATA_FILENAME);

    if (!oldFeedFile.exists())
      return;

    var spec = gIOService.newFileURI(oldFeedFile).spec;
    var ds = RDFS.GetDataSourceBlocking(spec);
    var root = RDFS.GetResource(OLD_FEED_DATA_RDF_PREFIX + url);

    function getTarget(prop) {
      var t = ds.GetTarget(root, prop, true);
      return t ? t.QueryInterface(Ci.nsIRDFLiteral).Value : null;
    }

    var title    = getTarget(RDFS.GetResource(FLOCK_NS + 'title'));
    var subtitle = getTarget(RDFS.GetResource(FLOCK_NS + 'description'));
    var author   = getTarget(RDFS.GetResource(FLOCK_NS + 'author'));
    var link     = getTarget(RDFS.GetResource(FLOCK_NS + 'link'));
    var format   = getTarget(RDFS.GetResource(FLOCK_NS + 'feedtype'));
    var image    = getTarget(RDFS.GetResource(FLOCK_NS + 'image'));
    var favicon  = getTarget(RDFS.GetResource(FLOCK_NS + 'favicon'));

    if (FORMAT_CONVERSIONS[format])
      format = FORMAT_CONVERSIONS[format];
    else if (format.indexOf('RSS') == 0)
      format = 'rssUnknown';
    else
      format = null;

    var feedURL = makeURI(url);
    if (!feedURL)
      return;

    var feedNode = this._newFeedNode(feedURL, null, title, subtitle,
                                     makeURI(link), author, format,
                                     makeURI(image), false);

    var faviconURL = makeURI(favicon);
    if (faviconURL)
      feedNode.favicon = faviconURL.spec;

    folder.subscribeFeed(new Feed(feedNode, null, this._coop));

    var items = [];

    var children = RDFCU.MakeSeq(ds, root).GetElements();
    while (children && children.hasMoreElements()) {
      var item = this._migrateFeedItem(ds, children.getNext(), false);
      if (item)
        items.push(item);
    }

    items.sort(feedItemSorter);

    for each (var i in items) {
      var args = i.item;
      args.unshift(feedNode);
      this._storeFeedItem.apply(this, args);
    }

    children = feedNode.children.enumerateBackwards();
    if (children.hasMoreElements())
      feedNode.datevalue = children.getNext().datevalue;
    else
      feedNode.datevalue = new Date();
  },
  _migrateOldFlagged: function FM__migrateOldFlagged(ctxt) {
    var RDFS = Cc['@mozilla.org/rdf/rdf-service;1']
      .getService(Ci.nsIRDFService);
    var RDFCU = Cc['@mozilla.org/rdf/container-utils;1']
      .getService(Ci.nsIRDFContainerUtils);

    var spec = gIOService.newFileURI(ctxt.oldFlaggedFile).spec;
    var ds = RDFS.GetDataSourceBlocking(spec);
    var root = RDFS.GetResource(FLAGGED_RDF_ROOT);

    var context = new this._coop.FeedContext({ name: NEWS_CONTEXT_NAME });
    var flaggedItems = context.flaggedItems;

    var feedProp = RDFS.GetResource(FLOCK_NS + 'feed');

    var children = RDFCU.MakeSeq(ds, root).GetElements();
    while (children && children.hasMoreElements()) {
      var child = children.getNext();
      child.QueryInterface(Ci.nsIRDFResource);

      var feedURL = ds.GetTarget(child, feedProp, true);
      if (!feedURL)
        continue;

      feedURL.QueryInterface(Ci.nsIRDFLiteral);

      var feedNode = getFeedNode(feedURL.Value, this._coop);
      if (feedNode) {
        var args = this._migrateFeedItem(ds, child, true).item;
        args.unshift(feedNode);
        var node = this._storeFeedItem.apply(this, args);
        if (node)
          flaggedItems.children.insertSortedOn(DATEVALUE, node);
      }
    }

    var oldFile = ctxt.oldFlaggedFile.clone();
    oldFile.moveTo(null, OLD_FLAGGED_RDF_FILE_RELIC);
    //ctxt.oldFlaggedFile.remove(false);
  },
  _migrateFeedItem: function FM__migrateFeedItem(ds, res, doFlagged) {
    var RDFS = Cc['@mozilla.org/rdf/rdf-service;1']
      .getService(Ci.nsIRDFService);

    res.QueryInterface(Ci.nsIRDFResource);

    function getTarget(prop) {
      var t = ds.GetTarget(res, prop, true);
      return t ? t.QueryInterface(Ci.nsIRDFLiteral).Value : null;
    }

    var flagged = getTarget(RDFS.GetResource(FLOCK_NS + 'flagged'));
    if (flagged == 'true' && !doFlagged)
      return;

    var id = res.Value;
    if (id.indexOf(OLD_FEED_DATA_POST_PREFIX) == 0)
      id = id.substr(OLD_FEED_DATA_POST_PREFIX.length);
    else
      id = null;

    var title      = getTarget(RDFS.GetResource(FLOCK_NS + 'title'));
    var link       = getTarget(RDFS.GetResource(FLOCK_NS + 'link'));
    var datevalue  = getTarget(RDFS.GetResource(FLOCK_NS + 'datevalue'));
    var author     = getTarget(RDFS.GetResource(FLOCK_NS + 'author'));
    var content    = getTarget(RDFS.GetResource(FLOCK_NS + 'description'));
    var read       = getTarget(RDFS.GetResource(FLOCK_NS + 'read'));

    datevalue = new Date(Number(datevalue));
    refDate = new Date(0);

    var item = [id, title, makeURI(link), author, datevalue, refDate, content,
                content, read == 'true', doFlagged];

    return ({ item: item, datevalue: datevalue, indexvalue: 0 });
  },

  getInterfaces: function FM_getInterfaces(countRef) {
    var interfaces = [Ci.flockIFeedManager, Ci.flockIPollingService,
                      Ci.flockIMigratable, Ci.flockIHousekeeping,
                      Ci.flockIStreamOwner,
                      Ci.nsIObserver, Ci.nsIClassInfo, Ci.nsISupports];
    countRef.value = interfaces.length;
    return interfaces;
  },
  getHelperForLanguage: function FM_getHelperForLanguage(language) {
    return null;
  },
  contractID: FM_CONTRACTID,
  classDescription: FM_CLASSNAME,
  classID: FM_CLASSID,
  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
  flags: Ci.nsIClassInfo.SINGLETON,

  QueryInterface: function FM_QueryInterface(iid) {
    if (iid.equals(Ci.flockIFeedManager) ||
        iid.equals(Ci.flockIPollingService) ||
        iid.equals(Ci.flockIMigratable) ||
        iid.equals(Ci.flockIHousekeeping) ||
        iid.equals(Ci.flockIStreamOwner) ||
        iid.equals(Ci.nsIObserver) ||
        iid.equals(Ci.nsIClassInfo) ||
        iid.equals(Ci.nsISupports))
      return this;
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
}


function GenericComponentFactory(ctor) {
  this._ctor = ctor;
}

GenericComponentFactory.prototype = {

  _ctor: null,

  // nsIFactory
  createInstance: function(outer, iid) {
    if (outer != null)
      throw Cr.NS_ERROR_NO_AGGREGATION;
    return (new this._ctor()).QueryInterface(iid);
  },

  // nsISupports
  QueryInterface: function(iid) {
    if (iid.equals(Ci.nsIFactory) ||
        iid.equals(Ci.nsISupports))
      return this;
    throw Cr.NS_ERROR_NO_INTERFACE;
  },
};

var Module = {
  QueryInterface: function(iid) {
    if (iid.equals(Ci.nsIModule) ||
        iid.equals(Ci.nsISupports))
      return this;

    throw Cr.NS_ERROR_NO_INTERFACE;
  },

  getClassObject: function(cm, cid, iid) {
    if (!iid.equals(Ci.nsIFactory))
      throw Cr.NS_ERROR_NOT_IMPLEMENTED;

    if (cid.equals(FEED_CLASSID))
      return new GenericComponentFactory(Feed);
    if (cid.equals(ITEM_CLASSID))
      return new GenericComponentFactory(FeedItem);
    if (cid.equals(FC_CLASSID))
      return new GenericComponentFactory(FeedContext);
    if (cid.equals(FF_CLASSID))
      return new GenericComponentFactory(FeedFolder);
    if (cid.equals(FM_CLASSID))
      return new GenericComponentFactory(FeedManager);

    throw Cr.NS_ERROR_NO_INTERFACE;
  },

  registerSelf: function(cm, file, location, type) {
    var cr = cm.QueryInterface(Ci.nsIComponentRegistrar);
    cr.registerFactoryLocation(FEED_CLASSID, FEED_CLASSNAME, FEED_CONTRACTID,
                               file, location, type);
    cr.registerFactoryLocation(ITEM_CLASSID, ITEM_CLASSNAME, ITEM_CONTRACTID,
                               file, location, type);
    cr.registerFactoryLocation(FC_CLASSID, FC_CLASSNAME, FC_CONTRACTID,
                               file, location, type);
    cr.registerFactoryLocation(FF_CLASSID, FF_CLASSNAME, FF_CONTRACTID,
                               file, location, type);
    cr.registerFactoryLocation(FM_CLASSID, FM_CLASSNAME, FM_CONTRACTID,
                               file, location, type);

    var catman = Cc['@mozilla.org/categorymanager;1']
      .getService(Ci.nsICategoryManager);

    catman.addCategoryEntry('flock-rdf-setup', FM_CLASSNAME,
                            'service,' + FM_CONTRACTID,
                            true, true);

    catman.addCategoryEntry('flockMigratable', FM_CLASSNAME, FM_CONTRACTID,
                            true, true);
    catman.addCategoryEntry('flockHousekeeping', FM_CLASSNAME, FM_CONTRACTID,
                            true, true);
  },

  unregisterSelf: function(cm, location, type) {
    var cr = cm.QueryInterface(Ci.nsIComponentRegistrar);
    cr.unregisterFactoryLocation(FEED_CLASSID, location);
    cr.unregisterFactoryLocation(ITEM_CLASSID, location);
    cr.unregisterFactoryLocation(FC_CLASSID, location);
    cr.unregisterFactoryLocation(FF_CLASSID, location);
    cr.unregisterFactoryLocation(FM_CLASSID, location);
  },

  canUnload: function(cm) {
    return true;
  },
};

function NSGetModule(compMgr, fileSpec)
{
  return Module;
}
