/*
    File:   FeedPortalKit.js
    Author: l.m.orchard (lorchard@organic.com)

    This module offers a class named FeedPortalKit, useful in constructing
    syndication-feed-driven headline portals using unobtrusive JS techniques.

    Feeds for display on the page are listed in an OPML page, which is loaded
    by this object.  The titles of feeds in the OPML should match up with the
    title attributes of container elements on the HTML page.  Using the
    Behaviour module, this object can then be used to load up feed content and
    populate elements on the page with feed items.
    
    Synopsis:

        // This will inject feed items into all <ul class="portalfeed">
        // elements on the page whose titles match feed titles in the OPML.
        
        var OPML_URL = 'xml/feeds.xml';
        var behaviours = {
            'ul.portalfeed' : wireUpPortalFeed
        };

        function wireUpPortalFeed(ele) {
            var title = ele.getAttribute('title');
            var feeds = fpk.getFeedsByTitle(title);

            if (feeds.length) {
                forEach(feeds, function(feed) {
                    fpk.loadFeed(feed, ele, true);
                });
            }
        }

        function init() {
            fpk = new FeedPortalKit();
            fpk.loadOPML(OPML_URL, function() {
                Behaviour.register(behaviours);
                Behaviour.apply();
            });
        }
        addLoadEvent(init);
*/

function FeedPortalKit() { this.init() };

FeedPortalKit.prototype = {
    
    // Configuration constants, open to tweaking.
    MAX_ITEMS:         7,
    TIME_DELAY_START:  2500,
    DO_FADE:           true,
    FADE_TIME:         2000,
    LAST_VIEWED_DATE:  isoTimestamp('2005-12-14T00:00:00-0500'),
    
    /*
        Method: renderFeedItem

        Given feed and item objects, construct an HTML rendering of it
        as a DOM node.

        Parameters:
            feed - Feed description object parsed from OPML.
            item - Feed item to be added.
        
        Returns:
            HTML DOM node to be inserted into the page.
    */
    renderFeedItem: function(feed, item, id) {
        return LI({'class':'item', 'id': id},
                    SPAN(null, Date.strftime('%Y %b %d - ', item['date'])),
                    A({'class':'link', 'href':item['link'], 'target':'_blank'}, 
                       item['title'])
                );
    },
    
    emptyFeedMessage: function(feed) {
        return LI({'class':'item'}, 'No items available in this feed');
    },
    
    // Private constants, no user-serviceable parts.
    _OPML_ATTRS:       ['title', 'description', 'htmlurl', 'xmlUrl'],
    _RSS_ITEM_ATTRS:   ['title', 'link', 'description', 'dc:date', 'source', 'pubDate' ],
    _NS_DC:            'http://purl.org/dc/elements/1.1/',
    _id_cnt:           0,
    _time_delays:      {},

    /*
        Method: init

        Initialize the FeedPortalKit instance.
    */
    init: function() {
        this.feeds = [];
    },

    /*
        Method: loadOPML

        Load up the OPML to get a list of feeds to use.

        Parameters:
            opml_url - URL to an OPML file containing feeds.
            fn - Function to be called once the OPML has been loaded.

        See Also:
            <_fetchedOPML>
    */
    loadOPML: function(opml_url, fn) {
        var that = this; // Makes closures happy.
        logDebug("Loading OPML from "+opml_url);
      
        // Initiate OPML fetch.
        var d = doSimpleXMLHttpRequest(opml_url);
        d.addCallback(function(rv) { 
            that._fetchedOPML(rv);
            if (fn) fn();
        });
        d.addErrback(function(rv) { 
            logError("Failed in fetching OPML: "+opml_url+" "+rv.message); 
        })
    },

    /*
        Method: _fetchedOPML

        Handle the arrival of OPML data, parse and load up feed list.

        Parameters:
            rv - Return value from XMLHttpRequest
    */
    _fetchedOPML: function(rv) {
        var opml  = rv.responseXML;
        var nodes = opml.getElementsByTagName('outline');
        logDebug("Found "+nodes.length+" feeds.");

        for (var i=0; i<nodes.length; i++) {
            var feed = this._extractOPMLFeedDetails(nodes[i]);
            this.feeds.push(feed);
            logDebug("\t"+feed['title']);
        }
    },

    /*
        Method: _extractOPMLFeedDetails

        Given a node from an OPML document, extract a set of values from it.

        Parameters:
            node - OPML node from which to extract values
    */
    _extractOPMLFeedDetails: function(node) {
        var feed = {};
        forEach(this._OPML_ATTRS, function(x) { 
            feed[x] = node.getAttribute(x); 
        });
        return feed;
    },

    /*
        Method: getFeedsByTitle

        Given a title, search through feeds list for matches.
    
        Parameters:
            title - String value for which to search on titles
        
        Returns:
            List of feeds matching given title.

    */
    getFeedsByTitle: function(title) {
        return filter(function(x) { return (x.title == title); }, this.feeds);
    },
    
    /*
        Method: loadFeed

        Load items from a feed into an element on the page.

        Parameters:
            feed - Feed description object parsed from OPML.
            parent_ele - Element on page into which items will be added.
            clear - Flag whether to clear existing element content on load.

        See Also:
            <_fetchedFeed>
    */
    loadFeed: function(feed, parent_ele, clear, limit, onload_func) {
        if (!limit) limit = this.MAX_ITEMS;
        var that     = this;
        var feed_url = "/motorsports/" + feed['xmlUrl'];
        logDebug("Loading feed from "+feed_url);
        
        this._time_delays[feed_url] = this.TIME_DELAY_START;
        
        var d = doSimpleXMLHttpRequest(feed_url);
        d.addCallback(function(rv) { 
            that._fetchedFeed(rv, feed, parent_ele, clear, limit, onload_func); });
        d.addErrback(function(rv) { 
          logError("Failed in fetching feed: "+feed_url+" - "+rv.message); 
        });
    
    },
    
    /*
        Method: _fetchedFeed

        Handles the arrival of feed data, manages process of inserting
        rendered feed items into the parent element.

        Parameters:
            rv - Return value from XMLHttpRequest
            feed - Feed description object parsed from OPML.
            parent_ele - Element on page into which items will be added.
            clear - Flag whether to clear existing element content on load.

        See Alsp:
            <_addItemToFeedPortal>
    */
    _fetchedFeed: function(rv, feed, parent_ele, clear, limit, onload_func) {
        var feed_xml = rv.responseXML;
        if (!feed_xml) {
            logError("Error parsing feed "+feed['xmlUrl']+".");
            logDebug(rv.responseText);
            return;
        }
        
        var items = feed_xml.getElementsByTagName('item');
        logDebug("Fetched feed with "+items.length+" items.");

        if (clear) {
            var cn = filter(function (x) { return true; }, 
                parent_ele.childNodes);
            forEach(cn, function(child_node) {
              parent_ele.removeChild(child_node);
            });
        };
        
        if (items.length == 0) {
            var empty_msg_ele = this.emptyFeedMessage(feed);
            parent_ele.appendChild(empty_msg_ele);
        };
    
        logDebug("Limit is set at "+limit);
        var limit = Math.min(items.length, limit);
        for (var i=0; i<limit; i++) {
            var item = this._extractFromRSSItem(items[i]);
            this._addItemToFeedPortal(parent_ele, feed, item);
        }

        if (onload_func) onload_func();
    
    },
    
    /*
        Method: _extractFromRSSItem

        Given a item from an RSS feed, extract a set of values from it.

        Parameters:
            node - RSS item node from which to extract values
    */
    _extractFromRSSItem: function(node) {
        var item = {};
        forEach(this._RSS_ITEM_ATTRS, function(x) {
            var cns = node.getElementsByTagName(x);
            if (cns.length > 0) {
                item[x] = scrapeText(cns[0]); //.firstChild.nodeValue;
            }
        });
        
        // Grab and parse the item date
        var date_nodes = (node.getElementsByTagNameNS) ?
            node.getElementsByTagNameNS(this._NS_DC,'date') :
            // Damn you, MSIE
            node.getElementsByTagName('dc:date');
        
        if (date_nodes.length > 0) {
            var date_txt = scrapeText(date_nodes[0]);
            item['date'] = isoTimestamp(date_txt);
        }
        
        return item;
    },
    
    /*
        Method: _addItemToFeedPortal

        Perform the work of adding a new feed item to a parent element.
        
        Parameters:
            parent_ele - Element on page into which items will be added.
            feed - Feed description object parsed from OPML.
            item - Feed item to be added.

        See Also:
            <renderFeedItem>
            <_reallyAddItemElement>
    */
    _addItemToFeedPortal: function(parent_ele, feed, item) {
        var that     = this;
        var feed_url = feed['xmlUrl'];
        
        // Get a rendering for this feed item.
        var new_id = 'feeditem'+(this._id_cnt++);
        var new_ele = this.renderFeedItem(feed, item, new_id);
        
        // Come up with a fake-out delay for adding this element
        var curr_delay = this._time_delays[feed_url];
        var rnd = curr_delay * Math.random();
        this._time_delays[feed_url] -= rnd;
        // Decide whether or not a delay is really called for in adding this item.
        /*  Firefox on MAC OS10 was not displaying feeds because the curr_delay was set
            as NaN.  The setTimeout function was never called and the elements were never 
            written to the page.  Commented out portions HACK the code that was turned over 
            by L. Orchard.        
        */
        /*if (curr_delay == 0) {
            this._reallyAddItemElement(parent_ele, feed, item, new_ele, false);
        } else {
            alert("delay of " + curr_delay);
            setTimeout(function() { 
                alert("delay called");
                that._reallyAddItemElement(parent_ele, feed, item, new_ele, true) 
            }, curr_delay);
        }*/
        this._reallyAddItemElement(parent_ele, feed, item, new_ele, false);
    },

    /*
        Method: _reallyAddItemElement

        Perform the work of adding a new feed item to a parent element.
        Separated out as a separate method in order to facilitate delayed 
        animated builds of feed display.
        
        Parameters:
            parent_ele - Element on page into which items will be added.
            feed - Feed description object parsed from OPML.
            item - Feed item to be added.
            new_ele - The DOM element to be added
            before - Whether the new element should be added at the head of parent.

        See Also:
            <renderFeedItem>
            <_reallyAddItemElement>
    */
    _reallyAddItemElement: function(parent_ele, feed, item, new_ele, before) {
      if (before){
            parent_ele.insertBefore(new_ele, parent_ele.firstChild);
      }else{
            parent_ele.appendChild(new_ele);
      }
        // Initiate a fade on "new" items.
        if (this.DO_FADE && item['date'] > this.LAST_VIEWED_DATE)
            Fat.fade_element(new_ele.id, 30, this.FADE_TIME);
    }
    
};
