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

    try {
      var xmlData = new XML(this._xml.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;
        }
      }
    }
  },

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

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

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

      _index:    -1,

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

      createFolderHierarchy: function(hierarchy) {
        var destFolder = null;
        for each (var folder in hierarchy) {
          if (folder instanceof Ci.flockIFeedFolder) {
            destFolder = folder;
          } else {
            var newFolder;
            try {
              newFolder = destFolder.addFolder(folder);
              this._logger.info('Created folder: ' + folder);
            }
            catch (e) {
              newFolder = destFolder.getChildFolder(folder);
            }
            destFolder = newFolder;
          }
        }
        return destFolder;
      },

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

        var destFolder = this.createFolderHierarchy(this._feed.hierarchy);

        this._feed.hierarchy.length = 0;
        this._feed.hierarchy.push(destFolder);

        this._logger.info('Subscribing ' + this._feed.url + ' to folder ' +
                          destFolder.getTitle());
        destFolder.subscribeFeed(feed);
        Cc["@flock.com/metrics-service;1"]
          .getService(Ci.flockIMetricsService)
          .report("FeedsSidebar-AddFeed", "OpmlImport");

        if (this._listener)
          this._listener.onSubscribe(feed, this._ctxt,
                                     this._index, this._feeds.length);

        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++;

        while (this._index < this._feeds.length) {
          if (this._feed.url == null) {
            this.createFolderHierarchy(this._feed.hierarchy);
            this._index++;
            continue;
          }

          var url = null;
          try {
            url = ios.newURI(this._feed.url, null, null);
          }
          catch (e) { }

          if (url) {
            this._logger.info('Fetching feed ' + this._feed.url);
            fm.getFeed(url, this);
            return;
          }

          if (this._listener) {
            this._logger.warn('Bad feed URL: ' + this._feed.url);
            var error = Cc['@flock.com/error;1'].createInstance(Ci.flockIError);
            error.errorCode = error.OPML_BAD_FEED_URL;
            this._listener.onError(this._feed.url, this._ctxt, error,
                                   this._index, this._feeds.length);
          }

          this._index++;
        }

        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() {
}

MarkReadListener.prototype = {
  onSubscribe: function MARK_READ_onSubscribe(feed, ctxt, progress, progressMax) {
  },
  onFinish: function MARK_READ_onFinish(ctxt) {
  },
  onLoadError: function MARK_READ_onLoadError(ctxt, error) {
  },
  onError: function MARK_READ_onError(url, ctxt, error, progress, progressMax) {
  },
  onGetFeed: function MARK_READ_onGetFeed(feed, ctxt) {
    feed.markRead();
  },

  getInterfaces: function MARK_READ_getInterfaces(countRef) {
    var interfaces = [Ci.flockIOpmlSubscribeListener, Ci.nsIClassInfo,
                      Ci.nsISupports];
    countRef.value = interfaces.length;
    return interfaces;
  },
  getHelperForLanguage: function MARK_READ_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 MARK_READ_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;
}
