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

const DEBUG = true; /* set to false to suppress debug messages */

const DEBUG_FEED_XML = false /* set to true to save out raw feed xml data */

const FLOCK_FEED_SERVICE_CONTRACTID = '@flock.com/feed-service;1';
const FLOCK_FEED_SERVICE_CID        = Components.ID('{d6ed5524-cf20-4948-97fa-d590cb1d38b6}');
const flockIFeedService             = Components.interfaces.flockIFeedService;

const FEED_TEMPLATE_DIR             = "chrome://browser/content/flock/feed/templates";
const FEED_ERROR_HTML_DIR           = "chrome://flock/locale/feed/errors";

const FEED_TEMPLATE_SINGLE_FEED     = "feed.html";
const FEED_TEMPLATE_SINGLE_BY_TOPIC = "single_feed_by_topic.html";
const FEED_TEMPLATE_AGG_BY_FEED     = "feed.html";
const FEED_TEMPLATE_AGG_BY_DATE     = "feed.html";
const FEED_TEMPLATE_AGG_BY_TOPIC    = "agg_by_topic.html";
const FEED_TEMPLATE_ROOT_VIEW       = "root_view.html";

const FEED_HTML_ERROR_BAD_FEED_DATA = "bad_feed_data.html";
const FEED_HTML_ERROR_NETWORK       = "network.html";
const FEED_HTML_ERROR_BAD_REQUEST   = "no_params.html";
const FEED_HTML_ERROR_NO_FEEDS      = "no_feeds.html";

const PREF_FEEDS_STYLESHEET         = 'flock.feeds.stylesheet';
const PREF_FEEDS_DEFAULT_STYLE      = 'flock.feeds.default_view_style';
const PREF_FEEDS_DEFAULT_COLUMNS    = 'flock.feeds.default_columns';
const PREF_FEEDS_SHOW_UNREAD        = 'flock.feeds.show_unread';
const PREF_FEEDS_SHOW_READ          = 'flock.feeds.show_read';
const PREF_FEEDS_SHOW_FLAGGED       = 'flock.feeds.show_saved';

const PREF_FEEDS_REFRESH_BRANCH        = 'flock.feeds.refresh';
const PREF_FEEDS_REFRESH_SUBSCRIPTIONS = '.subscriptions';
const PREF_FEEDS_REFRESH_DISCOVERY     = '.discovery';

const DEFAULT_STYLESHEET            = 'chrome://browser/skin/flock/feed/master.css';
const DEFAULT_STYLE                 = 'excerpts';
const DEFAULT_COLUMNS               = 2;
const DEFAULT_REFRESH_SUBSCRIPTIONS = 60;
const DEFAULT_REFRESH_DISCOVERY     = 24 * 60;

const FEED_RDF_NS                   = "http://flock.com/rdf#"

const FEED_DATA_DIR                 = "feeds";
const FEED_ALTERNATE_DATA_DIR       = "feeds_alt";

const FEED_DATA_FILENAME            = "feed.rdf";
const FEED_CACHE_FILENAME           = "feed.xml";
const FEED_IMAGE_DIR                = "images";

const FEED_DATA_RDF_PREFIX          = "urn:flock:feed:";

const FEED_DATA_RDF_FEED_ROOT       = FEED_DATA_RDF_PREFIX + "feeds";
const FEED_DATA_RDF_DATE_ROOT       = FEED_DATA_RDF_PREFIX + "dates";

const FEEDS_RDF_FILENAME            = "flock_feeds.rdf";
const SUBSCRIPTIONS_RDF_FILENAME    = "flock_subscriptions.rdf";
const ROOT_RDF_FILENAME             = "flock_feeds_root.rdf";
const FLAGGED_RDF_FILENAME          = "flock_feeds_flagged.rdf";
const DISCOVERY_RDF_FILENAME        = "flock_feeds_discovery.rdf";

const SUBSCRIPTIONS_RDF_PARENT      = "urn:flock:feed";
const SUBSCRIPTIONS_RDF_ROOT        = FEED_DATA_RDF_PREFIX + "subscriptions";
const COLLECTIONS_RDF_ROOT          = SUBSCRIPTIONS_RDF_ROOT + ":collections:";
const FLAGGED_RDF_NODE              = FEED_DATA_RDF_PREFIX + "flagged";

const FLAGGED_COLLECTION_NAME       = 'saved';

const CATCH_ALL_COLLECTION          = 'Uncategorized';

const MAX_POSTS_PER_FEED            = 50;

const MAX_POSTS_PER_PAGE            = 20;

const SNIPPET_MAX_WORDS             = 50;

const TRIM_CHARACTER_LIMIT          = 30;

const REFRESH_DELAY                 = 500;
const UPDATE_SUBSCRIPTION_DELAY     = 750;

const UNKNOWN_FEED_TYPE             = "Unknown";

const nsISupports                   = Components.interfaces.nsISupports;
const nsIClassInfo                  = Components.interfaces.nsIClassInfo;
const nsITimer                      = Components.interfaces.nsITimer;
const nsITimerCallback              = Components.interfaces.nsITimerCallback;
const nsIObserver                   = Components.interfaces.nsIObserver;
const nsIProperties                 = Components.interfaces.nsIProperties;
const nsIFile                       = Components.interfaces.nsIFile;
const nsIIOService                  = Components.interfaces.nsIIOService;
const nsIFileProtocolHandler        = Components.interfaces.nsIFileProtocolHandler;
const nsIXMLHttpRequest             = Components.interfaces.nsIXMLHttpRequest;
const nsIDOMSerializer              = Components.interfaces.nsIDOMSerializer;
const nsIRDFResource                = Components.interfaces.nsIRDFResource;
const flockIFeedUtility             = Components.interfaces.flockIFeedUtility;
const flockIFavoritesService        = Components.interfaces.flockIFavoritesService;
const flockIFavoriteProperties      = Components.interfaces.flockIFavoriteProperties;

const TIMER_CONTRACTID              = '@mozilla.org/timer;1';
const DIRECTORY_SERVICE_CONTRACTID  = '@mozilla.org/file/directory_service;1';
const IO_SERVICE_CONTRACTID         = '@mozilla.org/network/io-service;1';
const XMLHTTPREQUEST_CONTRACTID     = '@mozilla.org/xmlextras/xmlhttprequest;1';
const DOMSERIALIZER_CONTRACTID      = '@mozilla.org/xmlextras/xmlserializer;1';
const FEED_UTILITY_CONTRACTID       = '@flock.com/feed-utility;1';
const FAVORITES_CONTRACTID          = '@mozilla.org/rdf/datasource;1?name=flock-favorites';

const flockIHTML2XHTML              = Components.interfaces.flockIHTML2XHTML;
const flockIHTML2TEXT               = Components.interfaces.flockIHTML2TEXT;
const flockIOpmlService             = Components.interfaces.flockIOpmlService;

const HTML2XHTML_CONTRACTID         = "@flock.com/html2xhtml;1";
const HTML2TEXT_CONTRACTID          = "@flock.com/html2text;1";
const OPML_SERVICE_CONTRACTID       = "@flock.com/opml-service;1";

const FAV_FIELD_NAME                = flockIFavoriteProperties.FIELD_NAME;
const FAV_FIELD_LOCAL               = flockIFavoriteProperties.FIELD_LOCAL;
const FAV_FIELD_PRIVATE             = flockIFavoriteProperties.FIELD_PRIVATE;

/* from nspr's prio.h */
const PR_RDONLY                     = 0x01;
const PR_WRONLY                     = 0x02;
const PR_RDWR                       = 0x04;
const PR_CREATE_FILE                = 0x08;

/* orientation constants */
const ORIENT_ABOVE  = -1;
const ORIENT_INSIDE = 0;
const ORIENT_BELOW  = 1;

// Clustering configuration (for sorting by topic).
const USE_STEMMER = false;      // stem words?
const USE_STOPWORDS = true;     // eliminate common words?
const USE_BIGRAMS = true;       // use bigrams?
const BOOSTLINKS = 7;           // the amount to boost links by (XXX what
            // about links to www.google.com or similar irrelevant stuff?)
const BOOSTTITLES = 5;          // the amount to boost title words by

// Service implementation
function flockFeedService()
{
    debug("Constructed");

    this.initialized = false;

    var obs = Components.classes["@mozilla.org/observer-service;1"]
        .getService(Components.interfaces.nsIObserverService);

    obs.addObserver(this, 'profile-after-change', false);
    obs.addObserver(this, 'xpcom-shutdown', false);
    obs.addObserver(this, 'content-message', false)
}

function fixFeedColonURL(url)
{
    if (url.indexOf('feed:') == 0) {
        var tmpurl = url.substr('feed:'.length);
        if (tmpurl.indexOf('//') == 0)
            tmpurl = 'http:' + tmpurl;
        url = tmpurl;
    }
    return url;
}

function checkProtocol(url)
{
    url = fixFeedColonURL(url);

    if (url.indexOf('http:') != 0 && url.indexOf('https:') != 0) {
        debug('Unsupported scheme: ' + url);
        throw 'Bad protocol';
    }

    return url;
}

flockFeedService.prototype.notify =
function (timer)
{
    debug("starting refresh request");
    this.refreshAllFeeds(null);
}

flockFeedService.prototype.test =
function ()
{
    debug("testing!");
    return 'poop';
}

flockFeedService.prototype.init =
function ()
{
    this.feedStores = {};

    this.timer = null;

    this.update_timer = null;
    this.update_callback = null;

    var feedDir = Components.classes[DIRECTORY_SERVICE_CONTRACTID]
        .getService(nsIProperties).get('ProfD', nsIFile);
 
    var feedAltDir = feedDir.clone(); 
    var subsFile = feedDir.clone();
    var feedsFile = feedDir.clone();
    var rootFile = feedDir.clone();
    var flagFile = feedDir.clone();
    var discoveryFile = feedDir.clone();
 
    feedDir.append(FEED_DATA_DIR); 
    if (!feedDir.exists())
        feedDir.createUnique(nsIFile.DIRECTORY_TYPE, 0755);

    feedAltDir.append(FEED_ALTERNATE_DATA_DIR);
    if (feedAltDir.exists())
        feedAltDir.remove(true);

    subsFile.append(SUBSCRIPTIONS_RDF_FILENAME);
    if (!subsFile.exists())
        subsFile.createUnique(nsIFile.NORMAL_TYPE, 0600);

    feedsFile.append(FEEDS_RDF_FILENAME);
    if (feedsFile.exists())
        feedsFile.remove(false);

    rootFile.append(ROOT_RDF_FILENAME);
    if (!rootFile.exists())
        rootFile.createUnique(nsIFile.NORMAL_TYPE, 0600);

    flagFile.append(FLAGGED_RDF_FILENAME);
    if (!flagFile.exists())
        flagFile.createUnique(nsIFile.NORMAL_TYPE, 0600);

    discoveryFile.append(DISCOVERY_RDF_FILENAME);
    if (!discoveryFile.exists())
        discoveryFile.createUnique(nsIFile.NORMAL_TYPE, 0600);

    var ios = Components.classes[IO_SERVICE_CONTRACTID]
        .getService(nsIIOService);

    var fileHandler = ios.getProtocolHandler('file')
        .QueryInterface(nsIFileProtocolHandler);

    var spec = fileHandler.getURLSpecFromFile(subsFile);

    this.ds = new RDFDataSource(spec);

    var parent = this.ds.getNode(SUBSCRIPTIONS_RDF_PARENT);
    parent.makeSeq();

    this.root = this.ds.getNode(SUBSCRIPTIONS_RDF_ROOT);
    this.root.makeSeq();
    this.root.addTargetOnce(FEED_RDF_NS + 'type', 'root');

    var tval = this.root.getTarget(FEED_RDF_NS + 'total')
    if (!tval) {
        this.root.addTargetOnce(FEED_RDF_NS + 'total', '0');
        tval = this.root.getTarget(FEED_RDF_NS + 'total')
    }

    var uval = this.root.getTarget(FEED_RDF_NS + 'unread')
    if (!uval)
        this.root.addTargetOnce(FEED_RDF_NS + 'unread', tval.getValue());

    var nval = this.root.getTarget(FEED_RDF_NS + 'new');
    if (!nval) {
        this.root.addTargetOnce(FEED_RDF_NS + 'new', '0');
        nval = this.root.getTarget(FEED_RDF_NS + 'new');
    }

    parent.addChild(this.root);

    var flaggedNode = this.ds.getNode(FLAGGED_RDF_NODE);
    flaggedNode.addTargetOnce(FEED_RDF_NS + 'type', 'flagged_collection');
    parent.addChild(flaggedNode);

    this.ds.save();

    spec = fileHandler.getURLSpecFromFile(rootFile);
    this.rootDS = new RDFDataSource(spec);

    this.rootDS.root = this.rootDS.getNode(SUBSCRIPTIONS_RDF_PARENT);
    this.rootDS.root.makeSeq();
    this.rootDS.save();

    spec = fileHandler.getURLSpecFromFile(flagFile);
    this.flaggedDS = new RDFDataSource(spec);

    this.flaggedDS.root = this.flaggedDS.getNode(SUBSCRIPTIONS_RDF_PARENT);
    this.flaggedDS.root.makeSeq();
    this.flaggedDS.save();

    spec = fileHandler.getURLSpecFromFile(discoveryFile);
    this.discoveryDS = new RDFDataSource(spec);
    this.discoveryDS.save();

    this.listeners = [];

    var prefRoot = Components.classes['@mozilla.org/preferences-service;1']
        .getService(Components.interfaces.nsIPrefService);
    var prefBranch = prefRoot.getBranch(PREF_FEEDS_REFRESH_BRANCH)
        .QueryInterface(Components.interfaces.nsIPrefBranch2);

    prefBranch.addObserver('', this, false);
    this.observe(null, 'nsPref:changed', 'pref-init');

    var feedsrv = this;

    var sync = {
        notify: function (timer) {
            feedsrv.refreshAllFeeds(null);
        }
    }

    var timer = Components.classes[TIMER_CONTRACTID].createInstance(nsITimer);
    timer.initWithCallback(sync, 5 * 1000, nsITimer.TYPE_ONE_SHOT);

    this.initialized = true;
}

flockFeedService.prototype.observe =
function (subject, topic, state)
{
    switch (topic) {
        case 'profile-after-change':
            var obs = Components.classes["@mozilla.org/observer-service;1"]
                .getService(Components.interfaces.nsIObserverService);
            obs.removeObserver(this, 'profile-after-change');
            this.init();
            return;

        case 'xpcom-shutdown':
            var obs = Components.classes["@mozilla.org/observer-service;1"]
                .getService(Components.interfaces.nsIObserverService);
            obs.removeObserver(this, 'content-message'); 
            obs.removeObserver(this, 'xpcom-shutdown'); 
            return;

        case 'content-message':
            try {
                var util = Components.classes[FEED_UTILITY_CONTRACTID]
                    .getService(flockIFeedUtility);
                util.handleContentMessage(subject.wrappedJSObject);
            } catch (e) {
                 debug("utility error: " + e);
            }
            return;

        case 'nsPref:changed':
            break;

        default:
            return;
    }

    var prefRoot = Components.classes['@mozilla.org/preferences-service;1']
        .getService(Components.interfaces.nsIPrefService);
    var prefBranch = prefRoot.getBranch(PREF_FEEDS_REFRESH_BRANCH);

    if (state == PREF_FEEDS_REFRESH_SUBSCRIPTIONS || state == 'pref-init') {
        if (this.timer)
            this.timer.cancel();

        var val;
        try {
            val = prefBranch.getIntPref(PREF_FEEDS_REFRESH_SUBSCRIPTIONS);
        } catch (e) {
            val = DEFAULT_REFRESH_SUBSCRIPTIONS;
        }

        this.refresh_interval = val * 60 * 1000;
        debug("Pref change (subscription): " + this.refresh_interval);

        this.timer = Components.classes[TIMER_CONTRACTID]
            .createInstance(nsITimer);
        this.timer.initWithCallback(this, this.refresh_interval,
                                    nsITimer.TYPE_REPEATING_SLACK);
    }

    if (state == PREF_FEEDS_REFRESH_DISCOVERY || state == 'pref-init') {
        var val;
        try {
            val = prefBranch.getIntPref(PREF_FEEDS_REFRESH_DISCOVERY);
        } catch (e) {
            val = DEFAULT_REFRESH_DISCOVERY;
        }

        this.discovery_refresh_interval = val * 60 * 1000;
        debug("Pref change (discovery): " + this.discovery_refresh_interval);
    }
}

flockFeedService.prototype.hasFeed =
function (url)
{
    var feedFile = getFeedFile(fixFeedColonURL(url));
    //var curTime = new Date();
    var hasFeed = (feedFile.exists());
                   //feedFile.lastModifiedTime > curTime - this.refresh_interval);
    debug('hasFeed ' + url + ': ' + hasFeed);
    return hasFeed;
}

flockFeedService.prototype.getFeed =
function (url)
{
    return getFeedInternal(fixFeedColonURL(url));
}

flockFeedService.prototype.getFeedStore =
function (url)
{
    var feed_ds = this.feedStores[url];
    if (!feed_ds) {
        var feed_rawds = this.getFeed(url);
        if (!feed_rawds)
            return null;

        feed_ds = new RDFDataSource();
        feed_ds.Init(feed_rawds);

        this.feedStores[url] = feed_ds;
    }

    return feed_ds;
}

flockFeedService.prototype.getFeedData =
function (url, count)
{
    var urls = [];

    if (url.indexOf('feed:') == 0) {
        urls.push(fixFeedColonURL(url));
    } else if (url.indexOf('feeds:') == 0) {
        urls = getFeedsURLs(this, url);
    }

    var tdata = this.getTData(urls, {}, true);

    var postlist;
    if (tdata) {
        postlist = tdata.allPosts;
        if (postlist.length > count.value) {
            postlist.splice(count.value);
        }
    } else {
        postlist = [];
    }

    count.value = postlist.length;
    return postlist;
}

flockFeedService.prototype.refreshFeed =
function (url)
{
    if (!this.initialized)
        return;

    var req = Components.classes[XMLHTTPREQUEST_CONTRACTID]
        .createInstance(nsIXMLHttpRequest);

    req.overrideMimeType('text/xml');

    if (url.indexOf('feed:') == 0) {
        url = fixFeedColonURL(url);
    } else if (url.indexOf('feeds:') == 0) {
        var urls = getFeedsURLs(this, url);

        if (urls.length > 0) {
            refreshFeeds(this, urls, 0, urls.length);
            return;
        }
    }

    try {
        checkProtocol(url);
        req.open('GET', url, true);
    }
    catch(e) {
        // likely a url error;
        debug("Error loading " + url);
        return;
    }

    var feedsrv = this;

    req.onreadystatechange = function (aEvt) {
        if (req.readyState == 4) {
            var status;
            try {
                status = req.status;
            }
            catch (e) {
                status = 0;
            }

            if (status == 200)
                TryParseFeed(feedsrv, url, req.responseXML);
            else
                debug("Error loading " + url);
        }
    };

    req.send(null); 
}

flockFeedService.prototype.refreshAllFeeds =
function (listener)
{
    debug('refreshing feeds');

    var cutoff = Date.now() - this.refresh_interval;

    var urls = [];

    var nodes = this.ds.getAllResources();
    while (nodes.hasMoreElements()) {
        var node = nodes.getNext();
        var val = node.getValue();

        if (val.indexOf(SUBSCRIPTIONS_RDF_PARENT) != 0) {
            var lastVal = node.getTarget(FEED_RDF_NS + 'last_refreshed');
            if (lastVal) {
                var lastRefresh = new Date(lastVal.getValue());
                if (!(lastRefresh.getTime() >= cutoff))
                    urls.push(val);
            } else {
                urls.push(val);
            }
        }
    }

    var favsvc = Components.classes[FAVORITES_CONTRACTID]
        .getService(flockIFavoritesService);
    var favurls = favsvc.getAllFeeds();
    while (favurls.hasMoreElements()) {
        var urlNode = favurls.getNext().QueryInterface(nsIRDFResource);
        var url = urlNode.ValueUTF8;
        if (url.indexOf('feed:') == 0) {
            url = fixFeedColonURL(url);
        } else if (url.indexOf('feeds:') == 0) {
            var furls = getFeedsURLs(this, url);
            if (furls.length == 0)
                continue;
            url = furls[0];
        }
        var ds = this.getFeedStore(url);
        if (ds) {
            var root = ds.getNode(FEED_DATA_RDF_PREFIX + url);
            var lastVal = root.getTarget(FEED_RDF_NS + "last_refreshed");
            if (lastVal) {
                var lastRefresh = new Date(lastVal.getValue());
                if (!(lastRefresh.getTime() >= cutoff))
                    urls.push(val);
            } else {
                urls.push(url);
            }
        } else {
            urls.push(url);
        }
    }

    if (urls.length > 0)
        refreshFeeds(this, urls, 0, urls.length, listener);
    else
        debug('none to refresh');
}

flockFeedService.prototype.isFeed =
function (dom)
{
    return this.getFeedType(dom) ? true : false;
}

flockFeedService.prototype.getFeedType =
function (dom)
{
    return SniffFeedType(dom);
}

flockFeedService.prototype.loadFeedURL =
function (spec, channel)
{
    var url = fixFeedColonURL(spec);
    var params = getDefaultParams();

    params.base_url = 'feeds:' + encodeURIComponent(url) + '&';

    if (url)
        this.loadFeedHTML(url, channel, params);
    else
        this.throwError(channel, FEED_HTML_ERROR_BAD_REQUEST);
}

flockFeedService.prototype.loadAggregateURL =
function (spec, channel)
{
    var args = [];
    var params = getDefaultParams();
    var valid_params = ['page', 'sort', 'time', 'style', 'filter', 'display'];

    var query = spec.substr('feeds:'.length);
    var qurls = query.split('&');

    for (var i = 0; i < qurls.length; i++) {
        var qurl = qurls[i];

        if (qurl.length == 0)
            continue;

        if (qurl.indexOf('=') != -1) {
            var found = false;

            for (var j = 0; j < valid_params.length; j++) {
                var param = valid_params[j];

                if (qurl.indexOf(param + '=') == 0) {
                    params[param] = qurl.substr(param.length + 1);
                    found = true;
                    break;
                }
            }

            if (!found)
                args.push(decodeURIComponent(qurl));
        } else {
            args.push(decodeURIComponent(qurl));
        }
    }

    var collection = null;

    var urls = [];
    var isRoot = false;
    var doFlagged = false;

    if (args.length) {
        var seen_collection = false;

        for (var i = 0; i < args.length; i++) {
            var arg = args[i];

            // XXX: could be better
            if (arg.indexOf('://') == -1) {
                if (arg == FLAGGED_COLLECTION_NAME) {
                    doFlagged = true;
                    break;
                }

                var curls = getCollectionUrls(this, arg);
                if (curls) {
                    seen_collection = true;
                    curls.forEach(function (u) { urls.push(u) });
                }
            } else {
                urls.push(arg);
            }
        }

        if (seen_collection && args.length == 1)
            collection = args[0];
    } else {
        collection = 'Front Page';

        var curls = getCollectionUrls(this, null);
        if (curls)
            curls.forEach(function (u) { urls.push(u) });

        isRoot = (params.display != 'all');
    }

    if (doFlagged) {
        this.loadFlaggedView(channel, params);
    } else {
        params.collection = collection;
    
        params.base_url = 'feeds:';
        var query_args = args.map(function (u) { return encodeURIComponent(u) }).join('&');
        if (query_args)
            params.base_url += query_args + '&';

        switch (urls.length) {
            case 0:
                this.throwError(channel, FEED_HTML_ERROR_NO_FEEDS);
                break;
            case 1:
                if (isRoot)
                    this.loadRootView(urls, channel, params);
                else
                    this.loadSingleHTML(urls[0], channel, params);
                break;
            default:
                if (isRoot)
                    this.loadRootView(urls, channel, params);
                else
                    this.loadAggregateHTML(urls, channel, params);
                break;
        }
    }
}

flockFeedService.prototype.loadFeedView =
function (spec, channel)
{
    if (spec.indexOf('feed:') == 0)
        this.loadFeedURL(spec, channel);
    else if (spec.indexOf('feeds:') == 0)
        this.loadAggregateURL(spec, channel);
    else
        this.throwError(channel, FEED_HTML_ERROR_BAD_REQUEST);
}

flockFeedService.prototype.prioritizeFeeds =
function (len, urls, listener)
{
    getPriorityFeeds(this, urls, 0, donePriorityFeeds, listener);
}

flockFeedService.prototype.prioritizeFeedsQuick =
function (len, feeds, count)
{
    var priorities = {
        "application/atom+xml": 100,
        "application/rss+xml": 50,
        "text/xml": 40
    };

    var title_priorities = {
       "Atom 1.0": 100,
       "Atom 0.3": 75,
       "Atom": 70,
       "RSS 2.0": 50,
       "RSS 0.92": 40,
       "RSS": 30
    };

    function sorter(a,b) {
        var t1 = priorities[a.type];
        var t2 = priorities[b.type];

        if (!t1) t1 = title_priorities[a.title];
        if (!t2) t2 = title_priorities[b.title];

        if (!t1) t1 = 0;
        if (!t2) t2 = 0;

        return t2 - t1;
    }

    feeds.sort(sorter);

    var retfeeds = [];

    for (var i = 1; i < feeds.length; i++) {
        var feed = feeds[i];

        if (!title_priorities[feed.title])
            retfeeds.push(feed);
    }

    retfeeds.unshift(feeds[0]);

    count.value = retfeeds.length;
    return retfeeds;
}

flockFeedService.prototype.joinFeedsURLs =
function (len, urls)
{
    var returls = [];

    for (var i = 0; i < urls.length; i++) {
        var url = urls[i];
        if (url.indexOf('feeds:') == 0) {
            var furls = getFeedsURLs(this, url);
            furls.forEach(function (u) { returls.push(u) });
        } else {
            returls.push(fixFeedColonURL(url));
        }
    }

    return 'feeds:' + returls.map(function (u) { return encodeURIComponent(u) }).join('&');
}

flockFeedService.prototype.subscribeFeed =
function (url, target, orientation, defer)
{
    if (url.indexOf('feeds:') == 0) {
        var urls = getFeedsURLs(this, url);
        for (var i = 0; i < urls.length; i++)
            this.subscribeFeed(urls[i]);
        return;
    }

    var pos = url.length - '.opml'.length;
    if (url.lastIndexOf('.opml', pos) == pos) {
        var opmlsrv = Components.classes[OPML_SERVICE_CONTRACTID]
            .getService(flockIOpmlService);
        opmlsrv.subscribeOpml(url, null);
        return;
    }

    try {
        url = checkProtocol(url);
    }
    catch (e) {
        return;
    }

    var subscribedTarget = FEED_RDF_NS + 'subscribed';

    var node = this.ds.getNode(url);

    var val = node.getTarget(subscribedTarget);
    if (val && val.getValue() == 'true') {
        if (target) {
            this.moveFeed(url, target, orientation);
            return;
        }
    } else {
        node.addTargetOnce(subscribedTarget, 'true');

        if (target)
            placeFeed(this.ds, node, target, orientation, null);
        else
            this.root.addChild(node);
    }

    if (!defer) {
        updateSubscriptions(this, null, true);

        var feedsrv = this;

        var oneshot = {
            notify: function (timer) {
                feedsrv.refreshFeed(url);
            }
        }

        var timer = Components.classes[TIMER_CONTRACTID]
            .createInstance(nsITimer);
        timer.initWithCallback(oneshot, 1, nsITimer.TYPE_ONE_SHOT);
    }
}

/*
flockFeedService.prototype.addFeed =
function (url, collection)
{
    var urlNode = this.ds.getNode(url);
    var collectionNode = this.ds.getNode(COLLECTIONS_RDF_ROOT + collection);

    if (this.root.getChildIndex(collectionNode) == -1)
        throw "Collection does not exist";

    var val = urlNode.getTarget(FEED_RDF_NS + 'subscribed');
    if (!val || val.getValue() != 'true')
        throw "Feed URL is not subscribed";

    if (collectionNode.getChildIndex(urlNode) != -1)
        throw "Feed already exists in this collection";

    collectionNode.addChild(urlNode);

    updateSubscriptions(this);
}
*/

flockFeedService.prototype.unsubscribeFeed =
function (url)
{
    var urlNode = this.ds.getNode(url);
    //collectionNode.removeChildForReal(urlNode);
    this.ds.deleteRecursive(urlNode);

    var feedDir = getFeedDir(url);
    if (feedDir.exists())
        feedDir.remove(true);

    delete this.feedStores[url];

    updateSubscriptions(this);
}

flockFeedService.prototype.isSubscription =
function (url)
{
    var urlNode = this.ds.getNode(url);

    var subscribedTarget = FEED_RDF_NS + 'subscribed';

    var val = urlNode.getTarget(subscribedTarget);
    return val && val.getValue() == 'true';
}

flockFeedService.prototype.grabNewSubscriptions =
function (len, urls, listener)
{
    var feedsrv = this;

    var oneshot = {
        notify: function (timer) {
            refreshFeeds(feedsrv, urls, 0, urls.length, listener);
        }
    }

    var timer = Components.classes[TIMER_CONTRACTID]
        .createInstance(nsITimer);
    timer.initWithCallback(oneshot, 1, nsITimer.TYPE_ONE_SHOT);
}

flockFeedService.prototype.markRead =
function (url, id)
{
    debug("READ: " + url + ", " + id);

    markReadOrUnread(this, url, id, -1);
}

flockFeedService.prototype.markUnread =
function (url, id)
{
    debug("UNREAD: " + url + ", " + id);

    markReadOrUnread(this, url, id, 1);
}

flockFeedService.prototype.markAllReadOrUnreadFeed =
function (url, direction)
{
    url = fixFeedColonURL(url);

    var urlNode = this.ds.getNode(url);
    var counts;

    if (direction == -1) {
        var val = urlNode.getTarget(FEED_RDF_NS + 'unread');
        if (val && val.getValue() == '0')
            return 0;
    } else {
        counts = getCounts(urlNode);
        if (counts.unread != 0 && counts.total == counts.unread)
            return 0;
    }

    var ds = this.getFeedStore(url);
    if (!ds) {
        debug("Error: " + url + " doesn't have a feed!");
        return 0;
    }

    var read = direction == -1;
    var changed = 0;

    var root = ds.getNode(FEED_DATA_RDF_PREFIX + url);

    if (root.getChildCount() == 0) {
        urlNode.addTargetOnce(FEED_RDF_NS + 'unread', '0');
        this.rootDS.save();
        return 0;
    }

    var posts = root.getChildren();
    while (posts && posts.hasMoreElements()) {
        var current;
        var post = posts.getNext();
        var val = post.getTarget(FEED_RDF_NS + 'read');
        if (val)
           current = val.getValue() == 'true';
        else
           current = false;
        if (current != read) {
            post.addTargetOnce(FEED_RDF_NS + 'read', read ? 'true' : 'false');
            changed++;

            var node = this.rootDS.getNode(post.getValue());
            val = node.getTarget(FEED_RDF_NS + 'read');
            if (val && val.getValue()) {
                node.addTargetOnce(FEED_RDF_NS + 'read', read ? 'true' : 'false');
            } else {
                this.rootDS.deleteRecursive(node);
            }
        }
    }

    if (changed) {
        if (direction == -1) {
            urlNode.addTargetOnce(FEED_RDF_NS + 'unread', '0');
        } else {
            urlNode.addTargetOnce(FEED_RDF_NS + 'unread', String(counts.total));
        }

        ds.save();
        this.rootDS.save();
    }

    return changed;
}

flockFeedService.prototype.markAllReadOrUnread =
function (urn, direction)
{
    var rawDS = this.ds.getRawDataSource();
    rawDS.beginUpdateBatch();

    var root;
    if (urn == null)
        root = this.root;
    else
        root = this.ds.getNode(urn);

    var anyChange = false;

    var val = root.getTarget(FEED_RDF_NS + 'type');
    if (!val)
        return;

    var type = val.getValue();
    if (type == 'collection' || type == 'root') {
        var children = root.getChildren();
        while (children && children.hasMoreElements()) {
            var node = children.getNext();

            var val = node.getTarget(FEED_RDF_NS + 'type');
            if (!val)
                continue;

            var type = val.getValue();

            switch (type) {
                case 'feed':
                    var url = node.getValue();
                    var changed = this.markAllReadOrUnreadFeed(url, direction);
                    if (changed)
                        anyChange = true;
                    break;

                case 'collection':
                    var colChildren = node.getChildren();
                    while (colChildren && colChildren.hasMoreElements()) {
                        var childNode = colChildren.getNext();
                        var url = childNode.getValue();
                        var changed = this.markAllReadOrUnreadFeed(url, direction);
                        if (changed)
                            anyChange = true;
                    }
                    break;

                default:
                    break;
            }
        }
    } else if (type == 'feed') {
        var url = root.getValue();
        var changed = this.markAllReadOrUnreadFeed(url, direction);
        if (changed)
            anyChange = true;
    }

    rawDS.endUpdateBatch();

    if (anyChange)
        updateSubscriptions(this);
}

flockFeedService.prototype.markAllRead =
function (urn)
{
    debug("markAllRead: " + urn);
    this.markAllReadOrUnread(urn, -1);
}

flockFeedService.prototype.markAllUnread =
function (urn)
{
    debug("markAllUnread: " + urn);
    this.markAllReadOrUnread(urn, 1);
}

flockFeedService.prototype.flagPost =
function (url, id)
{
    debug("FLAG: " + url + ", " + id);

    doFlag(this, url, id, true);
}

flockFeedService.prototype.unflagPost =
function (url, id)
{
    debug("UNFLAG: " + url + ", " + id);

    doFlag(this, url, id, false);
}

flockFeedService.prototype.moveFeed =
function (url, target, orientation)
{
    var urlNode = this.ds.getNode(url);

    var fromNode = getParentFromNode(urlNode);
    if (!fromNode)
        throw "Invalid url";

    placeFeed(this.ds, urlNode, target, orientation, fromNode);

    updateSubscriptions(this);
}

flockFeedService.prototype.addCollection =
function (collection)
{
    var node = this.ds.getNode(COLLECTIONS_RDF_ROOT + collection);
    this.root.addChild(node, false);
    node.makeSeq();
    node.addTargetOnce(FEED_RDF_NS + 'type', 'collection');
    node.addTargetOnce(FEED_RDF_NS + 'title', collection);
    node.addTargetOnce(FEED_RDF_NS + 'total', '0');
    node.addTargetOnce(FEED_RDF_NS + 'unread', '0');

    updateSubscriptions(this);
}

flockFeedService.prototype.removeCollection =
function (collection)
{
    var collectionNode = this.ds.getNode(collection);
    this.ds.deleteRecursive(collectionNode);

    updateSubscriptions(this);
}

flockFeedService.prototype.moveCollection =
function (collection, target, orientation)
{
    var node = this.ds.getNode(collection);

    var index = this.root.getChildIndex(node);
    if (index == -1)
        throw "Collection does not exist";

    var targetNode = null;
    if (target) {
        targetNode = this.ds.getNode(target);

        if (this.root.getChildIndex(targetNode) == -1)
            throw "Target collection does not exist";
    }

    if (targetNode) {
        switch (orientation) {
            case ORIENT_ABOVE:
            case ORIENT_BELOW:
                var newIndex = this.root.getChildIndex(targetNode);

                if (orientation == ORIENT_BELOW && index > newIndex)
                    newIndex++;

                this.root.removeChildAt(index);
                this.root.addChildAt(node, newIndex);
                break;

            default:
                throw "Bad orientation";
                break;
        }
    }
    else {
        this.root.removeChildAt(index);
        this.root.addChild(node);
    }
    
    updateSubscriptions(this);
}

flockFeedService.prototype.renameItem =
function (item, title)
{
    var node = this.ds.getNode(item);

    var val = node.getTarget(FEED_RDF_NS + 'type');
    if (!val)
        throw "Item does not exist";

    var type = val.getValue();

    switch (type) {
        case 'feed':
            node.addTargetOnce(FEED_RDF_NS + 'title', title);
            break;

        case 'collection':
            var newNode = this.ds.getNode(COLLECTIONS_RDF_ROOT + title);
            if (this.root.getChildIndex(newNode) != -1)
                return;

            var rawDS = this.ds.getRawDataSource();
            rawDS.beginUpdateBatch();

            this.root.removeChildForReal(node);
            this.root.addChild(newNode);

            newNode.makeSeq();

            newNode.addTargetOnce(FEED_RDF_NS + 'type', 'collection');
            newNode.addTargetOnce(FEED_RDF_NS + 'title', title);

            var children = node.getChildren();
            while (children && children.hasMoreElements()) {
                var child = children.getNext();
                newNode.addChild(child);
            }

            while (node.getChildCount() > 0) {
                node.removeChildAt(1);
            }

            rawDS.endUpdateBatch();
            break;

        default:
            throw "Item is not a feed or a collection";
            break;
    }

    updateSubscriptions(this);
}

flockFeedService.prototype.getSubscriptions =
function ()
{
    return this.ds.getRawDataSource();
}

flockFeedService.prototype.getCollections =
function (count)
{
    var colls = [];

    var children = this.root.getChildren();
    while (children && children.hasMoreElements()) {
        var child = children.getNext();

        var val = child.getTarget(FEED_RDF_NS + 'type');
        if (!val)
            continue;

        var type = val.getValue();

        if (type == 'collection') {
            val = child.getTarget(FEED_RDF_NS + 'title');
            if (val && val.getValue())
                colls.push({ title: val.getValue(), urn: child.getValue() });
        }
    }

    count.value = colls.length;
    return colls;
}

flockFeedService.prototype.getTotalUnreadCount =
function ()
{
    var val = this.root.getTarget(FEED_RDF_NS + 'unread');
    if (val)
        return Number(val.getValue());
    else
        return 0;
}

flockFeedService.prototype.addObserver =
function (listener)
{
    this.listeners.push(listener);
}

flockFeedService.prototype.removeObserver =
function (listener)
{
    this.listeners.splice(this.listeners.indexOf(listener), 1);
}

flockFeedService.prototype.getNewCount =
function ()
{
    debug("getNewCount");

    var val = this.root.getTarget(FEED_RDF_NS + 'new');
    if (!val)
        return 0;

    var count = Number(val.getValue());
    if (!count)
        count = 0;

    return count;
}

flockFeedService.prototype.resetNewCount =
function ()
{
    debug("resetNewCount");

    this.root.addTargetOnce(FEED_RDF_NS + 'new', '0');
    this.ds.save();

    var action = function (listener) {
        listener.onChanged();
        listener.onNewCountUpdate(0);
    }

    this.listeners.forEach(action);
}

flockFeedService.prototype.setDefaultStyle =
function (urn, style)
{
/*
    var node;
    if (urn == null)
        node = this.root;
    else
        node = this.ds.getNode(urn);

    node.addTargetOnce(FEED_RDF_NS + 'view_style', style);

    this.ds.save();
*/
    setCharPref(PREF_FEEDS_DEFAULT_STYLE, style);
}

flockFeedService.prototype.setDefaultColumns =
function (columns)
{
    setIntPref(PREF_FEEDS_DEFAULT_COLUMNS, columns);
}

flockFeedService.prototype.setShowUnread =
function (show)
{
    setBoolPref(PREF_FEEDS_SHOW_UNREAD, show);
}

flockFeedService.prototype.setShowRead =
function (show)
{
    setBoolPref(PREF_FEEDS_SHOW_READ, show);
}

flockFeedService.prototype.setShowFlagged =
function (show)
{
    setBoolPref(PREF_FEEDS_SHOW_FLAGGED, show);
}

// the nsIClassInfo implementation
flockFeedService.prototype.flags = nsIClassInfo.SINGLETON;
flockFeedService.prototype.classDescription = "Flock Feed Service";
flockFeedService.prototype.getInterfaces =
function (count)
{
    var interfaceList = [flockIFeedService, nsIClassInfo, nsITimerCallback, nsIObserver];
    count.value = interfaceList.length;
    return interfaceList;
}

flockFeedService.prototype.getHelperForLanguage =
function (language)
{
    return null;
}

// the nsISupports implementation
flockFeedService.prototype.QueryInterface =
function (iid)
{
    if (!iid.equals(flockIFeedService) && 
        !iid.equals(nsIObserver) &&
        !iid.equals(nsITimerCallback) &&
        !iid.equals(nsIClassInfo) &&
        !iid.equals(nsISupports))
        throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
}

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

feedModule.registerSelf =
function (compMgr, fileSpec, location, type)
{
    compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
    compMgr.registerFactoryLocation(FLOCK_FEED_SERVICE_CID, 
                                    'Flock Feeds JS Component',
                                    FLOCK_FEED_SERVICE_CONTRACTID, 
                                    fileSpec, 
                                    location,
                                    type);

    var catman = Components.classes['@mozilla.org/categorymanager;1']
        .getService(Components.interfaces.nsICategoryManager);
    catman.addCategoryEntry('app-startup', 'Flock Feeds JS Component',
                            'service,' + FLOCK_FEED_SERVICE_CONTRACTID,
                            true, true);
}

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

feedModule.canUnload =
function(compMgr)
{
    debug("Unloading feed service component.");
    return true;
}
    
/* factory object */
var feedServiceFactory = new Object();

feedServiceFactory.createInstance =
function (outer, iid)
{
    debug("CI: " + iid);
    if (outer != null)
        throw Components.results.NS_ERROR_NO_AGGREGATION;

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

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

if (DEBUG) {
    var builtin_debug = debug;
    var debug = function (s) {
        builtin_debug("-*- flockFeedService: " + s + "\n");
    }
} else {
    var debug = function (s) {}
}

function getStreamData(istream)
{
    var sstream = Components.classes['@mozilla.org/scriptableinputstream;1'].
        createInstance(Components.interfaces.nsIScriptableInputStream);

    sstream.init(istream); 

    var data = sstream.read(sstream.available());

    sstream.close();
    istream.close();

    return data;
}

function getContentData(url)
{
    var ios = Components.classes[IO_SERVICE_CONTRACTID]
        .getService(nsIIOService);

    var channel = ios.newChannel(url, null, null);
    var cstream = channel.open();

    var data = getStreamData(cstream);

    var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
        .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);

    converter.charset = 'UTF-8';

    return converter.ConvertToUnicode(data);
}

function getTemplate(filename)
{
    var url = FEED_TEMPLATE_DIR + '/' + filename;
    return getContentData(url);
}

function escape_url(url)
{
    return hex_md5(url);
}

function getFeedDir(url)
{
    var feedDir = Components.classes[DIRECTORY_SERVICE_CONTRACTID]
        .getService(nsIProperties).get('ProfD', nsIFile);

    feedDir.append(FEED_DATA_DIR);
    feedDir.append(escape_url(url));

    return feedDir;
}

function getFeedFile(url)
{
    var feedFile = getFeedDir(url);
    feedFile.append(FEED_DATA_FILENAME);
    return feedFile;
}

function getFeedCacheFile(url)
{
    var feedFile = getFeedDir(url);
    feedFile.append(FEED_CACHE_FILENAME);
    return feedFile;
}

function getFeedImageDir(url)
{
    var feedFile = getFeedDir(url);
    feedFile.append(FEED_IMAGE_DIR);
    return feedFile;
}

function getFeedInternal(url)
{
    var feedFile = getFeedFile(url);

    if (!feedFile.exists())
        return null;

    var ios = Components.classes[IO_SERVICE_CONTRACTID]
        .getService(nsIIOService);

    var fileHandler = ios.getProtocolHandler('file')
        .QueryInterface(nsIFileProtocolHandler);

    var spec = fileHandler.getURLSpecFromFile(feedFile);

    var ds = new RDFDataSource(spec);

    return ds.getRawDataSource();
}

function saveXMLData(file, xmlDocument)
{
    if (file.exists())
        file.remove(false);

    file.createUnique(nsIFile.NORMAL_FILE_TYPE, 0644);

    var ostream = Components.classes['@mozilla.org/network/file-output-stream;1']
        .createInstance(Components.interfaces.nsIFileOutputStream);

    ostream.init(file, PR_WRONLY | PR_CREATE_FILE, 0644, 0);
    
    var serializer = Components.classes["@mozilla.org/xmlextras/xmlserializer;1"]
        .createInstance(Components.interfaces.nsIDOMSerializer);
        
    serializer.serializeToStream(xmlDocument, ostream, 'UTF-8');
    
    ostream.flush();
    ostream.close();
}

function SniffFeedType(dom)
{
    var nodes = dom.getElementsByTagName('feed');
    if (nodes.length > 0) {
        var node = nodes[0];
        var ns = node.getAttribute('xmlns');

        if (ns == 'http://www.w3.org/2005/Atom')
            return 'Atom 1.0';
        else
            return 'Atom 0.3';
    }

    nodes = dom.getElementsByTagName('RDF');
    if (nodes.length > 0) {
        var node = nodes[0];

        if (dom.getElementsByTagName('channel').length == 0)
            return null;

        var ns = node.getAttribute('xmlns');
        if (ns == 'http://purl.org/rss/1.0/')
            return 'RSS 1.0';
        else
            return 'RSS 0.9';
    }

    nodes = dom.getElementsByTagName('rss');
    if (nodes.length > 0) {
        var node = nodes[0];

        if (dom.getElementsByTagName('channel').length == 0)
            return null;

        var version = node.getAttribute('version');

        if (version)
            return 'RSS ' + version;
        else
            return 'RSS';
    }

    return null;
}

// XXX: This function badly needs to be broken up
function TryParseFeed(feedsrv, url, xmlDocument, doDiscovery)
{
    debug("Parsing " + url);

    var ds, root;
    if (!doDiscovery) {
        try {
            if (DEBUG_FEED_XML) {
                var cacheFile = getFeedCacheFile(url);
                saveXMLData(cacheFile, xmlDocument);
            } else if (!xmlDocument) {
                throw "Bad feed XML";
            }
        }
        catch (e) {
            debug("Invalid XML at " + url);
            return;
        }

        var feedFile = getFeedFile(url);

        if (!feedFile.exists())
            feedFile.createUnique(nsIFile.NORMAL_FILE_TYPE, 0644);

        var ios = Components.classes[IO_SERVICE_CONTRACTID]
            .getService(nsIIOService);

        var fileHandler = ios.getProtocolHandler('file')
            .QueryInterface(nsIFileProtocolHandler);

        var spec = fileHandler.getURLSpecFromFile(feedFile);

        ds = new RDFDataSource(spec);

        root = ds.getNode(FEED_DATA_RDF_PREFIX + url);
        root.makeSeq();

        var feedroot = ds.getNode(FEED_DATA_RDF_FEED_ROOT);
        feedroot.makeSeq();
        feedroot.removeChildForReal(root);
        ds.deleteRecursive(feedroot);

        var dateroot = ds.getNode(FEED_DATA_RDF_DATE_ROOT);
        dateroot.makeSeq();

        var dchildren = dateroot.getChildren();
        while (dchildren && dchildren.hasMoreElements()) {
            var dchild = dchildren.getNext();
            var plist = [];
            var dnchildren = dchild.getChildren();
            while (dnchildren && dnchildren.hasMoreElements()) {
                var dnchild = dnchildren.getNext();
                plist.push(dnchild);
            }
            for (var i = 0; i < plist.length; i++)
                dchild.removeChildForReal(plist[i]);
        }

        ds.deleteRecursive(dateroot);
    } else {
        ds = null;
    }

    var now = (new Date()).toString();

    var isAtom = false;

    var dom = xmlDocument;

    var channel = null;

    if (dom) {
        var feedtype = SniffFeedType(dom);
        if (!feedtype)
            feedtype = UNKNOWN_FEED_TYPE;

        if (ds)
            root.addTargetOnce(FEED_RDF_NS + "feedtype", feedtype);
    
        var feeds = dom.getElementsByTagName('feed');

        if (feeds.length > 0) {
            channel = feeds[0];
            isAtom = true;
        } else {
            var channels = dom.getElementsByTagName('channel');
            if (channels.length > 0) {
                channel = channels[0];
            }
        }
    }

    if (channel == null) {
        debug("Error parsing " + url);

        if (ds) {
            var deleteStore = true;

            if (feedsrv) {
                var urlNode = feedsrv.ds.getNode(url);
                var val = urlNode.getTarget(FEED_RDF_NS + 'last_refreshed');
                if (val && val.getValue()) {
                    deleteStore = false;
                } else {
                    feedsrv.ds.deleteRecursive(urlNode);
                }
            }

            if (deleteStore) {
                if (feedsrv)
                    delete feedsrv.feedStores[url];
                feedFile.remove(false);
            }

            if (dom) {
                var opml = dom.getElementsByTagName('opml');
                if (opml.length) {
                    var opmlsrv = Components.classes[OPML_SERVICE_CONTRACTID]
                        .getService(flockIOpmlService);
                    opmlsrv.subscribeDOM(url, dom, null);
                }
            }
        } else {
            var node = feedsrv.discoveryDS.getNode(url);
            node.addTargetOnce(FEED_RDF_NS + 'feedtype', UNKNOWN_FEED_TYPE);
            node.addTargetOnce(FEED_RDF_NS + 'last_refreshed', now);
            feedsrv.discoveryDS.save();
        }
        return;
    }

    if (feedsrv && ds) {
        feedURI = ios.newURI(url, null, null);
        faviconURL = feedURI.prePath + '/favicon.ico';
        getFavicon(feedsrv, url, faviconURL);
    }

    var feed = { date: now };

    for (var i = 0; i < channel.childNodes.length; i++) {
        var n = channel.childNodes[i];
        if (n.nodeType == Components.interfaces.nsIDOMNode.ELEMENT_NODE) {
            switch (n.nodeName) {
                case "title":
                case "subtitle":
                case "link":
                case "description":
                case "tagline":
                case "dc:date":
                case "pubDate":
                case "author":
                //case "ttl":
                //case "id":
                    var name = n.nodeName;
                    var node_data;

                    if (name == "tagline" || name == "subtitle")
                        name = "description";
                    else if (name == "managingEditor")
                        name = "author";

                    if (name == "description")
                        node_data = getHTMLContent(n);
                    else if (isAtom && name == "author")
                        node_data = getAuthor(n);
                    else if (name == "link")
                        node_data = getLinkURL(n);
                    else
                        node_data = getNodeText(n);

                    if (name == "dc:date" || name == "pubDate" ||
                        name == "updated" || name == "modified") {
                        name = "date";
                        node_data = parseDate(node_data);
                    }

                    if (node_data)
                        feed[name] = node_data;

                    break;
            }
        }
    }

    if (!feed.title)
        feed.title = url;

    feed.title = feed.title.replace(/\n/g, ' ').replace(/\r/g, ' ');
    feed.datevalue = getDateValue(new Date(feed.date));
    delete feed.date;

    if (ds) {
        for (var f in feed) {
            root.addTargetOnce(FEED_RDF_NS + f, feed[f]);
        }
    
        root.addTargetOnce(FEED_RDF_NS + "last_refreshed", now);

        var images = dom.getElementsByTagName('image');
        if (images.length > 0) {
            var image = images[0];

            for (var i = 0; i < image.childNodes.length; i++) {
                var c = image.childNodes[i];
                if (c.nodeType == Components.interfaces.nsIDOMNode.ELEMENT_NODE) {
                    switch (c.nodeName) {
                        case "url":
                            root.addTargetOnce(FEED_RDF_NS + 'image',
                                               getNodeText(c));
                            break;
                    }
                }
            }
        }
    } else {
        var node = feedsrv.discoveryDS.getNode(url);
        node.addTargetOnce(FEED_RDF_NS + 'title', feed.title);
        node.addTargetOnce(FEED_RDF_NS + 'feedtype', feedtype);
        node.addTargetOnce(FEED_RDF_NS + 'last_refreshed', now);
        feedsrv.discoveryDS.save();
        return;
    }

    var items;
    if (isAtom) {
        items = dom.getElementsByTagName('entry'); 
    } else {
        items = dom.getElementsByTagName('item'); 
    }

    var posts = {};
    var authors = {};
    for (var i = 0; i < items.length; i++) {
        var n = items[i];
        var post = {};
        var guidPermaLink = !isAtom;

        for (var j = 0; j < n.childNodes.length; j++) {
            var c = n.childNodes[j];
            if (c.nodeType == Components.interfaces.nsIDOMNode.ELEMENT_NODE) {
                switch (c.nodeName) {
                    case "title":
                    case "author":
                    case "link":
                    case "description":
                    case "content:encoded":
                    case "content":
                    case "summary":
                    case "dc:date":
                    case "pubDate":
                    case "modified":
                    case "updated":
                    case "issued":
                    case "published":
                    case "guid":
                    case "id":
                        var name = c.nodeName;
                        var node_data;

                        if (name == 'content:encoded')
                            name = "content";

                        if (name == "content" || name == "description") {
                            if (isAtom)
                                node_data = getAtomContent(c);
                            else
                                node_data = getHTMLContent(c);
                        } else if (isAtom && name == "author")
                            node_data = getAuthor(c);
                        else if (name == "link")
                            node_data = getLinkURL(c);
                        else
                            node_data = getNodeText(c);

                        if (name == "dc:date" || name == "pubDate" ||
                            name == "issued" || name == "published") {
                            name = "date";
                            node_data = parseDate(node_data);
                        } else if (name == "updated" || name == "modified") {
                            name = "updated";
                            node_data = parseDate(node_data);
                        }

                        if (!isAtom && name == "guid") {
                            if (c.hasAttribute("isPermaLink") &&
                                c.getAttribute("isPermaLink") == "false")
                                guidPermaLink = false;
                        }

                        if (node_data)
                            post[name] = node_data;

                        break;
                }
            }
        }

        var post_id = null;
        if (post.id)
            post_id = post.id;
        else if (post.guid)
            post_id = post.guid;
        else if (post.link)
            post_id = post.link;
/*
        else if (post.date)
            post_id = post.date;
        else if (post.title)
            post_id = post.title;
*/

        var post_link = null;
        if (post.link)
            post_link = post.link;
        else if (isAtom && post.id)
            post_link = post.id;
        else if (guidPermaLink && post.guid)
            post_link = post.guid;

        var post_desc = 'Empty post';
        if (post.content)
            post_desc = post.content;
        else if (post.description)
            post_desc = post.description;
        else if (post.summary)
            post_desc = post.summary;
        else if (post.link)
            post_desc = post.link;

        delete post.content;
        delete post.summary;

        if (!post.title) {
            var html2text = Components.classes[HTML2TEXT_CONTRACTID]
                .createInstance(flockIHTML2TEXT);

            var post_text = html2text.convert(post_desc);
            post.title = post_text.substr(0, TRIM_CHARACTER_LIMIT) + '...';
        }

        post.title = post.title.replace(/\n/g, ' ').replace(/\r/g, ' ');

        if (!post.date && post.updated)
            post.date = post.updated;

        post.link = post_link;
        post.description = post_desc;
      
        if (feed.link) 
            post.feedurl = feed.link;

        post.feedtitle = feed.title;

        var date;
        if (post.date) {
            date = new Date(post.date);
        } else {
            date = getDateFromValue(feed.datevalue);
        }

        if (post.author)
            authors[post.author] = 1;
        else
            authors[''] = 1;

        post.datevalue = getDateValue(date);

        var date_only = new Date(date.toDateString());

        post.indexvalue = String(i);

        var pnode;
        if (post_id)
            pnode = ds.getNode(FEED_DATA_RDF_PREFIX + "post:" + post_id);
        else
            pnode = ds.getAnonymousNode();

        for (var p in post) {
            pnode.addTargetOnce(FEED_RDF_NS + p, post[p]);
        }

        root.addChild(pnode);
    }

    if (isAtom && !feed.author) {
        var authorCount = 0;
        var author1;
        for (var a in authors) {
            authorCount++;
            if (authorCount > 1)
                break;
            author1 = a;
        }

        if (authorCount == 1 && author1) {
            root.addTargetOnce(FEED_RDF_NS + "author", author1);
        }
    }

    if (feedsrv) {
        var posts;

        var totalRemove = 0;
        var unreadRemove = 0;

        if (root.getChildCount() > MAX_POSTS_PER_FEED) {
            var postlist = [];
            posts = root.getChildren();
            while (posts && posts.hasMoreElements()) {
                var post = posts.getNext();
                var postinfo = {id: post.getValue()};
                getFeedNodeTargets(post, postinfo, ['datevalue', 'indexvalue']);
                postlist.push(postinfo);
            }

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

            postlist = postlist.slice(MAX_POSTS_PER_FEED);
            for (var i = 0; i < postlist.length; i++) {
                var post = ds.getNode(postlist[i].id);
                var val = post.getTarget(FEED_RDF_NS + 'flagged');
                if (val && val.getValue() == 'true')
                    continue;
                val = post.getTarget(FEED_RDF_NS + 'seen');
                if (val && val.getValue() == 'true') {
                    totalRemove++;
                    val = post.getTarget(FEED_RDF_NS + 'read');
                    if (!val || val.getValue != 'true')
                        unreadRemove++;
                }
                debug("removing: " + postlist[i].id);
                root.removeChildForReal(post);
                ds.deleteRecursive(post);
            }
        }

        var newCount = 0;
        var unreadCount = 0;

        posts = root.getChildren();
        while (posts && posts.hasMoreElements()) {
            var post = posts.getNext();
            var val = post.getTarget(FEED_RDF_NS + 'seen');
            if (!val || val.getValue() != 'true') {
                newCount++;
                post.addTargetOnce(FEED_RDF_NS + 'seen', 'true');
            }
            val = post.getTarget(FEED_RDF_NS + 'read');
            if (!val || val.getValue() != 'true') {
                unreadCount++;
            }
        } 

        if (feedsrv.feedStores[url])
            feedsrv.feedStores[url] = ds;

        var urlNode = feedsrv.ds.getNode(url);
        var subscribedTarget = FEED_RDF_NS + 'subscribed';

        var val = urlNode.getTarget(subscribedTarget);
        if (val && val.getValue() == 'true') {
            var lastVal = urlNode.getTarget(FEED_RDF_NS + 'last_refreshed');
            var newSub = !(lastVal && lastVal.getValue());

            urlNode.addTargetOnce(FEED_RDF_NS + 'last_refreshed', now);
            urlNode.addTargetOnce(FEED_RDF_NS + 'type', 'feed');

            val = urlNode.getTarget(FEED_RDF_NS + 'title');
            if (!val)
                urlNode.addTargetOnce(FEED_RDF_NS + 'title', feed.title);

            feedsrv.feedStores[url] = ds;

            urlNode.addTargetOnce(FEED_RDF_NS + 'total',
                                  String(root.getChildCount()));

            if (newSub) {
                feedsrv.markAllReadOrUnreadFeed(url, -1);
                unreadCount = 0;
            }

            urlNode.addTargetOnce(FEED_RDF_NS + 'unread', String(unreadCount));

            updateSubscriptions(feedsrv, newCount ? newCount : null, true);

            debug("Updating root for " + url);
            var unreadlist = [];
            var readlist = [];
            var posts = root.getChildren();
            while (posts && posts.hasMoreElements()) {
                var post = posts.getNext();
                var postinfo = {};
                getFeedNodeTargets(post, postinfo,
                                   ['title', 'datevalue', 'indexvalue',
                                    'author', 'link', 'description',
                                    'read', 'flagged']);
                postinfo.feed = url;
                postinfo.excerpt = getSnippet(postinfo, 30);
                postinfo.datevalue = Number(postinfo.datevalue);
                postinfo.indexvalue = Number(postinfo.indexvalue);
                postinfo.id = post.getValue();
                postinfo.read = postinfo.read == 'true' ? true : false;
                postinfo.flagged = postinfo.flagged == 'true' ? true : false;
                if (!postinfo.read) {
                    unreadlist.push(postinfo);
                } else {
                    readlist.push(postinfo);
                }
            }

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

            unreadlist.sort(sorter);
            readlist.sort(sorter);

            var postlist = unreadlist.concat(readlist);

            var node = feedsrv.rootDS.getNode(url);
            feedsrv.rootDS.deleteRecursive(node);
            node = feedsrv.rootDS.getNode(url);

            for (var f in feed) {
                node.addTargetOnce(FEED_RDF_NS + f, feed[f]);
            }

            if (postlist.length) {
                postlist = postlist.splice(0, 8);
                findImagesInPosts(postlist);

                node.addTargetOnce(FEED_RDF_NS + 'total',
                                   String(root.getChildCount()));
                node.makeSeq();

                for (var i = 0; i < postlist.length; i++) {
                    var post = postlist[i];

                    var id = post.id;
                    var pnode = feedsrv.rootDS.getNode(id);

                    for (var p in post) {
                        pnode.addTargetOnce(FEED_RDF_NS + p, String(post[p]));
                    }

                    node.addChild(pnode);
                }

                feedsrv.rootDS.root.addChild(node);
            }

            feedsrv.rootDS.save();
        }
    }

    ds.save();
}

function getNodeText(node)
{
    var text = "";
    for (var i = 0; i < node.childNodes.length; i++) {
        var n = node.childNodes[i];
        if (n.nodeType == Components.interfaces.nsIDOMNode.TEXT_NODE ||
            n.nodeType == Components.interfaces.nsIDOMNode.CDATA_SECTION_NODE) {
            text += n.nodeValue;
        }
    }
    return text;
} 

function getLinkURL(node)
{
    if (node.hasAttribute("href")) {
        if (!node.hasAttribute("rel") || node.getAttribute("rel") == "alternate")
            return node.getAttribute("href");
    } else
        return getNodeText(node);
}

function getHTMLContent(node)
{
    return getNodeText(node);

    /*
    var html2xhtml = Components.classes[HTML2XHTML_CONTRACTID]
        .createInstance(flockIHTML2XHTML);

    return html2xhtml.convert(getNodeText(node));
    */
}

function getAuthor(node)
{
    for (var i = 0; i < node.childNodes.length; i++) {
        var n = node.childNodes[i];
        if (n.nodeType == Components.interfaces.nsIDOMNode.ELEMENT_NODE) {
            if (n.nodeName == "name")
                return getNodeText(n);
        }
    }

    return null;
}

function getAtomContent(node)
{
    if (node.hasAttribute("type") &&
        (node.getAttribute("type") == "xhtml" ||
         node.getAttribute("type") == "application/xhtml+xml")) {
        var serializer = Components.classes[DOMSERIALIZER_CONTRACTID]
            .createInstance(nsIDOMSerializer);

        for (var i = 0; i < node.childNodes.length; i++) {
            var n = node.childNodes[i];
            if (n.nodeType == Components.interfaces.nsIDOMNode.ELEMENT_NODE) {
                return serializer.serializeToString(n);
            }
        }
    }
    else if (node.hasAttribute("type") &&
             (node.getAttribute("type") == "html" ||
              node.getAttribute("type") == "text/html")) {
        return getHTMLContent(node);
    }

    return getNodeText(node);
}

function refreshFeeds(feedsrv, urls, current, total, listener)
{
    if (current == total) {
        debug("done refreshing feeds");
        if (listener)
            listener.onProgress(current, total, 'Done');
        return;
    }

    var url = urls[current];

    debug('Loading ' + url);

    if (url.indexOf('feed:') == 0) {
        url = fixFeedColonURL(url);
    } else if (url.indexOf('feeds:') == 0) {
        var furls = getFeedsURLs(this, url);
        if (furls.length == 0) {
            refreshFeeds(feedsrv, urls, current + 1, total, listener);
            return;
        }
        url = furls[0];
    }

    if (listener)
        listener.onProgress(current, total, 'Loading ' + url);

    var req = Components.classes[XMLHTTPREQUEST_CONTRACTID]
        .createInstance(nsIXMLHttpRequest);

    req.overrideMimeType('text/xml');

    try {
        checkProtocol(url);
        req.open('GET', url, true);
    }
    catch(e) {
        // likely a url error;
        debug("Error loading " + url);
        refreshFeeds(feedsrv, urls, current + 1, total, listener);
        return;
    }

    req.onreadystatechange = function (aEvt) {
        if (req.readyState == 4) {
            var status;
            try {
                status = req.status;
            }
            catch (e) {
                status = 0;
            }

            if (status == 200)
                TryParseFeed(feedsrv, url, req.responseXML);
            else
                debug("Error loading " + url);

            var oneshot = {
                notify: function (obj) {
                    refreshFeeds(feedsrv, urls, current + 1, total, listener);
                }
            };

            var timer = Components.classes[TIMER_CONTRACTID]
                .createInstance(nsITimer);
            timer.initWithCallback(oneshot, REFRESH_DELAY, nsITimer.TYPE_ONE_SHOT);
        }
    };

    try {
        req.send(null);
    }
    catch(e) {
        // likely a url error;
        debug("Error sending to " + url);
        refreshFeeds(feedsrv, urls, current + 1, total, listener);
        return;
    }
}

function getFeedNodeTargets(node, store, targets)
{
    for (var i = 0; i < targets.length; i++) {
        var target = targets[i];
        var val = node.getTarget(FEED_RDF_NS + target);
        if (val) { 
            store[target] = val.getValue();
        }
    }
}

flockFeedService.prototype.initTData =
function (params)
{
    var page = Number(params.page);
    if (!page || page < 1)
        page = 1;

    var stylesheet = getCharPref(PREF_FEEDS_STYLESHEET, DEFAULT_STYLESHEET);

    var tdata = { feeds: {}, page: page, stylesheet: stylesheet };

    tdata.columns = getIntPref(PREF_FEEDS_DEFAULT_COLUMNS, DEFAULT_COLUMNS);

    tdata.showUnread = getBoolPref(PREF_FEEDS_SHOW_UNREAD, true);
    tdata.showRead = getBoolPref(PREF_FEEDS_SHOW_READ, true);
    tdata.showFlagged = getBoolPref(PREF_FEEDS_SHOW_FLAGGED, true);

    var default_style = getCharPref(PREF_FEEDS_DEFAULT_STYLE, DEFAULT_STYLE);

    /*
    if (tdata.subscriptionURN) {
        var node = this.ds.getNode(tdata.subscriptionURN);
        var val = node.getTarget(FEED_RDF_NS + 'view_style');
        if (val && val.getValue())
            default_style = val.getValue();
    }
    */

    if (!params.style)
        params.style = default_style;

    switch (params.style) {
        case 'excerpts':
        case 'full':
        case 'headlines':
        //case 'topics':
            break;
        default:
            params.style = DEFAULT_STYLE;
            break;
    }

    tdata.postStyle = params.style;

    tdata.singleFeed = false;
    tdata.subscriptionURN = null;

    return tdata;
}

flockFeedService.prototype.getTData =
function (urls, params, no_view)
{
    var tdata = this.initTData(params);

    tdata.singleFeed = urls.length == 1;

    if (params.collection) {
        tdata.subscriptionURN = COLLECTIONS_RDF_ROOT + params.collection;
    } else if (urls.length == 1) {
        tdata.subscriptionURN = urls[0];
    }

    var filter = params.filter;

    var unreadlist = [];
    var readlist = [];
    var flaggedlist = [];

    var found = false;
    for (i = 0; i < urls.length; i++) {
        var url = urls[i];

        if (url.indexOf('http://') == 0)
            var singleurl = 'feed' + url.substr('http'.length);
        else
            var singleurl = 'feed:' + url;

        var feed_ds = this.getFeedStore(url);
        if (!feed_ds)
            continue;

        var feed = feed_ds.getNode(FEED_DATA_RDF_PREFIX + url);
        var feedinfo = { url: url, srcurl: url, singleurl: singleurl };
        getFeedNodeTargets(feed, feedinfo,
                           ['link', 'title', 'subtitle', 'author',
                            'favicon', 'image', 'feedtype']);

        if (!feedinfo.feedtype || feedinfo.feedtype == UNKNOWN_FEED_TYPE)
            continue;

        found = true;
        tdata.feeds[url] = feedinfo;

        var root = feed_ds.getNode(FEED_DATA_RDF_PREFIX + url);
        var posts = root.getChildren();
        while (posts && posts.hasMoreElements()) { 
            var post = posts.getNext();
            var postinfo = {};
            getFeedNodeTargets(post, postinfo,
                               ['title', 'datevalue', 'indexvalue', 'author',
                                'link', 'description', 'read', 'flagged']);
            postinfo.feed = url;
            postinfo.excerpt = getSnippet(postinfo, params.maxwords);
            postinfo.datevalue = Number(postinfo.datevalue);
            postinfo.indexvalue = Number(postinfo.indexvalue);
            postinfo.id = post.getValue();
            postinfo.read = postinfo.read == 'true' ? true : false;
            postinfo.flagged = postinfo.flagged == 'true' ? true : false;
            if (filter == 'read' && !postinfo.read ||
                filter == 'unread' && postinfo.read)
                continue;
            if (!postinfo.read) {
                postinfo.group = 'unread';
                unreadlist.push(postinfo);
            } else if (postinfo.flagged) {
                postinfo.group = 'flagged';
                flaggedlist.push(postinfo);
            } else {
                postinfo.group = 'read';
                readlist.push(postinfo);
            }
        }
    }

    if (urls.length > 0 && !found)
        return null;

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

    var postlist = [];
    if (tdata.showUnread) {
        unreadlist.sort(sorter);
        postlist = postlist.concat(unreadlist);
    }
    if (tdata.showFlagged) {
        flaggedlist.sort(sorter);
        postlist = postlist.concat(flaggedlist);
    }
    if (tdata.showRead) {
        readlist.sort(sorter);
        postlist = postlist.concat(readlist);
    }

    return this.finishTData(tdata, postlist, urls, params, no_view);
}

flockFeedService.prototype.finishTData =
function (tdata, postlist, urls, params, no_view)
{
    if (!no_view) {
        var cutoff;

        switch (params.time) {
            case 'last24hours':
                cutoff = Date.now() - (24 * 60 * 60 * 1000);
                break;
            case 'last48hours':
                cutoff = Date.now() - (48 * 60 * 60 * 1000);
                break;
            case 'lastweek':
                cutoff = Date.now() - (7 * 24 * 60 * 60 * 1000);
                break;
            default:
                cutoff = 0;
                break;
        }

        if (cutoff > 0) {
            var timeFilter = function(p) {
                return p.datevalue >= cutoff;
            }
            postlist = postlist.filter(timeFilter);
        }
    }

    tdata.totalPosts = postlist.length;

    if (no_view) {
        tdata.allPosts = postlist;
    } else {
        tdata.allPosts = postlist.splice(MAX_POSTS_PER_PAGE * (tdata.page - 1),
                                         MAX_POSTS_PER_PAGE);

        makeViewURLs(this, tdata, urls, params)
    }


    return tdata;
}


flockFeedService.prototype.aggDates =
function (urls, params)
{
    var tdata = this.getTData(urls, params)
    if (!tdata)
        return null;

    getViewTitle(tdata, params);

    var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"].getService(Components.interfaces.nsILocaleService);
    var stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService);
    var stringBundle = stringBundleService.createBundle("chrome://flock/locale/feed/feed.properties", localeService.getApplicationLocale());

    tdata.stringBundle = stringBundle;

    var template = getTemplate(FEED_TEMPLATE_AGG_BY_DATE);
    return template.process(tdata);
}

flockFeedService.prototype.aggFeeds =
function (urls, params)
{
    var tdata = this.getTData(urls, params)
    if (!tdata)
        return null;

    getViewTitle(tdata, params);

    for (var feed in tdata.feeds) {
        tdata.feeds[feed].posts = [];
    }
    for (var i = 0; i < tdata.allPosts.length; i++) {
        tdata.feeds[tdata.allPosts[i].feed].posts.push(tdata.allPosts[i]);
    }

    var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"].getService(Components.interfaces.nsILocaleService);
    var stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService);
    var stringBundle = stringBundleService.createBundle("chrome://flock/locale/feed/feed.properties", localeService.getApplicationLocale());

    tdata.stringBundle = stringBundle;

    var template = getTemplate(FEED_TEMPLATE_AGG_BY_FEED);
    return template.process(tdata);
}

flockFeedService.prototype.aggByTopic =
function (urls, params)
{
    var tdata = this.getTData(urls, params);
    if (!tdata)
        return null;

    for (var i = 0; i < tdata.allPosts.length; i++) {
        var table = getWords(tdata.allPosts[i]);
        tdata.allPosts[i].words = '';
        for (var str in table) {
            tdata.allPosts[i].words += str + '|' + table[str] +'\n';
        }
    }
    var clusterer = Components.classes['@flock.com/clusterer;1']
        .getService(Components.interfaces.flockIClusterer);
    var clusters = clusterer.cluster(tdata.allPosts.length, tdata.allPosts);
    tdata.topics = [];
    tdata.other = []
    for (var i = 0; i < clusters.length; i++) {
        var cluster = clusters.queryElementAt(i, Components.interfaces
                                                           .flockICluster);
        if (cluster.posts.length == 1) {
            tdata.other.push(tdata.allPosts[cluster.posts
                .queryElementAt(0, Components.interfaces.nsISupportsPRUint32)
                .data]);
            continue;
        }

        var posts = new Array(cluster.posts.length);
        for (var j = 0; j < cluster.posts.length; j++) {
            posts[j] = tdata.allPosts[cluster.posts
                .queryElementAt(j, Components.interfaces.nsISupportsPRUint32)
                .data];
        }
        tdata.topics.push({
            srcurl : "cluster" + i,
            word : cluster.keyword,
            word2 : cluster.keyword2,
            posts : posts
        });
    }

    var date_sorter = function(a, b) {
        var res = b.datevalue - a.datevalue;
        if (res == 0) return a.indexvalue - b.indexvalue;
        else return res;
    };
    var cluster_sorter = function(a, b) {
        if (a.posts.length == b.posts.length)
            return date_sorter(a.posts[0], b.posts[0]);
        return b.posts.length - a.posts.length;
    };
    tdata.topics.sort(cluster_sorter);
    tdata.other.sort(date_sorter);
    for (var i = 0; i < tdata.topics.length; i++) {
        tdata.topics[i].posts.sort(date_sorter);
    }

    return tdata;
}

flockFeedService.prototype.subscriptionLayout =
function ()
{
    var collections = {};
    var catch_all = [];

    var unread = 0;
    var total = 0;

    var children = this.root.getChildren();
    while (children && children.hasMoreElements()) {
       var child = children.getNext();

       var val = child.getTarget(FEED_RDF_NS + 'type');
       if (!val)
           continue;

       var type = val.getValue();

       if (type == 'collection') {
           val = child.getTarget(FEED_RDF_NS + 'title');
           if (!val)
               continue;

           var collection = val.getValue();
           if (!collections[collection]) {
               var newcoll = {};
               newcoll.feeds = [];

               val = child.getTarget(FEED_RDF_NS + 'unread');
               if (!val)
                   continue;

               newcoll.unread = Number(val.getValue());

               val = child.getTarget(FEED_RDF_NS + 'total');
               if (!val)
                   continue;

               newcoll.total = Number(val.getValue());

               collections[collection] = newcoll;
           }
 
           var subchildren = child.getChildren();
           while (subchildren && subchildren.hasMoreElements()) {
               var subchild = subchildren.getNext();
               collections[collection].feeds.push(subchild.getValue());
           }
       } else {
           var uval = child.getTarget(FEED_RDF_NS + 'unread');
           if (!uval)
               continue;

           var tval = child.getTarget(FEED_RDF_NS + 'total');
           if (!tval)
               continue;

           unread += Number(uval.getValue());
           total += Number(tval.getValue());

           catch_all.push(child.getValue());
       }
    }

    collections[CATCH_ALL_COLLECTION] =
        { unread: unread, total: total, feeds: catch_all };

    for (var i in collections) {
        if (collections[i].feeds.length == 0)
            delete collections[i];
    }

    return collections;
}

function findImagesInPosts(posts)
{
    for (var i = 0; i < posts.length; i++) {
        var post = posts[i];
        var desc = post.description;

        var matches = desc.match(/<img [^>]*src="([^"]*)"[^>]*>/);
        if (matches && matches.length > 1 && matches[1]) {
            post.excerpt = post.excerpt.replace(/<img [^>]*src="[^"]*"[^>]*>/g, '');
            for (var j = 1; j < matches.length; j++) {
                var match = matches[j];
                if (match.search('feedburner.com') == -1) {
                    post.image = match;
                    break;
                }
            }
        }
    }
}

flockFeedService.prototype.aggRootView =
function (urls, params)
{
    debug("aggRootView");

    var stylesheet = getCharPref(PREF_FEEDS_STYLESHEET, DEFAULT_STYLESHEET);

    var tdata = { feeds: {}, stylesheet: stylesheet };
    tdata.sections = [];

    var collections = this.subscriptionLayout();

    var allPosts = 0;

    for (var name in collections) {
        var collinfo = collections[name];

        var coll = collinfo.feeds;

        var unreadlist = [];
        var readlist = [];
        var totalPosts = 0;

        for (var i = 0; i < coll.length; i++) {
            var feed = coll[i];
            var urlNode = this.rootDS.getNode(feed);
            var val = urlNode.getTarget(FEED_RDF_NS + 'total');
            if (val && val.getValue()) {
                var feedinfo = { url: feed };
                getFeedNodeTargets(urlNode, feedinfo,
                                   ['link', 'title', 'subtitle', 'author',
                                    'favicon', 'image']);
                tdata.feeds[feed] = feedinfo;

                totalPosts += Number(val.getValue());

                var posts = urlNode.getChildren();
                while (posts && posts.hasMoreElements()) {
                    var post = posts.getNext();
                    var postinfo = {};
                    getFeedNodeTargets(post, postinfo,
                                       ['title', 'datevalue', 'indexvalue',
                                        'author', 'link', 'description',
                                        'excerpt', 'read', 'flagged', 'feed',
                                        'image']);
                    postinfo.datevalue = Number(postinfo.datevalue);
                    postinfo.indexvalue = Number(postinfo.indexvalue);
                    postinfo.id = post.getValue();
                    postinfo.read = postinfo.read == 'true' ? true : false;
                    postinfo.flagged = postinfo.flagged == 'true' ? true : false;
                    if (!postinfo.read) {
                        unreadlist.push(postinfo);
                    } else {
                        readlist.push(postinfo);
                    }
                }
            }
        }

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

        unreadlist.sort(sorter);
        readlist.sort(sorter);

        var posts = unreadlist.concat(readlist);
        if (posts.length == 0)
            continue;

        var culled = [ posts[0] ];
        var culled_fallback = [];

        var feeds = {};
        feeds[posts[0].feed] = 1;

        var feeds_fallback = {};

        for (var i = 1; i < posts.length; i++) {
            var post = posts[i];
            if (!feeds[post.feed]) {
                feeds[post.feed] = 1;
                culled.push(post);
            } else if (!feeds_fallback[post.feed]) {
                feeds_fallback[post.feed] = 1;
                culled_fallback.push(post);
            }
        }

        if (!culled)
            continue;

        if (culled.length < 4) {
            culled = culled.concat(culled_fallback);
        }

        posts = culled.splice(0, 4);

        for (var i = 1; i < posts.length; i++) {
            if (posts[i].image) {
                posts.unshift(posts.splice(i, 1)[0]);
                break;
            }
        }

        var toppost = posts.shift();
 
        var section = { title: name, link: name==CATCH_ALL_COLLECTION?null:'feeds:' + encodeURIComponent(name),
                        toppost: toppost, posts: posts,
                        morePosts: totalPosts > 4,
                        unread: collinfo.unread, total: collinfo.total };

        tdata.sections.push(section);

        allPosts += totalPosts;
    }

    tdata.allPosts = allPosts;

    var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"].getService(Components.interfaces.nsILocaleService);
    var stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService);
    var stringBundle = stringBundleService.createBundle("chrome://flock/locale/feed/feed.properties", localeService.getApplicationLocale());

    tdata.stringBundle = stringBundle;

    var template = getTemplate(FEED_TEMPLATE_ROOT_VIEW);
    return template.process(tdata);
}

flockFeedService.prototype.aggFlaggedView =
function (params)
{
    params.base_url = 'feeds:' + FLAGGED_COLLECTION_NAME + '&';

    var tdata = this.initTData(params);
    if (!tdata)
        return null;

    var postlist = [];

    var posts = this.flaggedDS.root.getChildren();
    while (posts && posts.hasMoreElements()) {
        var post = posts.getNext();
        var postinfo = {};
        getFeedNodeTargets(post, postinfo,
                           ['title', 'datevalue', 'indexvalue', 'author',
                            'link', 'description', 'read', 'flagged', 'feed']);
        postinfo.excerpt = getSnippet(postinfo, params.maxwords);
        postinfo.datevalue = Number(postinfo.datevalue);
        postinfo.indexvalue = Number(postinfo.indexvalue);
        postinfo.id = post.getValue();
        postinfo.read = postinfo.read == 'true' ? true : false;
        postinfo.flagged = postinfo.flagged == 'true' ? true : false;
        postlist.push(postinfo);

        if (!tdata.feeds[postinfo.feed]) {
            var feedinfo = {};
            getFeedNodeTargets(post, feedinfo,
                               ['feed-url', 'feed-link', 'feed-subtitle',
                                'feed-author', 'feed-favicon', 'feed-image',
                                'feed-feedtype']);
            for (var i in feedinfo) {
                feedinfo[i.replace('feed-', '')] = feedinfo[i];
                delete feedinfo[i];
            }
            tdata.feeds[postinfo.feed] = feedinfo;
        }
    }

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

    postlist.sort(sorter);

    this.finishTData(tdata, postlist, [FLAGGED_COLLECTION_NAME], params, false);

    var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"].getService(Components.interfaces.nsILocaleService);
    var stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService);
    var stringBundle = stringBundleService.createBundle("chrome://flock/locale/feed/feed.properties", localeService.getApplicationLocale());

    tdata.stringBundle = stringBundle;

    tdata.viewtitle = stringBundle.GetStringFromName("flock.feed.temp.savedArts");
    tdata.viewlink = null;

    var template = getTemplate(FEED_TEMPLATE_AGG_BY_DATE);
    return template.process(tdata);
}

flockFeedService.prototype.loadTopicsHTML =
function (url, channel, params)
{
    debug("loadTopicsHTML: " + url);

    if (channel.bypassCache || !this.hasFeed(url)) {
        channel.bypassCache = false; // so we don't bypass infinitely

        var req = Components.classes[XMLHTTPREQUEST_CONTRACTID]
            .createInstance(nsIXMLHttpRequest);

        req.overrideMimeType('text/xml');

        try {
            checkProtocol(url);
            req.open('GET', url, true);
        }
        catch(e) {
            // likely a url error;
            this.throwError(channel, FEED_HTML_ERROR_NETWORK);
            return;
        }

        var feedsrv = this;

        req.onreadystatechange = function (aEvt) {
            if (req.readyState == 4) {
                var status;
                try {
                    status = req.status;
                }
                catch (e) {
                    status = 0;
                }

                if (status == 200) {
                    TryParseFeed(feedsrv, url, req.responseXML);
                    feedsrv.loadTopicsHTML(url, channel, params);
                }
                else
                    feedsrv.throwError(channel, FEED_HTML_ERROR_NETWORK);
            } 
        };

        req.send(null);
        return;
    }

    var urls = [url];
    var tdata = this.aggByTopic(urls, params);
    if (!tdata) {
        this.throwError(channel, FEED_HTML_ERROR_BAD_FEED_DATA);
        return;
    }

    tdata.feed = tdata.feeds[url];

    var node = this.ds.getNode(url);
    var val = node.getTarget(FEED_RDF_NS + 'title');
    if (val && val.getValue())
        tdata.viewtitle = val.getValue();
    else
        tdata.viewtitle = tdata.feed.title;

    tdata.viewlink = tdata.feed.link;

    var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"].getService(Components.interfaces.nsILocaleService);
    var stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService);
    var stringBundle = stringBundleService.createBundle("chrome://flock/locale/feed/feed.properties", localeService.getApplicationLocale());

    tdata.stringBundle = stringBundle;

    var template = getTemplate(FEED_TEMPLATE_SINGLE_BY_TOPIC);
    sendData(channel, template.process(tdata));
}

flockFeedService.prototype.aggTopics =
function (urls, params)
{
    var tdata = this.aggByTopic(urls, params);
    if (!tdata)
        return null;

    getViewTitle(tdata, params);

    var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"].getService(Components.interfaces.nsILocaleService);
    var stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService);
    var stringBundle = stringBundleService.createBundle("chrome://flock/locale/feed/feed.properties", localeService.getApplicationLocale());

    tdata.stringBundle = stringBundle;

    var template = getTemplate(FEED_TEMPLATE_AGG_BY_TOPIC);
    return template.process(tdata);
}

function cleanWords(words) {
    // Takes in a string, cleans it up a bit, and returns an array of the
    // words in it.
    // Throws out all punctuation and escaped characters (such as &nbsp;)
    // except for apostrophes, dashes, and periods that occur in the middle of
    // a word.
    return words.replace(/&((#8217)|(#39)|(apos)|(rsquo));/g, "'")
                .replace(/&((ndash)|(#8211));/g, '-')
                .replace(/&[^; ]*;/g, ' ') // get rid of other html entities
                .replace(/[^a-zA-Z0-9'.-]/g, ' ') // other punctuation
                .replace(/(['.-]\s)|(\s['.-])|(^['.-])|(['.-]$)/g, ' ')
                .split(/\s+/);
}

function addWord(word, tf, table, stem, stop)
{
    if (tf == 0) return;
    if (stop && STOPWORDS.hasOwnProperty(word)) return;
    if (stem) word = stemWord(word);
    if (table.hasOwnProperty(word)) {
        table[word] += tf;
    } else {
        table[word] = tf;
    }
}

function getWords(postinfo) {
    var table = {};
    var desc = postinfo.description.toLowerCase();
    var words = cleanWords(desc.replace(/<[^>]*>/g,' '));
    for (var i = 0; i < words.length; i++) {
        addWord(words[i], 1, table, USE_STEMMER, USE_STOPWORDS);
    }

    if (USE_BIGRAMS) {
        for (var i = 0; i < words.length-1; i++) {
            // XXX: might want to stem words[i] and words[i+1], not the same
            // as USE_STEMMER though.  Could also give bigrams a different TF
            // weight.
            if (USE_STOPWORDS &&
                (STOPWORDS.hasOwnProperty(words[i]) ||
                 STOPWORDS.hasOwnProperty(words[i+1]))) continue;
            addWord(words[i] + ' ' + words[i+1], 1, table,
                    false, false); // stem/stop doesn't make sense for bigrams
        }
    }
    var links = desc.match(/http[^'" \t\n]+/);
    if (links != null) {
        for (var i = 0; i < links.length; i++) {
            addWord(links[i], BOOSTLINKS, table, false, false);
        }
    }
    if (postinfo.title != null) {
        words = cleanWords(postinfo.title.toLowerCase());
        for (var i = 0; i < words.length; i++) {
            addWord(words[i], BOOSTTITLES, table, USE_STEMMER, USE_STOPWORDS);
        }
        if (USE_BIGRAMS) {
            for (var i = 0; i < words.length-1; i++) {
                if (USE_STOPWORDS &&
                    (STOPWORDS.hasOwnProperty(words[i]) ||
                     STOPWORDS.hasOwnProperty(words[i+1]))) continue;
                addWord(words[i] + ' ' + words[i+1], BOOSTTITLES, table, false,
                        USE_STOPWORDS);
            }
        }
    }
    if (postinfo.link != null) {
        addWord(postinfo.link.toLowerCase(), BOOSTLINKS, table, false, false);
    }

    return table;
}

flockFeedService.prototype.loadFeedHTML =
function (url, channel, params)
{
    debug("loadFeedHTML: " + url);

    if (channel.bypassCache || !this.hasFeed(url)) {
        channel.bypassCache = false; // so we don't bypass infinitely

        var req = Components.classes[XMLHTTPREQUEST_CONTRACTID]
            .createInstance(nsIXMLHttpRequest);

        req.overrideMimeType('text/xml');

        try {
            checkProtocol(url);
            req.open('GET', url, true);
        }
        catch(e) {
            // likely a url error;
            this.throwError(channel, FEED_HTML_ERROR_NETWORK);
            return;
        }

        var feedsrv = this;

        req.onreadystatechange = function (aEvt) {
            if (req.readyState == 4) {
                var status;
                try {
                    status = req.status;
                }
                catch (e) {
                    status = 0;
                }

                if (status == 200) {
                    TryParseFeed(feedsrv, url, req.responseXML);
                    feedsrv.loadFeedHTML(url, channel, params);
                }
                else
                    feedsrv.throwError(channel, FEED_HTML_ERROR_NETWORK);
            } 
        };

        req.send(null);
        return;
    }

    var urls = [url];
    var tdata = this.getTData(urls, params);
    if (!tdata) {
        this.throwError(channel, FEED_HTML_ERROR_BAD_FEED_DATA);
        return;
    }

    tdata.feed = tdata.feeds[url];

    var node = this.ds.getNode(url);
    var val = node.getTarget(FEED_RDF_NS + 'title');
    if (val && val.getValue())
        tdata.viewtitle = val.getValue();
    else
        tdata.viewtitle = tdata.feed.title;

    tdata.viewlink = tdata.feed.link;

    var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"].getService(Components.interfaces.nsILocaleService);
    var stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService);
    var stringBundle = stringBundleService.createBundle("chrome://flock/locale/feed/feed.properties", localeService.getApplicationLocale());

    tdata.stringBundle = stringBundle;

    var template = getTemplate(FEED_TEMPLATE_SINGLE_FEED);
    sendData(channel, template.process(tdata));
}

flockFeedService.prototype.loadFeeds =
function (load_urls, index, urls, channel, params)
{
    if (index >= load_urls.length) {
        this.loadAggregateHTML(urls, channel, params);
        return;
    }

    var url = load_urls[index];

    var req = Components.classes[XMLHTTPREQUEST_CONTRACTID]
        .createInstance(nsIXMLHttpRequest);

    req.overrideMimeType('text/xml');

    try {
        checkProtocol(url);
        req.open('GET', url, true);
    }
    catch(e) {
        // likely a url error;
        debug("Error loading " + url);
        // need to remove this url from the view so we don't keep trying to
        // get it
        load_urls.splice(index, 1);
        for (var i = 0; i < urls.length; i++) {
            if (urls[i] == url) {
                urls.splice(i, 1);
                i--;
            }
        }
        this.loadFeeds(load_urls, index, urls, channel, params);
    }

    var feedsrv = this;

    req.onreadystatechange = function (aEvt) {
       if (req.readyState == 4) {
           var status;
           try {
               status = req.status;
           }
           catch (e) {
               status = 0;
           }

           if (status == 200)
               TryParseFeed(feedsrv, url, req.responseXML);
           else {
               debug("Error loading " + url);
               // need to remove this url from the view so we don't keep
               // trying to get it
               load_urls.splice(index, 1);
               for (var i = 0; i < urls.length; i++) {
                   if (urls[i] == url) {
                       urls.splice(i, 1);
                       i--;
                   }
               }
           }

           var oneshot = {
               notify: function (obj) {
                   feedsrv.loadFeeds(load_urls, index + 1, urls, channel, params);
               }
           };

           var timer = Components.classes[TIMER_CONTRACTID]
               .createInstance(nsITimer);
           timer.initWithCallback(oneshot, 1, nsITimer.TYPE_ONE_SHOT);
       }
    };

    req.send(null);
}

flockFeedService.prototype.loadSingleHTML =
function (url, channel, params)
{
    if (params.style == 'topics')
        this.loadTopicsHTML(url, channel, params);
    else
        this.loadFeedHTML(url, channel, params);
}

flockFeedService.prototype.loadAggregateHTML =
function (urls, channel, params)
{
    debug("loadAggregateHTML: " + urls);

    var load_urls = [];

    for (var i = 0; i < urls.length; i++) {
        var url = urls[i];

        if (channel.bypassCache || !this.hasFeed(url))
            load_urls.push(url);
    }
    channel.bypassCache = false; // so we don't bypass infinitely

    if (load_urls.length) {
        this.loadFeeds(load_urls, 0, urls, channel, params);
        return;
    }
 
    var data = null;

    if (params.style != 'topics') {
        switch (params.sort) {
            case 'feeds':
                data = this.aggFeeds(urls, params);
                break;
            case 'dates':
                data = this.aggDates(urls, params);
                break;
        }
    } else {
        data = this.aggTopics(urls, params);
    }

    if (data)
        sendData(channel, data);
    else
        this.throwError(channel, FEED_HTML_ERROR_BAD_FEED_DATA);
}

flockFeedService.prototype.loadRootView =
function (urls, channel, params)
{
    debug("loadRootView");

    data = this.aggRootView(urls, params);

    if (data)
        sendData(channel, data);
    else
        this.throwError(channel, FEED_HTML_ERROR_BAD_FEED_DATA);
}

flockFeedService.prototype.loadFlaggedView =
function (channel, params)
{
    debug("loadFlaggedView");

    data = this.aggFlaggedView(params);

    if (data)
        sendData(channel, data);
    else
        this.throwError(channel, FEED_HTML_ERROR_BAD_FEED_DATA);
}

flockFeedService.prototype.throwError =
function (channel, errorType)
{
    var url = FEED_ERROR_HTML_DIR + '/' + errorType;
    var template = getContentData(url);

    sendData(channel, template);
}

function sendData(channel, data)
{
    var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
        .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);

    converter.charset = 'UTF-8';

    var convdata = converter.ConvertFromUnicode(data) + converter.Finish();

    var stream = Components.classes["@mozilla.org/io/string-input-stream;1"]
        .createInstance(Components.interfaces.nsIStringInputStream);

    var len = convdata.length;
    stream.setData(convdata, len);

    channel.sendStream(stream, 0, len);

    channel.finish();
}

function donePriorityFeeds(feedsrv, urls, listener)
{
    var feeds = {};

    for (var u = 0; u < urls.length; u++) {
        var url = fixFeedColonURL(urls[u]);
        var node = feedsrv.discoveryDS.getNode(url);

        var tval = node.getTarget(FEED_RDF_NS + 'feedtype');
        if (!tval || !tval.getValue() || tval.getValue() == UNKNOWN_FEED_TYPE) {
            debug("Error: " + url + " doesn't have a valid feed!");
            continue;
        }

        var title = url;

        var val = node.getTarget(FEED_RDF_NS + 'title');
        if (val && val.getValue())
            title = val.getValue();

        if (!feeds[title])
            feeds[title] = new Array();

        var feedinfo = {};
        feedinfo.type = tval.getValue();
        feedinfo.url = url;
        feedinfo.title = title;

        feeds[title].push(feedinfo);
    }
 
    var priorities = {
       "Atom 1.0": 100,
       "Atom 0.3": 75,
       "RSS 2.0": 50,
       "RSS 0.92": 40
    };

    function sorter(a,b) {
        var t1 = priorities[a.type];
        var t2 = priorities[b.type];

        if (!t1) t1 = 0;
        if (!t2) t2 = 0;

        return t2 - t1;
    }

    var retfeeds = [];

    for (var t in feeds) {
         feeds[t].sort(sorter);
         retfeeds.push(feeds[t][0]);
    }

    listener.onComplete(retfeeds.length, retfeeds);
}

function getDateValue(date)
{
   return String(date.getTime());
}

function getDateFromValue(datevalue)
{
    var val = Number(datevalue);
    return new Date(val);
}

/* this is a parser for the W3C subset of ISO 8601 dates
 * http://www.w3.org/TR/NOTE-datetime
 */
const re_iso8601 = new RegExp(
    "^(\\d{4})(-(\\d{2,3})(-(\\d{2})" +
    "(T(\\d{2}):(\\d{2})(:(\\d{2})(\\.(\\d+))?)?" +
    "(Z|(([-+])(\\d{2}):(\\d{2})))?)?)?)?$"
);


function parseDate(datestr)
{
    var date = null;

    datestr = datestr.replace(/^\s*|\s*$/g, '');

    var dateval = Date.parse(datestr);

    if (dateval) {
        date = new Date(dateval);
    } else {
        var m = datestr.match(re_iso8601);

        if (m) {
            date = new Date(Date.UTC(m[1]))

            m[3]  && date.setUTCMonth   (m[3] - 1);
            m[5]  && date.setUTCDate    (m[5]);
            m[7]  && date.setUTCHours   (m[7]);
            m[8]  && date.setUTCMinutes (m[8]);
            m[10] && date.setUTCSeconds (m[10]);

            m[12] && date.setUTCMilliseconds(('0.' + m[12]) * 1000);

            if (m[14]) {
                var tz_off = m[16] * 60 + Number(m[17]);
                tz_off *= m[15] == '-' ? -1 : 1;
                tz_off *= 60 * 1000;

                date.setTime(date.getTime() - tz_off);
            }

        }
    }

    if (date)
        return date.toString();
    else
        return null;
}

function getPriorityFeeds(feedsrv, urls, index, internalComplete, externalComplete)
{
    if (index >= urls.length) {
        internalComplete(feedsrv, urls, externalComplete);
        return;
    }

    var url = fixFeedColonURL(urls[index]);

    var node = feedsrv.discoveryDS.getNode(url);
    var val = node.getTarget(FEED_RDF_NS + 'last_refreshed');
    if (val) {
        var cutoff = Date.now() - feedsrv.discovery_refresh_interval;
        var lastRefresh = new Date(val.getValue());
        if (lastRefresh.getTime() > cutoff) {
            getPriorityFeeds(feedsrv, urls, index + 1,
                             internalComplete, externalComplete);
            return;
        }
    }

    var req = Components.classes[XMLHTTPREQUEST_CONTRACTID]
        .createInstance(nsIXMLHttpRequest);

    req.overrideMimeType('text/xml');

    try {
        checkProtocol(url);
        req.open('GET', url, true);
    }
    catch(e) {
        // likely a url error;
        debug("Error loading " + url + "\n");
        getPriorityFeeds(feedsrv, urls, index + 1,
                         internalComplete, externalComplete);
        return;
    }

    req.onreadystatechange = function (aEvt) {
        if (req.readyState == 4) {
            var status;
            try {
                status = req.status;
            }
            catch (e) {
                status = 0;
            }

            if (status == 200)
                TryParseFeed(feedsrv, url, req.responseXML, true);
            else
                debug("Error loading " + url + "\n");

            getPriorityFeeds(feedsrv, urls, index + 1,
                             internalComplete, externalComplete);
        }
    };

    req.send(null);
}

function getDefaultParams()
{
    return { sort: 'dates', time: 'everything', filter: 'all',
             display: 'summary' };
}

function getFeedsURLs(feedsrv, url)
{
    var urls = [];

    var query = url.substr('feeds:'.length);
    var qurls = query.split('&');

    for (var i = 0; i < qurls.length; i++) {
        var qurl = qurls[i];

        if (qurl.length == 0)
            continue;

        if (qurl.indexOf('=') != -1)
            continue;

        var testurl = decodeURIComponent(qurl);

        if (testurl.indexOf('://') == -1) {
            var curls = getCollectionUrls(feedsrv, testurl);
            if (curls)
                curls.forEach(function (u) { urls.push(u) });
        } else {
            urls.push(testurl);
        }
    }

    return urls;
}

function getViewTitle(tdata, params)
{
    if (params.collection) {
        tdata.viewtitle = params.collection;
    } else {
        var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"].getService(Components.interfaces.nsILocaleService);
        var stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService);
        var stringBundle = stringBundleService.createBundle("chrome://flock/locale/feed/feed.properties", localeService.getApplicationLocale());

        tdata.stringBundle = stringBundle;

        tdata.viewtitle = stringBundle.GetStringFromName("flock.feed.temp.aggNews");
    }
    tdata.viewlink = null;
}

function makeViewURLs(feedsrv, tdata, urls, params)
{
    var sortkeys = { dates: 'Date', readstatus: 'ReadStatus', feeds: 'Source' };
    var timespecs = ['Last24Hours', 'Last48Hours', 'LastWeek', 'Everything'];
    var styles = { excerpts: 'Excerpts', headlines: 'HeadlinesOnly',
                   full: 'FullArticles', topics: 'Topics' }
    var filters = ['Unread', 'Read', 'All'];

    if (urls[0] != FLAGGED_COLLECTION_NAME) {
        var subscribed = false;
        for (var i = 0; i < urls.length; i++) {
            var url = urls[i];
            var feed = tdata.feeds[url];
            if (!feed)
                feed = {};
            if (feedsrv.isSubscription(url)) {
                subscribed = true;
                feed.subscribed = true;
            } else {
                feed.subscribed = false;
            }
        }
    }

    if (!subscribed)
        tdata.feedurls = params.base_url.substr('feeds:'.length);

    tdata.base_url = params.base_url;

    var squery = '';
    if (params.sort)
        squery = '&' + 'sort=' + params.sort;

    var tquery = ''
    if (params.time)
        tquery = '&' + 'time=' + params.time;

    var yquery = '';
    if (params.yquery)
        yquery = '&' + 'style=' + params.style;

    var fquery = '';
    if (params.filter)
        fquery = '&' + 'filter=' + params.filter;

    tdata.paginateURL = params.base_url + squery + tquery + yquery + fquery;

    for (var key in sortkeys) {
        var base = 'sortBy' + sortkeys[key];

        if (params.sort == key) {
            tdata[base + 'URL'] = '#';
            tdata[base + 'State'] = 'selected';
        } else {
            tdata[base + 'URL'] = params.base_url + 'sort=' + key +
                                  tquery + yquery + fquery;
            tdata[base + 'State'] = '';
        }
    }

    for (var i in timespecs) {
        var key = timespecs[i].toLowerCase();
        var base = 'show' + timespecs[i];

        if (params.time == key) {
            tdata[base + 'URL'] = '#';
            tdata[base + 'State'] = 'selected';
        } else {
            tdata[base + 'URL'] = params.base_url + 'time=' + key +
                                  squery + yquery + fquery;
            tdata[base + 'State'] = '';
        }
    }

    for (var key in styles) {
        var base = 'show' + styles[key];

        if (params.style == key) {
            tdata[base + 'URL'] = '#';
            tdata[base + 'State'] = 'selected';
        } else {
            tdata[base + 'URL'] = params.base_url + 'style=' + key +
                                  squery + tquery + fquery;
            tdata[base + 'State'] = '';
        }
    }

    for (var i in filters) {
        var key = filters[i].toLowerCase();
        var base = 'showPosts' + filters[i];
 
        if (params.filter == key) {
            tdata[base + 'URL'] = '#';
            tdata[base + 'State'] = 'selected';
        } else {
            tdata[base + 'URL'] = params.base_url + 'filter=' + key +
                                  squery + tquery + yquery;
            tdata[base + 'State'] = '';
        }
    }
}

function updateAllCounts(feedsrv)
{
    debug("updateAllCounts");

    var rawDS = feedsrv.ds.getRawDataSource();
    rawDS.beginUpdateBatch();

    var total = 0;
    var unread = 0;

    var children = feedsrv.root.getChildren();
    while (children && children.hasMoreElements()) {
        var node = children.getNext();

        var val = node.getTarget(FEED_RDF_NS + 'type');
        if (!val)
            continue;

        var type = val.getValue();

        switch (type) {
            case 'feed':
                var counts = getCounts(node);
                total += counts.total;
                unread += counts.unread;
                break;

            case 'collection':
                var colTotal = 0;
                var colUnread = 0;

                var colChildren = node.getChildren();
                while (colChildren && colChildren.hasMoreElements()) {
                    var childNode = colChildren.getNext();
                    var counts = getCounts(childNode);
                    colTotal += counts.total;
                    colUnread += counts.unread;
                }

                node.addTargetOnce(FEED_RDF_NS + 'total', String(colTotal)); 
                node.addTargetOnce(FEED_RDF_NS + 'unread', String(colUnread)); 

                total += colTotal;
                unread += colUnread;
                break;

            default:
                break;
        }
    }

    feedsrv.root.addTargetOnce(FEED_RDF_NS + 'total', String(total)); 
    feedsrv.root.addTargetOnce(FEED_RDF_NS + 'unread', String(unread)); 

    rawDS.endUpdateBatch();
}

function updateSubscriptionsCallback(feedsrv, newCount)
{
    updateAllCounts(feedsrv);

    if (newCount) {
        var count = feedsrv.getNewCount() + newCount;

        feedsrv.root.addTargetOnce(FEED_RDF_NS + 'new', String(count));
        feedsrv.ds.save();

        var action = function (listener) {
            listener.onChanged();
            if (newCount != null) {
                listener.onNewCountUpdate(count);
            }
        }

        feedsrv.listeners.forEach(action);
    }

    feedsrv.update_timer = null;
    feedsrv.update_callback = null;
}

function updateSubscriptions(feedsrv, newCount, defer)
{
    if (feedsrv.update_timer)
        feedsrv.update_timer.cancel();

    if (!defer) {
        updateSubscriptionsCallback(feedsrv, newCount);
        return;
    }

    if (!feedsrv.update_timer) {
        feedsrv.update_timer = Components.classes[TIMER_CONTRACTID]
            .createInstance(nsITimer);
    }

    if (!feedsrv.update_callback) {
        feedsrv.update_callback = {
            notify: function (obj) {
                updateSubscriptionsCallback(this.feedsrv, this.newCount);
            },
            feedsrv: feedsrv,
            newCount: 0
        }
    }

    if (newCount)
        feedsrv.update_callback.newCount += newCount;

    feedsrv.update_timer.initWithCallback(feedsrv.update_callback,
                                          UPDATE_SUBSCRIPTION_DELAY,
                                          nsITimer.TYPE_ONE_SHOT);
}

function getCollectionUrls(feedsrv, name)
{
    var urls = [];

    if (!name || name == CATCH_ALL_COLLECTION) {
        var nodes = feedsrv.root.getChildren();
        while (nodes && nodes.hasMoreElements()) {
            var node = nodes.getNext();
            var nodename = node.getValue();

            if (nodename.indexOf(COLLECTIONS_RDF_ROOT) == 0 &&
                name != CATCH_ALL_COLLECTION) {
                var curls = getCollectionUrls(feedsrv, nodename.substr(COLLECTIONS_RDF_ROOT.length));
                if (curls)
                    curls.forEach(function (u) { urls.push(u) });
            } else {
                urls.push(nodename);
            }
        }
    } else {
        var collectionNode = feedsrv.ds.getNode(COLLECTIONS_RDF_ROOT + name);

        if (feedsrv.root.getChildIndex(collectionNode) == -1)
            return null;

        var feeds = collectionNode.getChildren();
        while (feeds && feeds.hasMoreElements()) {
            var feed = feeds.getNext();
            urls.push(feed.getValue());
        }
    }

    if (urls.length)
        return urls;
    else
        return null;
}

function getParentFromNode(node)
{
    var props = node.getInProperties();
    if (props && props.hasMoreElements()) {
        var prop = props.getNext()
        return node.getSource(prop);
    } else
        return null;
}

function getCounts(node)
{
    var counts = { total: 0, unread: 0 };

    var val;

    val = node.getTarget(FEED_RDF_NS + 'total');
    if (val)
        counts.total = Number(val.getValue());

    val = node.getTarget(FEED_RDF_NS + 'unread');
    if (val)
        counts.unread = Number(val.getValue());

    return counts;
}

function placeFeed(ds, node, target, orientation, fromNode)
{
    var toNode = ds.getNode(target); 

    var val = toNode.getTarget(FEED_RDF_NS + 'type');
    if (!val)
        throw "Invalid target";

    var target_type = val.getValue();

    if (orientation == null) {
        if (target_type == 'collection')
            orientation = ORIENT_INSIDE;
        else
            orientation = ORIENT_BELOW;
    }

    switch (orientation) {
        case ORIENT_INSIDE:
            if (target_type != 'collection')
                throw "Target is not a collection";

            if (fromNode) {
                if (fromNode.getValue() == toNode.getValue())
                    return;

                fromNode.removeChildForReal(node);
            }

            toNode.addChild(node);
            break;

        case ORIENT_ABOVE:
        case ORIENT_BELOW:
            var parentNode = getParentFromNode(toNode);
            var index = parentNode.getChildIndex(toNode);

            var oldIndex = -1;
            if (fromNode) {
                if (fromNode.getValue() == parentNode.getValue())
                    oldIndex = fromNode.getChildIndex(node);

                fromNode.removeChildForReal(node);
            }

            if (orientation == ORIENT_BELOW && (oldIndex == -1 || oldIndex > index))
                index++;

            parentNode.addChildAt(node, index);
            break;

        default:
            throw "Bad orientation";
            break;
    }
}

function setPostAttribute(feedsrv, attrib, url, id, set, needOld)
{
    url = fixFeedColonURL(url);

    var ds = feedsrv.getFeedStore(url);
    if (!ds)
        return;

    var post = ds.getNode(id);

    var changed = set;
    if (needOld) {
        var val = post.getTarget(FEED_RDF_NS + attrib);
        if (val && val.getValue() == 'true')
            changed = !changed;
    }

    post.addTargetOnce(FEED_RDF_NS + attrib, set ? 'true' : 'false');
    ds.save();

    var node = feedsrv.rootDS.getNode(post.getValue());
    val = node.getTarget(FEED_RDF_NS + attrib);
    if (val && val.getValue()) {
        node.addTargetOnce(FEED_RDF_NS + attrib, set ? 'true' : 'false');
    } else {
        feedsrv.rootDS.deleteRecursive(node);
    }
    feedsrv.rootDS.save();

    if (needOld) {
        return changed;
    } else {
        var props = {id: post.getValue(), feed: url};
        var rdf_props = post.getProperties();
        while (rdf_props && rdf_props.hasMoreElements()) {
            var prop = rdf_props.getNext().getValue();
            val = post.getTarget(prop);
            prop = prop.replace(FEED_RDF_NS, '');
            props[prop] = val.getValue();
        }
        var feedinfo = { url: url };
        var feednode = ds.getNode(FEED_DATA_RDF_PREFIX + url);
        getFeedNodeTargets(feednode, feedinfo,
                           ['link', 'title', 'subtitle', 'author',
                            'favicon', 'image', 'feedtype']);
        props.feedinfo = feedinfo;
        return props;
    }
}

function markReadOrUnread(feedsrv, url, id, direction)
{
    var changed = setPostAttribute(feedsrv, 'read', url, id, direction == -1, true);

    if (!changed)
        return;

    url = fixFeedColonURL(url);
    var urlNode = feedsrv.ds.getNode(url);
    var counts = getCounts(urlNode);

    counts.unread += direction;
    urlNode.addTargetOnce(FEED_RDF_NS + 'unread', String(counts.unread));

    updateSubscriptions(feedsrv);
}

// XXX: duplicated code. This really should be pushed down into the favorites
// service layer
function createFavorite(favsvc, url, name)
{
    var props = { url: url,
                  name: name,
                  local: true,
                  private: false,
                  fields: FAV_FIELD_NAME | FAV_FIELD_PRIVATE | FAV_FIELD_LOCAL }
   
    if (getCharPref('flock.favorites.webservice.id')) {
        if (getBoolPref('flock.favorites.shareByDefault')) {
            props.local = false;
            if (getBoolPref('flock.favorites.privateByDefault')) {
                if (favsvc.webService && 
                    (favsvc.webService.supportedFields & FAV_FIELD_PRIVATE))
                    props.local = true;
            }
        }
    }
 
    favsvc.createFavorite(url, props);
}

function doFlag(feedsrv, url, id, flag)
{
    var props = setPostAttribute(feedsrv, 'flagged', url, id, flag, false);

    var node = feedsrv.flaggedDS.getNode(props.id);

    if (flag) {
        var val = node.getTarget(FEED_RDF_NS + 'id');
        if (!val || !val.getValue())
            feedsrv.flaggedDS.root.addChild(node);

        for (var p in props) {
            if (p != 'feedinfo')
                node.addTargetOnce(FEED_RDF_NS + p, props[p]);
        }
        for (var p in props.feedinfo) {
            node.addTargetOnce(FEED_RDF_NS + 'feed-' + p, props.feedinfo[p]);
        }
    } else {
        feedsrv.flaggedDS.deleteRecursive(node);
    }

    feedsrv.flaggedDS.save();

    if (props.link) {
        var favsvc = Components.classes[FAVORITES_CONTRACTID]
            .getService(flockIFavoritesService);
        
        if (flag) {
            if (!favsvc.isFavorite(props.link))
                createFavorite(favsvc, props.link, props.title);
        } else {
            if (favsvc.isFavorite(props.link))
                favsvc.removeFavorite(props.link);
        }
    }
}

function getSnippet(postinfo, maxwords)
{
   var desc = postinfo.description.replace(/<\S[^><]*>/g,"").replace('&nbsp;',"");
   var words = desc.split(/\s+/);

   if (!maxwords)
       maxwords = SNIPPET_MAX_WORDS;

   if (words.length <= SNIPPET_MAX_WORDS)
       return postinfo.description;

   var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"].getService(Components.interfaces.nsILocaleService);
   var stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService);
   var stringBundle = stringBundleService.createBundle("chrome://flock/locale/feed/feed.properties", localeService.getApplicationLocale());

   var msg = stringBundle.GetStringFromName("flock.feed.temp.readmore");

   return words.slice(0, SNIPPET_MAX_WORDS + 1).join(' ') + '&hellip; '
       + '<a href="' + postinfo.link + '">'+msg+'</a>';
}

function getCharPref(prefName, defaultValue)
{
    try {
        var prefs = Components.classes['@mozilla.org/preferences-service;1']
            .getService(Components.interfaces.nsIPrefBranch);
        return prefs.getCharPref(prefName);
    } catch(e) {
        return defaultValue;
    }
}

function setCharPref(prefName, value)
{
    try {
        var prefs = Components.classes['@mozilla.org/preferences-service;1']
            .getService(Components.interfaces.nsIPrefBranch);
        prefs.setCharPref(prefName, value);
    } catch(e) {
        return;
    }
}

function getIntPref(prefName, defaultValue)
{
    try {
        var prefs = Components.classes['@mozilla.org/preferences-service;1']
            .getService(Components.interfaces.nsIPrefBranch);
        return prefs.getIntPref(prefName);
    } catch(e) {
        return defaultValue;
    }
}

function setIntPref(prefName, value)
{
    try {
        var prefs = Components.classes['@mozilla.org/preferences-service;1']
            .getService(Components.interfaces.nsIPrefBranch);
        prefs.setIntPref(prefName, value);
    } catch(e) {
        return;
    }
}

function getBoolPref(prefName, defaultValue)
{
    try {
        var prefs = Components.classes['@mozilla.org/preferences-service;1']
            .getService(Components.interfaces.nsIPrefBranch);
        return prefs.getBoolPref(prefName);
    } catch(e) {
        return defaultValue;
    }
}

function setBoolPref(prefName, value)
{
    try {
        var prefs = Components.classes['@mozilla.org/preferences-service;1']
            .getService(Components.interfaces.nsIPrefBranch);
        prefs.setBoolPref(prefName, value);
    } catch(e) {
        return;
    }
}

function getFavicon(feedsrv, feedURL, url)
{
    var req = Components.classes[XMLHTTPREQUEST_CONTRACTID]
        .createInstance(nsIXMLHttpRequest);

    try {
        checkProtocol(url);
        req.open('GET', url, true);
    }
    catch(e) {
        return;
    }

    req.onreadystatechange = function (aEvt) {
        if (req.readyState == 4) {
            var status;
            try {
                status = req.status;
            }
            catch (e) {
                status = 0;
            }

            if (status == 200)
                insertFavicon(feedsrv, feedURL, url);
        }
    };

    req.send(null);
}

function insertFavicon(feedsrv, feedURL, url)
{
    var feedFile = getFeedFile(feedURL);

    var ios = Components.classes[IO_SERVICE_CONTRACTID]
        .getService(nsIIOService);

    var fileHandler = ios.getProtocolHandler('file')
        .QueryInterface(nsIFileProtocolHandler);

    var spec = fileHandler.getURLSpecFromFile(feedFile);

    var ds = new RDFDataSource(spec);

    var root = ds.getNode(FEED_DATA_RDF_PREFIX + feedURL);
    root.addTargetOnce(FEED_RDF_NS + 'favicon', url);

    ds.save();

    var urlNode = feedsrv.ds.getNode(feedURL);
    var subscribedTarget = FEED_RDF_NS + 'subscribed';

    var val = urlNode.getTarget(subscribedTarget);
    if (val && val.getValue() == 'true') {
        urlNode.addTargetOnce(FEED_RDF_NS + 'favicon', url);
        updateSubscriptions(feedsrv, null, true);

        var node = feedsrv.rootDS.getNode(feedURL);
        node.addTargetOnce(FEED_RDF_NS + 'favicon', url);
        feedsrv.rootDS.save();
    }
}

Date.prototype.toPrettyTimeString = function() {
    return this.toLocaleFormat('%I:%M %p').replace(/^0/, '');
};

const TIME_DAY = 24 * 60 * 60 * 1000;

Date.prototype.toPrettyDateTimeString = function() {
    var now = new Date();
    var now_date = new Date(now.toDateString());

    var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"].getService(Components.interfaces.nsILocaleService);
    var stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService);
    var stringBundle = stringBundleService.createBundle("chrome://flock/locale/feed/feed.properties", localeService.getApplicationLocale());

    var localFromat      = stringBundle.GetStringFromName("flock.feed.temp.time");
    var localShortFromat = stringBundle.GetStringFromName("flock.feed.temp.timeShort");

    var date = null; 

    if (this.getTime() >= (now_date.getTime() + 2 * TIME_DAY))
        date = this.toLocaleFormat(localFromat);
    else if (this.getTime() >= (now_date.getTime() + TIME_DAY)) {
        date = this.toLocaleFormat(localShortFromat);
        date = date.replace('%1',stringBundle.GetStringFromName("flock.feed.temp.tomorrow"));
    } else if (this.getTime() >= now_date.getTime()) {
        date = this.toLocaleFormat(localShortFromat);
        date = date.replace('%1',stringBundle.GetStringFromName("flock.feed.temp.today"));
    } else if (this.getTime() >= (now_date.getTime() - TIME_DAY)) {
        date = this.toLocaleFormat(localShortFromat);
        date = date.replace('%1',stringBundle.GetStringFromName("flock.feed.temp.yesterday"));
    } else
        date = this.toLocaleFormat(localFromat);

    return date.replace(/ 0/g, ' ');
};

Date.prototype.toPrettyDateString = function() {
    var now = new Date();
    var now_date = new Date(now.toDateString());

    var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"].getService(Components.interfaces.nsILocaleService);
    var stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService);
    var stringBundle = stringBundleService.createBundle("chrome://flock/locale/feed/feed.properties", localeService.getApplicationLocale());

    if (this.getTime() >= (now_date.getTime() + 2 * TIME_DAY))
        return this.toLocaleFormat('%A, %B %d, %Y').replace(' 0', ' ');
    else if (this.getTime() >= (now_date.getTime() + TIME_DAY))
        return stringBundle.GetStringFromName("flock.feed.temp.tomorrow");
    else if (this.getTime() >= now_date.getTime())
        return stringBundle.GetStringFromName("flock.feed.temp.today");
    else if (this.getTime() >= (now_date.getTime() - TIME_DAY))
        return stringBundle.GetStringFromName("flock.feed.temp.yesterday");
    else
        return this.toLocaleFormat('%A, %B %d, %Y').replace(' 0', ' ');
};

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

loadLibraryFromSpec('chrome://browser/content/flock/contrib/rdfds.js');
loadLibraryFromSpec('chrome://browser/content/flock/contrib/tptemplate.js');
loadLibraryFromSpec('chrome://browser/content/flock/common/md5.js');

if (USE_STOPWORDS)
    loadLibraryFromSpec('chrome://browser/content/flock/feed/xpinclude/english.stop.js');

if (USE_STEMMER)
    loadLibraryFromSpec('chrome://browser/content/flock/feed/xpinclude/porterstemmer.js');

/*
 * TODO
 *
 * handle ttl, hour stuff
 */

function dd(obj) {
    for (p in obj) {
        try {
            debug(p + ' :: ' + obj[p]);
            if (typeof obj[p] == 'object') {
                for (q in obj[p]) {
                    debug(p + '.' + q + ' :: ' + obj[p][q]);
                }
            }
        } catch(e) {
            continue;
        }
    }
}
