// vim: ts=4 sw=4 expandtab cindent foldmethod=marker
//
// 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 Cc = Components.classes;
const Ci = Components.interfaces;

const FLOCK_OPML_SERVICE_CONTRACTID = '@flock.com/opml-service;1';
const FLOCK_OPML_SERVICE_CID        = Components.ID('{a3948322-0bb8-413c-8c6d-b0959d6d5bad}');
const flockIOpmlService             = Ci.flockIOpmlService;

const POLL_INTERVAL                 = 1000 * 60 * 60;

const OPML_DATA_DIR                 = "opml";
const OPML_DATA_FILENAME            = "opml.xml";

const OPML_TEMPLATE_DIR             = "chrome://browser/content/flock/opml/templates";

const OPML_TEMPLATE_BASE            = "opml.html";
const OPML_TEMPLATE_NODE            = "opmlnode.html";
const OPML_TEMPLATE_INFO            = "opmlinfo.html";

const nsISupports                   = Ci.nsISupports;
const nsIClassInfo                  = Ci.nsIClassInfo;
const nsITimer                      = Ci.nsITimer;
const nsITimerCallback              = Ci.nsITimerCallback;
const nsIProperties                 = Ci.nsIProperties;
const nsIFile                       = Ci.nsIFile;
const nsIXMLHttpRequest             = Ci.nsIXMLHttpRequest;
const nsIDOMSerializer              = Ci.nsIDOMSerializer;
const flockIFeedService             = Ci.flockIFeedService;

const TIMER_CONTRACTID              = '@mozilla.org/timer;1';
const IO_SERVICE_CONTRACTID         = '@mozilla.org/network/io-service;1';
const DIRECTORY_SERVICE_CONTRACTID  = '@mozilla.org/file/directory_service;1';
const DOMSERIALIZER_CONTRACTID      = '@mozilla.org/xmlextras/xmlserializer;1';
const DOMPARSER_CONTRACTID          = '@mozilla.org/xmlextras/domparser;1';
const XMLHTTPREQUEST_CONTRACTID     = '@mozilla.org/xmlextras/xmlhttprequest;1';
const FILE_INPUT_STREAM_CONTRACTID  = '@mozilla.org/network/file-input-stream;1';
const FEEDSVC_CONTRACTID            = '@flock.com/feed-service;1';

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

function fixOpmlColonURL(url)
{
    if (url.substr(0, 'opml:'.length) == 'opml:') {
        var tmpurl = url.substring('opml:'.length);
        if (tmpurl.substr(0, '//'.length) == '//')
            tmpurl = 'http:' + tmpurl;
        else
            tmpurl = decodeURIComponent(tmpurl);
        url = tmpurl;
    }
    return url;
}

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

flockOpmlService.prototype.init =
function ()
{
    var opmlDir = Cc[DIRECTORY_SERVICE_CONTRACTID].getService(nsIProperties)
                                                  .get('ProfD', nsIFile);
   
    opmlDir.append(OPML_DATA_DIR); 

    if (!opmlDir.exists())
        opmlDir.createUnique(nsIFile.DIRECTORY_TYPE, 0755);
}

function DumpXMLData(xmlData, prefix)
{
    debug(prefix + xmlData.@title + ' ' + xmlData.@htmlUrl + ' ' +
          xmlData.@xmlUrl + '\n');
    for each (var outline in xmlData.outline) {
        DumpXMLData(outline, prefix + '  ');
    }
}

flockOpmlService.prototype.loadXmlData =
function (file)
{
    var stream = Cc["@mozilla.org/network/file-input-stream;1"].
        createInstance(Ci.nsIFileInputStream);
    stream.init(file, 1, 0, false);

    var sstream = Cc["@mozilla.org/scriptableinputstream;1"].
        createInstance(Ci.nsIScriptableInputStream);
    sstream.init(stream);

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

    sstream.close();
    stream.close();

    data = data.replace(/<\?xml[^?]*\?>/, '');
    var xmlData = new XML(data);
    // DumpXMLData(xmlData..body.outline, '');
    return xmlData;
}

flockOpmlService.prototype.getFeedsInOpml =
function (url, countObj)
{
    this.refreshOpml(url); // XXX this is moronic, since the refresh won't
                           // happen until after we return from this function

    url = fixOpmlColonURL(url);
    var feeds = [];

    var file = getOpmlFile(url);
    var xmlData = this.loadXmlData(file);
    for each (var outline in xmlData..outline) {
        var xmlUrl = outline.@xmlUrl;
        debug('title: ' + outline.@title + ', url: ' + xmlUrl);
        if (xmlUrl && xmlUrl.toString().length > 0) {
            feeds.push(xmlUrl.toString());
        }
    }
    countObj.value = feeds.length;
    return feeds;
}

flockOpmlService.prototype.refreshOpml =
function (url)
{
    var req = Cc[XMLHTTPREQUEST_CONTRACTID].createInstance(nsIXMLHttpRequest);

    url = fixOpmlColonURL(url);

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

    req.onreadystatechange = function (aEvt) {
        if (req.readyState == 4) {
            if (req.status == 200)
                TryParseOpml(url, req.responseXML);
            else
                debug("Error loading " + url);
        }
    };

    req.send(null); 
}

flockOpmlService.prototype.isOpml =
function (dom)
{
    return isOpmlInternal(dom);
}

flockOpmlService.prototype.loadOpmlView =
function (spec, channel)
{
    var url = fixOpmlColonURL(spec);

    var tdata = { opmlURL: url };
    var template = getTemplate(OPML_TEMPLATE_INFO);
    var data = template.process(tdata);
    sendData(channel, data);
    return;

    /*
    this.refreshOpml(spec); // XXX this is moronic, since the refresh won't
                            // happen until after we return from this function

    var file = getOpmlFile(fixOpmlColonURL(spec));
    var xmlData = this.loadXmlData(file);

    var tdata = { data : this.makeTemplate(xmlData..body).string };
    var template = getTemplate(OPML_TEMPLATE_BASE);
    var data = template.process(tdata);
    sendData(channel, data);

    // debug('loading feed view: ' + url);
    // var feedSvc = Cc[FEEDSVC_CONTRACTID].getService(flockIFeedService);
    // feedSvc.loadFeedView(url, channel);
    */

}

function getAttributeString(attribute)
{
    if (attribute && attribute.toString().length > 0) {
        return attribute.toString();
    } else {
        return null;
    }
}

flockOpmlService.prototype.makeTemplate =
function (root)
{
    var data = {
        children : [],
        xmlUrl   : getAttributeString(root.@xmlUrl),
        htmlUrl  : getAttributeString(root.@htmlUrl),
        title    : getAttributeString(root.@title),
        aggUrl   : null
    };
    if (data.title == null) {
        data.title = getAttributeString(root.@text);
    }

    var tmpAggUrl = ''; // the aggUrl without feeds:page=1 in front, to return
    if (data.xmlUrl) {
        tmpAggUrl += '&' + escape(data.xmlUrl);
    }
    for each (var outline in root.outline) {
        debug('title: ' + outline.@title + ', url: ' + outline.@xmlUrl);
        var child = this.makeTemplate(outline);
        data.children.push(child.string);
        tmpAggUrl += child.aggUrl;
    }
    if (data.children.length > 0 && tmpAggUrl.length > 0)
        data.aggUrl = 'feeds:page=1' + tmpAggUrl;

    var template = getTemplate(OPML_TEMPLATE_NODE);
    return { string : template.process(data), aggUrl : tmpAggUrl };
}

flockOpmlService.prototype.subscribeOpml =
function (url, listener)
{
    url = fixOpmlColonURL(url);

    var isLocalFile = false;

    if (url.indexOf('file://') == 0) {
        isLocalFile = true;
    } else if (url.indexOf('http:') != 0 && url.indexOf('https:') != 0) {
        debug('Unsupported scheme: ' + url);
        return;
    }

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

    req.overrideMimeType('text/xml');

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

    var opmlsrv = this;

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

            if (status == 200 || (isLocalFile && req.responseXML))
                opmlsrv.subscribeDOM(url, req.responseXML, listener);
            else
                debug("Error loading " + url);
        }
    };

    req.send(null);
}

flockOpmlService.prototype.subscribeDOM =
function (url, xmlDocument, listener)
{
    var xmlData;

    try {
        var serializer = Cc[DOMSERIALIZER_CONTRACTID]
            .createInstance(nsIDOMSerializer);

        var data = serializer.serializeToString(xmlDocument.documentElement);
        xmlData = new XML(data);
    }
    catch (e) {
        debug("Invalid XML at " + url);
        return;
    }

    var body = xmlData.body;

    if (xmlData.name() != 'opml' || !body) {
        debug("No OPML data at " + url);
        return;
    }

    var root = { title: xmlData.head.title, feeds: [] };

    var sections = [];

    for each (var outline in body.outline) {
        if (outline.hasComplexContent()) {
            var section = {};
            section.title = outline.@title;
            if (!section.title || section.title.toString().length == 0)
                section.title = outline.@text;
            section.feeds = [];

            for each (var suboutline in outline..outline) {
                var xmlUrl = suboutline.@xmlUrl;
                if (xmlUrl && xmlUrl.toString().length > 0) {
                    var text = suboutline.@text;
                    if (!text || text.toString().length == 0)
                        text = suboutline.@title;
                    if (text && text.toString().length > 0)
                        section.feeds.push({title: text, url: xmlUrl});
                }
            }

            sections.push(section);
        } else {
            var xmlUrl = outline.@xmlUrl;
            if (xmlUrl && xmlUrl.toString().length > 0) {
                var text = outline.@text;
                if (!text || text.toString().length == 0)
                    text = outline.@title;
                if (text && text.toString().length > 0)
                    root.feeds.push({title: text, url: xmlUrl});
            }
        }
    }

    var urls = [];

    var feedsrv = Cc[FEEDSVC_CONTRACTID].getService(flockIFeedService);
    
    function addSection(section)
    {
        var target = null;

        if (section.title) {
            target = String(section.title);
            target = target.replace(/^\s+/, '').replace(/^s+$/, '');
            debug("Creating collection " + target);
            feedsrv.addCollection(target);
            target = 'urn:flock:feed:subscriptions:collections:' + target;
        }

        for each (var feed in section.feeds) {
            debug("Subscribing " + feed.url);
            feedsrv.subscribeFeed(feed.url, target,
                                  flockIFeedService.ORIENT_INSIDE,
                                  true);
            urls.push(feed.url);
        }
    }

    for each (var section in sections) {
        if (section.feeds.length == 0)
            continue;

        addSection(section);
    }

    if (urls.length) {
        for each (var feed in root.feeds) {
            debug("Subscribing " + feed.url);
            feedsrv.subscribeFeed(feed.url, null, null, true);
            urls.push(feed.url);
        }
    } else {
        addSection(root);
    }

    if (urls.length)
        feedsrv.grabNewSubscriptions(urls.length, urls, listener);
}

/* Boring framework stuff... {{{*/
function flockOpmlService()
{
    debug("Constructed");

    this.init() 

/*  // XXX at some point we want to poll for updates to the file...
    this.timer = Cc[TIMER_CONTRACTID].createInstance(nsITimer);
    this.timer.initWithCallback(this, POLL_INTERVAL,
                                nsITimer.TYPE_REPEATING_SLACK);
*/
}

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

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

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

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

opmlModule.registerSelf =
function (compMgr, fileSpec, location, type)
{
    compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
    compMgr.registerFactoryLocation(FLOCK_OPML_SERVICE_CID, 
                                    "Flock OPML JS Component",
                                    FLOCK_OPML_SERVICE_CONTRACTID, 
                                    fileSpec, 
                                    location,
                                    type);
}

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

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

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

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

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

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

function escape_url(url)
{
    var rval = url.replace(/\/+$/, '');
    rval = rval.replace(/[\?\/\\:.]+/g, '_');
    return escape(rval);
}

function getOpmlDir(url)
{
    var opmlDir = Cc[DIRECTORY_SERVICE_CONTRACTID].getService(nsIProperties)
                                                  .get('ProfD', nsIFile);

    opmlDir.append(OPML_DATA_DIR);
    opmlDir.append(escape_url(url));

    return opmlDir;
}

function getOpmlFile(url)
{
    var opmlFile = getOpmlDir(url);
    opmlFile.append(OPML_DATA_FILENAME);
    return opmlFile;
}

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

    file.createUnique(nsIFile.NORMAL_FILE_TYPE, 0644);

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

    ostream.init(file, PR_WRONLY | PR_CREATE_FILE, 0644, 0);
    
    var serializer = Cc[DOMSERIALIZER_CONTRACTID].createInstance(nsIDOMSerializer);
        
    serializer.serializeToStream(xmlDocument, ostream, 'UTF-8');
    
    ostream.flush();
    ostream.close();
}

function isOpmlInternal(dom)
{
    var nodes = dom.getElementsByTagName('opml');
    if (nodes.length > 0) {
        return true;
    } else {
        return false;
    }
}

function TryParseOpml(url, xmlDocument)
{
    debug("Parsing " + url);

    var file = getOpmlFile(url);

    try {
        saveXMLData(file, xmlDocument);
    }
    catch (e) {
        debug("Invalid XML at " + url);
        return;
    }
 
    if (!isOpmlInternal(xmlDocument)) {
        debug("Not OPML at " + url);
        return;
    }
}

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

    sstream.init(istream); 

    var data = '';
    var str = sstream.read(-1);
    while (str.length > 0) {
        data += str;
        str = sstream.read(-1);
    }

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

    return data;
}

function getTemplate(filename)
{
    var url = OPML_TEMPLATE_DIR + '/' + filename;

    var ios = Cc[IO_SERVICE_CONTRACTID].createInstance(Ci.nsIIOService);

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

    var data = getStreamData(cstream);

    return data;
}

function sendData(channel, data)
{
    var stream = Cc["@mozilla.org/io/string-input-stream;1"]
        .createInstance(Ci.nsIStringInputStream);

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

    channel.sendStream(stream, 0, len);

    channel.finish();
}

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

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