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

const OPML_CONTRACTID = '@flock.com/opml-service;1';
const OPML_CLASSID    = Components.ID('{a3948322-0bb8-413c-8c6d-b0959d6d5bad}');
const OPML_CLASSNAME  = 'Flock OPML Service';

const MARK_READ_CONTRACTID = '@flock.com/opml-subscribe-listener-mark-read;1';
const MARK_READ_CLASSID    = Components.ID('{4cb09bdd-cca3-4f8d-86b4-d4b315c42b45}');
const MARK_READ_CLASSNAME  = 'Flock OPML Mark Read on Subscribe Listener';


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

const XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>\n'

const MAX_OPML_DEPTH = 20;

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


function writeXML(xml, file) {
  var xmlStr = XML_HEADER + xml.toXMLString();

  if (file.exists())
    file.remove(false);

  file.createUnique(Ci.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 converter = Cc['@mozilla.org/intl/scriptableunicodeconverter']
    .createInstance(Ci.nsIScriptableUnicodeConverter);
  converter.charset = 'UTF-8';

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

  ostream.write(convdata, convdata.length);

  ostream.flush();
  ostream.close();
}


function OpmlRequest(url, folder, flatRoot, listener, ctxt, logger) {
  this._url      = url;
  this._folder   = folder;
  this._flatRoot = flatRoot;
  this._listener = listener;
  this._ctxt     = ctxt;
  this._logger   = logger;

  var ios = Cc['@mozilla.org/network/io-service;1']
    .getService(Ci.nsIIOService);
  this._channel = ios.newChannelFromURI(url, null, null);
}

OpmlRequest.prototype = {
  get: function OR_get() {
    this._channel.asyncOpen(this, null);
  },

  onStartRequest: function OR_onStartRequest(request, context) {
    this._xml = '';
  },
  onStopRequest: function OR_onStopRequest(request, context, status) {
    if (Components.isSuccessCode(status))
      this._subscribe();
    else if (this._listener)
      this._listener.onLoadError(this._ctxt, null);
  },
  onDataAvailable: function OR_onDataAvailable(request, context, inputStream,
                                               sourceOffset, count) {
    var sstream = Cc['@mozilla.org/scriptableinputstream;1']
      .createInstance(Ci.nsIScriptableInputStream);
    sstream.init(inputStream);

    this._xml += sstream.read(count);
  },

  _sendLoadError: function OR__sendLoadError(errorCode) {
    if (this._listener) {
      var error = Cc['@flock.com/error;1'].createInstance(Ci.flockIError);
      error.errorCode = errorCode;
      this._listener.onLoadError(this._ctxt, error);
    }
  },

  _subscribe: function OR__subscribe() {
    var url = this._url.spec;
    this._logger.info('got OPML data from ' + url);

    var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                    .createInstance(Ci.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-8";

    try {
      var rawData = converter.ConvertToUnicode(this._xml);
      var xmlData = new XML(rawData.replace(/<\?xml[^?]*\?>/, ""));
    }
    catch (e) {
      this._logger.error('Invalid XML at ' + url);
      this._sendLoadError(Ci.flockIError.OPML_INVALID_XML);
      return;
    }

    var body = xmlData.body;

    if (xmlData.name() != 'opml' || !body) {
      this._logger.error('No OPML data at ' + url);
      this._sendLoadError(Ci.flockIError.OPML_NO_DATA);
      return;
    }

    var root;
    if (!this._flatRoot) {
      var title = xmlData.head.title;
      if (title && title.toString().length > 0)
        root = [xmlData.head.title.toString()];
      else
        root = ['Imported Feeds'];
    } else {
      root = [this._folder];
    }

    this._feeds = [];
    this._tooDeep = false;

    this._getFeeds(root, body);

    if (this._tooDeep) {
      this._logger.error('Hierarchy at ' + url + ' is too deep');
      this._sendLoadError(Ci.flockIError.OPML_TOO_DEEP);
      return;
    }

    if (this._feeds.length == 0) {
      this._logger.error('No feeds in OPML at ' + url);
      this._sendLoadError(Ci.flockIError.OPML_NO_FEEDS);
      return;
    }

    this._logger.info(this._feeds.length + ' feeds found in ' + url);
    this._subscribeFeeds();
  },

  _getFeeds: function OR__getFeeds(hierarchy, node) {
    for each (var outline in node.outline) {
      var xmlUrl = outline.@xmlUrl;
      if (xmlUrl && xmlUrl.toString().length > 0 &&
          !outline.hasComplexContent()) {
        var feed = { hierarchy: hierarchy,
                     url: xmlUrl.toString(),
                     title: null
                   };

        text = outline.@text;
        if (!text || text.toString().length == 0)
          text = outline.@title;

        if (text && text.toString().length > 0)
          feed.title = text.toString();

        this._logger.info('Found feed ' + feed.url);
        this._feeds.push(feed);
      } else {
        var title = outline.@title;
        if (!title || title.toString().length == 0)
          title = outline.@text;

        if (title && title.toString().length > 0) {
          var newHierarchy = hierarchy.concat(title.toString());
          if (newHierarchy.length > MAX_OPML_DEPTH) {
            this._tooDeep = true;
            return;
          }

          this._logger.info('Found folder ' + title.toString());

          if (outline.hasComplexContent()) {
            this._getFeeds(newHierarchy, outline);
          } else {
            var feed = { hierarchy: newHierarchy,
                         url: null
                       };
            this._feeds.push(feed);
          }

          if (this._tooDeep)
            return;
        }
      }
    }
  },

  _createFolderHierarchy: function OR__createFolderHierarchy(aHierarchy) {
    var destFolder = null;
    for each (var folder in aHierarchy) {
      if (folder instanceof Ci.flockIFeedFolder) {
        destFolder = folder;
      } else {
        var newFolder = destFolder.getChildFolder(folder);
        if (!newFolder) {
          newFolder = destFolder.addFolder(folder);
          this._logger.info("Created folder: " + folder);
        }
        destFolder = newFolder;
      }
    }
    return destFolder;
  },

  _subscribeFeeds: function OR__subscribeFeeds() {
    var fm = Cc['@flock.com/feed-manager;1']
             .getService(Ci.flockIFeedManager);

    var metrics = Cc["@flock.com/metrics-service;1"]
                  .getService(Ci.flockIMetricsService);

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

    function makeURI(aURL) {
      try {
        return ios.newURI(aURL, null, null);
      } catch (ex) {
        return null;
      }
    }

    var subscribedFeeds = [];

    for (var index = 0; index < this._feeds.length; index++) {
      var feed = this._feeds[index];
      if (feed.url == null) {
        this._createFolderHierarchy(feed.hierarchy);
      } else {
        var feedURI = makeURI(feed.url);

        if (feedURI) {
          if (!this._listener ||
               this._listener.onSubscribe(feedURI, this._ctxt,
                                          index, this._feeds.length))
          {
            var folder = this._createFolderHierarchy(feed.hierarchy);
            feed.hierarchy = [folder];

            this._logger.info("Subscribing " + feed.url
                              + " to folder " + folder.getTitle());
            folder.subscribeURL(feed.url, feed.title);

            subscribedFeeds.push({ URI: feedURI, url: feed.url });

            metrics.report("FeedsSidebar-AddFeed", "OpmlImport");
          }
        } else {
          this._logger.warn("Bad feed URL: " + feed.url);

          if (this._listener) {
            var error = Cc["@flock.com/error;1"]
                        .createInstance(Ci.flockIError);
            error.errorCode = error.OPML_BAD_FEED_URL;
            this._listener.onError(feed.url, this._ctxt, error,
                                   index, this._feeds.length);
          }
        }
      }
    }

    var subscriber = {
      _feeds:    subscribedFeeds,
      
      _listener: this._listener,
      _ctxt:     this._ctxt,
      _logger:   this._logger,
      _url:      this._url,

      _index:    -1,

      get _feed() { return this._feeds[this._index]; },

      onGetFeedComplete: function(feed) {
        if (this._listener) {
          this._listener.onGetFeed(feed, this._ctxt);
        }

        this.processNextFeed();
      },
      onError: function(error) {
        this._logger.warn('Error fetching ' + this._feed.url);

        if (this._listener)
          this._listener.onError(this._feed.url, this._ctxt, error,
                                 this._index, this._feeds.length);

        this.processNextFeed();
      },

      processNextFeed: function() {
        this._index++;

        if (this._index < this._feeds.length) {
          this._logger.info("Fetching feed " + this._feed.url);
          fm.getFeedBypassCache(this._feed.URI, this, true);
        } else {
          this._logger.info("Finished OPML subscription of " + this._url.spec);

          if (this._listener) {
            this._listener.onFinish(this._ctxt);
          }
        }
      }
    }

    subscriber.processNextFeed();
  },

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


function OpmlService() {
  this._logger = Cc['@flock.com/logger;1'].createInstance(Ci.flockILogger);
  this._logger.init('opmlservice');
  this._logger.info('created');
}

OpmlService.prototype = {
  subscribe: function OPML_subscribe(url, folder, flatRoot, listener, ctxt) {
    this._logger.info('subscribing OPML from ' + url);

    var req = new OpmlRequest(url, folder, flatRoot, listener, ctxt, this._logger);
    req.get();
  },

  export: function OPML_export(folder, title, file) {
    var opml =
<opml version="1.1">
<head>
  <title>{title}</title>
</head>
<body/>
</opml>;

    this._exportFolder(folder, opml.body);

    writeXML(opml, file);
  },

  _exportFolder: function OPML__exportFolder(folder, parentNode) {
    var children = folder.getChildren();

    while (children && children.hasMoreElements()) {
      var child = children.getNext();
      var node = <outline/>;

      if (child instanceof Ci.flockIFeedFolder) {
        node.@text = child.getTitle();
        this._exportFolder(child, node);
      } else {
        node.@type = 'rss';
        node.@text = child.getTitle();
        node.@title = child.getTitle();
        node.@xmlUrl = child.getURL().spec;
      }

      parentNode.outline += node;
    }
  },

  getInterfaces: function OPML_getInterfaces(countRef) {
    var interfaces = [Ci.flockIOpmlService, Ci.nsIClassInfo, Ci.nsISupports];
    countRef.value = interfaces.length;
    return interfaces;
  },
  getHelperForLanguage: function OPML_getHelperForLanguage(language) {
    return null;
  },
  contractID: OPML_CONTRACTID,
  classDescription: OPML_CLASSNAME,
  classID: OPML_CLASSID,
  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
  flags: Ci.nsIClassInfo.SINGLETON,

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


function MarkReadListener() {
  var fm = Cc['@flock.com/feed-manager;1']
          .getService(Ci.flockIFeedManager);
  this._fc = fm.getFeedContext("news");
}

MarkReadListener.prototype = {
  onSubscribe: function MR_onSubscribe(aURL, aCtxt, aProgress, aProgressMax) {
    return true;
  },
  onFinish: function MR_onFinish(aCtxt) {
  },
  onLoadError: function MR_onLoadError(aCtxt, aError) {
  },
  onError: function MR_onError(aURL, aCtxt, aError, aProgress, aProgressMax) {
  },
  onGetFeed: function MR_onGetFeed(aFeed, aCtxt) {
    if (!this._fc.existsSubscription(aFeed.getURL())) {
      aFeed.markRead();
    }
  },

  getInterfaces: function MR_getInterfaces(countRef) {
    var interfaces = [Ci.flockIOpmlSubscribeListener, Ci.nsIClassInfo,
                      Ci.nsISupports];
    countRef.value = interfaces.length;
    return interfaces;
  },
  getHelperForLanguage: function MR_getHelperForLanguage(language) {
    return null;
  },
  contractID: MARK_READ_CONTRACTID,
  classDescription: MARK_READ_CLASSNAME,
  classID: MARK_READ_CLASSID,
  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
  flags: Ci.nsIClassInfo.SINGLETON,

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


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

GenericComponentFactory.prototype = {

  _ctor: null,

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

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

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

    throw Cr.NS_ERROR_NO_INTERFACE;
  },

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

    if (cid.equals(OPML_CLASSID))
      return new GenericComponentFactory(OpmlService)
    if (cid.equals(MARK_READ_CLASSID))
      return new GenericComponentFactory(MarkReadListener)

    throw Cr.NS_ERROR_NO_INTERFACE;
  },

  registerSelf: function(cm, file, location, type) {
    var cr = cm.QueryInterface(Ci.nsIComponentRegistrar);
    cr.registerFactoryLocation(OPML_CLASSID, OPML_CLASSNAME,
                               OPML_CONTRACTID,
                               file, location, type);
    cr.registerFactoryLocation(MARK_READ_CLASSID, MARK_READ_CLASSNAME,
                               MARK_READ_CONTRACTID,
                               file, location, type);
  },

  unregisterSelf: function(cm, location, type) {
    var cr = cm.QueryInterface(Ci.nsIComponentRegistrar);
    cr.unregisterFactoryLocation(OPML_CLASSID, location);
    cr.unregisterFactoryLocation(MARK_READ_CLASSID, location);
  },

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

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